Compare commits

..

1 Commits

Author SHA1 Message Date
Joshua Goins
508c2bee46 Don't show "Hide Image" button for really tiny images
Otherwise, it looks really buggy.
2026-01-14 20:42:47 -05:00
311 changed files with 54714 additions and 89037 deletions

View File

@@ -12,7 +12,7 @@ include:
- /gitlab-templates/linux-qt6.yml - /gitlab-templates/linux-qt6.yml
- /gitlab-templates/linux-qt6-next.yml - /gitlab-templates/linux-qt6-next.yml
- /gitlab-templates/windows-qt6.yml - /gitlab-templates/windows-qt6.yml
# - /gitlab-templates/freebsd-qt6.yml - /gitlab-templates/freebsd-qt6.yml
- /gitlab-templates/flatpak.yml - /gitlab-templates/flatpak.yml
- /gitlab-templates/snap-snapcraft-lxd.yml - /gitlab-templates/snap-snapcraft-lxd.yml
- /gitlab-templates/craft-android-qt6-apks.yml - /gitlab-templates/craft-android-qt6-apks.yml

View File

@@ -28,6 +28,7 @@ Dependencies:
'frameworks/qqc2-desktop-style': '@latest-kf6' 'frameworks/qqc2-desktop-style': '@latest-kf6'
'frameworks/kio': '@latest-kf6' 'frameworks/kio': '@latest-kf6'
'frameworks/kwindowsystem': '@latest-kf6' 'frameworks/kwindowsystem': '@latest-kf6'
'frameworks/kstatusnotifieritem': '@latest-kf6'
- 'on': ['Linux', 'FreeBSD'] - 'on': ['Linux', 'FreeBSD']
'require': 'require':
'frameworks/kdbusaddons': '@latest-kf6' 'frameworks/kdbusaddons': '@latest-kf6'
@@ -42,4 +43,3 @@ Options:
per-test-timeout: 90 per-test-timeout: 90
require-passing-tests-on: ['Linux', 'Android', 'FreeBSD', 'Windows'] require-passing-tests-on: ['Linux', 'Android', 'FreeBSD', 'Windows']
run-qmllint: True run-qmllint: True
enable-lsan: True

View File

@@ -88,7 +88,7 @@ if(ANDROID)
) )
else() else()
find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Widgets) find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Widgets)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle KIO WindowSystem) find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle KIO WindowSystem StatusNotifierItem)
find_package(KF6SyntaxHighlighting ${KF_MIN_VERSION} REQUIRED) find_package(KF6SyntaxHighlighting ${KF_MIN_VERSION} REQUIRED)
set_package_properties(KF6QQC2DesktopStyle PROPERTIES set_package_properties(KF6QQC2DesktopStyle PROPERTIES
TYPE RUNTIME TYPE RUNTIME
@@ -106,7 +106,7 @@ if (NOT ANDROID AND NOT WIN32 AND NOT APPLE AND NOT HAIKU)
find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED) find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED)
endif() endif()
find_package(QuotientQt6 0.9.5) find_package(QuotientQt6 0.9.3)
set_package_properties(QuotientQt6 PROPERTIES set_package_properties(QuotientQt6 PROPERTIES
TYPE REQUIRED TYPE REQUIRED
DESCRIPTION "Qt wrapper around Matrix API" DESCRIPTION "Qt wrapper around Matrix API"
@@ -178,7 +178,7 @@ add_definitions(-DQT_NO_FOREACH)
add_subdirectory(src) add_subdirectory(src)
if (BUILD_TESTING) if (BUILD_TESTING)
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test HttpServer QuickTest) find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test HttpServer)
add_subdirectory(autotests) add_subdirectory(autotests)
# add_subdirectory(appiumtests) # add_subdirectory(appiumtests)
if (NOT ANDROID) if (NOT ANDROID)

View File

@@ -25,10 +25,15 @@ Qt-based SDK for the [Matrix Protocol](https://spec.matrix.org/).
## Features ## 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 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 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. 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 ## Get it
Details where to find stable releases for NeoChat can be found on its [homepage](https://apps.kde.org/neochat). 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 the KDE community website's get involved section under [development](https://community.kde.org/Get_Involved/development). This
is primarily aimed at Linux development. 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/). development environments for [Windows](https://community.kde.org/Get_Involved/development/Windows) and [Android](https://develop.kde.org/docs/packaging/android/building_applications/).
## Running ## 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 ## Tests
@@ -61,12 +66,12 @@ be complete.
![coverage](https://invent.kde.org/network/neochat/badges/master/pipeline.svg) ![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. contributions would be especially welcome.
## Contributing ## 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. [NeoChat repository](https://invent.kde.org/network/neochat) of the KDE Gitlab instance.
- [Code of Conduct](https://kde.org/code-of-conduct) - [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 ## 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/). NeoChat is a fork of [Spectral](https://gitlab.com/spectral-im/spectral/).

View File

@@ -11,7 +11,7 @@ add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" )
ecm_add_test( ecm_add_test(
neochatroomtest.cpp neochatroomtest.cpp
LINK_LIBRARIES neochat Qt::Test Qt::HttpServer neochat_server LINK_LIBRARIES neochat Qt::Test
TEST_NAME neochatroomtest TEST_NAME neochatroomtest
) )
@@ -41,10 +41,16 @@ ecm_add_test(
ecm_add_test( ecm_add_test(
chatbarcachetest.cpp chatbarcachetest.cpp
LINK_LIBRARIES neochat Qt::Test Qt::HttpServer neochat_server LINK_LIBRARIES neochat Qt::Test
TEST_NAME chatbarcachetest TEST_NAME chatbarcachetest
) )
ecm_add_test(
chatdocumenthandlertest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME chatdocumenthandlertest
)
ecm_add_test( ecm_add_test(
timelinemessagemodeltest.cpp timelinemessagemodeltest.cpp
LINK_LIBRARIES neochat Qt::Test LINK_LIBRARIES neochat Qt::Test
@@ -98,51 +104,3 @@ ecm_add_test(
LINK_LIBRARIES neochat Qt::Test neochat_server LINK_LIBRARIES neochat Qt::Test neochat_server
TEST_NAME roommanagertest 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 <QVariantList>
#include "accountmanager.h" #include "accountmanager.h"
#include "blockcache.h"
#include "chatbarcache.h" #include "chatbarcache.h"
#include "enums/messagecomponenttype.h"
#include "models/actionsmodel.h" #include "models/actionsmodel.h"
#include "server.h" #include "server.h"
@@ -90,8 +88,8 @@ void ActionsTest::testActions()
QFETCH(std::optional<QString>, resultText); QFETCH(std::optional<QString>, resultText);
QFETCH(std::optional<Quotient::RoomMessageEvent::MsgType>, type); QFETCH(std::optional<Quotient::RoomMessageEvent::MsgType>, type);
auto cache = new ChatBarCache(room); auto cache = new ChatBarCache();
cache->cache() += Block::CacheItem{.type = MessageComponentType::Text, .content = QTextDocumentFragment::fromMarkdown(command)}; cache->setText(command);
auto result = ActionsModel::handleAction(room, cache); auto result = ActionsModel::handleAction(room, cache);
QCOMPARE(resultText, std::get<std::optional<QString>>(result)); QCOMPARE(resultText, std::get<std::optional<QString>>(result));
QCOMPARE(type, std::get<std::optional<Quotient::RoomMessageEvent::MsgType>>(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

@@ -11,14 +11,9 @@
#include <Quotient/syncdata.h> #include <Quotient/syncdata.h>
#include <qtestcase.h> #include <qtestcase.h>
#include <KLocalizedString>
#include "accountmanager.h"
#include "blockcache.h"
#include "chatbarcache.h" #include "chatbarcache.h"
#include "neochatroom.h" #include "neochatroom.h"
#include "server.h"
#include "testutils.h" #include "testutils.h"
using namespace Quotient; using namespace Quotient;
@@ -29,14 +24,14 @@ class ChatBarCacheTest : public QObject
private: private:
Connection *connection = nullptr; Connection *connection = nullptr;
NeoChatRoom *room = nullptr; TestUtils::TestRoom *room = nullptr;
Server server;
QString eventId;
private Q_SLOTS: private Q_SLOTS:
void initTestCase(); void initTestCase();
void empty(); void empty();
void noRoom();
void badParent();
void reply(); void reply();
void replyMissingUser(); void replyMissingUser();
void edit(); void edit();
@@ -45,38 +40,15 @@ private Q_SLOTS:
void ChatBarCacheTest::initTestCase() void ChatBarCacheTest::initTestCase()
{ {
Connection::setRoomType<NeoChatRoom>(); connection = Connection::makeMockConnection(u"@bob:kde.org"_s);
server.start(); room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, "test-min-sync.json"_L1);
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());
} }
void ChatBarCacheTest::empty() void ChatBarCacheTest::empty()
{ {
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room)); QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
QCOMPARE(chatBarCache->cache().toString(), QString()); QCOMPARE(chatBarCache->text(), QString());
QCOMPARE(chatBarCache->isReplying(), false); QCOMPARE(chatBarCache->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString()); QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), false); QCOMPARE(chatBarCache->isEditing(), false);
@@ -86,20 +58,51 @@ void ChatBarCacheTest::empty()
QCOMPARE(chatBarCache->attachmentPath(), QString()); 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() void ChatBarCacheTest::reply()
{ {
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room)); 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->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->isReplying(), true);
QCOMPARE(chatBarCache->replyId(), eventId); QCOMPARE(chatBarCache->replyId(), u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->isEditing(), false); QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString()); QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@foo:server.com"_s)); QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@example:example.org"_s));
QCOMPARE(chatBarCache->relationMessage(), u"foo"_s); QCOMPARE(chatBarCache->relationMessage(), u"This is an example\ntext message"_s);
QCOMPARE(chatBarCache->attachmentPath(), QString()); QCOMPARE(chatBarCache->attachmentPath(), QString());
QCOMPARE(chatBarCache->relationAuthorIsPresent(), true); QCOMPARE(chatBarCache->relationAuthorIsPresent(), true);
} }
@@ -107,28 +110,24 @@ void ChatBarCacheTest::reply()
void ChatBarCacheTest::replyMissingUser() void ChatBarCacheTest::replyMissingUser()
{ {
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room)); 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->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->isReplying(), true);
QCOMPARE(chatBarCache->replyId(), eventId); QCOMPARE(chatBarCache->replyId(), u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->isEditing(), false); QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString()); QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@foo:server.com"_s)); QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@example:example.org"_s));
QCOMPARE(chatBarCache->relationMessage(), u"foo"_s); QCOMPARE(chatBarCache->relationMessage(), u"This is an example\ntext message"_s);
QCOMPARE(chatBarCache->attachmentPath(), QString()); QCOMPARE(chatBarCache->attachmentPath(), QString());
QCOMPARE(chatBarCache->relationAuthorIsPresent(), true); QCOMPARE(chatBarCache->relationAuthorIsPresent(), true);
QSignalSpy relationAuthorIsPresentSpy(chatBarCache.get(), &ChatBarCache::relationAuthorIsPresentChanged); QSignalSpy relationAuthorIsPresentSpy(chatBarCache.get(), &ChatBarCache::relationAuthorIsPresentChanged);
// sync again, which will simulate the reply user leaving the room // sync again, which will simulate the reply user leaving the room
room->syncNewEvents(u"test-min-sync-extra-sync.json"_s);
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); QTRY_COMPARE(relationAuthorIsPresentSpy.count(), 1);
QCOMPARE(chatBarCache->relationAuthorIsPresent(), false); QCOMPARE(chatBarCache->relationAuthorIsPresent(), false);
@@ -138,32 +137,32 @@ void ChatBarCacheTest::edit()
{ {
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room)); 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->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(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->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString()); QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), true); QCOMPARE(chatBarCache->isEditing(), true);
QCOMPARE(chatBarCache->editId(), eventId); QCOMPARE(chatBarCache->editId(), u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@foo:server.com"_s)); QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@example:example.org"_s));
QCOMPARE(chatBarCache->relationMessage(), u"foo"_s); QCOMPARE(chatBarCache->relationMessage(), u"This is an example\ntext message"_s);
QCOMPARE(chatBarCache->attachmentPath(), QString()); QCOMPARE(chatBarCache->attachmentPath(), QString());
} }
void ChatBarCacheTest::attachment() void ChatBarCacheTest::attachment()
{ {
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room)); 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->setEditId(eventId); chatBarCache->setEditId(u"$153456789:example.org"_s);
chatBarCache->setAttachmentPath(u"some/path"_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->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString()); QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), false); 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

@@ -0,0 +1,20 @@
{
"state": {
"events": [
{
"content": {
"membership": "leave"
},
"event_id": "$1432735824666PhrSA:example.org",
"origin_server_ts": 1432735824666,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"state_key": "@example:example.org",
"type": "m.room.member",
"unsigned": {
"replaces_state": "$143273582443PhrSn:example.org"
}
}
]
}
}

View File

@@ -40,6 +40,7 @@ private Q_SLOTS:
void nullSingleLineDisplayName(); void nullSingleLineDisplayName();
void time(); void time();
void nullTime(); void nullTime();
void timeString();
void highlighted(); void highlighted();
void nullHighlighted(); void nullHighlighted();
void hidden(); void hidden();
@@ -99,12 +100,12 @@ void EventHandlerTest::time()
{ {
const auto event = room->messageEvents().at(0).get(); 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()); const auto txID = room->postJson("m.room.message"_L1, event->fullJson());
QCOMPARE(room->pendingEvents().size(), 1); QCOMPARE(room->pendingEvents().size(), 1);
const auto pendingIt = room->findPendingEvent(txID); 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); room->discardMessage(txID);
QCOMPARE(room->pendingEvents().size(), 0); QCOMPARE(room->pendingEvents().size(), 0);
@@ -113,10 +114,40 @@ void EventHandlerTest::time()
void EventHandlerTest::nullTime() void EventHandlerTest::nullTime()
{ {
QTest::ignoreMessage(QtWarningMsg, "time called with room set to nullptr."); 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."); 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::LocalTime)).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() void EventHandlerTest::highlighted()

View File

@@ -19,7 +19,13 @@ class LinkPreviewerTest : public QObject
{ {
Q_OBJECT Q_OBJECT
private:
Connection *connection = nullptr;
TestUtils::TestRoom *room = nullptr;
private Q_SLOTS: private Q_SLOTS:
void initTestCase();
void linkPreviewsMatch_data(); void linkPreviewsMatch_data();
void linkPreviewsMatch(); void linkPreviewsMatch();
@@ -30,6 +36,12 @@ private Q_SLOTS:
void linkPreviewsReject(); 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() void LinkPreviewerTest::linkPreviewsMatch_data()
{ {
QTest::addColumn<QString>("inputString"); QTest::addColumn<QString>("inputString");

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/quotient_common.h>
#include <Quotient/syncdata.h> #include <Quotient/syncdata.h>
#include <KLocalizedString>
#include "accountmanager.h"
#include "server.h"
#include "testutils.h" #include "testutils.h"
using namespace Quotient; using namespace Quotient;
@@ -22,8 +18,7 @@ class NeoChatRoomTest : public QObject {
private: private:
Connection *connection = nullptr; Connection *connection = nullptr;
NeoChatRoom *room = nullptr; TestUtils::TestRoom *room = nullptr;
Server server;
private Q_SLOTS: private Q_SLOTS:
void initTestCase(); void initTestCase();
@@ -32,27 +27,8 @@ private Q_SLOTS:
void NeoChatRoomTest::initTestCase() void NeoChatRoomTest::initTestCase()
{ {
Connection::setRoomType<NeoChatRoom>(); connection = Connection::makeMockConnection(u"@bob:kde.org"_s);
server.start(); room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-min-sync.json"_s);
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);
} }
void NeoChatRoomTest::eventTest() 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

@@ -121,13 +121,13 @@ void Server::start()
QFile key(QStringLiteral(DATA_DIR) + u"/localhost.key"_s); QFile key(QStringLiteral(DATA_DIR) + u"/localhost.key"_s);
void(key.open(QFile::ReadOnly)); void(key.open(QFile::ReadOnly));
config.setPrivateKey(QSslKey(&key, QSsl::Rsa)); config.setPrivateKey(QSslKey(&key, QSsl::Rsa));
config.setLocalCertificate(QSslCertificate::fromPath(QStringLiteral(DATA_DIR) + u"/localhost.crt"_s).constFirst()); config.setLocalCertificate(QSslCertificate::fromPath(QStringLiteral(DATA_DIR) + u"/localhost.crt"_s).front());
m_sslServer.setSslConfiguration(config); m_sslServer.setSslConfiguration(config);
if (!m_sslServer.listen(QHostAddress::LocalHost, 1234) || !m_server.bind(&m_sslServer)) { if (!m_sslServer.listen(QHostAddress::LocalHost, 1234) || !m_server.bind(&m_sslServer)) {
qFatal() << "Server failed to listen on a port."; qFatal() << "Server failed to listen on a port.";
return; return;
} else { } else {
qInfo() << "Server listening"; qWarning() << "Server listening";
} }
} }
@@ -203,32 +203,12 @@ QString Server::sendEvent(const QString &roomId, const QString &eventType, const
return eventId; 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) void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &responder)
{ {
QJsonObject joinRooms; QJsonObject joinRooms;
auto token = request.query().queryItemValue(u"since"_s).toInt(); auto token = request.query().queryItemValue(u"since"_s).toInt();
const auto changes = m_state.mid(token); for (const auto &change : m_state.mid(token)) {
for (const auto &change : changes) {
for (const auto &newRoom : change.newRooms) { for (const auto &newRoom : change.newRooms) {
QJsonArray stateEvents; QJsonArray stateEvents;
stateEvents += QJsonObject{ stateEvents += QJsonObject{
@@ -273,7 +253,7 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
} }
} }
for (const auto &change : changes) { for (const auto &change : m_state.mid(token)) {
for (const auto &invitation : change.invitations) { 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. // 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(); auto stateEvents = joinRooms[invitation.roomId][u"state"_s][u"events"_s].toArray();
@@ -300,7 +280,7 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
} }
} }
for (const auto &change : changes) { for (const auto &change : m_state.mid(token)) {
for (const auto &ban : change.bans) { 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. // 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(); auto stateEvents = joinRooms[ban.roomId][u"state"_s][u"events"_s].toArray();
@@ -327,7 +307,7 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
} }
} }
for (const auto &change : changes) { for (const auto &change : m_state.mid(token)) {
for (const auto &join : change.joins) { 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. // 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(); auto stateEvents = joinRooms[join.roomId][u"state"_s][u"events"_s].toArray();
@@ -354,19 +334,7 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
} }
} }
for (const auto &change : changes) { for (const auto &change : m_state.mid(token)) {
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) { for (const auto &event : change.events) {
// TODO the room might be in a different join state. // 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(); auto timeline = joinRooms[event.fullJson[u"room_id"_s].toString()][u"timeline"_s][u"events"_s].toArray();
@@ -398,5 +366,6 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
syncData[u"rooms"_s] = rooms; syncData[u"rooms"_s] = rooms;
} }
qWarning() << syncData;
responder.write(QJsonDocument(syncData), QHttpServerResponder::StatusCode::Ok); responder.write(QJsonDocument(syncData), QHttpServerResponder::StatusCode::Ok);
} }

View File

@@ -35,7 +35,6 @@ struct Changes {
QJsonObject fullJson; QJsonObject fullJson;
}; };
QList<Event> events; QList<Event> events;
QList<Event> stateEvents;
}; };
struct RoomData { struct RoomData {
@@ -68,7 +67,6 @@ public:
*/ */
QString createServerNoticesRoom(const QString &matrixId); QString createServerNoticesRoom(const QString &matrixId);
QString sendEvent(const QString &roomId, const QString &eventType, const QJsonObject &content); 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: private:
QHttpServer m_server; QHttpServer m_server;

View File

@@ -73,16 +73,6 @@ void WindowControllerTest::toggle()
instance.toggleWindow(); instance.toggleWindow();
QCOMPARE(window.windowState(), Qt::WindowNoState); QCOMPARE(window.windowState(), Qt::WindowNoState);
QCOMPARE(window.isVisible(), false); 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) QTEST_MAIN(WindowControllerTest)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

@@ -37,8 +37,6 @@ qt_add_library(neochat STATIC
texttospeechhelper.cpp texttospeechhelper.cpp
models/limitermodel.cpp models/limitermodel.cpp
models/limitermodel.h models/limitermodel.h
supportcontroller.cpp
supportcontroller.h
) )
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
@@ -70,6 +68,7 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/AttachmentPane.qml qml/AttachmentPane.qml
qml/QuickFormatBar.qml qml/QuickFormatBar.qml
qml/UserDetailDialog.qml qml/UserDetailDialog.qml
qml/OpenFileDialog.qml
qml/KeyVerificationDialog.qml qml/KeyVerificationDialog.qml
qml/ConfirmLogoutDialog.qml qml/ConfirmLogoutDialog.qml
qml/VerificationMessage.qml qml/VerificationMessage.qml
@@ -78,6 +77,7 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/EmojiSas.qml qml/EmojiSas.qml
qml/VerificationCanceled.qml qml/VerificationCanceled.qml
qml/MessageSourceSheet.qml qml/MessageSourceSheet.qml
qml/LocationChooser.qml
qml/InvitationView.qml qml/InvitationView.qml
qml/AvatarTabButton.qml qml/AvatarTabButton.qml
qml/OsmLocationPlugin.qml qml/OsmLocationPlugin.qml
@@ -103,10 +103,10 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
qml/HoverLinkIndicator.qml qml/HoverLinkIndicator.qml
qml/AvatarNotification.qml qml/AvatarNotification.qml
qml/ReasonDialog.qml qml/ReasonDialog.qml
qml/NewPollDialog.qml
qml/UserMenu.qml qml/UserMenu.qml
qml/MeetingDialog.qml qml/MeetingDialog.qml
qml/SeenByDialog.qml qml/SeenByDialog.qml
qml/SupportDialog.qml
DEPENDENCIES DEPENDENCIES
QtCore QtCore
QtQuick QtQuick
@@ -171,7 +171,12 @@ ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)
target_sources(neochat-app PRIVATE ${NEOCHAT_ICON}) target_sources(neochat-app PRIVATE ${NEOCHAT_ICON})
if(NOT ANDROID) 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_link_libraries(neochat PUBLIC KF6::WindowSystem)
target_compile_definitions(neochat PUBLIC -DHAVE_WINDOWSYSTEM) target_compile_definitions(neochat PUBLIC -DHAVE_WINDOWSYSTEM)
endif() endif()

View File

@@ -29,8 +29,10 @@
#include "proxycontroller.h" #include "proxycontroller.h"
#include "roommanager.h" #include "roommanager.h"
#if !defined(Q_OS_ANDROID) #if defined(Q_OS_WIN) || defined(Q_OS_MAC)
#include "trayicon.h" #include "trayicon.h"
#elif !defined(Q_OS_ANDROID)
#include "trayicon_sni.h"
#endif #endif
#ifdef HAVE_KUNIFIEDPUSH #ifdef HAVE_KUNIFIEDPUSH

View File

@@ -74,11 +74,6 @@ QHash<int, QByteArray> CommonRoomsModel::roleNames() const
}; };
} }
bool CommonRoomsModel::loading() const
{
return m_loading;
}
void CommonRoomsModel::reload() void CommonRoomsModel::reload()
{ {
if (!m_connection || m_userId.isEmpty()) { if (!m_connection || m_userId.isEmpty()) {
@@ -94,26 +89,15 @@ void CommonRoomsModel::reload()
return; return;
} }
m_loading = true; m_connection->callApi<NeochatGetCommonRoomsJob>(m_userId).then([this](const auto job) {
Q_EMIT loadingChanged(); const auto &replyData = job->jsonData();
beginResetModel();
m_connection->callApi<NeochatGetCommonRoomsJob>(m_userId) for (const auto &roomId : replyData[u"joined"_s].toArray()) {
.then([this](const auto job) { m_commonRooms.push_back(roomId.toString());
const auto &replyData = job->jsonData(); }
beginResetModel(); endResetModel();
for (const auto &roomId : replyData[u"joined"_s].toArray()) { Q_EMIT countChanged();
m_commonRooms.push_back(roomId.toString()); });
}
endResetModel();
Q_EMIT countChanged();
m_loading = false;
Q_EMIT loadingChanged();
})
.onFailure([this] {
m_loading = false;
Q_EMIT loadingChanged();
});
} }
#include "moc_commonroomsmodel.cpp" #include "moc_commonroomsmodel.cpp"

View File

@@ -21,7 +21,6 @@ class CommonRoomsModel : public QAbstractListModel
Q_PROPERTY(NeoChatConnection *connection WRITE setConnection READ connection NOTIFY connectionChanged REQUIRED) Q_PROPERTY(NeoChatConnection *connection WRITE setConnection READ connection NOTIFY connectionChanged REQUIRED)
Q_PROPERTY(QString userId WRITE setUserId READ userId NOTIFY userIdChanged REQUIRED) Q_PROPERTY(QString userId WRITE setUserId READ userId NOTIFY userIdChanged REQUIRED)
Q_PROPERTY(int count READ rowCount NOTIFY countChanged) Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
public: public:
enum Roles { enum Roles {
@@ -44,13 +43,10 @@ public:
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
bool loading() const;
Q_SIGNALS: Q_SIGNALS:
void connectionChanged(); void connectionChanged();
void userIdChanged(); void userIdChanged();
void countChanged(); void countChanged();
void loadingChanged();
private: private:
void reload(); void reload();
@@ -58,5 +54,4 @@ private:
QPointer<NeoChatConnection> m_connection; QPointer<NeoChatConnection> m_connection;
QString m_userId; QString m_userId;
QList<QString> m_commonRooms; QList<QString> m_commonRooms;
bool m_loading = false;
}; };

View File

@@ -92,9 +92,7 @@ void NotificationsModel::setConnection(NeoChatConnection *connection)
void NotificationsModel::loadData() void NotificationsModel::loadData()
{ {
if (!m_connection) { Q_ASSERT(m_connection);
return;
}
if (m_job || (m_notifications.size() && m_nextToken.isEmpty())) { if (m_job || (m_notifications.size() && m_nextToken.isEmpty())) {
return; return;
} }

View File

@@ -207,6 +207,10 @@
</entry> </entry>
</group> </group>
<group name="FeatureFlags"> <group name="FeatureFlags">
<entry name="Threads" type="bool">
<label>Enable threads</label>
<default>false</default>
</entry>
<entry name="Phone3PId" type="bool"> <entry name="Phone3PId" type="bool">
<label>Enable add phone numbers as 3PIDs</label> <label>Enable add phone numbers as 3PIDs</label>
<default>false</default> <default>false</default>

View File

@@ -38,7 +38,7 @@ NotificationsManager::NotificationsManager(QObject *parent)
{ {
} }
void NotificationsManager::handleNotifications(const QPointer<NeoChatConnection> &connection) void NotificationsManager::handleNotifications(QPointer<NeoChatConnection> connection)
{ {
if (KNotificationPermission::checkPermission() == Qt::PermissionStatus::Granted) { if (KNotificationPermission::checkPermission() == Qt::PermissionStatus::Granted) {
startNotificationJob(connection); startNotificationJob(connection);
@@ -68,7 +68,7 @@ void NotificationsManager::startNotificationJob(QPointer<NeoChatConnection> conn
} }
} }
void NotificationsManager::processNotificationJob(const QPointer<NeoChatConnection> &connection, const GetNotificationsJob *job, const bool initialization) void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> connection, Quotient::GetNotificationsJob *job, bool initialization)
{ {
if (!job || !connection || !connection->isLoggedIn()) { if (!job || !connection || !connection->isLoggedIn()) {
return; return;
@@ -82,7 +82,8 @@ void NotificationsManager::processNotificationJob(const QPointer<NeoChatConnecti
if (!m_initialTimestamp.contains(connectionId)) { if (!m_initialTimestamp.contains(connectionId)) {
m_initialTimestamp[connectionId] = notification["ts"_L1].toVariant().toLongLong(); m_initialTimestamp[connectionId] = notification["ts"_L1].toVariant().toLongLong();
} else { } else {
if (const auto timestamp = notification["ts"_L1].toVariant().toLongLong(); timestamp > m_initialTimestamp[connectionId]) { qint64 timestamp = notification["ts"_L1].toVariant().toLongLong();
if (timestamp > m_initialTimestamp[connectionId]) {
m_initialTimestamp[connectionId] = timestamp; m_initialTimestamp[connectionId] = timestamp;
} }
} }
@@ -159,29 +160,29 @@ void NotificationsManager::processNotificationJob(const QPointer<NeoChatConnecti
} }
} }
bool NotificationsManager::shouldPostNotification(const QPointer<NeoChatConnection> &connection, const QJsonValue &notification) bool NotificationsManager::shouldPostNotification(QPointer<NeoChatConnection> connection, const QJsonValue &notification)
{ {
if (connection == nullptr || !connection->isLoggedIn()) { if (connection == nullptr || !connection->isLoggedIn()) {
return false; return false;
} }
const auto room = connection->room(notification["room_id"_L1].toString()); auto room = connection->room(notification["room_id"_L1].toString());
if (room == nullptr) { if (room == nullptr) {
return false; return false;
} }
// If the room is the current room and the application is active, the notification // If the room is the current room and the application is active the notification
// should not be shown. // should not be shown.
// This is set up so that if the application is inactive, the notification will // This is setup so that if the application is inactive the notification will
// always be posted, even if the room is the current room. // always be posted, even if the room is the current room.
if (RoomManager::instance().currentRoom() && room->id() == RoomManager::instance().currentRoom()->id() bool isCurrentRoom = RoomManager::instance().currentRoom() && room->id() == RoomManager::instance().currentRoom()->id();
&& QGuiApplication::applicationState() == Qt::ApplicationActive) { if (isCurrentRoom && QGuiApplication::applicationState() == Qt::ApplicationActive) {
return false; return false;
} }
// If the notification timestamp is earlier than the initial timestamp, assume // If the notification timestamp is earlier than the initial timestamp assume
// the notification is old and shouldn't be posted. // the notification is old and shouldn't be posted.
const auto timestamp = notification["ts"_L1].toDouble(); qint64 timestamp = notification["ts"_L1].toDouble();
if (timestamp < m_initialTimestamp[connection->user()->id()]) { if (timestamp < m_initialTimestamp[connection->user()->id()]) {
return false; return false;
} }
@@ -198,7 +199,7 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
const QString &text, const QString &text,
const QImage &icon, const QImage &icon,
const QString &replyEventId, const QString &replyEventId,
const bool canReply, bool canReply,
qint64 timestamp) qint64 timestamp)
{ {
const QString roomId = room->id(); const QString roomId = room->id();
@@ -270,8 +271,10 @@ void NotificationsManager::postInviteNotification(NeoChatRoom *rawRoom)
if (NeoChatConfig::rejectUnknownInvites()) { if (NeoChatConfig::rejectUnknownInvites()) {
auto job = room->connection()->callApi<NeochatGetCommonRoomsJob>(roomMemberEvent->senderId()); auto job = room->connection()->callApi<NeochatGetCommonRoomsJob>(roomMemberEvent->senderId());
connect(job, &BaseJob::result, this, [this, job, room] { connect(job, &BaseJob::result, this, [this, job, room] {
if (QJsonObject replyData = job->jsonData(); replyData.contains(u"joined"_s)) { QJsonObject replyData = job->jsonData();
if (!replyData["joined"_L1].toArray().isEmpty()) { if (replyData.contains(u"joined"_s)) {
const bool inAnyOfOurRooms = !replyData["joined"_L1].toArray().isEmpty();
if (inAnyOfOurRooms) {
doPostInviteNotification(room); doPostInviteNotification(room);
} else { } else {
room->forget(); room->forget();
@@ -283,7 +286,7 @@ void NotificationsManager::postInviteNotification(NeoChatRoom *rawRoom)
} }
} }
void NotificationsManager::doPostInviteNotification(const QPointer<NeoChatRoom> &room) void NotificationsManager::doPostInviteNotification(QPointer<NeoChatRoom> room)
{ {
const auto roomMemberEvent = room->currentState().get<RoomMemberEvent>(room->localMember().id()); const auto roomMemberEvent = room->currentState().get<RoomMemberEvent>(room->localMember().id());
if (roomMemberEvent == nullptr) { if (roomMemberEvent == nullptr) {
@@ -292,18 +295,18 @@ void NotificationsManager::doPostInviteNotification(const QPointer<NeoChatRoom>
const auto sender = room->member(roomMemberEvent->senderId()); const auto sender = room->member(roomMemberEvent->senderId());
QImage avatar_image; QImage avatar_image;
if (!room->member(roomMemberEvent->senderId()).avatarUrl().isEmpty()) { if (roomMemberEvent && !room->member(roomMemberEvent->senderId()).avatarUrl().isEmpty()) {
avatar_image = room->member(roomMemberEvent->senderId()).avatar(128, 128, {}); avatar_image = room->member(roomMemberEvent->senderId()).avatar(128, 128, {});
} else { } else {
qWarning() << "using this room's avatar"; qWarning() << "using this room's avatar";
avatar_image = room->avatar(128); avatar_image = room->avatar(128);
} }
const auto notification = new KNotification(u"invite"_s); KNotification *notification = new KNotification(u"invite"_s);
notification->setText(i18n("%1 invited you to a room", sender.htmlSafeDisplayName())); notification->setText(i18n("%1 invited you to a room", sender.htmlSafeDisplayName()));
notification->setTitle(room->displayName()); notification->setTitle(room->displayName());
notification->setPixmap(createNotificationImage(avatar_image, nullptr)); notification->setPixmap(createNotificationImage(avatar_image, nullptr));
const auto defaultAction = notification->addDefaultAction(i18n("Open this invitation in NeoChat")); auto defaultAction = notification->addDefaultAction(i18n("Open this invitation in NeoChat"));
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() { connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
if (!room) { if (!room) {
return; return;
@@ -364,9 +367,11 @@ void NotificationsManager::postPushNotification(const QByteArray &message)
{ {
const auto json = QJsonDocument::fromJson(message).object(); const auto json = QJsonDocument::fromJson(message).object();
const auto type = json["notification"_L1]["type"_L1].toString();
// the only two types of push notifications we support right now // the only two types of push notifications we support right now
if (const auto type = json["notification"_L1]["type"_L1].toString(); type == u"m.room.message"_s || type == u"m.room.encrypted"_s) { if (type == u"m.room.message"_s || type == u"m.room.encrypted"_s) {
const auto notification = new KNotification("message"_L1); auto notification = new KNotification("message"_L1);
const auto sender = json["notification"_L1]["sender_display_name"_L1].toString(); const auto sender = json["notification"_L1]["sender_display_name"_L1].toString();
const auto roomName = json["notification"_L1]["room_name"_L1].toString(); const auto roomName = json["notification"_L1]["room_name"_L1].toString();
@@ -386,13 +391,13 @@ void NotificationsManager::postPushNotification(const QByteArray &message)
} }
#ifdef HAVE_KIO #ifdef HAVE_KIO
const auto openAction = notification->addAction(i18n("Open NeoChat")); auto openAction = notification->addAction(i18n("Open NeoChat"));
connect(openAction, &KNotificationAction::activated, notification, [=]() { connect(openAction, &KNotificationAction::activated, notification, [=]() {
QString properId = roomId; QString properId = roomId;
properId = properId.replace(u"#"_s, QString()); properId = properId.replace(u"#"_s, QString());
properId = properId.replace(u"!"_s, QString()); properId = properId.replace(u"!"_s, QString());
const auto job = new KIO::ApplicationLauncherJob(KService::serviceByDesktopName(u"org.kde.neochat"_s)); auto *job = new KIO::ApplicationLauncherJob(KService::serviceByDesktopName(u"org.kde.neochat"_s));
job->setUrls({QUrl::fromUserInput(u"matrix:r/%1"_s.arg(properId))}); job->setUrls({QUrl::fromUserInput(u"matrix:r/%1"_s.arg(properId))});
job->start(); job->start();
}); });
@@ -423,12 +428,13 @@ QPixmap NotificationsManager::createNotificationImage(const QImage &icon, NeoCha
painter.setBrush(Qt::white); painter.setBrush(Qt::white);
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height()); painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
const QBrush brush(icon.scaledToHeight(biggestDimension)); QBrush brush(icon.scaledToHeight(biggestDimension));
painter.setBrush(brush); painter.setBrush(brush);
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height()); painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
if (room) { if (room != nullptr) {
if (const auto roomAvatar = room->avatar(imageRect.width(), imageRect.height()); !roomAvatar.isNull() && icon != roomAvatar) { const QImage roomAvatar = room->avatar(imageRect.width(), imageRect.height());
if (!roomAvatar.isNull() && icon != roomAvatar) {
const QRect lowerQuarter{imageRect.center(), imageRect.size() / 2}; const QRect lowerQuarter{imageRect.center(), imageRect.size() / 2};
painter.setBrush(Qt::white); painter.setBrush(Qt::white);

View File

@@ -58,7 +58,7 @@ public:
/** /**
* @brief Handle the notifications for the given connection. * @brief Handle the notifications for the given connection.
*/ */
void handleNotifications(const QPointer<NeoChatConnection> &connection); void handleNotifications(QPointer<NeoChatConnection> connection);
private: private:
QHash<QString, qint64> m_initialTimestamp; QHash<QString, qint64> m_initialTimestamp;
@@ -67,8 +67,8 @@ private:
QStringList m_connActiveJob; QStringList m_connActiveJob;
void startNotificationJob(QPointer<NeoChatConnection> connection); void startNotificationJob(QPointer<NeoChatConnection> connection);
static QPixmap createNotificationImage(const QImage &icon, NeoChatRoom *room); QPixmap createNotificationImage(const QImage &icon, NeoChatRoom *room);
bool shouldPostNotification(const QPointer<NeoChatConnection> &connection, const QJsonValue &notification); bool shouldPostNotification(QPointer<NeoChatConnection> connection, const QJsonValue &notification);
void postNotification(NeoChatRoom *room, void postNotification(NeoChatRoom *room,
const QString &sender, const QString &sender,
const QString &text, const QString &text,
@@ -77,7 +77,7 @@ private:
bool canReply, bool canReply,
qint64 timestamp); qint64 timestamp);
void doPostInviteNotification(const QPointer<NeoChatRoom> &room); void doPostInviteNotification(QPointer<NeoChatRoom> room);
QHash<QString, std::pair<qint64, KNotification *>> m_notifications; QHash<QString, std::pair<qint64, KNotification *>> m_notifications;
QHash<QString, QPointer<KNotification>> m_invitations; QHash<QString, QPointer<KNotification>> m_invitations;
@@ -85,5 +85,5 @@ private:
bool permissionAsked = false; bool permissionAsked = false;
private Q_SLOTS: private Q_SLOTS:
void processNotificationJob(const QPointer<NeoChatConnection> &connection, const Quotient::GetNotificationsJob *job, bool initialization); void processNotificationJob(QPointer<NeoChatConnection> connection, Quotient::GetNotificationsJob *job, bool initialization);
}; };

View File

@@ -5,7 +5,6 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Controls as QQC2 import QtQuick.Controls as QQC2
import QtMultimedia
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.components as KirigamiComponents import org.kde.kirigamiaddons.components as KirigamiComponents
@@ -19,25 +18,27 @@ KirigamiComponents.ConvergentContextMenu {
required property NeoChatConnection connection required property NeoChatConnection connection
required property Kirigami.ApplicationWindow window required property Kirigami.ApplicationWindow window
data: MediaDevices { Kirigami.Action {
id: devices text: i18nc("@action:button", "Show QR Code")
icon.name: "view-barcode-qr-symbolic"
onTriggered: {
(Qt.createComponent('org.kde.neochat', 'QrCodeMaximizeComponent').createObject(QQC2.Overlay.overlay, {
text: "https://matrix.to/#/" + root.connection.localUser.id,
title: root.connection.localUser.displayName,
subtitle: root.connection.localUser.id,
// Note: User::avatarUrl does not set user_id, and thus cannot be used directly here. Hence the makeMediaUrl.
avatarSource: root.connection.localUser.avatarUrl.toString().length > 0 ? root.connection.makeMediaUrl(root.connection.localUser.avatarUrl) : ""
}) as QrCodeMaximizeComponent).open();
}
} }
Kirigami.Action { Kirigami.Action {
text: i18nc("@action:button", "Open Profile") text: i18nc("@action:inmenu", "Switch Account")
icon.name: "im-user-symbolic" icon.name: "system-switch-user"
onTriggered: RoomManager.resolveResource(root.connection.localUserId, "qr") // Use "qr" action to make sure a room isn't passed, see RoomManager::visitUser shortcut: "Ctrl+U"
} onTriggered: (Qt.createComponent("org.kde.neochat", "AccountSwitchDialog").createObject(QQC2.Overlay.overlay, {
Kirigami.Action {
text: i18nc("@action:inmenu", "Scan a QR Code")
icon.name: "document-scan-symbolic"
visible: devices.videoInputs.length > 0
onTriggered: (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent("org.kde.neochat", "QrScannerPage"), {
connection: root.connection connection: root.connection
}, { }) as Kirigami.Dialog).open();
title: i18nc("@title", "Scan a QR Code")
})
} }
Kirigami.Action { Kirigami.Action {
@@ -54,6 +55,14 @@ KirigamiComponents.ConvergentContextMenu {
} }
} }
Kirigami.Action {
text: i18nc("@action:inmenu", "Devices")
icon.name: "computer-symbolic"
onTriggered: {
NeoChatSettingsView.open('devices');
}
}
Kirigami.Action { Kirigami.Action {
text: i18nc("@action:inmenu", "Open Developer Tools") text: i18nc("@action:inmenu", "Open Developer Tools")
icon.name: "tools" icon.name: "tools"
@@ -67,6 +76,14 @@ KirigamiComponents.ConvergentContextMenu {
}) })
} }
Kirigami.Action {
text: i18nc("@action:inmenu", "Open Secret Backup")
icon.name: "unlock"
onTriggered: root.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UnlockSSSSDialog'), {}, {
title: i18nc("@title:window", "Open Key Backup")
})
}
Kirigami.Action { Kirigami.Action {
text: i18nc("@action:inmenu", "Verify This Device") text: i18nc("@action:inmenu", "Verify This Device")
icon.name: "security-low" icon.name: "security-low"
@@ -86,25 +103,10 @@ KirigamiComponents.ConvergentContextMenu {
} }
Kirigami.Action { Kirigami.Action {
text: i18nc("@action:inmenu Open support dialog", "Support") text: i18nc("@action:inmenu", "Logout…")
icon.name: "help-contents-symbolic" icon.name: "im-kick-user"
onTriggered: { onTriggered: (Qt.createComponent("org.kde.neochat", "ConfirmLogoutDialog").createObject(QQC2.Overlay.overlay, {
(Qt.createComponent("org.kde.neochat", "SupportDialog").createObject(QQC2.Overlay.overlay, {
connection: root.connection,
}) as SupportDialog).open();
}
}
Kirigami.Action {
separator: true
}
Kirigami.Action {
text: i18nc("@action:inmenu", "Switch Account")
icon.name: "system-switch-user"
shortcut: "Ctrl+U"
onTriggered: (Qt.createComponent("org.kde.neochat", "AccountSwitchDialog").createObject(QQC2.Overlay.overlay, {
connection: root.connection connection: root.connection
}) as Kirigami.Dialog).open(); }) as Kirigami.Dialog).open()
} }
} }

View File

@@ -61,10 +61,10 @@ Kirigami.Dialog {
} }
onClicked: { onClicked: {
root.close();
((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat.login', 'WelcomePage'), {}, { ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat.login', 'WelcomePage'), {}, {
title: i18nc("@title:window", "Login") title: i18nc("@title:window", "Login")
}); });
root.close();
} }
Keys.onUpPressed: { Keys.onUpPressed: {
accountView.currentIndex = accountView.count - 1; accountView.currentIndex = accountView.count - 1;

View File

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

View File

@@ -4,6 +4,9 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Controls
import org.kde.kirigami as Kirigami
import org.kde.neochat import org.kde.neochat

View File

@@ -20,9 +20,9 @@ Components.AbstractMaximizeComponent {
property NeochatRoomMember author property NeochatRoomMember author
/** /**
* @brief The timestamp of the event as a neoChatDateTime. * @brief The timestamp of the message.
*/ */
required property neoChatDateTime dateTime property var time
/** /**
* @brief The code text to show. * @brief The code text to show.
@@ -64,7 +64,7 @@ Components.AbstractMaximizeComponent {
} }
QQC2.Label { QQC2.Label {
id: dateTimeLabel id: dateTimeLabel
text: root.dateTime.relativeDateTime text: root.time.toLocaleString(Qt.locale(), Locale.ShortFormat)
color: Kirigami.Theme.disabledTextColor color: Kirigami.Theme.disabledTextColor
elide: Text.ElideRight elide: Text.ElideRight
} }
@@ -79,7 +79,7 @@ Components.AbstractMaximizeComponent {
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
QQC2.TextArea { QQC2.TextArea {
id: codeTextEdit id: codeText
topPadding: Kirigami.Units.smallSpacing topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing bottomPadding: Kirigami.Units.smallSpacing
leftPadding: lineNumberColumn.width + lineNumberColumn.anchors.leftMargin + Kirigami.Units.smallSpacing * 2 leftPadding: lineNumberColumn.width + lineNumberColumn.anchors.leftMargin + Kirigami.Units.smallSpacing * 2
@@ -100,15 +100,15 @@ Components.AbstractMaximizeComponent {
SyntaxHighlighter { SyntaxHighlighter {
property string definitionName: Repository.definitionForName(root.language).name property string definitionName: Repository.definitionForName(root.language).name
textEdit: definitionName == "None" ? null : codeTextEdit textEdit: definitionName == "None" ? null : codeText
definition: definitionName definition: definitionName
} }
ColumnLayout { ColumnLayout {
id: lineNumberColumn id: lineNumberColumn
anchors { anchors {
top: codeTextEdit.top top: codeText.top
topMargin: codeTextEdit.topPadding + 1 topMargin: codeText.topPadding + 1
left: codeTextEdit.left left: codeText.left
leftMargin: Kirigami.Units.smallSpacing leftMargin: Kirigami.Units.smallSpacing
} }
spacing: 0 spacing: 0
@@ -116,7 +116,7 @@ Components.AbstractMaximizeComponent {
id: repeater id: repeater
model: LineModel { model: LineModel {
id: lineModel id: lineModel
Component.onCompleted: setDocument(codeTextEdit.textDocument) document: codeText.textDocument
} }
delegate: QQC2.Label { delegate: QQC2.Label {
id: label id: label
@@ -150,6 +150,4 @@ Components.AbstractMaximizeComponent {
color: Kirigami.Theme.backgroundColor color: Kirigami.Theme.backgroundColor
} }
} }
onOpened: forceActiveFocus()
} }

View File

@@ -3,6 +3,8 @@
import QtQuick import QtQuick
import QtQuick.Controls as QQC2 import QtQuick.Controls as QQC2
import QtQuick.Layouts
import QtQml.Models
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.labs.components as KirigamiComponents import org.kde.kirigamiaddons.labs.components as KirigamiComponents

View File

@@ -13,21 +13,22 @@ Kirigami.PromptDialog {
required property NeoChatRoom room required property NeoChatRoom room
title: root.room.isSpace ? i18nc("@title:dialog", "Confirm Leaving Space") : i18nc("@title:dialog", "Confirm Leaving Room") title: i18nc("@title:dialog", "Confirm Leaving Room")
subtitle: root.room ? i18nc("Do you really want to leave <room name>?", "Do you really want to leave %1?", root.room.displayNameForHtml) : "" subtitle: root.room ? i18nc("Do you really want to leave <room name>?", "Do you really want to leave %1?", root.room.displayNameForHtml) : ""
dialogType: Kirigami.PromptDialog.Warning dialogType: Kirigami.PromptDialog.Warning
standardButtons: QQC2.Dialog.Cancel
onAccepted: root.room.forget() onRejected: {
root.close();
}
footer: QQC2.DialogButtonBox { footer: QQC2.DialogButtonBox {
standardButtons: QQC2.Dialog.Cancel
QQC2.Button { QQC2.Button {
text: i18nc("@action:button Leave this room/space", "Leave") text: i18nc("@action:button", "Leave Room")
icon.name: "arrow-left-symbolic"
onClicked: root.accept()
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
icon.name: "arrow-left-symbolic"
onClicked: root.room.forget();
} }
} }
} }

View File

@@ -15,16 +15,22 @@ Kirigami.PromptDialog {
title: i18nc("@title:dialog", "Sign out") title: i18nc("@title:dialog", "Sign out")
subtitle: i18n("Are you sure you want to sign out?") subtitle: i18n("Are you sure you want to sign out?")
dialogType: Kirigami.PromptDialog.Warning dialogType: Kirigami.PromptDialog.Warning
standardButtons: QQC2.Dialog.Cancel
onAccepted: root.connection.logout(true) onRejected: {
root.close();
}
footer: QQC2.DialogButtonBox { footer: QQC2.DialogButtonBox {
standardButtons: QQC2.Dialog.Cancel
QQC2.Button { QQC2.Button {
text: i18nc("@action:button", "Sign out") text: i18nc("@action:button", "Sign out")
onClicked: root.accept()
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
onClicked: {
root.connection.logout(true);
root.close();
root.accepted();
}
} }
} }
} }

View File

@@ -17,5 +17,12 @@ Kirigami.PromptDialog {
standardButtons: QQC2.DialogButtonBox.Open | QQC2.DialogButtonBox.Cancel standardButtons: QQC2.DialogButtonBox.Open | QQC2.DialogButtonBox.Cancel
onAccepted: Qt.openUrlExternally(root.link) onAccepted: {
Qt.openUrlExternally(root.link);
root.close();
}
onRejected: {
root.close();
}
} }

View File

@@ -39,7 +39,7 @@ Kirigami.Page {
icon.name: "document-edit" icon.name: "document-edit"
onTriggered: { onTriggered: {
root.room.setRoomState(root.type, root.stateKey, sourceTextArea.text); root.room.setRoomState(root.type, root.stateKey, sourceTextArea.text);
root.Kirigami.PageStack.closeDialog(); root.closeDialog();
} }
enabled: QmlUtils.isValidJson(sourceTextArea.text) enabled: QmlUtils.isValidJson(sourceTextArea.text)
} }
@@ -85,7 +85,7 @@ Kirigami.Page {
id: repeater id: repeater
model: LineModel { model: LineModel {
id: lineModel id: lineModel
Component.onCompleted: setDocument(sourceTextArea.textDocument) document: sourceTextArea.textDocument
} }
delegate: QQC2.Label { delegate: QQC2.Label {
id: label id: label

View File

@@ -41,11 +41,13 @@ ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
QQC2.Button { QQC2.Button {
anchors.bottom: parent.bottom
text: i18n("They match") text: i18n("They match")
icon.name: "dialog-ok" icon.name: "dialog-ok"
onClicked: root.accept() onClicked: root.accept()
} }
QQC2.Button { QQC2.Button {
anchors.bottom: parent.bottom
text: i18n("They don't match") text: i18n("They don't match")
icon.name: "dialog-cancel" icon.name: "dialog-cancel"
onClicked: root.reject() onClicked: root.reject()

View File

@@ -9,6 +9,8 @@ import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.delegates as Delegates import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.kirigamiaddons.labs.components as Components import org.kde.kirigamiaddons.labs.components as Components
import org.kde.neochat
Delegates.RoundedItemDelegate { Delegates.RoundedItemDelegate {
id: root id: root

View File

@@ -166,7 +166,7 @@ ColumnLayout {
} }
RowLayout { RowLayout {
visible: (root.currentRoom.connection as NeoChatConnection).canCheckMutualRooms visible: root.currentRoom.connection.canCheckMutualRooms
spacing: 0 spacing: 0
Layout.topMargin: Kirigami.Units.largeSpacing * 2 Layout.topMargin: Kirigami.Units.largeSpacing * 2

View File

@@ -1,8 +1,6 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org> // SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Controls as QQC2 import QtQuick.Controls as QQC2
import QtQuick.Layouts import QtQuick.Layouts
@@ -10,6 +8,7 @@ import QtQuick.Window
import QtQml import QtQml
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.neochat import org.kde.neochat
Kirigami.Page { Kirigami.Page {

View File

@@ -2,6 +2,8 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtLocation import QtLocation
import QtPositioning import QtPositioning
@@ -43,8 +45,6 @@ Components.AbstractMaximizeComponent {
} }
] ]
onOpened: forceActiveFocus()
PositionSource { PositionSource {
id: positionSource id: positionSource

View File

@@ -100,7 +100,7 @@ Kirigami.ApplicationWindow {
function onCurrentRoomChanged() { function onCurrentRoomChanged() {
if (RoomManager.currentRoom && root.pageStack.depth <= 1 && root.initialized && Kirigami.Settings.isMobile) { if (RoomManager.currentRoom && root.pageStack.depth <= 1 && root.initialized && Kirigami.Settings.isMobile) {
let roomPage = root.pageStack.push(Qt.createComponent('org.kde.neochat', 'RoomPage')); let roomPage = pageStack.push(Qt.createComponent('org.kde.neochat', 'RoomPage'));
roomPage.forceActiveFocus(); roomPage.forceActiveFocus();
roomPage.backRequested.connect(event => { roomPage.backRequested.connect(event => {
RoomManager.clearCurrentRoom(); RoomManager.clearCurrentRoom();
@@ -151,6 +151,8 @@ Kirigami.ApplicationWindow {
} }
contextDrawer: RoomDrawer { contextDrawer: RoomDrawer {
id: contextDrawer
// This is a memory for all user initiated actions on the drawer, i.e. clicking the button // This is a memory for all user initiated actions on the drawer, i.e. clicking the button
// It is used to ensure that user choice is remembered when changing pages and expanding and contracting the window width // It is used to ensure that user choice is remembered when changing pages and expanding and contracting the window width
property bool drawerUserState: NeoChatConfig.autoRoomInfoDrawer property bool drawerUserState: NeoChatConfig.autoRoomInfoDrawer
@@ -176,9 +178,9 @@ Kirigami.ApplicationWindow {
// Connect to the onClicked function of the RoomDrawer handle button // Connect to the onClicked function of the RoomDrawer handle button
Connections { Connections {
target: root.contextDrawer.handle.children[0] target: contextDrawer.handle.children[0]
function onClicked() { function onClicked() {
root.contextDrawer.drawerUserState = root.contextDrawer.drawerOpen; contextDrawer.drawerUserState = contextDrawer.drawerOpen;
} }
} }

View File

@@ -4,7 +4,6 @@
import QtQuick import QtQuick
import QtQuick.Window import QtQuick.Window
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard import org.kde.kirigamiaddons.formcard as FormCard
@@ -25,8 +24,6 @@ Kirigami.Dialog {
signal roomSelected(string roomId, string displayName, url avatarUrl, string alias, string topic, int memberCount, bool isJoined) signal roomSelected(string roomId, string displayName, url avatarUrl, string alias, string topic, int memberCount, bool isJoined)
title: i18nc("@title", "Manually Enter a Room") title: i18nc("@title", "Manually Enter a Room")
showCloseButton: false
standardButtons: QQC2.Dialog.Cancel
width: Math.min(root.Window.window.width, Kirigami.Units.gridUnit * 24) width: Math.min(root.Window.window.width, Kirigami.Units.gridUnit * 24)
leftPadding: 0 leftPadding: 0
@@ -34,26 +31,35 @@ Kirigami.Dialog {
topPadding: 0 topPadding: 0
bottomPadding: 0 bottomPadding: 0
onAccepted: { standardButtons: Kirigami.Dialog.Cancel
// We don't necessarily have all the info so fill out the best we can. customFooterActions: [
let roomId = roomIdAliasText.isAlias() ? "" : roomIdAliasText.text; Kirigami.Action {
let displayName = ""; enabled: roomIdAliasText.isValidText
let avatarUrl = ""; text: i18n("OK")
let alias = roomIdAliasText.isAlias() ? roomIdAliasText.text : ""; icon.name: "dialog-ok"
let topic = ""; onTriggered: {
let memberCount = -1; // We don't necessarily have all the info so fill out the best we can.
let isJoined = false; let roomId = roomIdAliasText.isAlias() ? "" : roomIdAliasText.text;
if (roomIdAliasText.room) { let displayName = "";
roomId = roomIdAliasText.room.id; let avatarUrl = "";
displayName = roomIdAliasText.room.displayName; let alias = roomIdAliasText.isAlias() ? roomIdAliasText.text : "";
avatarUrl = roomIdAliasText.room.avatarUrl.toString().length > 0 ? connection.makeMediaUrl(roomIdAliasText.room.avatarUrl) : ""; let topic = "";
alias = roomIdAliasText.room.canonicalAlias; let memberCount = -1;
topic = roomIdAliasText.room.topic; let isJoined = false;
memberCount = roomIdAliasText.room.joinedCount; if (roomIdAliasText.room) {
isJoined = true; roomId = roomIdAliasText.room.id;
displayName = roomIdAliasText.room.displayName;
avatarUrl = roomIdAliasText.room.avatarUrl.toString().length > 0 ? connection.makeMediaUrl(roomIdAliasText.room.avatarUrl) : "";
alias = roomIdAliasText.room.canonicalAlias;
topic = roomIdAliasText.room.topic;
memberCount = roomIdAliasText.room.joinedCount;
isJoined = true;
}
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined);
root.close();
}
} }
root.roomSelected(roomId, displayName, avatarUrl, alias, topic, memberCount, isJoined); ]
}
contentItem: ColumnLayout { contentItem: ColumnLayout {
spacing: 0 spacing: 0
@@ -66,9 +72,9 @@ Kirigami.Dialog {
return null; return null;
} }
if (isAlias()) { if (isAlias()) {
return root.connection.roomByAlias(text) as NeoChatRoom; return root.connection.roomByAlias(text);
} else { } else {
return root.connection.room(text) as NeoChatRoom; return root.connection.room(text);
} }
} }
@@ -104,16 +110,4 @@ Kirigami.Dialog {
roomIdAliasText.forceActiveFocus(); roomIdAliasText.forceActiveFocus();
timer.restart(); timer.restart();
} }
footer: QQC2.DialogButtonBox {
QQC2.Button {
text: i18nc("@action:button Join this room/space", "Join")
icon.name: "checkmark"
enabled: roomIdAliasText.isValidText
onClicked: root.accept()
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
}
}
} }

View File

@@ -24,16 +24,25 @@ Kirigami.Dialog {
signal userSelected(string userId) signal userSelected(string userId)
title: i18nc("@title", "User ID") title: i18nc("@title", "User ID")
showCloseButton: false
width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24) width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24)
leftPadding: 0 leftPadding: 0
rightPadding: 0 rightPadding: 0
topPadding: 0 topPadding: 0
bottomPadding: 0 bottomPadding: 0
standardButtons: QQC2.Dialog.Cancel
onAccepted: root.userSelected(userIdText.text) standardButtons: Kirigami.Dialog.Cancel
customFooterActions: [
Kirigami.Action {
enabled: userIdText.isValidText
text: i18n("OK")
icon.name: "dialog-ok"
onTriggered: {
root.userSelected(userIdText.text)
root.accept();
}
}
]
contentItem: ColumnLayout { contentItem: ColumnLayout {
spacing: 0 spacing: 0
@@ -70,16 +79,4 @@ Kirigami.Dialog {
userIdText.forceActiveFocus(); userIdText.forceActiveFocus();
timer.restart(); timer.restart();
} }
footer: QQC2.DialogButtonBox {
QQC2.Button {
text: i18nc("@action:button Perform an action with this user ID", "Ok")
icon.name: "checkmark"
enabled: userIdText.isValidText
onClicked: root.accept()
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
}
}
} }

View File

@@ -1,10 +1,7 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com> // SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
@@ -15,16 +12,11 @@ Kirigami.PromptDialog {
title: hasExistingMeeting ? i18nc("@title", "Join Meeting") : i18nc("@title", "Start Meeting") title: hasExistingMeeting ? i18nc("@title", "Join Meeting") : i18nc("@title", "Start Meeting")
subtitle: hasExistingMeeting ? i18nc("@info:label", "You are about to join a Jitsi meeting in your web browser.") : i18nc("@info:label", "You are about to start a new Jitsi meeting in your web browser.") subtitle: hasExistingMeeting ? i18nc("@info:label", "You are about to join a Jitsi meeting in your web browser.") : i18nc("@info:label", "You are about to start a new Jitsi meeting in your web browser.")
standardButtons: QQC2.Dialog.Cancel standardButtons: Kirigami.Dialog.Cancel
footer: QQC2.DialogButtonBox { customFooterActions: Kirigami.Action {
QQC2.Button { icon.name: "camera-video-symbolic"
icon.name: "camera-video-symbolic" text: hasExistingMeeting ? i18nc("@action:button Join the Jitsi meeting", "Join") : i18nc("@action:button Start a new Jitsi meeting", "Start")
text: root.hasExistingMeeting ? i18nc("@action:button Join the Jitsi meeting", "Join") : i18nc("@action:button Start a new Jitsi meeting", "Start") onTriggered: root.accept()
onClicked: root.accept()
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
}
} }
} }

View File

@@ -103,7 +103,7 @@ Kirigami.Page {
id: repeater id: repeater
model: LineModel { model: LineModel {
id: lineModel id: lineModel
Component.onCompleted: setDocument(sourceTextArea.textDocument) document: sourceTextArea.textDocument
} }
delegate: QQC2.Label { delegate: QQC2.Label {
id: label id: label

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