Compare commits

..

2 Commits

Author SHA1 Message Date
Carlos De Maine
bf754711e3 skip windows as one test is failing 2025-07-06 09:34:01 +10:00
Carlos De Maine
32d0f846ee Testing for VM based CI 2025-07-06 09:15:21 +10:00
446 changed files with 98069 additions and 191175 deletions

View File

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

View File

@@ -2,7 +2,7 @@
"id": "org.kde.neochat",
"branch": "master",
"runtime": "org.kde.Platform",
"runtime-version": "6.10",
"runtime-version": "6.9",
"sdk": "org.kde.Sdk",
"command": "neochat",
"tags": [
@@ -20,32 +20,21 @@
"--talk-name=org.kde.kwalletd5",
"--talk-name=org.kde.StatusNotifierWatcher",
"--talk-name=org.freedesktop.secrets",
"--talk-name=org.kde.kuiserver",
"--own-name=org.kde.StatusNotifierItem-2-2"
],
"cleanup": [
"/include",
"/lib/*.a",
"/lib/cmake",
"/lib/pkgconfig",
"/share/ndk-modules"
],
"modules": [
{
"name": "opencv",
"name": "kirigamiaddons",
"config-opts": [
"-DBUILD_TESTS=OFF",
"-DWITH_GTK=OFF",
"-DBUILD_LIST=core,imgproc"
"-DBUILD_TESTING=OFF"
],
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "git",
"url": "https://github.com/opencv/opencv"
"url": "https://invent.kde.org/libraries/kirigami-addons.git"
}
],
"builddir": true
]
},
{
"name": "kquickimageeditor",
@@ -65,7 +54,6 @@
"name": "olm",
"buildsystem": "cmake-ninja",
"config-opts": [
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5",
"-DOLM_TESTS=OFF"
],
"sources": [
@@ -94,8 +82,8 @@
"sources": [
{
"type": "archive",
"url": "https://download.gnome.org/sources/libsecret/0.21/libsecret-0.21.7.tar.xz",
"sha256": "6b452e4750590a2b5617adc40026f28d2f4903de15f1250e1d1c40bfd68ed55e",
"url": "https://download.gnome.org/sources/libsecret/0.21/libsecret-0.21.6.tar.xz",
"sha256": "747b8c175be108c880d3adfb9c3537ea66e520e4ad2dccf5dce58003aeeca090",
"x-checker-data": {
"type": "gnome",
"name": "libsecret",
@@ -165,20 +153,16 @@
"name": "kunifiedpush",
"buildsystem": "cmake-ninja",
"builddir": true,
"config-opts": [
"-DENABLE_TESTING=OFF",
"-DKUNIFIEDPUSH_CLIENT_ONLY=ON"
],
"sources": [
{
"type": "archive",
"url": "https://download.kde.org/stable/release-service/25.08.3/src/kunifiedpush-25.08.3.tar.xz",
"sha256": "e8c924438d5359f0fa0930ab35111012076e3a0ff4e959d6929595571383320a",
"url": "https://download.kde.org/stable/kunifiedpush/kunifiedpush-1.0.0.tar.xz",
"sha256": "2ddeba21306d0307114ec50a2c38159ec62359f9fc6cdd58da30a369fbd550cf",
"x-checker-data": {
"type": "anitya",
"project-id": 8763,
"project-id": 375055,
"stable-only": true,
"url-template": "https://download.kde.org/stable/release-service/$version/src/kunifiedpush-$version.tar.xz"
"url-template": "https://download.kde.org/stable/kunifiedpush/kunifiedpush-$version.tar.xz"
}
}
]

View File

@@ -3,19 +3,20 @@
include:
- project: sysadmin/ci-utilities
ref: work/switch-vm-ci
file:
- /gitlab-templates/reuse-lint.yml
- /gitlab-templates/json-validation.yml
- /gitlab-templates/xml-lint.yml
- /gitlab-templates/yaml-lint.yml
- /gitlab-templates/android-qt6.yml
#- /gitlab-templates/reuse-lint.yml
#- /gitlab-templates/json-validation.yml
#- /gitlab-templates/xml-lint.yml
#- /gitlab-templates/yaml-lint.yml
#- /gitlab-templates/android-qt6.yml
- /gitlab-templates/linux-qt6.yml
- /gitlab-templates/linux-qt6-next.yml
- /gitlab-templates/windows-qt6.yml
# - /gitlab-templates/freebsd-qt6.yml
#- /gitlab-templates/windows-qt6.yml
#- /gitlab-templates/freebsd-qt6.yml
- /gitlab-templates/flatpak.yml
- /gitlab-templates/snap-snapcraft-lxd.yml
- /gitlab-templates/craft-android-qt6-apks.yml
- /gitlab-templates/craft-appimage-qt6.yml
#- /gitlab-templates/craft-android-qt6-apks.yml
#- /gitlab-templates/craft-appimage-qt6.yml
- /gitlab-templates/craft-windows-x86-64-qt6.yml
- /gitlab-templates/craft-windows-appx-qt6.yml

View File

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

View File

@@ -7,15 +7,15 @@
cmake_minimum_required(VERSION 3.16)
# KDE Applications version, managed by release script.
set(RELEASE_SERVICE_VERSION_MAJOR "26")
set(RELEASE_SERVICE_VERSION_MINOR "03")
set(RELEASE_SERVICE_VERSION_MAJOR "25")
set(RELEASE_SERVICE_VERSION_MINOR "11")
set(RELEASE_SERVICE_VERSION_MICRO "70")
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
set(KF_MIN_VERSION "6.17")
set(QT_MIN_VERSION "6.9")
set(KF_MIN_VERSION "6.12")
set(QT_MIN_VERSION "6.5")
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_REQUIRED ON)
set(KDE_COMPILERSETTINGS_LEVEL 6.17)
set(KDE_COMPILERSETTINGS_LEVEL 6.0)
include(FeatureSummary)
include(ECMSetupVersion)
@@ -39,16 +39,17 @@ include(ECMCheckOutboundLicense)
include(ECMQtDeclareLoggingCategory)
include(ECMAddAndroidApk)
include(ECMQmlModule)
include(ECMDeprecationSettings)
include(GenerateExportHeader)
include(ECMGenerateHeaders)
if (NOT ANDROID)
include(KDEClangFormat)
endif()
set(QUOTIENT_FORCE_NAMESPACED_INCLUDES TRUE)
if(NEOCHAT_FLATPAK)
include(cmake/Flatpak.cmake)
endif()
ecm_set_disabled_deprecation_versions(Qt 6.9.0 KF 6.17.0)
set(QUOTIENT_FORCE_NAMESPACED_INCLUDES TRUE)
ecm_setup_version(${PROJECT_VERSION}
VARIABLE_PREFIX NEOCHAT
@@ -65,7 +66,7 @@ if (QT_KNOWN_POLICY_QTP0004)
qt_policy(SET QTP0004 NEW)
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
TYPE REQUIRED
PURPOSE "Basic application components"
@@ -74,7 +75,7 @@ set_package_properties(KF6Kirigami PROPERTIES
TYPE REQUIRED
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)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS Purpose)
@@ -88,7 +89,7 @@ if(ANDROID)
)
else()
find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Widgets)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle KIO WindowSystem)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle KIO WindowSystem StatusNotifierItem Crash)
find_package(KF6SyntaxHighlighting ${KF_MIN_VERSION} REQUIRED)
set_package_properties(KF6QQC2DesktopStyle PROPERTIES
TYPE RUNTIME
@@ -106,7 +107,7 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE AND NOT HAIKU)
find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED)
endif()
find_package(QuotientQt6 0.9.5)
find_package(QuotientQt6 0.9.1)
set_package_properties(QuotientQt6 PROPERTIES
TYPE REQUIRED
DESCRIPTION "Qt wrapper around Matrix API"
@@ -178,7 +179,7 @@ add_definitions(-DQT_NO_FOREACH)
add_subdirectory(src)
if (BUILD_TESTING)
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test HttpServer QuickTest)
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test HttpServer)
add_subdirectory(autotests)
# add_subdirectory(appiumtests)
if (NOT ANDROID)

View File

@@ -25,10 +25,15 @@ Qt-based SDK for the [Matrix Protocol](https://spec.matrix.org/).
## Features
NeoChat aims to be a fully featured application for the Matrix specification. As such, most parts of the current specification are supported, with the notable exceptions
of VoIP, threads, and some aspects of End-to-End Encryption. There are a few other smaller omissions due to the Matrix spec constantly
NeoChat aims to be a fully featured application for the Matrix specification. As such most parts of the current specification are supported, with the notable exceptions
of VoIP, threads, and some aspects of End-to-End Encryption. There are a few other smaller omissions due to the fact that the Matrix spec is constantly
evolving, but the aim remains to provide eventual support for the entire spec.
Due to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:
- Polls - MSC3381
- Sticker Packs - MSC2545
- Location Events - MSC3488
## Get it
Details where to find stable releases for NeoChat can be found on its [homepage](https://apps.kde.org/neochat).
@@ -43,12 +48,12 @@ The best way to build KDE apps during development is to use `kdesrc-build`. The
the KDE community website's get involved section under [development](https://community.kde.org/Get_Involved/development). This
is primarily aimed at Linux development.
For Windows and Android, [Craft](https://invent.kde.org/packaging/craft) is the primary choice. There are guides for setting up
For Windows and Android [Craft](https://invent.kde.org/packaging/craft) is the primary choice. There are guides for setting up
development environments for [Windows](https://community.kde.org/Get_Involved/development/Windows) and [Android](https://develop.kde.org/docs/packaging/android/building_applications/).
## Running
Start the executable in your preferred way either from the build directory or from the installed location.
Just start the executable in your preferred way - either from the build directory or from the installed location.
## Tests
@@ -61,12 +66,12 @@ be complete.
![coverage](https://invent.kde.org/network/neochat/badges/master/pipeline.svg)
Currently, the number of tests is limited but growing. If anyone wants to help improve this, those
Currently the number of tests is limited, but growing. If anyone wants to help improve this, those
contributions would be especially welcome.
## Contributing
As is the case throughout the KDE ecosystem, contributions are welcome from all. The code base is managed in the
As is the case throughout the KDE ecosystem contributions are welcome from all. The code base is managed in the
[NeoChat repository](https://invent.kde.org/network/neochat) of the KDE Gitlab instance.
- [Code of Conduct](https://kde.org/code-of-conduct)
@@ -81,7 +86,7 @@ The best place to reach the maintainers is on the KDE Matrix instance in the Neo
## Acknowledgement
NeoChat uses [libQuotient](https://github.com/quotient-im/libQuotient/) as its Matrix SDK.
NeoChat utilizes [libQuotient](https://github.com/quotient-im/libQuotient/) as its Matrix SDK.
NeoChat is a fork of [Spectral](https://gitlab.com/spectral-im/spectral/).

View File

@@ -88,9 +88,3 @@ path = "memorytests/memtest-sync.json"
precedence = "aggregate"
SPDX-FileCopyrightText = "2024 James Graham <james.h.graham@protonmail.com>"
SPDX-License-Identifier = "BSD-2-Clause"
[[annotations]]
path = ".contextProperties.ini"
precedence = "aggregate"
SPDX-FileCopyrightText = "2025 Tobias Fella <tobias.fella@kde.org>"
SPDX-License-Identifier = "BSD-2-Clause"

View File

@@ -11,7 +11,7 @@ add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" )
ecm_add_test(
neochatroomtest.cpp
LINK_LIBRARIES neochat Qt::Test Qt::HttpServer neochat_server
LINK_LIBRARIES neochat Qt::Test
TEST_NAME neochatroomtest
)
@@ -41,10 +41,16 @@ ecm_add_test(
ecm_add_test(
chatbarcachetest.cpp
LINK_LIBRARIES neochat Qt::Test Qt::HttpServer neochat_server
LINK_LIBRARIES neochat Qt::Test
TEST_NAME chatbarcachetest
)
ecm_add_test(
chatdocumenthandlertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME chatdocumenthandlertest
)
ecm_add_test(
timelinemessagemodeltest.cpp
LINK_LIBRARIES neochat Qt::Test
@@ -86,63 +92,3 @@ ecm_add_test(
LINK_LIBRARIES neochat Qt::Test neochat_server
TEST_NAME actionstest
)
ecm_add_test(
servernoticestest.cpp
LINK_LIBRARIES neochat Qt::Test neochat_server
TEST_NAME servernoticestest
)
ecm_add_test(
roommanagertest.cpp
LINK_LIBRARIES neochat Qt::Test neochat_server
TEST_NAME roommanagertest
)
ecm_add_test(
modeltest.cpp
LINK_LIBRARIES neochat Qt::Test neochat_server Devtools
TEST_NAME modeltest
)
ecm_add_test(
blockcachetest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME blockcachetest
)
macro(add_qml_tests)
if (WIN32)
set(_extra_args -platform offscreen)
endif()
foreach(test ${ARGV})
add_test(NAME ${test}
COMMAND qmltest
${_extra_args}
-input ${test}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
endforeach()
endmacro()
add_executable(qmltest qmltest.cpp
chatkeyhelpertesthelper.h
chatmarkdownhelpertestwrapper.h
chattextitemhelpertesthelper.h
)
qt_add_qml_module(qmltest URI NeoChatTestUtils)
target_link_libraries(qmltest
PRIVATE
Qt6::Qml
Qt6::QuickTest
LibNeoChat
LibNeoChatplugin
)
add_qml_tests(
chattextitemhelpertest.qml
chatmarkdownhelpertest.qml
chatkeyhelpertest.qml
)

View File

@@ -7,9 +7,7 @@
#include <QVariantList>
#include "accountmanager.h"
#include "blockcache.h"
#include "chatbarcache.h"
#include "enums/messagecomponenttype.h"
#include "models/actionsmodel.h"
#include "server.h"
@@ -65,7 +63,7 @@ void ActionsTest::testActions_data()
QTest::addColumn<std::optional<QString>>("resultText");
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);
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)
@@ -90,8 +88,8 @@ void ActionsTest::testActions()
QFETCH(std::optional<QString>, resultText);
QFETCH(std::optional<Quotient::RoomMessageEvent::MsgType>, type);
auto cache = new ChatBarCache(room);
cache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(command)};
auto cache = new ChatBarCache();
cache->setText(command);
auto result = ActionsModel::handleAction(room, cache);
QCOMPARE(resultText, std::get<std::optional<QString>>(result));
QCOMPARE(type, std::get<std::optional<Quotient::RoomMessageEvent::MsgType>>(result));

View File

@@ -1,52 +0,0 @@
// SPDX-FileCopyrightText: 2026 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QObject>
#include <QTest>
#include "blockcache.h"
#include "enums/messagecomponenttype.h"
using namespace Block;
class BlockCacheTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void toStringTest_data();
void toStringTest();
};
void BlockCacheTest::toStringTest_data()
{
QTest::addColumn<QString>("inputString");
QTest::addColumn<MessageComponentType::Type>("itemType");
QTest::addColumn<QString>("outputstring");
QTest::newRow("plainText") << u"test string"_s << MessageComponentType::Text << u"test string"_s;
QTest::newRow("list") << u"- list 1\n- list 2\n- list 3\n"_s << MessageComponentType::Text << u"- list 1\n- list 2\n- list 3"_s;
QTest::newRow("code") << u"for (some code) {\n\n do something\n\n}"_s << MessageComponentType::Code
<< u"```\nfor (some code) {\n do something\n}\n```"_s;
QTest::newRow("quote") << u"\"this is a quote\""_s << MessageComponentType::Quote << u"> this is a quote"_s;
QTest::newRow("heading") << u"# heading\n\nnext line"_s << MessageComponentType::Text << u"# heading\n\nnext line"_s;
}
void BlockCacheTest::toStringTest()
{
QFETCH(QString, inputString);
QFETCH(MessageComponentType::Type, itemType);
QFETCH(QString, outputstring);
Cache cache;
cache += CacheItem{
.type = itemType,
.content = QTextDocumentFragment::fromMarkdown(inputString),
};
QCOMPARE(cache.toString(), outputstring);
}
QTEST_MAIN(BlockCacheTest)
#include "blockcachetest.moc"

View File

@@ -6,19 +6,13 @@
#include <QObject>
#include <QTest>
#include <QSignalSpy>
#include <Quotient/roommember.h>
#include <Quotient/syncdata.h>
#include <qtestcase.h>
#include <KLocalizedString>
#include "accountmanager.h"
#include "blockcache.h"
#include "chatbarcache.h"
#include "neochatroom.h"
#include "server.h"
#include "testutils.h"
using namespace Quotient;
@@ -29,54 +23,30 @@ class ChatBarCacheTest : public QObject
private:
Connection *connection = nullptr;
NeoChatRoom *room = nullptr;
Server server;
QString eventId;
TestUtils::TestRoom *room = nullptr;
private Q_SLOTS:
void initTestCase();
void empty();
void noRoom();
void badParent();
void reply();
void replyMissingUser();
void edit();
void attachment();
};
void ChatBarCacheTest::initTestCase()
{
Connection::setRoomType<NeoChatRoom>();
server.start();
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
auto accountManager = new AccountManager(true, this);
QSignalSpy spy(accountManager, &AccountManager::connectionAdded);
connection = dynamic_cast<NeoChatConnection *>(accountManager->accounts()->front());
const auto roomId = server.createRoom(u"@user:localhost:1234"_s);
eventId = server.sendEvent(roomId,
u"m.room.message"_s,
QJsonObject{
{u"body"_s, u"foo"_s},
{u"msgtype"_s, u"m.text"_s},
});
QSignalSpy syncSpy(connection, &Connection::syncDone);
// We need to wait for two syncs, as the next one won't have the changes yet
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
room = dynamic_cast<NeoChatRoom *>(connection->room(roomId));
QVERIFY(room);
server.joinUser(room->id(), u"@foo:server.com"_s);
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
connection = Connection::makeMockConnection(u"@bob:kde.org"_s);
room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, "test-min-sync.json"_L1);
}
void ChatBarCacheTest::empty()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
QCOMPARE(chatBarCache->cache().toString(), QString());
QCOMPARE(chatBarCache->text(), QString());
QCOMPARE(chatBarCache->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), false);
@@ -86,84 +56,84 @@ void ChatBarCacheTest::empty()
QCOMPARE(chatBarCache->attachmentPath(), QString());
}
void ChatBarCacheTest::noRoom()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache());
chatBarCache->setReplyId(u"$153456789:example.org"_s);
// These should return empty even though a reply ID has been set because the
// ChatBarCache has no parent.
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationAuthor(), Quotient::RoomMember());
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationMessage(), QString());
}
void ChatBarCacheTest::badParent()
{
QScopedPointer<QObject> badParent(new QObject());
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(badParent.get()));
chatBarCache->setReplyId(u"$153456789:example.org"_s);
// These should return empty even though a reply ID has been set because the
// ChatBarCache has no parent.
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationAuthor(), Quotient::RoomMember());
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
QCOMPARE(chatBarCache->relationMessage(), QString());
}
void ChatBarCacheTest::reply()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(u"some text"_s)};
chatBarCache->setText(u"some text"_s);
chatBarCache->setAttachmentPath(u"some/path"_s);
chatBarCache->setReplyId(eventId);
chatBarCache->setReplyId(u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->cache().toString(), u"some text"_s);
QCOMPARE(chatBarCache->text(), u"some text"_s);
QCOMPARE(chatBarCache->isReplying(), true);
QCOMPARE(chatBarCache->replyId(), eventId);
QCOMPARE(chatBarCache->replyId(), u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@foo:server.com"_s));
QCOMPARE(chatBarCache->relationMessage(), u"foo"_s);
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@example:example.org"_s));
QCOMPARE(chatBarCache->relationMessage(), u"This is an example\ntext message"_s);
QCOMPARE(chatBarCache->attachmentPath(), QString());
QCOMPARE(chatBarCache->relationAuthorIsPresent(), true);
}
void ChatBarCacheTest::replyMissingUser()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(u"some text"_s)};
chatBarCache->setAttachmentPath(u"some/path"_s);
chatBarCache->setReplyId(eventId);
QCOMPARE(chatBarCache->cache().toString(), u"some text"_s);
QCOMPARE(chatBarCache->isReplying(), true);
QCOMPARE(chatBarCache->replyId(), eventId);
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@foo:server.com"_s));
QCOMPARE(chatBarCache->relationMessage(), u"foo"_s);
QCOMPARE(chatBarCache->attachmentPath(), QString());
QCOMPARE(chatBarCache->relationAuthorIsPresent(), true);
QSignalSpy relationAuthorIsPresentSpy(chatBarCache.get(), &ChatBarCache::relationAuthorIsPresentChanged);
// sync again, which will simulate the reply user leaving the room
QSignalSpy syncSpy(connection, &Connection::syncDone);
server.sendStateEvent(room->id(), u"m.room.member"_s, u"@foo:server.com"_s, {{u"membership"_s, u"leave"_s}});
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
QTRY_COMPARE(relationAuthorIsPresentSpy.count(), 1);
QCOMPARE(chatBarCache->relationAuthorIsPresent(), false);
}
void ChatBarCacheTest::edit()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(u"some text"_s)};
chatBarCache->setText(u"some text"_s);
chatBarCache->setAttachmentPath(u"some/path"_s);
connect(chatBarCache.get(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) {
connect(chatBarCache.get(), &ChatBarCache::relationIdChanged, this, [](const QString &oldEventId, const QString &newEventId) {
QCOMPARE(oldEventId, QString());
QCOMPARE(newEventId, eventId);
QCOMPARE(newEventId, QString(u"$153456789:example.org"_s));
});
chatBarCache->setEditId(eventId);
chatBarCache->setEditId(u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->cache().toString(), u"some text"_s);
QCOMPARE(chatBarCache->text(), u"some text"_s);
QCOMPARE(chatBarCache->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), true);
QCOMPARE(chatBarCache->editId(), eventId);
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@foo:server.com"_s));
QCOMPARE(chatBarCache->relationMessage(), u"foo"_s);
QCOMPARE(chatBarCache->editId(), u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@example:example.org"_s));
QCOMPARE(chatBarCache->relationMessage(), u"This is an example\ntext message"_s);
QCOMPARE(chatBarCache->attachmentPath(), QString());
}
void ChatBarCacheTest::attachment()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(u"some text"_s)};
chatBarCache->setEditId(eventId);
chatBarCache->setText(u"some text"_s);
chatBarCache->setEditId(u"$153456789:example.org"_s);
chatBarCache->setAttachmentPath(u"some/path"_s);
QCOMPARE(chatBarCache->cache().toString(), u"some text"_s);
QCOMPARE(chatBarCache->text(), u"some text"_s);
QCOMPARE(chatBarCache->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), false);

View File

@@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QObject>
#include <QTest>
#include "chatdocumenthandler.h"
#include "neochatconfig.h"
class ChatDocumentHandlerTest : public QObject
{
Q_OBJECT
private:
ChatDocumentHandler emptyHandler;
private Q_SLOTS:
void initTestCase();
void nullComplete();
};
void ChatDocumentHandlerTest::initTestCase()
{
// HACK: this is to stop KStatusNotifierItem SEGFAULTING on cleanup.
NeoChatConfig::self()->setSystemTray(false);
}
void ChatDocumentHandlerTest::nullComplete()
{
QTest::ignoreMessage(QtWarningMsg, "complete called with m_document set to nullptr.");
emptyHandler.complete(0);
}
QTEST_MAIN(ChatDocumentHandlerTest)
#include "chatdocumenthandlertest.moc"

View File

@@ -1,88 +0,0 @@
// SPDX-FileCopyrightText: 2026 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtTest
import org.kde.neochat.libneochat
import NeoChatTestUtils
TestCase {
name: "ChatKeyHelperTest"
TextEdit {
id: textEdit
Keys.onPressed: (event) => {
event.accepted = testHelper.keyHelper.handleKey(event.key, event.modifiers);
}
}
ChatTextItemHelper {
id: textItemHelper
textItem: textEdit
}
ChatKeyHelperTestHelper {
id: testHelper
textItem: textItemHelper
}
SignalSpy {
id: spyUp
target: testHelper.keyHelper
signalName: "unhandledUp"
}
SignalSpy {
id: spyDown
target: testHelper.keyHelper
signalName: "unhandledDown"
}
SignalSpy {
id: spyDelete
target: testHelper.keyHelper
signalName: "unhandledDelete"
}
SignalSpy {
id: spyBackSpace
target: testHelper.keyHelper
signalName: "unhandledBackspace"
}
function init(): void {
textEdit.clear();
spyUp.clear();
spyDown.clear();
spyDelete.clear();
spyBackSpace.clear();
textEdit.forceActiveFocus();
}
function cleanupTestCase(): void {
testHelper.textItem = null;
textItemHelper.textItem = null;
}
function test_upDown(): void {
textEdit.insert(0, "line 1\nline 2\nline 3")
textEdit.cursorPosition = 0;
keyClick(Qt.Key_Up);
compare(spyUp.count, 1);
compare(spyDown.count, 0);
keyClick(Qt.Key_Down);
compare(spyUp.count, 1);
compare(spyDown.count, 0);
keyClick(Qt.Key_Down);
compare(spyUp.count, 1);
compare(spyDown.count, 0);
keyClick(Qt.Key_Down);
compare(spyUp.count, 1);
compare(spyDown.count, 1);
}
}

View File

@@ -1,54 +0,0 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QObject>
#include <QQuickItem>
#include <QQuickTextDocument>
#include <QTextCursor>
#include <QTextDocumentFragment>
#include "chatkeyhelper.h"
#include "chattextitemhelper.h"
class ChatKeyHelperTestHelper : public QObject
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(ChatTextItemHelper *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
Q_PROPERTY(ChatKeyHelper *keyHelper READ keyHelper CONSTANT)
public:
explicit ChatKeyHelperTestHelper(QObject *parent = nullptr)
: QObject(parent)
, m_keyHelper(new ChatKeyHelper(this))
{
}
ChatTextItemHelper *textItem() const
{
return m_keyHelper->textItem();
}
void setTextItem(ChatTextItemHelper *textItem)
{
if (textItem == m_keyHelper->textItem()) {
return;
}
m_keyHelper->setTextItem(textItem);
Q_EMIT textItemChanged();
}
ChatKeyHelper *keyHelper() const
{
return m_keyHelper;
}
Q_SIGNALS:
void textItemChanged();
private:
QPointer<ChatKeyHelper> m_keyHelper;
};

View File

@@ -1,173 +0,0 @@
// SPDX-FileCopyrightText: 2026 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtTest
import org.kde.neochat.libneochat
import NeoChatTestUtils
TestCase {
name: "ChatMarkdownHelperTest"
TextEdit {
id: textEdit
textFormat: TextEdit.RichText
}
TextEdit {
id: textEdit2
}
ChatMarkdownHelperTestWrapper {
id: chatMarkdownHelper
textItem: textEdit
}
SignalSpy {
id: spyItem
target: chatMarkdownHelper
signalName: "textItemChanged"
}
SignalSpy {
id: spyUnhandledFormat
target: chatMarkdownHelper
signalName: "unhandledBlockFormat"
}
function initTestCase(): void {
textEdit.forceActiveFocus();
}
function cleanup(): void {
chatMarkdownHelper.clear();
compare(chatMarkdownHelper.checkText(""), true);
compare(chatMarkdownHelper.checkFormats([]), true);
compare(textEdit.cursorPosition, 0);
}
function test_item(): void {
spyItem.clear();
compare(chatMarkdownHelper.textItem, textEdit);
chatMarkdownHelper.textItem = textEdit2;
compare(chatMarkdownHelper.textItem, textEdit2);
chatMarkdownHelper.textItem = textEdit;
compare(chatMarkdownHelper.textItem, textEdit);
}
function test_textFormat_data() {
return [
{tag: "bold", input: "**b** ", outText: ["*", "**", "b", "b*", "b**", "b "], outFormats: [[], [], [RichFormat.Bold], [RichFormat.Bold], [RichFormat.Bold], []], unhandled: 0},
{tag: "italic", input: "*i* ", outText: ["*", "i", "i*", "i "], outFormats: [[], [RichFormat.Italic], [RichFormat.Italic], []], unhandled: 0},
{tag: "heading 1", input: "# h", outText: ["#", "# ", "h"], outFormats: [[], [], [RichFormat.Bold, RichFormat.Heading1]], unhandled: 0},
{tag: "heading 2", input: "## h", outText: ["#", "##", "## ", "h"], outFormats: [[], [], [], [RichFormat.Bold, RichFormat.Heading2]], unhandled: 0},
{tag: "heading 3", input: "### h", outText: ["#", "##", "###", "### ", "h"], outFormats: [[], [], [], [], [RichFormat.Bold, RichFormat.Heading3]], unhandled: 0},
{tag: "heading 4", input: "#### h", outText: ["#", "##", "###", "####", "#### ", "h"], outFormats: [[], [], [], [], [], [RichFormat.Bold, RichFormat.Heading4]], unhandled: 0},
{tag: "heading 5", input: "##### h", outText: ["#", "##", "###", "####", "#####", "##### ", "h"], outFormats: [[], [], [], [], [], [], [RichFormat.Bold, RichFormat.Heading5]], unhandled: 0},
{tag: "heading 6", input: "###### h", outText: ["#", "##", "###", "####", "#####", "######", "###### ", "h"], outFormats: [[], [], [], [], [], [] ,[], [RichFormat.Bold, RichFormat.Heading6]], unhandled: 0},
{tag: "quote", input: "> q", outText: [">", "> ", "q"], outFormats: [[], [], []], unhandled: 1},
{tag: "quote - no space", input: ">q", outText: [">", "q"], outFormats: [[], [], []], unhandled: 1},
{tag: "unorderedlist 1", input: "* l", outText: ["*", "* ", "l"], outFormats: [[], [], [RichFormat.UnorderedList]], unhandled: 0},
{tag: "unorderedlist 2", input: "- l", outText: ["-", "- ", "l"], outFormats: [[], [], [RichFormat.UnorderedList]], unhandled: 0},
{tag: "orderedlist 1", input: "1. l", outText: ["1", "1.", "1. ", "l"], outFormats: [[], [], [], [RichFormat.OrderedList]], unhandled: 0},
{tag: "orderedlist 2", input: "1) l", outText: ["1", "1)", "1) ", "l"], outFormats: [[], [], [], [RichFormat.OrderedList]], unhandled: 0},
{tag: "inline code", input: "`c` ", outText: ["`", "c", "c`", "c "], outFormats: [[], [RichFormat.InlineCode], [RichFormat.InlineCode], []], unhandled: 0},
{tag: "code", input: "``` ", outText: ["`", "``", "```", " "], outFormats: [[], [], [], []], unhandled: 1},
{tag: "strikethrough", input: "~~s~~ ", outText: ["~", "~~", "s", "s~", "s~~", "s "], outFormats: [[], [], [RichFormat.Strikethrough], [RichFormat.Strikethrough], [RichFormat.Strikethrough], []], unhandled: 0},
{tag: "underline", input: "_u_ ", outText: ["_", "u", "u_", "u "], outFormats: [[], [RichFormat.Underline], [RichFormat.Underline], []], unhandled: 0},
{tag: "multiple closable", input: "***_~~t~~_*** ", outText: ["*", "**", "*", "_", "~", "~~", "t", "t~", "t~~", "t_", "t*", "t**", "t*", "t "], outFormats: [[], [], [RichFormat.Bold], [RichFormat.Bold, RichFormat.Italic], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline, RichFormat.Strikethrough], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline, RichFormat.Strikethrough], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline, RichFormat.Strikethrough], [RichFormat.Bold, RichFormat.Italic, RichFormat.Underline], [RichFormat.Bold, RichFormat.Italic], [RichFormat.Bold, RichFormat.Italic], [RichFormat.Italic], []], unhandled: 0},
{tag: "nonclosable closable", input: "* **b** ", outText: ["*", "* ", "*", "**", "b", "b*", "b**", "b "], outFormats: [[], [], [RichFormat.UnorderedList], [RichFormat.UnorderedList], [RichFormat.Bold, RichFormat.UnorderedList], [RichFormat.Bold, RichFormat.UnorderedList], [RichFormat.Bold, RichFormat.UnorderedList], [RichFormat.UnorderedList]], unhandled: 0},
{tag: "not at line start", input: " 1) ", outText: [" ", " 1", " 1)", " 1) "], outFormats: [[], [], [], []], unhandled: 0},
]
}
function test_textFormat(data): void {
spyUnhandledFormat.clear();
compare(spyUnhandledFormat.count, 0);
for (let i = 0; i < data.input.length; i++) {
keyClick(data.input[i]);
compare(chatMarkdownHelper.checkText(data.outText[i]), true);
compare(chatMarkdownHelper.checkFormats(data.outFormats[i]), true);
}
compare(spyUnhandledFormat.count, data.unhandled);
}
function test_backspace(): void {
keyClick("*");
compare(chatMarkdownHelper.checkText("*"), true);
compare(chatMarkdownHelper.checkFormats([]), true);
keyClick("*");
compare(chatMarkdownHelper.checkText("**"), true);
compare(chatMarkdownHelper.checkFormats([]), true);
keyClick("b");
compare(chatMarkdownHelper.checkText("b"), true);
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
keyClick("o");
compare(chatMarkdownHelper.checkText("bo"), true);
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
keyClick("l");
compare(chatMarkdownHelper.checkText("bol"), true);
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
keyClick("d");
compare(chatMarkdownHelper.checkText("bold"), true);
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
keyClick(Qt.Key_Backspace);
compare(chatMarkdownHelper.checkText("bol"), true);
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
keyClick(Qt.Key_Backspace);
compare(chatMarkdownHelper.checkText("bo"), true);
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
keyClick("*");
compare(chatMarkdownHelper.checkText("bo*"), true);
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
keyClick("*");
compare(chatMarkdownHelper.checkText("bo**"), true);
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
keyClick(" ");
compare(chatMarkdownHelper.checkText("bo "), true);
compare(chatMarkdownHelper.checkFormats([]), true);
}
function test_cursorMove(): void {
keyClick("t");
keyClick("e");
keyClick("s");
keyClick("t");
compare(chatMarkdownHelper.checkText("test"), true);
compare(chatMarkdownHelper.checkFormats([]), true);
keyClick("*");
keyClick("*");
keyClick("b");
compare(chatMarkdownHelper.checkText("testb"), true);
compare(chatMarkdownHelper.checkFormats([RichFormat.Bold]), true);
textEdit.cursorPosition = 2;
keyClick("*");
keyClick("*");
keyClick("b");
compare(chatMarkdownHelper.checkText("tebstb"), true);
compare(chatMarkdownHelper.checkFormats([]), true);
}
function test_insertText(): void {
textEdit.insert(0, "test");
compare(chatMarkdownHelper.checkText("test"), true);
compare(chatMarkdownHelper.checkFormats([]), true);
textEdit.insert(4, "**b");
compare(chatMarkdownHelper.checkText("test**b"), true);
compare(chatMarkdownHelper.checkFormats([]), true);
textEdit.clear();
textEdit.insert(0, "test");
compare(chatMarkdownHelper.checkText("test"), true);
compare(chatMarkdownHelper.checkFormats([]), true);
textEdit.insert(2, "**b");
compare(chatMarkdownHelper.checkText("te**bst"), true);
compare(chatMarkdownHelper.checkFormats([]), true);
}
}

View File

@@ -1,82 +0,0 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QObject>
#include <QQuickItem>
#include <QTextCursor>
#include "chatmarkdownhelper.h"
#include "chattextitemhelper.h"
#include "enums/richformat.h"
class ChatMarkdownHelperTestWrapper : public QObject
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The QML text Item the ChatMerkdownHelper is handling.
*/
Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
public:
explicit ChatMarkdownHelperTestWrapper(QObject *parent = nullptr)
: QObject(parent)
, m_chatMarkdownHelper(new ChatMarkdownHelper(this))
, m_textItem(new ChatTextItemHelper(this))
{
m_chatMarkdownHelper->setTextItem(m_textItem);
connect(m_chatMarkdownHelper, &ChatMarkdownHelper::textItemChanged, this, &ChatMarkdownHelperTestWrapper::textItemChanged);
connect(m_chatMarkdownHelper, &ChatMarkdownHelper::unhandledBlockFormat, this, &ChatMarkdownHelperTestWrapper::unhandledBlockFormat);
}
QQuickItem *textItem() const
{
return m_textItem->textItem();
}
void setTextItem(QQuickItem *textItem)
{
m_textItem->setTextItem(textItem);
}
Q_INVOKABLE bool checkText(const QString &text)
{
const auto doc = m_textItem->document();
if (!doc) {
return false;
}
return text == doc->toPlainText();
}
Q_INVOKABLE bool checkFormats(QList<RichFormat::Format> formats)
{
const auto cursor = m_textItem->textCursor();
if (cursor.isNull()) {
return false;
}
return RichFormat::formatsAtCursor(cursor) == formats;
}
Q_INVOKABLE void clear()
{
auto cursor = m_textItem->textCursor();
if (cursor.isNull()) {
return;
}
cursor.select(QTextCursor::Document);
cursor.removeSelectedText();
cursor.setBlockCharFormat(RichFormat::charFormatForFormat(RichFormat::Paragraph));
cursor.setBlockFormat(RichFormat::blockFormatForFormat(RichFormat::Paragraph));
}
Q_SIGNALS:
void textItemChanged();
void unhandledBlockFormat(RichFormat::Format format);
private:
QPointer<ChatMarkdownHelper> m_chatMarkdownHelper;
QPointer<ChatTextItemHelper> m_textItem;
};

View File

@@ -1,301 +0,0 @@
// SPDX-FileCopyrightText: 2026 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtTest
import org.kde.neochat.libneochat
import NeoChatTestUtils
TestCase {
name: "ChatTextItemHelperTest"
TextEdit {
id: textEdit
}
TextEdit {
id: textEdit2
}
ChatTextItemHelper {
id: textItemHelper
textItem: textEdit
}
ChatTextItemHelperTestHelper {
id: testHelper
textItem: textItemHelper
}
SignalSpy {
id: spyItem
target: textItemHelper
signalName: "textItemChanged"
}
SignalSpy {
id: spyContentsChanged
target: textItemHelper
signalName: "contentsChanged"
}
SignalSpy {
id: spyContentsChange
target: textItemHelper
signalName: "contentsChange"
}
SignalSpy {
id: spyCursor
target: textItemHelper
signalName: "cursorPositionChanged"
}
function init(): void {
testHelper.setFixedChars("", "");
textEdit.clear();
textEdit2.clear();
spyItem.clear();
spyContentsChange.clear();
spyContentsChanged.clear();
spyCursor.clear();
}
function cleanupTestCase(): void {
testHelper.textItem = null;
textItemHelper.textItem = null;
}
function test_item(): void {
compare(textItemHelper.textItem, textEdit);
compare(spyItem.count, 0);
textItemHelper.textItem = textEdit2;
compare(textItemHelper.textItem, textEdit2);
compare(spyItem.count, 1);
textItemHelper.textItem = textEdit;
compare(textItemHelper.textItem, textEdit);
compare(spyItem.count, 2);
}
function test_fixedChars(): void {
textEdit.forceActiveFocus();
testHelper.setFixedChars("1", "2");
compare(textEdit.text, "12");
compare(textEdit.cursorPosition, 1);
compare(spyCursor.count, 0);
keyClick("b");
compare(textEdit.text, "1b2");
compare(textEdit.cursorPosition, 2);
compare(spyCursor.count, 1);
keyClick(Qt.Key_Left);
compare(textEdit.text, "1b2");
compare(textEdit.cursorPosition, 1);
compare(spyCursor.count, 2);
keyClick(Qt.Key_Left);
compare(textEdit.text, "1b2");
compare(textEdit.cursorPosition, 1);
compare(spyCursor.count, 3);
keyClick(Qt.Key_Right);
compare(textEdit.text, "1b2");
compare(textEdit.cursorPosition, 2);
compare(spyCursor.count, 4);
keyClick(Qt.Key_Right);
compare(textEdit.text, "1b2");
compare(textEdit.cursorPosition, 2);
compare(spyCursor.count, 5);
}
function test_document(): void {
// We can't get to the QTextDocument from QML so we have to use a helper function.
compare(testHelper.compareDocuments(textEdit.textDocument), true);
textEdit.insert(0, "test text");
compare(testHelper.lineCount(), 1);
textEdit.insert(textEdit.text.length, "\ntest text");
compare(testHelper.lineCount(), 2);
textEdit.clear()
compare(textEdit.text.length, 0);
}
function test_takeFirstBlock(): void {
textEdit.insert(0, "test text");
compare(testHelper.firstBlockText(), "test text");
compare(textEdit.text.length, 0);
textEdit.insert(0, "test text\nmore test text");
compare(testHelper.firstBlockText(), "test text");
compare(textEdit.text, "more test text");
compare(testHelper.firstBlockText(), "more test text");
compare(textEdit.text, "");
compare(textEdit.text.length, 0);
}
function test_fillFragments(): void {
textEdit.insert(0, "before fragment\nmid fragment\nafter fragment");
compare(testHelper.checkFragments("before fragment\nmid fragment", "after fragment", ""), true);
textEdit.clear();
textEdit.insert(0, "before fragment\nmid fragment\nafter fragment");
textEdit.cursorPosition = 16;
compare(testHelper.checkFragments("before fragment", "mid fragment", "after fragment"), true);
textEdit.clear();
textEdit.insert(0, "before fragment\nmid fragment\nafter fragment");
textEdit.cursorPosition = 29;
compare(testHelper.checkFragments("before fragment\nmid fragment", "after fragment", ""), true);
textEdit.clear();
}
function test_insertFragment(): void {
testHelper.insertFragment("test text");
compare(textEdit.text, "test text");
compare(textEdit.cursorPosition, 9);
testHelper.insertFragment("beginning ", 1);
compare(textEdit.text, "beginning test text");
compare(textEdit.cursorPosition, 10);
testHelper.insertFragment(" end", 2);
compare(textEdit.text, "beginning test text end");
compare(textEdit.cursorPosition, 23);
textEdit.clear();
testHelper.insertFragment("test text", 0, true);
compare(textEdit.text, "test text");
compare(textEdit.cursorPosition, 0);
}
function test_cursor(): void {
// We can't get to the QTextCursor from QML so we have to use a helper function.
compare(testHelper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
compare(textEdit.cursorPosition, testHelper.cursorPosition());
// Check we get the appropriate content and cursor change signals when inserting text.
textEdit.insert(0, "test text")
compare(spyContentsChange.count, 1);
compare(spyContentsChange.signalArguments[0][0], 0);
compare(spyContentsChange.signalArguments[0][1], 0);
compare(spyContentsChange.signalArguments[0][2], 9);
compare(spyContentsChanged.count, 1);
compare(spyCursor.count, 1);
compare(spyCursor.signalArguments[0][0], true);
compare(testHelper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
compare(textEdit.cursorPosition, testHelper.cursorPosition());
// Check we get only get a cursor change signal when moving the cursor.
textEdit.cursorPosition = 4;
compare(spyContentsChanged.count, 1);
compare(spyCursor.count, 2);
compare(spyCursor.signalArguments[1][0], false);
textEdit.selectAll();
compare(spyContentsChanged.count, 1);
compare(spyCursor.count, 2);
compare(testHelper.compareCursor(textEdit.cursorPosition, textEdit.selectionStart, textEdit.selectionEnd), true);
compare(textEdit.cursorPosition, testHelper.cursorPosition());
// Check we get the appropriate content and cursor change signals when removing text.
textEdit.clear();
compare(spyContentsChange.count, 2);
compare(spyContentsChange.signalArguments[1][0], 0);
compare(spyContentsChange.signalArguments[1][1], 9);
compare(spyContentsChange.signalArguments[1][2], 0);
compare(spyContentsChanged.count, 2);
compare(spyCursor.count, 3);
compare(spyCursor.signalArguments[2][0], true);
}
function test_setCursor(): void {
textEdit.insert(0, "test text");
compare(textEdit.cursorPosition, 9);
compare(spyCursor.count, 1);
testHelper.setCursorPosition(5);
compare(textEdit.cursorPosition, 5);
compare(spyCursor.count, 2);
testHelper.setCursorPosition(1);
compare(textEdit.cursorPosition, 1);
compare(spyCursor.count, 3);
textEdit.cursorVisible = false;
compare(textEdit.cursorVisible, false);
testHelper.setCursorVisible(true);
compare(textEdit.cursorVisible, true);
testHelper.setCursorVisible(false);
compare(textEdit.cursorVisible, false);
}
function test_setCursorFromTextItem(): void {
textEdit.insert(0, "line 1\nline 2");
textEdit2.insert(0, "line 1\nline 2");
testHelper.setCursorFromTextItem(textEdit2, false, 0);
compare(textEdit.cursorPosition, 7);
testHelper.setCursorFromTextItem(textEdit2, true, 7);
compare(textEdit.cursorPosition, 0);
testHelper.setCursorFromTextItem(textEdit2, false, 1);
compare(textEdit.cursorPosition, 8);
testHelper.setCursorFromTextItem(textEdit2, true, 8);
compare(textEdit.cursorPosition, 1);
testHelper.setFixedChars("1", "2");
testHelper.setCursorFromTextItem(textEdit2, false, 0);
compare(textEdit.cursorPosition, 8);
testHelper.setCursorFromTextItem(textEdit2, true, 7);
compare(textEdit.cursorPosition, 1);
}
function test_mergeFormat(): void {
textEdit.insert(0, "lots of text");
testHelper.setCursorPosition(0);
testHelper.mergeFormatOnCursor(RichFormat.Bold);
compare(testHelper.checkFormatsAtCursor([RichFormat.Bold]), true);
testHelper.mergeFormatOnCursor(RichFormat.Italic);
compare(testHelper.checkFormatsAtCursor([RichFormat.Bold, RichFormat.Italic]), true);
testHelper.setCursorPosition(6);
compare(testHelper.checkFormatsAtCursor([]), true);
testHelper.mergeFormatOnCursor(RichFormat.Underline);
compare(testHelper.checkFormatsAtCursor([RichFormat.Underline]), true);
testHelper.setCursorPosition(9);
compare(testHelper.checkFormatsAtCursor([]), true);
testHelper.mergeFormatOnCursor(RichFormat.Strikethrough);
compare(testHelper.checkFormatsAtCursor([RichFormat.Strikethrough]), true);
textEdit.clear();
textEdit.insert(0, "heading");
testHelper.mergeFormatOnCursor(RichFormat.Heading1);
compare(testHelper.checkFormatsAtCursor([RichFormat.Bold, RichFormat.Heading1]), true);
testHelper.mergeFormatOnCursor(RichFormat.Heading2);
compare(testHelper.checkFormatsAtCursor([RichFormat.Bold, RichFormat.Heading2]), true);
testHelper.mergeFormatOnCursor(RichFormat.Paragraph);
compare(testHelper.checkFormatsAtCursor([]), true);
textEdit.clear();
textEdit.insert(0, "text");
testHelper.mergeFormatOnCursor(RichFormat.UnorderedList);
compare(testHelper.checkFormatsAtCursor([RichFormat.UnorderedList]), true);
compare(testHelper.markdownText(), "- text");
testHelper.mergeFormatOnCursor(RichFormat.OrderedList);
compare(testHelper.checkFormatsAtCursor([RichFormat.OrderedList]), true);
compare(testHelper.markdownText(), "1. text");
textEdit.clear();
}
function test_list(): void {
compare(testHelper.canIndentListMoreAtCursor(), true);
testHelper.indentListMoreAtCursor();
compare(testHelper.canIndentListMoreAtCursor(), true);
testHelper.indentListMoreAtCursor();
compare(testHelper.canIndentListMoreAtCursor(), true);
testHelper.indentListMoreAtCursor();
compare(testHelper.canIndentListMoreAtCursor(), false);
compare(testHelper.canIndentListLessAtCursor(), true);
testHelper.indentListLessAtCursor();
compare(testHelper.canIndentListLessAtCursor(), true);
testHelper.indentListLessAtCursor();
compare(testHelper.canIndentListLessAtCursor(), true);
testHelper.indentListLessAtCursor();
compare(testHelper.canIndentListLessAtCursor(), false);
}
function test_forceActiveFocus(): void {
textEdit2.forceActiveFocus();
compare(textEdit.activeFocus, false);
testHelper.forceActiveFocus();
compare(textEdit.activeFocus, true);
}
}

View File

@@ -1,218 +0,0 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QObject>
#include <QQuickItem>
#include <QQuickTextDocument>
#include <QTextCursor>
#include <QTextDocumentFragment>
#include "chattextitemhelper.h"
class ChatTextItemHelperTestHelper : public QObject
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The QML text Item the TextItemHelper is handling.
*/
Q_PROPERTY(ChatTextItemHelper *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
public:
explicit ChatTextItemHelperTestHelper(QObject *parent = nullptr)
: QObject(parent)
{
}
ChatTextItemHelper *textItem() const
{
return m_textItem;
}
void setTextItem(ChatTextItemHelper *textItem)
{
if (textItem == m_textItem) {
return;
}
m_textItem = textItem;
Q_EMIT textItemChanged();
}
Q_INVOKABLE void setFixedChars(const QString &startChars, const QString &endChars)
{
if (!m_textItem) {
return;
}
m_textItem->setFixedChars(startChars, endChars);
}
Q_INVOKABLE bool compareDocuments(QQuickTextDocument *document)
{
if (!m_textItem) {
return false;
}
return document->textDocument() == m_textItem->document();
}
Q_INVOKABLE int lineCount()
{
if (!m_textItem) {
return -1;
}
return m_textItem->lineCount();
}
Q_INVOKABLE QString firstBlockText()
{
if (!m_textItem) {
return {};
}
return m_textItem->takeFirstBlock().toPlainText();
}
Q_INVOKABLE bool checkFragments(const QString &before, const QString &mid, const QString &after)
{
if (!m_textItem) {
return false;
}
bool hasBefore = false;
QTextDocumentFragment midFragment;
std::optional<QTextDocumentFragment> afterFragment = std::nullopt;
m_textItem->fillFragments(hasBefore, midFragment, afterFragment);
return hasBefore && m_textItem->document()->toPlainText() == before && midFragment.toPlainText() == mid && after.isEmpty()
? !afterFragment
: afterFragment->toPlainText() == after;
}
Q_INVOKABLE void insertFragment(const QString &text, ChatTextItemHelper::InsertPosition position = ChatTextItemHelper::Cursor, bool keepPosition = false)
{
if (!m_textItem) {
return;
}
const auto fragment = QTextDocumentFragment::fromPlainText(text);
m_textItem->insertFragment(fragment, position, keepPosition);
}
Q_INVOKABLE bool compareCursor(int cursorPosition, int selectionStart, int selectionEnd)
{
if (!m_textItem) {
return false;
}
const auto cursor = m_textItem->textCursor();
if (cursor.isNull()) {
return false;
}
const auto posSame = cursor.position() == cursorPosition;
const auto startSame = cursor.selectionStart() == selectionStart;
const auto endSame = cursor.selectionEnd() == selectionEnd;
return posSame && startSame && endSame;
}
Q_INVOKABLE int cursorPosition() const
{
if (!m_textItem) {
return -1;
}
return *m_textItem->cursorPosition();
}
Q_INVOKABLE void setCursorPosition(int pos)
{
if (!m_textItem) {
return;
}
m_textItem->setCursorPosition(pos);
}
Q_INVOKABLE void setCursorVisible(bool visible)
{
if (!m_textItem) {
return;
}
m_textItem->setCursorVisible(visible);
}
Q_INVOKABLE void setCursorFromTextItem(QQuickItem *item, bool infront, int cursorPos)
{
if (!m_textItem) {
return;
}
const auto textItem = new ChatTextItemHelper(this);
textItem->setTextItem(item);
textItem->setCursorPosition(cursorPos);
m_textItem->setCursorFromTextItem(textItem, infront);
textItem->deleteLater();
}
Q_INVOKABLE void mergeFormatOnCursor(RichFormat::Format format)
{
if (!m_textItem) {
return;
}
m_textItem->mergeFormatOnCursor(format);
}
Q_INVOKABLE bool checkFormatsAtCursor(QList<RichFormat::Format> formats)
{
const auto cursor = m_textItem->textCursor();
if (cursor.isNull()) {
return false;
}
return RichFormat::formatsAtCursor(cursor) == formats;
}
Q_INVOKABLE bool canIndentListMoreAtCursor() const
{
if (!m_textItem) {
return false;
}
return m_textItem->canIndentListMoreAtCursor();
}
Q_INVOKABLE bool canIndentListLessAtCursor() const
{
if (!m_textItem) {
return false;
}
return m_textItem->canIndentListLessAtCursor();
}
Q_INVOKABLE void indentListMoreAtCursor()
{
if (!m_textItem) {
return;
}
m_textItem->indentListMoreAtCursor();
}
Q_INVOKABLE void indentListLessAtCursor()
{
if (!m_textItem) {
return;
}
m_textItem->indentListLessAtCursor();
}
Q_INVOKABLE void forceActiveFocus() const
{
if (!m_textItem) {
return;
}
m_textItem->forceActiveFocus();
}
Q_INVOKABLE QString markdownText() const
{
if (!m_textItem) {
return {};
}
return m_textItem->markdownText();
}
Q_SIGNALS:
void textItemChanged();
private:
QPointer<ChatTextItemHelper> m_textItem;
};

View File

@@ -40,6 +40,7 @@ private Q_SLOTS:
void nullSingleLineDisplayName();
void time();
void nullTime();
void timeString();
void highlighted();
void nullHighlighted();
void hidden();
@@ -99,12 +100,12 @@ void EventHandlerTest::time()
{
const auto event = room->messageEvents().at(0).get();
QCOMPARE(EventHandler::dateTime(room, event), QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)));
QCOMPARE(EventHandler::time(room, event), QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)));
const auto txID = room->postJson("m.room.message"_L1, event->fullJson());
QCOMPARE(room->pendingEvents().size(), 1);
const auto pendingIt = room->findPendingEvent(txID);
QCOMPARE(EventHandler::dateTime(room, pendingIt->event(), true), pendingIt->lastUpdated());
QCOMPARE(EventHandler::time(room, pendingIt->event(), true), pendingIt->lastUpdated());
room->discardMessage(txID);
QCOMPARE(room->pendingEvents().size(), 0);
@@ -113,10 +114,39 @@ void EventHandlerTest::time()
void EventHandlerTest::nullTime()
{
QTest::ignoreMessage(QtWarningMsg, "time called with room set to nullptr.");
QCOMPARE(EventHandler::dateTime(nullptr, nullptr), QDateTime());
QCOMPARE(EventHandler::time(nullptr, nullptr), QDateTime());
QTest::ignoreMessage(QtWarningMsg, "time called with event set to nullptr.");
QCOMPARE(EventHandler::dateTime(room, nullptr), QDateTime());
QCOMPARE(EventHandler::time(room, nullptr), QDateTime());
}
void EventHandlerTest::timeString()
{
const auto event = room->messageEvents().at(0).get();
KFormat format;
QCOMPARE(EventHandler::timeString(room, event, false),
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)).toLocalTime().time(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(room, event, true),
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)).toLocalTime().date(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(room, event, u"hh:mm"_s), QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)).toString(u"hh:mm"_s));
const auto txID = room->postJson("m.room.message"_L1, event->fullJson());
QCOMPARE(room->pendingEvents().size(), 1);
const auto pendingIt = room->findPendingEvent(txID);
QCOMPARE(EventHandler::timeString(room, pendingIt->event(), false, QLocale::ShortFormat, true),
QLocale().toString(pendingIt->lastUpdated().toLocalTime().time(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(room, pendingIt->event(), true, QLocale::ShortFormat, true),
format.formatRelativeDate(pendingIt->lastUpdated().toLocalTime().date(), QLocale::ShortFormat));
QCOMPARE(EventHandler::timeString(room, pendingIt->event(), false, QLocale::LongFormat, true),
QLocale().toString(pendingIt->lastUpdated().toLocalTime().time(), QLocale::LongFormat));
QCOMPARE(EventHandler::timeString(room, pendingIt->event(), true, QLocale::LongFormat, true),
format.formatRelativeDate(pendingIt->lastUpdated().toLocalTime().date(), QLocale::LongFormat));
room->discardMessage(txID);
QCOMPARE(room->pendingEvents().size(), 0);
}
void EventHandlerTest::highlighted()

View File

@@ -19,7 +19,13 @@ class LinkPreviewerTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
TestUtils::TestRoom *room = nullptr;
private Q_SLOTS:
void initTestCase();
void linkPreviewsMatch_data();
void linkPreviewsMatch();
@@ -30,6 +36,12 @@ private Q_SLOTS:
void linkPreviewsReject();
};
void LinkPreviewerTest::initTestCase()
{
connection = Connection::makeMockConnection(u"@bob:example.org"_s);
room = new TestUtils::TestRoom(connection, u"!test:example.org"_s);
}
void LinkPreviewerTest::linkPreviewsMatch_data()
{
QTest::addColumn<QString>("inputString");

View File

@@ -10,7 +10,7 @@
#include <Quotient/roommember.h>
#include <Quotient/syncdata.h>
#include "models/eventmessagecontentmodel.h"
#include "models/messagecontentmodel.h"
#include "neochatconnection.h"
#include "testutils.h"
@@ -39,17 +39,17 @@ void MessageContentModelTest::initTestCase()
void MessageContentModelTest::missingEvent()
{
auto room = new TestUtils::TestRoom(connection, u"#firstRoom:kde.org"_s);
auto model1 = EventMessageContentModel(room, u"$153456789:example.org"_s);
auto model1 = MessageContentModel(room, u"$153456789:example.org"_s);
QCOMPARE(model1.rowCount(), 1);
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 = MessageContentModel(room, u"$153456789:example.org"_s, true);
QCOMPARE(model2.rowCount(), 1);
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);
QCOMPARE(model1.rowCount(), 2);

View File

@@ -1,621 +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 <QAbstractItemModelTester>
#include <QObject>
#include <QSignalSpy>
#include <QTest>
#include <QVariantList>
#include <Quotient/connection.h>
#include "accountmanager.h"
#include "contentprovider.h"
#include "enums/powerlevel.h"
#include "enums/roomsortparameter.h"
#include "models/accountemoticonmodel.h"
#include "models/actionsmodel.h"
#include "models/commonroomsmodel.h"
#include "models/completionmodel.h"
#include "models/completionproxymodel.h"
#include "models/customemojimodel.h"
#include "models/devicesmodel.h"
#include "models/devicesproxymodel.h"
#include "models/emojimodel.h"
#include "models/emoticonfiltermodel.h"
#include "models/eventmessagecontentmodel.h"
#include "models/imagepacksmodel.h"
#include "models/linemodel.h"
#include "models/livelocationsmodel.h"
#include "models/locationsmodel.h"
#include "models/messagecontentfiltermodel.h"
#include "models/messagecontentmodel.h"
#include "models/notificationsmodel.h"
#include "models/permissionsmodel.h"
#include "models/pinnedmessagemodel.h"
#include "models/pollanswermodel.h"
#include "models/publicroomlistmodel.h"
#include "models/pushrulemodel.h"
#include "models/readmarkermodel.h"
#include "models/roomsortparametermodel.h"
#include "models/searchmodel.h"
#include "models/serverlistmodel.h"
#include "models/spacechildrenmodel.h"
#include "models/spacechildsortfiltermodel.h"
#include "models/statefiltermodel.h"
#include "models/statekeysmodel.h"
#include "models/statemodel.h"
#include "models/stickermodel.h"
#include "models/threadmodel.h"
#include "models/threepidmodel.h"
#include "models/userdirectorylistmodel.h"
#include "models/userfiltermodel.h"
#include "models/webshortcutmodel.h"
#include "neochatroom.h"
#include "pollhandler.h"
#include "roommanager.h"
#include "server.h"
using namespace Quotient;
// TODO: Add data to all models as relevant.
// Performs basic tests on all models in NeoChat
// When adding a new test, create the model first, then the tester, then initialize the model (e.g., setConnection and setRoom).
// That way, the models are also tested for whether they can handle having no connection etc.
class ModelTest : public QObject
{
Q_OBJECT
private:
NeoChatConnection *connection = nullptr;
NeoChatRoom *room = nullptr;
QString eventId;
Server server;
private Q_SLOTS:
void initTestCase();
void testRoomTreeModel();
void testMessageContentModel();
void testEventMessageContentModel();
void testThreadModel();
void testThreadFetchModel();
void testThreadChatBarModel();
void testReactionModel();
void testPollAnswerModel();
void testLineModel();
void testSpaceChildrenModel();
void testItineraryModel();
void testPublicRoomListModel();
void testMessageFilterModel();
void testThreePIdModel();
void testMediaMessageFilterModel();
void testWebshortcutModel();
void testTimelineMessageModel();
void testReadMarkerModel();
void testSearchModel();
void testStateModel();
void testTimelineModel();
void testStateKeysModel();
void testPinnedMessageModel();
void testUserListModel();
void testStickerModel();
void testPowerLevelModel();
void testImagePacksModel();
void testCompletionModel();
void testRoomListModel();
void testCommonRoomsModel();
void testNotificationsModel();
void testLocationsModel();
void testServerListModel();
void testEmojiModel();
void testCustomEmojiModel();
void testPushRuleModel();
void testActionsModel();
void testDevicesModel();
void testUserDirectoryListModel();
void testAccountEmoticonModel();
void testPermissionsModel();
void testLiveLocationsModel();
void testRoomSortParameterModel();
void testSortFilterRoomTreeModel();
void testSortFilterSpaceListModel();
void testSortFilterRoomListModel();
void testSpaceChildSortFilterModel();
void testStateFilterModel();
void testMessageContentFilterModel();
void testUserFilterModel();
void testEmoticonFilterModel();
void testDevicesProxyModel();
void testCompletionProxyModel();
};
void ModelTest::initTestCase()
{
Connection::setRoomType<NeoChatRoom>();
server.start();
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
auto accountManager = new AccountManager(true, this);
QSignalSpy spy(accountManager, &AccountManager::connectionAdded);
connection = dynamic_cast<NeoChatConnection *>(accountManager->accounts()->front());
const auto roomId = server.createRoom(u"@user:localhost:1234"_s);
eventId = server.sendEvent(roomId,
u"m.room.message"_s,
QJsonObject{
{u"body"_s, u"foo"_s},
{u"msgtype"_s, u"m.text"_s},
});
server.sendEvent(roomId,
u"m.room.message"_s,
QJsonObject{
{u"body"_s, u"asdf"_s},
{u"m.relates_to"_s,
QJsonObject{
{u"event_id"_s, u"$GEucSt3TfVl6DVpKEyeOlRsXzjLv2ZCVgSQuQclFg1o"_s},
{u"is_falling_back"_s, true},
{u"m.in_reply_to"_s, QJsonObject{{u"event_id"_s, u"$GEucSt3TfVl6DVpKEyeOlRsXzjLv2ZCVgSQuQclFg1o"_s}}},
{u"rel_type"_s, u"m.thread"_s},
}},
{u"msgtype"_s, u"m.text"_s},
});
QSignalSpy syncSpy(connection, &Connection::syncDone);
// We need to wait for two syncs, as the next one won't have the changes yet
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
room = dynamic_cast<NeoChatRoom *>(connection->room(roomId));
QVERIFY(room);
}
void ModelTest::testRoomTreeModel()
{
auto roomTreeModel = new RoomTreeModel(this);
auto tester = new QAbstractItemModelTester(roomTreeModel, roomTreeModel);
tester->setUseFetchMore(true);
roomTreeModel->setConnection(connection);
}
void ModelTest::testMessageContentModel()
{
auto contentModel = std::make_unique<MessageContentModel>(room, eventId);
auto tester = new QAbstractItemModelTester(contentModel.get(), contentModel.get());
tester->setUseFetchMore(true);
}
void ModelTest::testEventMessageContentModel()
{
auto model = std::make_unique<EventMessageContentModel>(room, eventId);
auto tester = new QAbstractItemModelTester(model.get(), model.get());
tester->setUseFetchMore(true);
}
void ModelTest::testThreadModel()
{
auto model = std::make_unique<ThreadModel>(eventId, room);
auto tester = new QAbstractItemModelTester(model.get(), model.get());
tester->setUseFetchMore(true);
}
void ModelTest::testThreadFetchModel()
{
auto threadModel = std::make_unique<ThreadModel>(eventId, room);
auto model = new ThreadFetchModel(threadModel.get());
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
}
void ModelTest::testThreadChatBarModel()
{
auto threadModel = std::make_unique<ThreadModel>(eventId, room);
auto model = new ThreadChatBarModel(threadModel.get(), room);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
}
void ModelTest::testReactionModel()
{
auto messageContentModel = std::make_unique<MessageContentModel>(room);
auto model = new ReactionModel(messageContentModel.get(), eventId, room);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
}
void ModelTest::testPollAnswerModel()
{
auto handler = std::make_unique<PollHandler>(room, eventId);
auto model = new PollAnswerModel(handler.get());
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
}
void ModelTest::testLineModel()
{
auto model = new LineModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
auto document = new QTextDocument(this);
model->setDocument(document);
document->setPlainText(u"foo\nbar\n\nbaz"_s);
}
void ModelTest::testSpaceChildrenModel()
{
auto model = new SpaceChildrenModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setSpace(room);
}
void ModelTest::testItineraryModel()
{
auto model = new ItineraryModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
}
void ModelTest::testPublicRoomListModel()
{
auto model = new PublicRoomListModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
}
void ModelTest::testMessageFilterModel()
{
auto timelineModel = new TimelineModel(this);
auto model = new MessageFilterModel(this, timelineModel);
auto tester = new QAbstractItemModelTester(model, model);
timelineModel->setRoom(room);
tester->setUseFetchMore(true);
}
void ModelTest::testThreePIdModel()
{
auto model = new ThreePIdModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
}
void ModelTest::testMediaMessageFilterModel()
{
auto timelineModel = new TimelineModel(this);
auto messageFilterModel = new MessageFilterModel(this, timelineModel);
auto model = new MediaMessageFilterModel(this, messageFilterModel);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
timelineModel->setRoom(room);
}
void ModelTest::testWebshortcutModel()
{
auto model = new WebShortcutModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setSelectedText(u"Foo"_s);
}
void ModelTest::testTimelineMessageModel()
{
auto model = new TimelineMessageModel();
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setRoom(room);
}
void ModelTest::testReadMarkerModel()
{
auto model = std::make_unique<ReadMarkerModel>(eventId, room);
auto tester = new QAbstractItemModelTester(model.get(), model.get());
tester->setUseFetchMore(true);
}
void ModelTest::testSearchModel()
{
auto model = new SearchModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setSearchText(u"foo"_s);
model->setRoom(room);
}
void ModelTest::testStateModel()
{
auto model = new StateModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setRoom(room);
}
void ModelTest::testTimelineModel()
{
auto model = new TimelineModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setRoom(room);
}
void ModelTest::testStateKeysModel()
{
auto model = new StateKeysModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setEventType(u"m.room.member"_s);
model->setRoom(room);
}
void ModelTest::testPinnedMessageModel()
{
auto model = new PinnedMessageModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setRoom(room);
}
void ModelTest::testUserListModel()
{
auto model = new UserListModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setRoom(room);
}
void ModelTest::testStickerModel()
{
auto model = new StickerModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setPackIndex(0);
model->setRoom(room);
auto imagePacksModel = new ImagePacksModel(this);
model->setModel(imagePacksModel);
imagePacksModel->setRoom(room);
imagePacksModel->setShowEmoticons(true);
imagePacksModel->setShowStickers(true);
}
void ModelTest::testPowerLevelModel()
{
auto model = new PowerLevelModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
}
void ModelTest::testImagePacksModel()
{
auto model = new ImagePacksModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setRoom(room);
model->setShowEmoticons(true);
model->setShowStickers(true);
}
void ModelTest::testCompletionModel()
{
auto model = new CompletionModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setAutoCompletionType(CompletionModel::Room);
auto roomListModel = new RoomListModel(this);
roomListModel->setConnection(connection);
model->setRoomListModel(roomListModel);
}
void ModelTest::testRoomListModel()
{
auto model = new RoomListModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
}
void ModelTest::testCommonRoomsModel()
{
auto model = new CommonRoomsModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
model->setUserId(u"@user:example.com"_s);
}
void ModelTest::testNotificationsModel()
{
auto model = new NotificationsModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
}
void ModelTest::testLocationsModel()
{
auto model = new LocationsModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setRoom(room);
}
void ModelTest::testServerListModel()
{
auto model = new ServerListModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
}
void ModelTest::testEmojiModel()
{
auto tester = new QAbstractItemModelTester(&EmojiModel::instance(), &EmojiModel::instance());
tester->setUseFetchMore(true);
}
void ModelTest::testCustomEmojiModel()
{
auto tester = new QAbstractItemModelTester(&CustomEmojiModel::instance(), &CustomEmojiModel::instance());
tester->setUseFetchMore(true);
CustomEmojiModel::instance().setConnection(connection);
}
void ModelTest::testPushRuleModel()
{
auto model = new PushRuleModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
}
void ModelTest::testActionsModel()
{
auto tester = new QAbstractItemModelTester(&ActionsModel::instance(), &ActionsModel::instance());
tester->setUseFetchMore(true);
}
void ModelTest::testDevicesModel()
{
auto model = new DevicesModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
}
void ModelTest::testUserDirectoryListModel()
{
auto model = new UserDirectoryListModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
model->setSearchText(u"foo"_s);
}
void ModelTest::testAccountEmoticonModel()
{
auto model = new AccountEmoticonModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
}
void ModelTest::testPermissionsModel()
{
auto model = new PermissionsModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setRoom(room);
}
void ModelTest::testLiveLocationsModel()
{
auto model = new LiveLocationsModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setRoom(room);
}
void ModelTest::testRoomSortParameterModel()
{
auto model = new RoomSortParameterModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
}
void ModelTest::testSortFilterRoomTreeModel()
{
auto sourceModel = new RoomTreeModel(this);
auto model = new SortFilterRoomTreeModel(sourceModel, sourceModel);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
sourceModel->setConnection(connection);
}
void ModelTest::testSortFilterSpaceListModel()
{
auto sourceModel = new RoomListModel(this);
auto model = new SortFilterSpaceListModel(sourceModel, sourceModel);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
sourceModel->setConnection(connection);
}
void ModelTest::testSortFilterRoomListModel()
{
auto sourceModel = new RoomListModel(this);
auto model = new SortFilterRoomListModel(sourceModel, sourceModel);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
sourceModel->setConnection(connection);
}
void ModelTest::testSpaceChildSortFilterModel()
{
auto model = new SpaceChildSortFilterModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
auto spaceChildrenModel = new SpaceChildrenModel(this);
model->setSourceModel(spaceChildrenModel);
spaceChildrenModel->setSpace(nullptr);
}
void ModelTest::testStateFilterModel()
{
auto model = new StateFilterModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
auto stateModel = new StateModel(this);
model->setSourceModel(stateModel);
stateModel->setRoom(room);
}
void ModelTest::testMessageContentFilterModel()
{
auto model = new MessageContentFilterModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setSourceModel(ContentProvider::self().contentModelForEvent(room, eventId));
}
void ModelTest::testUserFilterModel()
{
auto model = new UserFilterModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
auto userListModel = new UserListModel(this);
model->setSourceModel(userListModel);
userListModel->setRoom(room);
}
void ModelTest::testEmoticonFilterModel()
{
auto model = new EmoticonFilterModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
auto accountEmoticonModel = new AccountEmoticonModel(this);
model->setSourceModel(accountEmoticonModel);
model->setShowEmojis(true);
model->setShowStickers(true);
accountEmoticonModel->setConnection(connection);
}
void ModelTest::testDevicesProxyModel()
{
auto model = new DevicesProxyModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
auto devicesModel = new DevicesModel(this);
model->setSourceModel(devicesModel);
devicesModel->setConnection(dynamic_cast<NeoChatConnection *>(connection));
}
void ModelTest::testCompletionProxyModel()
{
auto model = new CompletionProxyModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setSourceModel(&EmojiModel::instance());
}
QTEST_MAIN(ModelTest)
#include "modeltest.moc"

View File

@@ -9,10 +9,6 @@
#include <Quotient/quotient_common.h>
#include <Quotient/syncdata.h>
#include <KLocalizedString>
#include "accountmanager.h"
#include "server.h"
#include "testutils.h"
using namespace Quotient;
@@ -22,8 +18,7 @@ class NeoChatRoomTest : public QObject {
private:
Connection *connection = nullptr;
NeoChatRoom *room = nullptr;
Server server;
TestUtils::TestRoom *room = nullptr;
private Q_SLOTS:
void initTestCase();
@@ -32,27 +27,8 @@ private Q_SLOTS:
void NeoChatRoomTest::initTestCase()
{
Connection::setRoomType<NeoChatRoom>();
server.start();
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
auto accountManager = new AccountManager(true, this);
QSignalSpy spy(accountManager, &AccountManager::connectionAdded);
connection = dynamic_cast<NeoChatConnection *>(accountManager->accounts()->front());
const auto roomId = server.createRoom(u"@user:localhost:1234"_s);
server.sendEvent(roomId,
u"m.room.message"_s,
QJsonObject{
{u"body"_s, u"foo"_s},
{u"msgtype"_s, u"m.text"_s},
});
QSignalSpy syncSpy(connection, &Connection::syncDone);
// We need to wait for two syncs, as the next one won't have the changes yet
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
room = dynamic_cast<NeoChatRoom *>(connection->room(roomId));
QVERIFY(room);
connection = Connection::makeMockConnection(u"@bob:kde.org"_s);
room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-min-sync.json"_s);
}
void NeoChatRoomTest::eventTest()

View File

@@ -1,9 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020 Arjen Hiemstra <ahiemstra@heimr.nl>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include <quicktest.h>
QUICK_TEST_MAIN(NeoChat)

View File

@@ -9,7 +9,7 @@
#include <Quotient/events/roommessageevent.h>
#include "models/eventmessagecontentmodel.h"
#include "models/messagecontentmodel.h"
#include "testutils.h"
using namespace Quotient;
@@ -21,7 +21,7 @@ class ReactionModelTest : public QObject
private:
Connection *connection = nullptr;
TestUtils::TestRoom *room = nullptr;
EventMessageContentModel *parentModel;
MessageContentModel *parentModel;
private Q_SLOTS:
void initTestCase();
@@ -34,7 +34,7 @@ void ReactionModelTest::initTestCase()
{
connection = Connection::makeMockConnection(u"@bob:kde.org"_s);
room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-reactionmodel-sync.json"_s);
parentModel = new EventMessageContentModel(room, "123456"_L1);
parentModel = new MessageContentModel(room, "123456"_L1);
}
void ReactionModelTest::basicReaction()

View File

@@ -1,141 +0,0 @@
// SPDX-FileCopyrightText: 2024 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 <QVariantList>
#include "accountmanager.h"
#include "models/actionsmodel.h"
#include "roommanager.h"
#include "server.h"
#include "testutils.h"
using namespace Quotient;
class RoomManagerTest : public QObject
{
Q_OBJECT
private:
NeoChatConnection *connection = nullptr;
NeoChatRoom *room = nullptr;
Server server;
private Q_SLOTS:
void initTestCase();
void testMaximizeMedia();
void testResolveMatrixLinks();
};
void RoomManagerTest::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);
QSignalSpy syncSpy(connection, &Connection::syncDone);
// We need to wait for two syncs, as the next one won't have the changes yet
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
room = dynamic_cast<NeoChatRoom *>(connection->room(roomId));
QVERIFY(room);
RoomManager::instance().setConnection(connection);
QSignalSpy roomSpy(&RoomManager::instance(), &RoomManager::currentRoomChanged);
RoomManager::instance().resolveResource(room->id());
QVERIFY(roomSpy.size() > 0);
}
void RoomManagerTest::testMaximizeMedia()
{
QSignalSpy spy(&RoomManager::instance(), &RoomManager::showMaximizedMedia);
QSignalSpy syncSpy(connection, &Connection::syncDone);
QTest::ignoreMessage(QtMsgType::QtWarningMsg, "Tried to open media for empty event id");
RoomManager::instance().maximizeMedia(QString());
QVERIFY(!spy.wait(10));
QTest::ignoreMessage(QtMsgType::QtWarningMsg, "Tried to open media for unknown event id \"Doesn't exist\"");
RoomManager::instance().maximizeMedia(u"Doesn't exist"_s);
QVERIFY(!spy.wait(10));
const auto eventWithoutMedia = server.sendEvent(room->id(),
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());
QTest::ignoreMessage(QtMsgType::QtWarningMsg, u"Tried to open media for unknown event id \"%1\""_s.arg(eventWithoutMedia).toLatin1().data());
RoomManager::instance().maximizeMedia(eventWithoutMedia);
QVERIFY(!spy.wait(10));
// NOTE: This is supposed to test that maximizing pending media works correctly. This probably doesn't work in the UI yet, but at least the backend supports
// it. If the server ever learns how to process events, this becomes pointless and we need to find a way of preventing *these* events from arriving
auto pendingEventWithoutMedia = room->postText(u"Hello"_s);
QTest::ignoreMessage(QtMsgType::QtWarningMsg, u"Tried to open media for unknown event id \"%1\""_s.arg(pendingEventWithoutMedia).toLatin1().data());
RoomManager::instance().maximizeMedia(pendingEventWithoutMedia);
QVERIFY(!spy.wait(10));
const auto eventWithMedia = server.sendEvent(room->id(),
u"m.room.message"_s,
QJsonObject({
{u"body"_s, u"Foo"_s},
{u"filename"_s, u"foo.jpg"_s},
{u"info"_s,
QJsonObject{
{u"h"_s, 1000},
{u"w"_s, 2000},
{u"size"_s, 10000},
{u"mimetype"_s, u"image/png"_s},
}},
{u"msgtype"_s, u"m.image"_s},
{u"url"_s, u"mxc://foo.bar/asdf"_s},
}));
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
RoomManager::instance().maximizeMedia(eventWithMedia);
QVERIFY(spy.size() == 1);
QVERIFY(spy[0][0] == 0);
auto pendingEventWithMedia = room->postJson(u"m.room.message"_s,
QJsonObject({
{u"body"_s, u"Foo"_s},
{u"filename"_s, u"foo.jpg"_s},
{u"info"_s,
QJsonObject{
{u"h"_s, 1000},
{u"w"_s, 2000},
{u"size"_s, 10000},
{u"mimetype"_s, u"image/png"_s},
}},
{u"msgtype"_s, u"m.image"_s},
{u"url"_s, u"mxc://foo.bar/asdf"_s},
}));
RoomManager::instance().maximizeMedia(pendingEventWithMedia);
QVERIFY(spy.size() == 2);
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)
#include "roommanagertest.moc"

View File

@@ -4,12 +4,15 @@
#include "server.h"
#include <QFile>
#include <QHttpServer>
#include <QHttpServerResponder>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QSslCertificate>
#include <QSslKey>
#include <QSslServer>
#include <QUuid>
#include <Quotient/networkaccessmanager.h>
@@ -106,297 +109,127 @@ void Server::start()
m_server.route(u"/_matrix/client/v3/rooms/<arg>/invite"_s,
QHttpServerRequest::Method::Post,
[this](const QString &roomId, QHttpServerResponder &responder, const QHttpServerRequest &request) {
Changes changes;
changes.invitations += Changes::InviteUser{
.userId = QJsonDocument::fromJson(request.body()).object()[u"user_id"_s].toString(),
.roomId = roomId,
};
m_state += changes;
m_invitedUsers[roomId] += QJsonDocument::fromJson(request.body()).object()[u"user_id"_s].toString();
responder.write(QJsonDocument(QJsonObject{}), QHttpServerResponder::StatusCode::Ok);
});
m_server.route(u"/_matrix/client/r0/sync"_s, QHttpServerRequest::Method::Get, this, &Server::sync);
m_server.route(u"/_matrix/client/r0/sync"_s, QHttpServerRequest::Method::Get, [this](QHttpServerResponder &responder) {
QMap<QString, QJsonArray> stateEvents;
for (const auto &[roomId, matrixId] : m_roomsToCreate) {
stateEvents[roomId] += QJsonObject{
{u"content"_s, QJsonObject{{u"room_version"_s, u"11"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, roomId},
{u"sender"_s, matrixId},
{u"state_key"_s, QString()},
{u"type"_s, u"m.room.create"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
stateEvents[roomId] += QJsonObject{
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"join"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, roomId},
{u"sender"_s, matrixId},
{u"state_key"_s, matrixId},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
}
m_roomsToCreate.clear();
for (const auto &roomId : m_invitedUsers.keys()) {
const auto &values = m_invitedUsers[roomId];
for (const auto &value : values) {
stateEvents[roomId] += QJsonObject{
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"invite"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, roomId},
{u"sender"_s, u"@user:localhost:1234"_s},
{u"state_key"_s, value},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
}
}
m_invitedUsers.clear();
for (const auto &roomId : m_bannedUsers.keys()) {
const auto &values = m_bannedUsers[roomId];
for (const auto &value : values) {
stateEvents[roomId] += QJsonObject{
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"ban"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, roomId},
{u"sender"_s, u"@user:localhost:1234"_s},
{u"state_key"_s, value},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
}
}
m_bannedUsers.clear();
for (const auto &roomId : m_joinedUsers.keys()) {
const auto &values = m_joinedUsers[roomId];
for (const auto &value : values) {
stateEvents[roomId] += QJsonObject{
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"join"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, roomId},
{u"sender"_s, u"@user:localhost:1234"_s},
{u"state_key"_s, value},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
}
}
m_joinedUsers.clear();
QJsonObject rooms;
for (const auto &roomId : stateEvents.keys()) {
rooms[roomId] = QJsonObject{{u"state"_s, QJsonObject{{u"events"_s, stateEvents[roomId]}}}};
}
responder.write(QJsonDocument(QJsonObject{{u"rooms"_s, QJsonObject{{u"join"_s, rooms}}}}), QHttpServerResponder::StatusCode::Ok);
});
QSslConfiguration config;
QFile key(QStringLiteral(DATA_DIR) + u"/localhost.key"_s);
void(key.open(QFile::ReadOnly));
key.open(QFile::ReadOnly);
config.setPrivateKey(QSslKey(&key, QSsl::Rsa));
config.setLocalCertificate(QSslCertificate::fromPath(QStringLiteral(DATA_DIR) + u"/localhost.crt"_s).constFirst());
config.setLocalCertificate(QSslCertificate::fromPath(QStringLiteral(DATA_DIR) + u"/localhost.crt"_s).front());
m_sslServer.setSslConfiguration(config);
if (!m_sslServer.listen(QHostAddress::LocalHost, 1234) || !m_server.bind(&m_sslServer)) {
qFatal() << "Server failed to listen on a port.";
return;
} else {
qInfo() << "Server listening";
qWarning() << "Server listening";
}
}
QString Server::createRoom(const QString &matrixId)
{
const auto roomId = generateRoomId();
Changes changes;
changes.newRooms += Changes::NewRoom{
.initialMembers = {matrixId},
.roomId = {roomId},
.tags = {},
};
m_state += changes;
auto roomId = generateRoomId();
m_roomsToCreate += {roomId, matrixId};
return roomId;
}
void Server::inviteUser(const QString &roomId, const QString &matrixId)
{
Changes changes;
changes.invitations += Changes::InviteUser{
.userId = matrixId,
.roomId = roomId,
};
m_state += changes;
m_invitedUsers[roomId] += matrixId;
}
void Server::banUser(const QString &roomId, const QString &matrixId)
{
Changes changes;
changes.bans += Changes::BanUser{
.userId = matrixId,
.roomId = roomId,
};
m_state += changes;
m_bannedUsers[roomId] += matrixId;
}
void Server::joinUser(const QString &roomId, const QString &matrixId)
{
Changes changes;
changes.joins += Changes::JoinUser{
.userId = matrixId,
.roomId = roomId,
};
m_state += changes;
}
QString Server::createServerNoticesRoom(const QString &matrixId)
{
const auto roomId = generateRoomId();
Changes changes;
changes.newRooms += Changes::NewRoom{
.initialMembers = {matrixId},
.roomId = {roomId},
.tags = {u"m.server_notice"_s},
};
m_state += changes;
return roomId;
}
QString Server::sendEvent(const QString &roomId, const QString &eventType, const QJsonObject &content)
{
Changes changes;
const auto eventId = generateEventId();
changes.events += Changes::Event{
.fullJson = QJsonObject{{u"type"_s, eventType},
{u"content"_s, content},
{u"sender"_s, u"@foo:server.com"_s},
{u"event_id"_s, eventId},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, roomId}},
};
m_state += changes;
return eventId;
}
QString Server::sendStateEvent(const QString &roomId, const QString &eventType, const QString &stateKey, const QJsonObject &content)
{
Changes changes;
const auto eventId = generateEventId();
const auto json = QJsonObject{{u"type"_s, eventType},
{u"content"_s, content},
{u"sender"_s, u"@foo:server.com"_s},
{u"event_id"_s, eventId},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, roomId},
{u"state_key"_s, stateKey}};
changes.events += Changes::Event{
.fullJson = json,
};
changes.stateEvents += Changes::Event{.fullJson = json};
m_state += changes;
return eventId;
}
void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &responder)
{
QJsonObject joinRooms;
auto token = request.query().queryItemValue(u"since"_s).toInt();
const auto changes = m_state.mid(token);
for (const auto &change : changes) {
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 : changes) {
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 : changes) {
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 : changes) {
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 : changes) {
for (const auto &state : change.stateEvents) {
const auto &roomId = state.fullJson[u"room_id"_s].toString();
// TODO: The join could be for a room we haven't joined yet. Shouldn't be necessary for now, though.
auto stateEvents = joinRooms[roomId][u"state"_s][u"events"_s].toArray();
stateEvents.append(state.fullJson);
auto room = joinRooms[roomId].toObject();
room[u"state"_s] = QJsonObject{{u"events"_s, stateEvents}};
joinRooms[roomId] = room;
}
}
for (const auto &change : changes) {
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;
}
responder.write(QJsonDocument(syncData), QHttpServerResponder::StatusCode::Ok);
m_joinedUsers[roomId] += matrixId;
}

View File

@@ -2,52 +2,10 @@
// SPDX-License-Identifier: LGPL-2.0-or-later
#include <QHttpServer>
#include <QJsonObject>
#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;
QList<Event> stateEvents;
};
struct RoomData {
QStringList members;
QString id;
QStringList tags;
};
class Server : public QObject
class Server
{
Q_OBJECT
public:
Server();
@@ -63,18 +21,13 @@ public:
void banUser(const QString &roomId, const QString &matrixId);
void joinUser(const QString &roomId, const QString &matrixId);
/**
* Create a server notices room.
*/
QString createServerNoticesRoom(const QString &matrixId);
QString sendEvent(const QString &roomId, const QString &eventType, const QJsonObject &content);
QString sendStateEvent(const QString &roomId, const QString &eventType, const QString &stateKey, const QJsonObject &content);
private:
QHttpServer m_server;
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<std::pair<QString, QString>> m_roomsToCreate;
};

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-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/syncdata.h>
@@ -33,7 +32,7 @@ public:
if (!syncFileName.isEmpty()) {
QFile testSyncFile;
testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + syncFileName);
Q_UNUSED(testSyncFile.open(QIODevice::ReadOnly));
testSyncFile.open(QIODevice::ReadOnly);
const auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll());
Quotient::SyncRoomData roomData(id(), Quotient::JoinState::Join, testSyncJson.object());
update(std::move(roomData));
@@ -47,7 +46,7 @@ inline Quotient::event_ptr_tt<EventT> loadEventFromFile(const QString &eventFile
if (!eventFileName.isEmpty()) {
QFile testEventFile;
testEventFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + eventFileName);
Q_UNUSED(testEventFile.open(QIODevice::ReadOnly));
testEventFile.open(QIODevice::ReadOnly);
auto testSyncJson = QJsonDocument::fromJson(testEventFile.readAll()).object();
return Quotient::loadEvent<EventT>(testSyncJson);
}

View File

@@ -34,10 +34,6 @@ private Q_SLOTS:
void stripDisallowedTags();
void stripDisallowedAttributes();
void emptyCodeTags();
void addStyle_data();
void addStyle();
void dontAddStyle_data();
void dontAddStyle();
void sendSimpleStringCase();
void sendSingleParaMarkup();
@@ -75,9 +71,6 @@ private Q_SLOTS:
void componentOutput_data();
void componentOutput();
void updateSpoiler_data();
void updateSpoiler();
};
void TextHandlerTest::initTestCase()
@@ -96,26 +89,21 @@ void TextHandlerTest::initTestCase()
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 testOutputString1S = 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());
const QString testOutputString1 = u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s;
// 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 testOutputString2S = 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;
const QString testOutputString2 = u"<a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a>"_s;
TextHandler testTextHandler;
testTextHandler.setData(testInputString1);
QCOMPARE(testTextHandler.handleSendText(), testOutputString1S);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString1R);
QCOMPARE(testTextHandler.handleSendText(), testOutputString1);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString1);
testTextHandler.setData(testInputString2);
QCOMPARE(testTextHandler.handleSendText(), testOutputString2S);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString2R);
QCOMPARE(testTextHandler.handleSendText(), testOutputString2);
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString2);
}
void TextHandlerTest::stripDisallowedTags()
@@ -158,56 +146,6 @@ void TextHandlerTest::emptyCodeTags()
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()
{
const QString testInputString = u"This data should just be left alone."_s;
@@ -400,8 +338,7 @@ void TextHandlerTest::receiveRichInPlainOut()
void TextHandlerTest::receivePlainTextIn()
{
const QString testInputString = u"<plain text in tag bracket>\nTest link https://kde.org."_s;
const QString testOutputStringRich =
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;
const QString testOutputStringRich = u"&lt;plain text in tag bracket&gt;<br>Test link <a href=\"https://kde.org\">https://kde.org</a>."_s;
QString testOutputStringPlain = u"<plain text in tag bracket>\nTest link https://kde.org."_s;
// Make sure quotes are maintained in a plain string.
@@ -471,7 +408,7 @@ void TextHandlerTest::receivePlainStripMarkup()
void TextHandlerTest::receiveRichUserPill()
{
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;
testTextHandler.setData(testInputString);
@@ -523,23 +460,21 @@ void TextHandlerTest::receiveRichPlainUrl_data()
// so we can confirm consistent behaviour for complex urls.
QTest::addRow("link 1")
<< u"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\">Link already rich</a>"_s
<< u"<a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&amp;via=matrix.org&amp;via=fedora.im\" 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
// contains what looks like and email. It was broken into 3 but needs to
// be just single link.
QTest::addRow("link 2")
<< u"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/"_s
<< u"<a href=\"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/\" 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")
<< uR"(email@example.com <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("email") << uR"(email@example.com <a href="mailto:email@example.com">Link already rich</a>)"_s
<< uR"(<a href="mailto:email@example.com">email@example.com</a> <a href="mailto:email@example.com">Link already rich</a>)"_s;
QTest::addRow("mxid")
<< u"@user:kde.org <a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a>"_s
<< u"<b><a href=\"https://matrix.to/#/@user:kde.org\" 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;
QTest::addRow("mxid with prefix") << u"a @user:kde.org b"_s
<< u"a <b><a href=\"https://matrix.to/#/@user:kde.org\" style=\"text-decoration: none;\">@user:kde.org</a></b> 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 << u"a <b><a href=\"https://matrix.to/#/@user:kde.org\">@user:kde.org</a></b> b"_s;
}
/**
@@ -661,35 +596,5 @@ void TextHandlerTest::componentOutput()
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)
#include "texthandlertest.moc"

View File

@@ -161,7 +161,7 @@ void TimelineMessageModelTest::pendingEvent()
// different every time.
QFile testSyncFile;
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 root = testSyncJson.object();
auto timeline = root["timeline"_L1].toObject();
@@ -208,7 +208,7 @@ void TimelineMessageModelTest::idToRow()
auto room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-min-sync.json"_s);
model->setRoom(room);
QCOMPARE(model->indexForEventId(u"$153456789:example.org"_s).row(), 0);
QCOMPARE(model->indexforEventId(u"$153456789:example.org"_s).row(), 0);
}
void TimelineMessageModelTest::cleanup()

View File

@@ -73,16 +73,6 @@ void WindowControllerTest::toggle()
instance.toggleWindow();
QCOMPARE(window.windowState(), Qt::WindowNoState);
QCOMPARE(window.isVisible(), false);
// make sure we restore maximized state when toggling
instance.toggleWindow();
window.setVisibility(QWindow::Maximized);
QCOMPARE(window.windowState(), Qt::WindowMaximized);
instance.toggleWindow();
QCOMPARE(window.isVisible(), false);
instance.toggleWindow();
QCOMPARE(window.windowState(), Qt::WindowMaximized);
QCOMPARE(window.isVisible(), true);
}
QTEST_MAIN(WindowControllerTest)

14
cmake/Flatpak.cmake Normal file
View File

@@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
# SPDX-License-Identifier: BSD-2-Clause
include(GNUInstallDirs)
# Include FontConfig config which uses the Emoji One font from the
# KDE Flatpak SDK.
install(
FILES
${CMAKE_CURRENT_SOURCE_DIR}/cmake/Flatpak/99-noto-mono-color-emoji.conf
DESTINATION
${CMAKE_INSTALL_SYSCONFDIR}/fonts/local.conf
)

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<alias>
<family>serif</family>
<prefer>
<family>Noto Color Emoji</family>
</prefer>
</alias>
<alias>
<family>sans-serif</family>
<prefer>
<family>Noto Color Emoji</family>
</prefer>
</alias>
<alias>
<family>monospace</family>
<prefer>
<family>Noto Color Emoji</family>
</prefer>
</alias>
</fontconfig>

View File

@@ -3,12 +3,12 @@
add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}" )
qt_add_executable(timeline_memtest
qt_add_executable(timeline-memtest
main.cpp
)
target_link_libraries(timeline_memtest PRIVATE neochatplugin Timelineplugin)
target_link_libraries(timeline_memtest PUBLIC
target_link_libraries(timeline-memtest PRIVATE neochatplugin Timelineplugin)
target_link_libraries(timeline-memtest PUBLIC
Qt::Core
Qt::Quick
Qt::Qml
@@ -16,13 +16,14 @@ target_link_libraries(timeline_memtest PUBLIC
Qt::QuickControls2
Qt::Widgets
KF6::I18nQml
KF6::Kirigami
QuotientQt6
LibNeoChat
Timeline
)
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
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
QML_FILES
Main.qml
SOURCES

View File

@@ -28,7 +28,7 @@ int main(int argc, char **argv)
engine.rootContext()->setContextProperty(u"memTestTimelineModel"_s, memTestTimelineModel);
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();
}

View File

@@ -37,11 +37,7 @@ public:
if (!syncFileName.isEmpty()) {
QFile testSyncFile;
testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + syncFileName);
auto ok = testSyncFile.open(QIODevice::ReadOnly);
if (!ok) {
qWarning() << "Failed to open" << testSyncFile.fileName() << testSyncFile.errorString();
}
testSyncFile.open(QIODevice::ReadOnly);
auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll()).object();
auto timelineJson = testSyncJson["timeline"_L1].toObject();
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[tr]=NeoChat
Name[uk]=NeoChat
Name[x-test]=xxNeoChatxx
Name[zh_CN]=NeoChat
Name[zh_TW]=NeoChat
GenericName=Matrix Client
@@ -87,6 +88,7 @@ GenericName[sv]=Matrix-klient
GenericName[ta]=Matrix வாங்கி
GenericName[tr]=Matrix İstemcisi
GenericName[uk]=Клієнт Matrix
GenericName[x-test]=xxMatrix Clientxx
GenericName[zh_CN]=Matrix 客户端
GenericName[zh_TW]=Matrix 用戶端
Comment=Chat on Matrix
@@ -108,12 +110,10 @@ Comment[ia]=Conversation en ditecto sur Matrix
Comment[it]= su Matrix
Comment[ka]=ჩატი Matrix-ზე
Comment[ko]=Matrix에서 대화하기
Comment[lt]=Pokalbiai per Matrix
Comment[lv]=Tērzējiet „Matrix“ tīklā
Comment[nl]=Chat op Matrix
Comment[pl]=Rozmawiaj na Matriksie
Comment[pt_BR]=Bate papo na Matrix
Comment[ro]=Discutați pe Matrix
Comment[ru]=Общение в Matrix
Comment[sa]=Matrix इत्यत्र गपशपं कुर्वन्तु
Comment[sl]=Klepet na Matrixu
@@ -121,6 +121,7 @@ Comment[sv]=Chatta på Matrix
Comment[ta]=மேட்ரிக்ஸில் உரையாட உதவும்
Comment[tr]=Matrix üzerinde sohbet edin
Comment[uk]=Спілкування у Matrix
Comment[x-test]=xxChat on Matrixxx
Comment[zh_CN]=在 Matrix 上聊天
Comment[zh_TW]=在 Matrix 上聊天
MimeType=x-scheme-handler/matrix;

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

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 % Russian "INCLUDE">
]>
<!--
SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
SPDX-License-Identifier: CC-BY-SA-4.0
-->
<refentry lang="&language;">
<refentryinfo>
<title
>Руководство пользователя NeoChat</title>
<author
><firstname
>Carl</firstname
><surname
>Schwan</surname
> <contrib
>man-страница NeoChat.</contrib
> <email
>carl@carlschwan.eu</email
></author>
<date
>2022-11-01</date>
<releaseinfo
>22.09</releaseinfo>
<productname
>NeoChat</productname>
</refentryinfo>
<refmeta>
<refentrytitle>
<command
>neochat</command>
</refentrytitle>
<manvolnum
>1</manvolnum>
</refmeta>
<refnamediv>
<refname
>neochat</refname>
<refpurpose
>Клиент для взаимодействия с протоколом обмена сообщениями 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
>Описание</title>
<para
><command
>neochat</command
> — приложение для настольных и мобильных устройств, позволяющее общаться в чатах с помощью протокола Matrix. </para>
</refsect1>
<refsect1 id="options"
><title
>Параметры</title>
<variablelist>
<varlistentry>
<term
><option
>URI</option
></term>
<listitem>
<para
>URI-адрес пользователя или комнаты в Matrix, например: matrix:u/user:example.org и matrix:r/root:example.org. NeoChat попытается открыть указанную комнату или беседу. </para>
</listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1 id="bug">
<title
>Отчёты об ошибках</title>
<para
>Сообщать об ошибках и отправлять предложения по улучшению можно по адресу <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
>Смотрите также</title>
<simplelist>
<member
>Список наиболее часто задаваемых вопросов о 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
>Авторские права</title>
<para
>Авторские права &copy; Tobias Fella, 20202022 </para>
<para
>Авторские права &copy; Carl Schwan, 20202022 </para>
<para
>Лицензия: стандартная общественная лицензия GNU версии 3 или любой более поздней версии &lt;<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

View File

@@ -22,7 +22,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
>carl@carlschwan.eu</email
></author>
<date
>20221101</date>
>2022-11-01</date>
<releaseinfo
>22.09</releaseinfo>
<productname
@@ -111,9 +111,9 @@ SPDX-License-Identifier: CC-BY-SA-4.0
><title
>Telif Hakkı</title>
<para
>Telif hakkı &copy; 20202022 Tobias Fella </para>
>Telif hakkı &copy; 2020-2022 Tobias Fella </para>
<para
>Telif hakkı &copy; 20202022 Carl Schwan </para>
>Telif hakkı &copy; 2020-2022 Carl Schwan </para>
<para
>Lisans: GNU Genel Kamu Lisansa, 3. sürüm veya sonrası &lt;<ulink url="https://www.gnu.org/licenses/gpl-3.0.html"
>https://www.gnu.org/licenses/gpl-3.0.html</ulink

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

@@ -3,7 +3,7 @@
# SPDX-FileCopyrightText: 2020-2021 Tobias Fella <tobias.fella@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
qt_add_library(neochat STATIC
add_library(neochat STATIC
controller.cpp
controller.h
roommanager.cpp
@@ -35,10 +35,6 @@ qt_add_library(neochat STATIC
models/commonroomsmodel.h
texttospeechhelper.h
texttospeechhelper.cpp
models/limitermodel.cpp
models/limitermodel.h
supportcontroller.cpp
supportcontroller.h
)
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
@@ -52,8 +48,6 @@ if(ANDROID OR WIN32)
set_source_files_properties(qml/GlobalMenuStub.qml PROPERTIES
QT_QML_SOURCE_TYPENAME GlobalMenu
)
else()
set(EXTRA_IMPORTS org.kde.purpose)
endif()
ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
@@ -70,6 +64,7 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/AttachmentPane.qml
qml/QuickFormatBar.qml
qml/UserDetailDialog.qml
qml/OpenFileDialog.qml
qml/KeyVerificationDialog.qml
qml/ConfirmLogoutDialog.qml
qml/VerificationMessage.qml
@@ -78,6 +73,7 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/EmojiSas.qml
qml/VerificationCanceled.qml
qml/MessageSourceSheet.qml
qml/LocationChooser.qml
qml/InvitationView.qml
qml/AvatarTabButton.qml
qml/OsmLocationPlugin.qml
@@ -103,14 +99,11 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/HoverLinkIndicator.qml
qml/AvatarNotification.qml
qml/ReasonDialog.qml
qml/NewPollDialog.qml
qml/UserMenu.qml
qml/MeetingDialog.qml
qml/SeenByDialog.qml
qml/SupportDialog.qml
DEPENDENCIES
QtCore
QtQuick
io.github.quotient_im.libquotient
IMPORTS
org.kde.neochat.libneochat
org.kde.neochat.rooms
@@ -122,15 +115,13 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
org.kde.neochat.devtools
org.kde.neochat.login
org.kde.neochat.chatbar
org.kde.config
org.kde.syntaxhighlighting
${EXTRA_IMPORTS}
)
if(NOT ANDROID AND NOT WIN32)
qt_target_qml_sources(neochat QML_FILES
qml/ShareAction.qml
qml/GlobalMenu.qml
qml/EditMenu.qml
)
else()
qt_target_qml_sources(neochat QML_FILES
@@ -143,17 +134,10 @@ if(WIN32)
set_target_properties(neochat PROPERTIES OUTPUT_NAME "neochatlib")
endif()
qt_add_executable(neochat-app
add_executable(neochat-app
main.cpp
)
if(ANDROID)
set_target_properties(neochat-app PROPERTIES
OUTPUT_NAME "neochat-app"
PREFIX "lib"
)
endif()
if(TARGET Qt::WebView)
target_link_libraries(neochat-app PUBLIC Qt::WebView)
target_compile_definitions(neochat-app PUBLIC -DHAVE_WEBVIEW)
@@ -163,7 +147,6 @@ target_include_directories(neochat-app PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(neochat-app PRIVATE
neochat
KF6::IconThemes
)
ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)
@@ -171,7 +154,12 @@ ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)
target_sources(neochat-app PRIVATE ${NEOCHAT_ICON})
if(NOT ANDROID)
target_sources(neochat PRIVATE trayicon.cpp trayicon.h)
if (NOT WIN32 AND NOT APPLE)
target_sources(neochat PRIVATE trayicon_sni.cpp trayicon_sni.h)
target_link_libraries(neochat PRIVATE KF6::StatusNotifierItem)
else()
target_sources(neochat PRIVATE trayicon.cpp trayicon.h)
endif()
target_link_libraries(neochat PUBLIC KF6::WindowSystem)
target_compile_definitions(neochat PUBLIC -DHAVE_WINDOWSYSTEM)
endif()
@@ -189,7 +177,7 @@ else()
endif()
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models)
target_link_libraries(neochat PRIVATE neochatplugin Loginplugin Roomsplugin RoomInfoplugin MessageContentplugin Timelineplugin Spacesplugin Chatbarplugin Settingsplugin Devtoolsplugin)
target_link_libraries(neochat PRIVATE Loginplugin Roomsplugin RoomInfoplugin MessageContentplugin Timelineplugin Spacesplugin Chatbarplugin Settingsplugin Devtoolsplugin)
target_link_libraries(neochat PUBLIC
LibNeoChat
Timeline
@@ -209,9 +197,8 @@ target_link_libraries(neochat PUBLIC
KF6::ConfigGui
KF6::CoreAddons
KF6::SonnetCore
KF6::IconThemes
KF6::ItemModels
KF6::I18nQml
KirigamiApp
QuotientQt6
Login
Rooms
@@ -219,6 +206,10 @@ target_link_libraries(neochat PUBLIC
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)
if(NEOCHAT_FLATPAK)
@@ -329,7 +320,6 @@ if(ANDROID)
"kt-restore-defaults-symbolic"
"user-symbolic"
"mark-location-symbolic"
"amarok_playcount"
${KIRIGAMI_ADDONS_ICONS}
)
@@ -348,7 +338,14 @@ if(TARGET KF6::DBusAddons AND NOT WIN32)
target_compile_definitions(neochat PUBLIC -DHAVE_KDBUSADDONS)
endif()
if (TARGET KF6::KIOWidgets)
target_compile_definitions(neochat PUBLIC -DHAVE_KIO)
endif()
if (TARGET KUnifiedPush)
target_compile_definitions(neochat PUBLIC -DHAVE_KUNIFIEDPUSH)
target_link_libraries(neochat PUBLIC KUnifiedPush)
if (NOT ANDROID)
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})
@@ -358,8 +355,7 @@ endif()
install(TARGETS neochat-app ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
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 RENAME org.kde.neochat.desktop)
install(FILES plasma-runner-neochat.desktop DESTINATION ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins)
endif()
if (APPLE)

View File

@@ -5,6 +5,7 @@
#include "controller.h"
#include <Quotient/connection.h>
#include <qt6keychain/keychain.h>
#include <KLocalizedString>
@@ -13,24 +14,38 @@
#include <signal.h>
#include <Quotient/csapi/notifications.h>
#include <Quotient/events/roommemberevent.h>
#include <Quotient/qt_connection_util.h>
#include <Quotient/settings.h>
#include "accountmanager.h"
#include "enums/roomsortparameter.h"
#include "general_logging.h"
#include "mediasizehelper.h"
#include "models/actionsmodel.h"
#include "models/messagemodel.h"
#include "models/pushrulemodel.h"
#include "models/roomlistmodel.h"
#include "models/roomtreemodel.h"
#include "neochatconfig.h"
#include "neochatconnection.h"
#include "neochatroom.h"
#include "notificationsmanager.h"
#include "proxycontroller.h"
#include "roommanager.h"
#if !defined(Q_OS_ANDROID)
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
#include "trayicon.h"
#elif !defined(Q_OS_ANDROID)
#include "trayicon_sni.h"
#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
@@ -121,10 +136,8 @@ Controller::Controller(QObject *parent)
connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, &Controller::setQuitOnLastWindowClosed);
#endif
connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [this] {
#ifndef Q_OS_ANDROID
QObject::connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [this] {
delete m_trayIcon;
#endif
NeoChatConfig::self()->save();
});
@@ -190,15 +203,13 @@ void Controller::setAccountManager(AccountManager *manager)
m_accountManager = manager;
if (!m_accountManager) {
return;
if (m_accountManager) {
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)
@@ -244,10 +255,7 @@ void Controller::initActiveConnection(NeoChatConnection *oldConnection, NeoChatC
if (newConnection) {
connect(newConnection, &NeoChatConnection::errorOccured, this, &Controller::errorOccured);
connect(newConnection, &NeoChatConnection::badgeNotificationCountChanged, this, &Controller::updateBadgeNotificationCount);
// Refresh and update manually, in case we init too late for the badge count to actually change.
newConnection->refreshBadgeNotificationCount();
updateBadgeNotificationCount(newConnection->badgeNotificationCount());
}
Q_EMIT activeConnectionChanged(newConnection);
}
@@ -257,8 +265,8 @@ bool Controller::supportSystemTray() const
#ifdef Q_OS_ANDROID
return false;
#else
QStringList unsupportedPlatforms{u"GNOME"_s, u"Pantheon"_s};
return !unsupportedPlatforms.contains(QString::fromLatin1(qgetenv("XDG_CURRENT_DESKTOP")));
auto de = QString::fromLatin1(qgetenv("XDG_CURRENT_DESKTOP"));
return de != u"GNOME"_s && de != u"Pantheon"_s;
#endif
}
@@ -268,8 +276,11 @@ void Controller::setQuitOnLastWindowClosed()
if (supportSystemTray() && NeoChatConfig::self()->systemTray()) {
m_trayIcon = new TrayIcon(this);
m_trayIcon->show();
} else if (m_trayIcon) {
delete m_trayIcon;
} else {
if (m_trayIcon) {
delete m_trayIcon;
m_trayIcon = nullptr;
}
}
#endif
}
@@ -307,7 +318,8 @@ void Controller::listenForNotifications()
connect(timer, &QTimer::timeout, qGuiApp, &QGuiApplication::quit);
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.
@@ -325,7 +337,30 @@ void Controller::clearInvitationNotification(const QString &roomId)
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);
#endif // QT_VERSION_CHECK(6, 6, 0)
}
bool Controller::isFlatpak() const
@@ -346,10 +381,7 @@ QString Controller::loadFileContent(const QString &path) const
{
QUrl url(path);
QFile file(url.isLocalFile() ? url.toLocalFile() : url.toString());
if (!file.open(QFile::ReadOnly)) {
qCWarning(GENERAL) << "Failed to open file" << path;
return {};
}
file.open(QFile::ReadOnly);
return QString::fromLatin1(file.readAll());
}

View File

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

View File

@@ -33,10 +33,13 @@
#include <KWindowSystem>
#endif
#if __has_include("KCrash")
#include <KCrash>
#endif
#include <KIconTheme>
#include <KLocalizedQmlContext>
#include <KLocalizedContext>
#include <KLocalizedString>
#include <KirigamiApp>
#include "neochat-version.h"
@@ -101,11 +104,8 @@ Q_DECL_EXPORT
#endif
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();
QNetworkProxyFactory::setUseSystemConfiguration(true);
#ifdef HAVE_WEBVIEW
QtWebView::initialize();
@@ -113,10 +113,24 @@ int main(int argc, char *argv[])
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGLRhi);
#endif
KirigamiApp::App app(argc, argv);
KirigamiApp kirigamiApp;
#ifdef Q_OS_ANDROID
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
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
freopen("CONOUT$", "w", stdout);
freopen("CONOUT$", "w", stderr);
}
QApplication::setStyle(u"breeze"_s);
QFont font(u"Segoe UI Emoji"_s);
font.setPointSize(10);
@@ -163,9 +177,19 @@ int main(int argc, char *argv[])
KAboutData::setApplicationData(about);
QGuiApplication::setWindowIcon(QIcon::fromTheme(u"org.kde.neochat"_s));
#if __has_include("KCrash")
KCrash::initialize();
#endif
Connection::setEncryptionDefault(true);
Connection::setDirectChatEncryptionDefault(true);
#ifdef NEOCHAT_FLATPAK
// Copy over the included FontConfig configuration to the
// app's config dir:
QFile::copy(u"/app/etc/fonts/conf.d/99-noto-mono-color-emoji.conf"_s, u"/var/config/fontconfig/conf.d/99-noto-mono-color-emoji.conf"_s);
#endif
ColorSchemer colorScheme;
QCommandLineParser parser;
@@ -181,7 +205,7 @@ int main(int argc, char *argv[])
parser.addOption(testOption);
#ifdef HAVE_KUNIFIEDPUSH
QCommandLineOption dbusActivatedOption(u"dbus-activated"_s);
QCommandLineOption dbusActivatedOption(u"dbus-activated"_s, i18n("Internal usage only."));
dbusActivatedOption.setFlags(QCommandLineOption::Flag::HiddenFromHelp);
parser.addOption(dbusActivatedOption);
#endif
@@ -195,14 +219,8 @@ int main(int argc, char *argv[])
#ifdef HAVE_KUNIFIEDPUSH
if (parser.isSet(dbusActivatedOption)) {
#ifdef HAVE_KDBUSADDONS
// 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.
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
// We want to be replaceable by the main client
KDBusService service(KDBusService::Replace);
#ifdef HAVE_RUNNER
// If we are built with KRunner and KUnifiedPush support, we need to do something special.
@@ -261,7 +279,7 @@ int main(int argc, char *argv[])
});
#endif
KLocalization::setupLocalizedContext(&engine);
engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
engine.setNetworkAccessManagerFactory(new NetworkAccessManagerFactory());
if (parser.isSet("ignore-ssl-errors"_L1)) {
@@ -276,9 +294,7 @@ int main(int argc, char *argv[])
engine.addImageProvider(u"blurhash"_s, new BlurhashImageProvider);
if (!kirigamiApp.start("org.kde.neochat", "Main", &engine)) {
return -1;
}
engine.loadFromModule("org.kde.neochat", "Main");
if (!parser.positionalArguments().isEmpty() && !parser.isSet("share"_L1)) {
RoomManager::instance().setUrlArgument(parser.positionalArguments()[0]);

View File

@@ -5,7 +5,6 @@
#include "jobs/neochatgetcommonroomsjob.h"
#include <QGuiApplication>
#include <Quotient/room.h>
using namespace Quotient;
@@ -40,22 +39,8 @@ void CommonRoomsModel::setUserId(const QString &userId)
QVariant CommonRoomsModel::data(const QModelIndex &index, int roleName) const
{
auto roomId = m_commonRooms[index.row()];
auto room = connection()->room(roomId);
if (!room) {
return {};
}
switch (roleName) {
case Qt::DisplayRole:
case RoomNameRole:
return room->displayName();
case RoomAvatarRole:
return room->avatarUrl();
case RoomIdRole:
return roomId;
}
Q_UNUSED(index)
Q_UNUSED(roleName)
return {};
}
@@ -65,20 +50,6 @@ int CommonRoomsModel::rowCount(const QModelIndex &parent) const
return m_commonRooms.size();
}
QHash<int, QByteArray> CommonRoomsModel::roleNames() const
{
return {
{RoomIdRole, "roomId"},
{RoomNameRole, "roomName"},
{RoomAvatarRole, "roomAvatar"},
};
}
bool CommonRoomsModel::loading() const
{
return m_loading;
}
void CommonRoomsModel::reload()
{
if (!m_connection || m_userId.isEmpty()) {
@@ -94,26 +65,15 @@ void CommonRoomsModel::reload()
return;
}
m_loading = true;
Q_EMIT loadingChanged();
m_connection->callApi<NeochatGetCommonRoomsJob>(m_userId)
.then([this](const auto job) {
const auto &replyData = job->jsonData();
beginResetModel();
for (const auto &roomId : replyData[u"joined"_s].toArray()) {
m_commonRooms.push_back(roomId.toString());
}
endResetModel();
Q_EMIT countChanged();
m_loading = false;
Q_EMIT loadingChanged();
})
.onFailure([this] {
m_loading = false;
Q_EMIT loadingChanged();
});
m_connection->callApi<NeochatGetCommonRoomsJob>(m_userId).then([this](const auto job) {
const auto &replyData = job->jsonData();
beginResetModel();
for (const auto &roomId : replyData[u"joined"_s].toArray()) {
m_commonRooms.push_back(roomId.toString());
}
endResetModel();
Q_EMIT countChanged();
});
}
#include "moc_commonroomsmodel.cpp"

View File

@@ -7,6 +7,7 @@
#include <QQmlEngine>
#include "neochatconnection.h"
#include "neochatroom.h"
#include <Quotient/events/roommessageevent.h>
#include <Quotient/roommember.h>
@@ -21,13 +22,14 @@ class CommonRoomsModel : public QAbstractListModel
Q_PROPERTY(NeoChatConnection *connection WRITE setConnection READ connection NOTIFY connectionChanged REQUIRED)
Q_PROPERTY(QString userId WRITE setUserId READ userId NOTIFY userIdChanged REQUIRED)
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
public:
enum Roles {
RoomIdRole = Qt::UserRole,
RoomNameRole,
RoomAvatarRole,
TextRole = Qt::DisplayRole,
LongitudeRole,
LatitudeRole,
AssetRole,
AuthorRole,
};
Q_ENUM(Roles)
@@ -42,15 +44,10 @@ public:
[[nodiscard]] QVariant data(const QModelIndex &index, int roleName) const override;
[[nodiscard]] Q_INVOKABLE int rowCount(const QModelIndex &parent = {}) const override;
QHash<int, QByteArray> roleNames() const override;
bool loading() const;
Q_SIGNALS:
void connectionChanged();
void userIdChanged();
void countChanged();
void loadingChanged();
private:
void reload();
@@ -58,5 +55,4 @@ private:
QPointer<NeoChatConnection> m_connection;
QString m_userId;
QList<QString> m_commonRooms;
bool m_loading = false;
};

View File

@@ -1,41 +0,0 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "models/limitermodel.h"
LimiterModel::LimiterModel(QObject *parent)
: QSortFilterProxyModel(parent)
{
connect(this, &QSortFilterProxyModel::rowsInserted, this, &LimiterModel::extraCountChanged);
connect(this, &QSortFilterProxyModel::rowsRemoved, this, &LimiterModel::extraCountChanged);
connect(this, &QSortFilterProxyModel::modelReset, this, &LimiterModel::extraCountChanged);
}
int LimiterModel::maximumCount() const
{
return m_maximumCount;
}
void LimiterModel::setMaximumCount(int maximumCount)
{
if (m_maximumCount != maximumCount) {
m_maximumCount = maximumCount;
Q_EMIT maximumCountChanged();
}
}
int LimiterModel::extraCount() const
{
if (sourceModel()) {
return std::max(sourceModel()->rowCount() - maximumCount(), 0);
}
return 0;
}
bool LimiterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
Q_UNUSED(source_parent)
return source_row < maximumCount();
}
#include "moc_limitermodel.cpp"

View File

@@ -1,41 +0,0 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QQmlEngine>
#include <QSortFilterProxyModel>
/**
* @class LimiterModel
*
* @brief Takes a source QAbstractItemModel model and only displays a desired maximum amount.
*
* Also gives you the remaining (filtered out) items, useful for sticking in a label or somesuch.
*/
class LimiterModel : public QSortFilterProxyModel
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(int maximumCount READ maximumCount WRITE setMaximumCount NOTIFY maximumCountChanged)
Q_PROPERTY(int extraCount READ extraCount NOTIFY extraCountChanged)
public:
explicit LimiterModel(QObject *parent = nullptr);
[[nodiscard]] int maximumCount() const;
void setMaximumCount(int maximumCount);
[[nodiscard]] int extraCount() const;
Q_SIGNALS:
void maximumCountChanged();
void extraCountChanged();
protected:
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
private:
int m_maximumCount = 0;
};

View File

@@ -58,7 +58,7 @@ QVariant NotificationsModel::data(const QModelIndex &index, int role) const
QHash<int, QByteArray> NotificationsModel::roleNames() const
{
return {
{TextRole, "notificationText"},
{TextRole, "text"},
{RoomIdRole, "roomId"},
{AuthorName, "authorName"},
{AuthorAvatar, "authorAvatar"},
@@ -92,9 +92,7 @@ void NotificationsModel::setConnection(NeoChatConnection *connection)
void NotificationsModel::loadData()
{
if (!m_connection) {
return;
}
Q_ASSERT(m_connection);
if (m_job || (m_notifications.size() && m_nextToken.isEmpty())) {
return;
}

View File

@@ -176,7 +176,7 @@ void ServerListModel::initialize()
true,
false,
});
endResetModel();
beginResetModel();
}
#include "moc_serverlistmodel.cpp"

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