Compare commits

..

1 Commits

Author SHA1 Message Date
Joshua Goins
8ff23239f7 Render placeholder avatars in notifications, like we do in app
Right now if someone or a room doesn't have an avatar, there's a good
chance it will end up falling back to the NeoChat app icon (not very
descriptive.) It also tends to break the flow of conversations, that's
the whole reason to have an avatar in the notification in the first
place.

So I made it render a similar-looking avatar like it does in-app, by
re-implementing it in QPainter. I also took the time to refactor and
clean up the avatar generation for notifications so the logic should be
easier to follow.

To reduce the maintenance required, we re-use some functionality from
Kirigami Add-ons. The rest of the drawing logic is custom but sensible,
but some creative liberty was used to ensure it looks decent.
2026-01-17 12:18:34 -05:00
147 changed files with 18028 additions and 21296 deletions

View File

@@ -43,4 +43,3 @@ Options:
per-test-timeout: 90
require-passing-tests-on: ['Linux', 'Android', 'FreeBSD', 'Windows']
run-qmllint: True
enable-lsan: True

View File

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

View File

@@ -11,7 +11,7 @@ add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" )
ecm_add_test(
neochatroomtest.cpp
LINK_LIBRARIES neochat Qt::Test Qt::HttpServer neochat_server
LINK_LIBRARIES neochat Qt::Test
TEST_NAME neochatroomtest
)
@@ -41,7 +41,7 @@ ecm_add_test(
ecm_add_test(
chatbarcachetest.cpp
LINK_LIBRARIES neochat Qt::Test Qt::HttpServer neochat_server
LINK_LIBRARIES neochat Qt::Test
TEST_NAME chatbarcachetest
)
@@ -104,9 +104,3 @@ ecm_add_test(
LINK_LIBRARIES neochat Qt::Test neochat_server
TEST_NAME roommanagertest
)
ecm_add_test(
modeltest.cpp
LINK_LIBRARIES neochat Qt::Test neochat_server Devtools
TEST_NAME modeltest
)

View File

@@ -88,7 +88,7 @@ void ActionsTest::testActions()
QFETCH(std::optional<QString>, resultText);
QFETCH(std::optional<Quotient::RoomMessageEvent::MsgType>, type);
auto cache = new ChatBarCache(this);
auto cache = new ChatBarCache();
cache->setText(command);
auto result = ActionsModel::handleAction(room, cache);
QCOMPARE(resultText, std::get<std::optional<QString>>(result));

View File

@@ -11,13 +11,9 @@
#include <Quotient/syncdata.h>
#include <qtestcase.h>
#include <KLocalizedString>
#include "accountmanager.h"
#include "chatbarcache.h"
#include "neochatroom.h"
#include "server.h"
#include "testutils.h"
using namespace Quotient;
@@ -28,9 +24,7 @@ class ChatBarCacheTest : public QObject
private:
Connection *connection = nullptr;
NeoChatRoom *room = nullptr;
Server server;
QString eventId;
TestUtils::TestRoom *room = nullptr;
private Q_SLOTS:
void initTestCase();
@@ -46,31 +40,8 @@ private Q_SLOTS:
void ChatBarCacheTest::initTestCase()
{
Connection::setRoomType<NeoChatRoom>();
server.start();
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
auto accountManager = new AccountManager(true, this);
QSignalSpy spy(accountManager, &AccountManager::connectionAdded);
connection = dynamic_cast<NeoChatConnection *>(accountManager->accounts()->front());
const auto roomId = server.createRoom(u"@user:localhost:1234"_s);
eventId = server.sendEvent(roomId,
u"m.room.message"_s,
QJsonObject{
{u"body"_s, u"foo"_s},
{u"msgtype"_s, u"m.text"_s},
});
QSignalSpy syncSpy(connection, &Connection::syncDone);
// We need to wait for two syncs, as the next one won't have the changes yet
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
room = dynamic_cast<NeoChatRoom *>(connection->room(roomId));
QVERIFY(room);
server.joinUser(room->id(), u"@foo:server.com"_s);
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
connection = Connection::makeMockConnection(u"@bob:kde.org"_s);
room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, "test-min-sync.json"_L1);
}
void ChatBarCacheTest::empty()
@@ -89,9 +60,8 @@ void ChatBarCacheTest::empty()
void ChatBarCacheTest::noRoom()
{
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache());
chatBarCache->setReplyId(eventId);
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.
@@ -105,10 +75,9 @@ void ChatBarCacheTest::noRoom()
void ChatBarCacheTest::badParent()
{
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
QScopedPointer<QObject> badParent(new QObject());
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(badParent.get()));
chatBarCache->setReplyId(eventId);
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.
@@ -125,15 +94,15 @@ void ChatBarCacheTest::reply()
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(u"some text"_s);
chatBarCache->setAttachmentPath(u"some/path"_s);
chatBarCache->setReplyId(eventId);
chatBarCache->setReplyId(u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->text(), u"some text"_s);
QCOMPARE(chatBarCache->isReplying(), true);
QCOMPARE(chatBarCache->replyId(), eventId);
QCOMPARE(chatBarCache->replyId(), u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@foo:server.com"_s));
QCOMPARE(chatBarCache->relationMessage(), u"foo"_s);
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@example:example.org"_s));
QCOMPARE(chatBarCache->relationMessage(), u"This is an example\ntext message"_s);
QCOMPARE(chatBarCache->attachmentPath(), QString());
QCOMPARE(chatBarCache->relationAuthorIsPresent(), true);
}
@@ -143,26 +112,22 @@ void ChatBarCacheTest::replyMissingUser()
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(u"some text"_s);
chatBarCache->setAttachmentPath(u"some/path"_s);
chatBarCache->setReplyId(eventId);
chatBarCache->setReplyId(u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->text(), u"some text"_s);
QCOMPARE(chatBarCache->isReplying(), true);
QCOMPARE(chatBarCache->replyId(), eventId);
QCOMPARE(chatBarCache->replyId(), u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->isEditing(), false);
QCOMPARE(chatBarCache->editId(), QString());
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@foo:server.com"_s));
QCOMPARE(chatBarCache->relationMessage(), u"foo"_s);
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@example:example.org"_s));
QCOMPARE(chatBarCache->relationMessage(), u"This is an example\ntext message"_s);
QCOMPARE(chatBarCache->attachmentPath(), QString());
QCOMPARE(chatBarCache->relationAuthorIsPresent(), true);
QSignalSpy relationAuthorIsPresentSpy(chatBarCache.get(), &ChatBarCache::relationAuthorIsPresentChanged);
// sync again, which will simulate the reply user leaving the room
QSignalSpy syncSpy(connection, &Connection::syncDone);
server.sendStateEvent(room->id(), u"m.room.member"_s, u"@foo:server.com"_s, {{u"membership"_s, u"leave"_s}});
QVERIFY(syncSpy.wait());
QVERIFY(syncSpy.wait());
room->syncNewEvents(u"test-min-sync-extra-sync.json"_s);
QTRY_COMPARE(relationAuthorIsPresentSpy.count(), 1);
QCOMPARE(chatBarCache->relationAuthorIsPresent(), false);
@@ -174,19 +139,19 @@ void ChatBarCacheTest::edit()
chatBarCache->setText(u"some text"_s);
chatBarCache->setAttachmentPath(u"some/path"_s);
connect(chatBarCache.get(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) {
connect(chatBarCache.get(), &ChatBarCache::relationIdChanged, this, [](const QString &oldEventId, const QString &newEventId) {
QCOMPARE(oldEventId, QString());
QCOMPARE(newEventId, eventId);
QCOMPARE(newEventId, QString(u"$153456789:example.org"_s));
});
chatBarCache->setEditId(eventId);
chatBarCache->setEditId(u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->text(), u"some text"_s);
QCOMPARE(chatBarCache->isReplying(), false);
QCOMPARE(chatBarCache->replyId(), QString());
QCOMPARE(chatBarCache->isEditing(), true);
QCOMPARE(chatBarCache->editId(), eventId);
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@foo:server.com"_s));
QCOMPARE(chatBarCache->relationMessage(), u"foo"_s);
QCOMPARE(chatBarCache->editId(), u"$153456789:example.org"_s);
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@example:example.org"_s));
QCOMPARE(chatBarCache->relationMessage(), u"This is an example\ntext message"_s);
QCOMPARE(chatBarCache->attachmentPath(), QString());
}
@@ -194,7 +159,7 @@ void ChatBarCacheTest::attachment()
{
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
chatBarCache->setText(u"some text"_s);
chatBarCache->setEditId(eventId);
chatBarCache->setEditId(u"$153456789:example.org"_s);
chatBarCache->setAttachmentPath(u"some/path"_s);
QCOMPARE(chatBarCache->text(), u"some text"_s);

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

View File

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

View File

@@ -1,620 +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/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, nullptr, 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 = new ThreadModel(eventId, room);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
}
void ModelTest::testThreadFetchModel()
{
auto model = new ThreadFetchModel(new ThreadModel(eventId, room));
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
}
void ModelTest::testThreadChatBarModel()
{
auto model = new ThreadChatBarModel(new ThreadModel(eventId, room), 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->setRoom(room);
model->setAutoCompletionType(CompletionModel::Room);
model->setText(u"foo"_s, u"#foo"_s);
auto roomListModel = new RoomListModel(this);
roomListModel->setConnection(connection);
model->setRoomListModel(roomListModel);
}
void ModelTest::testRoomListModel()
{
auto model = new RoomListModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
}
void ModelTest::testCommonRoomsModel()
{
auto model = new CommonRoomsModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
model->setUserId(u"@user:example.com"_s);
}
void ModelTest::testNotificationsModel()
{
auto model = new NotificationsModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
}
void ModelTest::testLocationsModel()
{
auto model = new LocationsModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setRoom(room);
}
void ModelTest::testServerListModel()
{
auto model = new ServerListModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
}
void ModelTest::testEmojiModel()
{
auto tester = new QAbstractItemModelTester(&EmojiModel::instance(), &EmojiModel::instance());
tester->setUseFetchMore(true);
}
void ModelTest::testCustomEmojiModel()
{
auto tester = new QAbstractItemModelTester(&CustomEmojiModel::instance(), &CustomEmojiModel::instance());
tester->setUseFetchMore(true);
CustomEmojiModel::instance().setConnection(connection);
}
void ModelTest::testPushRuleModel()
{
auto model = new PushRuleModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
}
void ModelTest::testActionsModel()
{
auto tester = new QAbstractItemModelTester(&ActionsModel::instance(), &ActionsModel::instance());
tester->setUseFetchMore(true);
}
void ModelTest::testDevicesModel()
{
auto model = new DevicesModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
}
void ModelTest::testUserDirectoryListModel()
{
auto model = new UserDirectoryListModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
model->setSearchText(u"foo"_s);
}
void ModelTest::testAccountEmoticonModel()
{
auto model = new AccountEmoticonModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setConnection(connection);
}
void ModelTest::testPermissionsModel()
{
auto model = new PermissionsModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setRoom(room);
}
void ModelTest::testLiveLocationsModel()
{
auto model = new LiveLocationsModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setRoom(room);
}
void ModelTest::testRoomSortParameterModel()
{
auto model = new RoomSortParameterModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
}
void ModelTest::testSortFilterRoomTreeModel()
{
auto sourceModel = new RoomTreeModel(this);
auto model = new SortFilterRoomTreeModel(sourceModel, sourceModel);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
sourceModel->setConnection(connection);
}
void ModelTest::testSortFilterSpaceListModel()
{
auto sourceModel = new RoomListModel(this);
auto model = new SortFilterSpaceListModel(sourceModel, sourceModel);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
sourceModel->setConnection(connection);
}
void ModelTest::testSortFilterRoomListModel()
{
auto sourceModel = new RoomListModel(this);
auto model = new SortFilterRoomListModel(sourceModel, sourceModel);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
sourceModel->setConnection(connection);
}
void ModelTest::testSpaceChildSortFilterModel()
{
auto model = new SpaceChildSortFilterModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
auto spaceChildrenModel = new SpaceChildrenModel(this);
model->setSourceModel(spaceChildrenModel);
spaceChildrenModel->setSpace(nullptr);
}
void ModelTest::testStateFilterModel()
{
auto model = new StateFilterModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
auto stateModel = new StateModel(this);
model->setSourceModel(stateModel);
stateModel->setRoom(room);
}
void ModelTest::testMessageContentFilterModel()
{
auto model = new MessageContentFilterModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setSourceModel(ContentProvider::self().contentModelForEvent(room, eventId));
}
void ModelTest::testUserFilterModel()
{
auto model = new UserFilterModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
auto userListModel = new UserListModel(this);
model->setSourceModel(userListModel);
userListModel->setRoom(room);
}
void ModelTest::testEmoticonFilterModel()
{
auto model = new EmoticonFilterModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
auto accountEmoticonModel = new AccountEmoticonModel(this);
model->setSourceModel(accountEmoticonModel);
model->setShowEmojis(true);
model->setShowStickers(true);
accountEmoticonModel->setConnection(connection);
}
void ModelTest::testDevicesProxyModel()
{
auto model = new DevicesProxyModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
auto devicesModel = new DevicesModel(this);
model->setSourceModel(devicesModel);
devicesModel->setConnection(dynamic_cast<NeoChatConnection *>(connection));
}
void ModelTest::testCompletionProxyModel()
{
auto model = new CompletionProxyModel(this);
auto tester = new QAbstractItemModelTester(model, model);
tester->setUseFetchMore(true);
model->setSourceModel(&EmojiModel::instance());
}
QTEST_MAIN(ModelTest)
#include "modeltest.moc"

View File

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

View File

@@ -127,7 +127,7 @@ void Server::start()
qFatal() << "Server failed to listen on a port.";
return;
} else {
qInfo() << "Server listening";
qWarning() << "Server listening";
}
}
@@ -203,25 +203,6 @@ QString Server::sendEvent(const QString &roomId, const QString &eventType, const
return eventId;
}
QString Server::sendStateEvent(const QString &roomId, const QString &eventType, const QString &stateKey, const QJsonObject &content)
{
Changes changes;
const auto eventId = generateEventId();
const auto json = QJsonObject{{u"type"_s, eventType},
{u"content"_s, content},
{u"sender"_s, u"@foo:server.com"_s},
{u"event_id"_s, eventId},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, roomId},
{u"state_key"_s, stateKey}};
changes.events += Changes::Event{
.fullJson = json,
};
changes.stateEvents += Changes::Event{.fullJson = json};
m_state += changes;
return eventId;
}
void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &responder)
{
QJsonObject joinRooms;
@@ -353,18 +334,6 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
}
}
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 : m_state.mid(token)) {
for (const auto &event : change.events) {
// TODO the room might be in a different join state.
@@ -397,5 +366,6 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
syncData[u"rooms"_s] = rooms;
}
qWarning() << syncData;
responder.write(QJsonDocument(syncData), QHttpServerResponder::StatusCode::Ok);
}

View File

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

View File

@@ -193,7 +193,6 @@
<li xml:lang="ar">التصويت - MSC3381</li>
<li xml:lang="ca">Votacions - MSC3381</li>
<li xml:lang="ca-valencia">Votacions - MSC3381</li>
<li xml:lang="de">Umfragen MSC3381</li>
<li xml:lang="el">Δημοσκοπήσεις - MSC3381</li>
<li xml:lang="en-GB">Polls - MSC3381</li>
<li xml:lang="eo">Enketoj - MSC3381</li>
@@ -228,7 +227,6 @@
<li xml:lang="ar">حزم الملصقات - MSC2545</li>
<li xml:lang="ca">Paquets d'adhesius - MSC2545</li>
<li xml:lang="ca-valencia">Paquets d'adhesius - MSC2545</li>
<li xml:lang="de">Sticker-Pakete MSC2545</li>
<li xml:lang="el">Πακέτα αυτοκόλλητων - MSC2545</li>
<li xml:lang="en-GB">Sticker Packs - MSC2545</li>
<li xml:lang="eo">Glumark-Pakoj - MSC2545</li>
@@ -489,7 +487,6 @@
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="25.12.2" date="2026-02-05"/>
<release version="25.12.1" date="2026-01-08"/>
<release version="25.12.0" date="2025-12-11"/>
<release version="25.08.3" date="2025-11-06"/>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -220,6 +220,7 @@ target_link_libraries(neochat PUBLIC
KF6::ItemModels
KF6::I18nQml
KirigamiApp
KirigamiAddonsComponents
QuotientQt6
Login
Rooms

View File

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

View File

@@ -11,6 +11,7 @@
#include <KNotification>
#include <KNotificationPermission>
#include <KNotificationReplyAction>
#include <KirigamiAddons/Components/NameUtils>
#include <QPainter>
#include <Quotient/accountregistry.h>
@@ -38,7 +39,7 @@ NotificationsManager::NotificationsManager(QObject *parent)
{
}
void NotificationsManager::handleNotifications(const QPointer<NeoChatConnection> &connection)
void NotificationsManager::handleNotifications(QPointer<NeoChatConnection> connection)
{
if (KNotificationPermission::checkPermission() == Qt::PermissionStatus::Granted) {
startNotificationJob(connection);
@@ -68,7 +69,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()) {
return;
@@ -82,7 +83,8 @@ void NotificationsManager::processNotificationJob(const QPointer<NeoChatConnecti
if (!m_initialTimestamp.contains(connectionId)) {
m_initialTimestamp[connectionId] = notification["ts"_L1].toVariant().toLongLong();
} 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;
}
}
@@ -143,45 +145,38 @@ void NotificationsManager::processNotificationJob(const QPointer<NeoChatConnecti
body = notification["event"_L1]["content"_L1]["body"_L1].toString();
}
QImage avatar_image;
if (!sender.avatarUrl().isEmpty()) {
avatar_image = room->member(sender.id()).avatar(128, 128, {});
} else {
avatar_image = room->avatar(128);
}
postNotification(dynamic_cast<NeoChatRoom *>(room),
sender.displayName(),
room->member(sender.id()),
body,
avatar_image,
notification["event"_L1].toObject()["event_id"_L1].toString(),
true,
pair.first);
}
}
bool NotificationsManager::shouldPostNotification(const QPointer<NeoChatConnection> &connection, const QJsonValue &notification)
bool NotificationsManager::shouldPostNotification(QPointer<NeoChatConnection> connection, const QJsonValue &notification)
{
if (connection == nullptr || !connection->isLoggedIn()) {
return false;
}
const auto room = connection->room(notification["room_id"_L1].toString());
auto room = connection->room(notification["room_id"_L1].toString());
if (room == nullptr) {
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.
// 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.
if (RoomManager::instance().currentRoom() && room->id() == RoomManager::instance().currentRoom()->id()
&& QGuiApplication::applicationState() == Qt::ApplicationActive) {
bool isCurrentRoom = RoomManager::instance().currentRoom() && room->id() == RoomManager::instance().currentRoom()->id();
if (isCurrentRoom && QGuiApplication::applicationState() == Qt::ApplicationActive) {
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.
const auto timestamp = notification["ts"_L1].toDouble();
qint64 timestamp = notification["ts"_L1].toDouble();
if (timestamp < m_initialTimestamp[connection->user()->id()]) {
return false;
}
@@ -194,11 +189,10 @@ bool NotificationsManager::shouldPostNotification(const QPointer<NeoChatConnecti
}
void NotificationsManager::postNotification(NeoChatRoom *room,
const QString &sender,
const RoomMember &member,
const QString &text,
const QImage &icon,
const QString &replyEventId,
const bool canReply,
bool canReply,
qint64 timestamp)
{
const QString roomId = room->id();
@@ -221,11 +215,11 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
if (room->isDirectChat()) {
entry = text.toHtmlEscaped();
} else {
entry = i18n("%1: %2", sender, text.toHtmlEscaped());
entry = i18n("%1: %2", member.displayName(), text.toHtmlEscaped());
}
notification->setText(entry);
notification->setPixmap(createNotificationImage(icon, room));
notification->setPixmap(createNotificationImage(member, room));
auto defaultAction = notification->addDefaultAction(i18n("Open NeoChat in this room"));
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
@@ -270,8 +264,10 @@ void NotificationsManager::postInviteNotification(NeoChatRoom *rawRoom)
if (NeoChatConfig::rejectUnknownInvites()) {
auto job = room->connection()->callApi<NeochatGetCommonRoomsJob>(roomMemberEvent->senderId());
connect(job, &BaseJob::result, this, [this, job, room] {
if (QJsonObject replyData = job->jsonData(); replyData.contains(u"joined"_s)) {
if (!replyData["joined"_L1].toArray().isEmpty()) {
QJsonObject replyData = job->jsonData();
if (replyData.contains(u"joined"_s)) {
const bool inAnyOfOurRooms = !replyData["joined"_L1].toArray().isEmpty();
if (inAnyOfOurRooms) {
doPostInviteNotification(room);
} else {
room->forget();
@@ -283,7 +279,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());
if (roomMemberEvent == nullptr) {
@@ -291,19 +287,11 @@ void NotificationsManager::doPostInviteNotification(const QPointer<NeoChatRoom>
}
const auto sender = room->member(roomMemberEvent->senderId());
QImage avatar_image;
if (!room->member(roomMemberEvent->senderId()).avatarUrl().isEmpty()) {
avatar_image = room->member(roomMemberEvent->senderId()).avatar(128, 128, {});
} else {
qWarning() << "using this room's avatar";
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->setTitle(room->displayName());
notification->setPixmap(createNotificationImage(avatar_image, nullptr));
const auto defaultAction = notification->addDefaultAction(i18n("Open this invitation in NeoChat"));
notification->setPixmap(createNotificationImage(sender, room));
auto defaultAction = notification->addDefaultAction(i18n("Open this invitation in NeoChat"));
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
if (!room) {
return;
@@ -364,9 +352,11 @@ void NotificationsManager::postPushNotification(const QByteArray &message)
{
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
if (const auto type = json["notification"_L1]["type"_L1].toString(); type == u"m.room.message"_s || type == u"m.room.encrypted"_s) {
const auto notification = new KNotification("message"_L1);
if (type == u"m.room.message"_s || type == u"m.room.encrypted"_s) {
auto notification = new KNotification("message"_L1);
const auto sender = json["notification"_L1]["sender_display_name"_L1].toString();
const auto roomName = json["notification"_L1]["room_name"_L1].toString();
@@ -386,13 +376,13 @@ void NotificationsManager::postPushNotification(const QByteArray &message)
}
#ifdef HAVE_KIO
const auto openAction = notification->addAction(i18n("Open NeoChat"));
auto openAction = notification->addAction(i18n("Open NeoChat"));
connect(openAction, &KNotificationAction::activated, notification, [=]() {
QString properId = roomId;
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->start();
});
@@ -406,40 +396,125 @@ void NotificationsManager::postPushNotification(const QByteArray &message)
}
}
QPixmap NotificationsManager::createNotificationImage(const QImage &icon, NeoChatRoom *room)
QPixmap NotificationsManager::createNotificationImage(const Quotient::RoomMember &member, NeoChatRoom *room)
{
QImage senderIcon = member.avatar(avatarDimension, avatarDimension, {});
bool senderIconIsPlaceholder = false;
if (senderIcon.isNull()) {
senderIcon = createPlaceholderImage(member.displayName());
senderIconIsPlaceholder = true;
}
QImage icon;
if (room->isDirectChat()) {
icon = senderIcon;
} else {
QImage roomIcon = room->avatar(avatarDimension, avatarDimension);
bool roomIconIsPlaceholder = false;
if (roomIcon.isNull()) {
roomIcon = createPlaceholderImage(room->displayName());
roomIconIsPlaceholder = true;
}
icon = createCombinedNotificationImage(senderIcon, senderIconIsPlaceholder, roomIcon, roomIconIsPlaceholder);
}
return QPixmap::fromImage(icon);
}
QImage NotificationsManager::createCombinedNotificationImage(const QImage &senderIcon,
const bool senderIconIsPlaceholder,
const QImage &roomIcon,
const bool roomIconIsPlaceholder)
{
// Handle avatars that are lopsided in one dimension
const int biggestDimension = std::max(icon.width(), icon.height());
const QRect imageRect{0, 0, biggestDimension, biggestDimension};
const int biggestDimension = std::max(senderIcon.width(), senderIcon.height());
const QRectF imageRect = QRect{0, 0, biggestDimension, biggestDimension}.toRectF();
QImage roundedImage(imageRect.size(), QImage::Format_ARGB32);
QImage roundedImage(imageRect.size().toSize(), QImage::Format_ARGB32);
roundedImage.fill(Qt::transparent);
QPainter painter(&roundedImage);
painter.setRenderHint(QPainter::SmoothPixmapTransform);
painter.setPen(Qt::NoPen);
// Fill background for transparent avatars
painter.setBrush(Qt::white);
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
if (senderIconIsPlaceholder) {
painter.drawImage(imageRect, senderIcon);
} else {
// Fill background for transparent non-placeholder avatars
painter.setBrush(Qt::white);
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
const QBrush brush(icon.scaledToHeight(biggestDimension));
painter.setBrush(brush);
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
if (room) {
if (const auto roomAvatar = room->avatar(imageRect.width(), imageRect.height()); !roomAvatar.isNull() && icon != roomAvatar) {
const QRect lowerQuarter{imageRect.center(), imageRect.size() / 2};
painter.setBrush(Qt::white);
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
painter.setBrush(roomAvatar.scaled(lowerQuarter.size()));
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
}
painter.setBrush(senderIcon.scaledToHeight(biggestDimension));
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
}
return QPixmap::fromImage(roundedImage);
const QRectF lowerQuarter{imageRect.center(), imageRect.size() / 2.0};
if (roomIconIsPlaceholder) {
// Ditto for room icons, but we also want to "carve out" the transparent area for readability
painter.setCompositionMode(QPainter::CompositionMode_Clear);
painter.setBrush(Qt::transparent);
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
painter.drawImage(lowerQuarter, roomIcon);
} else {
painter.setBrush(Qt::white);
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
painter.setBrush(roomIcon.scaled(lowerQuarter.size().toSize()));
painter.drawRoundedRect(lowerQuarter, lowerQuarter.width(), lowerQuarter.height());
}
return roundedImage;
}
QImage NotificationsManager::createPlaceholderImage(const QString &name)
{
const QColor color = NameUtils().colorsFromString(name);
QImage image(avatarDimension, avatarDimension, QImage::Format_ARGB32);
image.fill(Qt::transparent);
QPainter painter(&image);
painter.setRenderHint(QPainter::Antialiasing);
// Draw background
QColor backgroundColor = color;
backgroundColor.setAlphaF(0.07); // Same as in Kirigami Add-ons.
painter.setBrush(backgroundColor);
painter.setPen(Qt::transparent);
painter.drawRoundedRect(image.rect(), image.width(), image.height());
constexpr float borderWidth = 3.0; // Slightly bigger than in Add-ons so it renders better with QPainter at these dimensions.
// Draw border
painter.setBrush(Qt::transparent);
painter.setPen(QPen(color, borderWidth));
painter.drawRoundedRect(image.rect().toRectF().marginsRemoved(QMarginsF(borderWidth, borderWidth, borderWidth, borderWidth)),
image.width(),
image.height());
const QString initials = NameUtils().initialsFromString(name);
QTextOption option;
option.setAlignment(Qt::AlignCenter);
// Calculation similar to the one found in Kirigami Add-ons.
constexpr int largeSpacing = 8; // Same as what's defined in kirigami.
constexpr int padding = std::max(0, std::min(largeSpacing, avatarDimension - largeSpacing * 2));
QFont font;
font.setPixelSize((avatarDimension - padding) / 2);
painter.setBrush(color);
painter.setPen(color);
painter.setFont(font);
painter.drawText(image.rect(), initials, option);
return image;
}
#include "moc_notificationsmanager.cpp"

View File

@@ -13,6 +13,11 @@
#include <Quotient/csapi/notifications.h>
#include <Quotient/jobs/basejob.h>
namespace Quotient
{
class RoomMember;
}
class NeoChatConnection;
class KNotification;
class NeoChatRoom;
@@ -58,7 +63,7 @@ public:
/**
* @brief Handle the notifications for the given connection.
*/
void handleNotifications(const QPointer<NeoChatConnection> &connection);
void handleNotifications(QPointer<NeoChatConnection> connection);
private:
QHash<QString, qint64> m_initialTimestamp;
@@ -67,23 +72,34 @@ private:
QStringList m_connActiveJob;
void startNotificationJob(QPointer<NeoChatConnection> connection);
static QPixmap createNotificationImage(const QImage &icon, NeoChatRoom *room);
bool shouldPostNotification(const QPointer<NeoChatConnection> &connection, const QJsonValue &notification);
void postNotification(NeoChatRoom *room,
const QString &sender,
const QString &text,
const QImage &icon,
const QString &replyEventId,
bool canReply,
qint64 timestamp);
/**
* @return A combined image of the sender and room's avatar.
*/
static QPixmap createNotificationImage(const Quotient::RoomMember &member, NeoChatRoom *room);
void doPostInviteNotification(const QPointer<NeoChatRoom> &room);
/**
* @return The sender and room icon combined together into one image. Used internally by createNotificationImage.
*/
static QImage createCombinedNotificationImage(const QImage &senderIcon, bool senderIconIsPlaceholder, const QImage &roomIcon, bool roomIconIsPlaceholder);
/**
* @return A placeholder avatar image, similar to the one found in Kirigami Add-ons.
*/
static QImage createPlaceholderImage(const QString &name);
bool shouldPostNotification(QPointer<NeoChatConnection> connection, const QJsonValue &notification);
void
postNotification(NeoChatRoom *room, const Quotient::RoomMember &member, const QString &text, const QString &replyEventId, bool canReply, qint64 timestamp);
void doPostInviteNotification(QPointer<NeoChatRoom> room);
QHash<QString, std::pair<qint64, KNotification *>> m_notifications;
QHash<QString, QPointer<KNotification>> m_invitations;
bool permissionAsked = false;
static constexpr int avatarDimension = 128;
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

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

View File

@@ -20,9 +20,9 @@ Components.AbstractMaximizeComponent {
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.
@@ -64,7 +64,7 @@ Components.AbstractMaximizeComponent {
}
QQC2.Label {
id: dateTimeLabel
text: root.dateTime.relativeDateTime
text: root.time.toLocaleString(Qt.locale(), Locale.ShortFormat)
color: Kirigami.Theme.disabledTextColor
elide: Text.ElideRight
}
@@ -116,7 +116,7 @@ Components.AbstractMaximizeComponent {
id: repeater
model: LineModel {
id: lineModel
Component.onCompleted: setDocument(codeText.textDocument)
document: codeText.textDocument
}
delegate: QQC2.Label {
id: label

View File

@@ -13,7 +13,7 @@ Kirigami.PromptDialog {
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) : ""
dialogType: Kirigami.PromptDialog.Warning

View File

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

View File

@@ -72,9 +72,9 @@ Kirigami.Dialog {
return null;
}
if (isAlias()) {
return root.connection.roomByAlias(text) as NeoChatRoom;
return root.connection.roomByAlias(text);
} else {
return root.connection.room(text) as NeoChatRoom;
return root.connection.room(text);
}
}

View File

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

View File

@@ -55,10 +55,10 @@ Kirigami.Page {
formats: Prison.Format.QRCode | Prison.Format.Aztec
onResultChanged: {
if (result.text.length > 0 && result.text != scanner.previousText) {
root.Kirigami.PageStack.closeDialog();
RoomManager.resolveResource(result.text, "qr");
scanner.previousText = result.text;
}
root.closeDialog();
}
videoSink: viewFinder.videoSink
}

View File

@@ -22,7 +22,7 @@ Kirigami.SearchDialog {
}
onAccepted: if (currentItem) {
(root.currentItem as RoomDelegate).clicked();
(currentItem as QQC2.ItemDelegate).clicked();
}
onTextChanged: RoomManager.sortFilterRoomListModel.filterText = text

View File

@@ -75,8 +75,6 @@ Kirigami.Page {
focus: true
padding: 0
background: null // This needs to stay null, because of transparency blur
onHeightChanged: {
// HACK: See TimelineView for the hack details.
// We get the height change here *first* so we are informed this is because of a window resize and not due to the pinned message.
@@ -382,10 +380,10 @@ Kirigami.Page {
popup.open();
}
function onShowMaximizedCode(author, dateTime, codeText, language) {
function onShowMaximizedCode(author, time, codeText, language) {
(Qt.createComponent('org.kde.neochat', 'CodeMaximizeComponent').createObject(QQC2.Overlay.overlay, {
author: author,
dateTime: dateTime,
time: time,
codeText: codeText,
language: language
}) as CodeMaximizeComponent).open();

View File

@@ -27,7 +27,7 @@ Kirigami.Page {
QQC2.Action {
shortcut: 'Escape'
onTriggered: Kirigami.PageStack.closeDialog()
onTriggered: root.closeDialog()
}
Notification {
@@ -53,16 +53,20 @@ Kirigami.Page {
model: root.model
anchors.fill: parent
onStateChanged: {
root.Kirigami.PageStack.closeDialog();
if (state === Purpose.PurposeJobController.Finished) {
if (jobView.job?.output?.url?.length > 0) {
sharingSuccess.text = i18nc("@info", "Shared url for image is <a href='%1'>%1</a>", jobView.job.output.url);
sharingSuccess.sendEvent();
Clipboard.saveText(jobView.job.output.url);
}
root.closeDialog();
} else if (state === Purpose.PurposeJobController.Error) {
// Show failure notification
sharingFailed.sendEvent();
root.closeDialog();
} else if (state === Purpose.PurposeJobController.Cancelled) {
// Do nothing
root.closeDialog();
}
}
}

View File

@@ -39,7 +39,6 @@ Kirigami.Dialog {
readonly property bool hasMutualRooms: root.model.count > 0
readonly property bool isRoomProfile: root.room
readonly property string shareUrl: "https://matrix.to/#/" + root.user.id
readonly property string displayName: root.room ? root.room.member(root.user.id).displayName : root.user.displayName
leftPadding: Kirigami.Units.largeSpacing * 2
rightPadding: Kirigami.Units.largeSpacing * 2
@@ -65,11 +64,10 @@ Kirigami.Dialog {
KirigamiComponents.Avatar {
id: avatar
Layout.preferredWidth: Kirigami.Units.iconSizes.huge
Layout.preferredHeight: Kirigami.Units.iconSizes.huge
Layout.preferredWidth: Kirigami.Units.iconSizes.large
Layout.preferredHeight: Kirigami.Units.iconSizes.large
name: root.displayName
name: root.room ? root.room.member(root.user.id).displayName : root.user.displayName
source: {
if (root.room) {
return root.room.member(root.user.id).avatarUrl;
@@ -82,29 +80,27 @@ Kirigami.Dialog {
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Kirigami.Heading {
level: 1
Layout.fillWidth: true
font.bold: true
clip: true // Intentional to limit insane Unicode in display names
elide: Text.ElideRight
wrapMode: Text.NoWrap
text: root.displayName
text: root.room ? root.room.member(root.user.id).displayName : root.user.displayName
textFormat: Text.PlainText
Layout.fillWidth: true
}
Kirigami.SelectableLabel {
id: idLabel
textFormat: TextEdit.PlainText
text: idLabelTextMetrics.elidedText
color: Kirigami.Theme.disabledTextColor
font: Kirigami.Theme.smallFont
Layout.fillWidth: true
TextMetrics {
id: idLabelTextMetrics
@@ -113,121 +109,110 @@ Kirigami.Dialog {
elideWidth: root.availableWidth - avatar.width - detailRow.spacing * 2 - detailRow.Layout.leftMargin - detailRow.Layout.rightMargin
}
}
Kirigami.ActionToolBar {
Layout.topMargin: Kirigami.Units.smallSpacing
actions: [
Kirigami.Action {
text: i18nc("@action:intoolbar Message this user directly", "Message")
icon.name: "document-send-symbolic"
onTriggered: {
root.close();
root.connection.requestDirectChat(root.user.id);
}
},
Kirigami.Action {
icon.name: "im-invisible-user-symbolic"
text: root.connection.isIgnored(root.user.id) ? i18nc("@action:intoolbar Unignore or 'unblock' this user", "Unignore") : i18nc("@action:intoolbar Ignore or 'block' this user", "Ignore")
onTriggered: {
root.close();
root.connection.isIgnored(root.user.id) ? root.connection.removeFromIgnoredUsers(root.user.id) : root.connection.addToIgnoredUsers(root.user.id);
}
},
Kirigami.Action {
text: i18nc("@action:intoolbar Copy shareable link for this user", "Copy Link")
icon.name: "username-copy-symbolic"
onTriggered: Clipboard.saveText(root.shareUrl)
},
Kirigami.Action {
text: i18nc("@action:intoolbar Search for this user's messages.", "Search Messages…")
icon.name: "search-symbolic"
onTriggered: {
((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RoomSearchPage'), {
room: root.room,
senderId: root.user.id
}, {
title: i18nc("@action:title", "Search")
});
root.close();
}
},
Kirigami.Action {
text: i18nc("@action:intoolbar", "Show QR Code")
icon.name: "view-barcode-qr-symbolic"
onTriggered: {
let qrCode = Qt.createComponent('org.kde.neochat', 'QrCodeMaximizeComponent').createObject(QQC2.Overlay.overlay, {
text: root.shareUrl,
title: root.room ? root.room.member(root.user.id).displayName : root.user.displayName,
subtitle: root.user.id,
avatarColor: root.room?.member(root.user.id).color,
avatarSource: root.room? root.room.member(root.user.id).avatarUrl : root.user.avatarUrl
}) as QrCodeMaximizeComponent;
root.close();
qrCode.open();
}
},
Kirigami.Action {
text: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report…")
icon.name: "dialog-warning-symbolic"
visible: root.connection.supportsMatrixSpecVersion("v1.13")
onTriggered: {
let dialog = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Report User"),
placeholder: i18nc("@info:placeholder", "Optionally give a reason for reporting this user"),
icon: "dialog-warning-symbolic",
actionText: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report"),
reporting: true,
connection: root.connection,
}, {
title: i18nc("@title", "Report User"),
width: Kirigami.Units.gridUnit * 25
}) as ReasonDialog;
dialog.accepted.connect(reason => {
root.connection.reportUser(root.user.id, reason);
});
}
},
Kirigami.Action {
visible: root.room
text: i18nc("@action:button", "View Main Profile")
icon.name: "user-properties-symbolic"
onTriggered: {
root.oldRoom = root.room;
root.room = null;
}
},
Kirigami.Action {
visible: !root.room && root.oldRoom
text: i18nc("@action:button", "View Room Profile")
icon.name: "user-properties-symbolic"
onTriggered: {
root.room = root.oldRoom;
root.oldRoom = null;
}
}
]
}
}
QQC2.AbstractButton {
contentItem: Barcode {
barcodeType: Barcode.QRCode
content: root.shareUrl
}
onClicked: {
const map = Qt.createComponent('org.kde.neochat', 'QrCodeMaximizeComponent').createObject(QQC2.Overlay.overlay, {
text: root.shareUrl,
title: root.displayName,
subtitle: root.user.id,
avatarColor: root.room?.member(root.user.id).color,
avatarSource: avatar.source,
}) as QrCodeMaximizeComponent;
root.close();
map.open();
}
Layout.preferredWidth: Kirigami.Units.iconSizes.large
Layout.preferredHeight: Kirigami.Units.iconSizes.large
Layout.rightMargin: Kirigami.Units.largeSpacing
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: root.shareUrl
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
Kirigami.ActionToolBar {
Layout.topMargin: Kirigami.Units.largeSpacing
actions: [
Kirigami.Action {
text: i18nc("@action:intoolbar Message this user directly", "Message")
icon.name: "document-send-symbolic"
onTriggered: {
root.close();
root.connection.requestDirectChat(root.user.id);
}
},
Kirigami.Action {
icon.name: "im-invisible-user-symbolic"
text: root.connection.isIgnored(root.user.id) ? i18nc("@action:intoolbar Unignore or 'unblock' this user", "Unignore") : i18nc("@action:intoolbar Ignore or 'block' this user", "Ignore")
onTriggered: {
root.close();
root.connection.isIgnored(root.user.id) ? root.connection.removeFromIgnoredUsers(root.user.id) : root.connection.addToIgnoredUsers(root.user.id);
}
},
Kirigami.Action {
text: i18nc("@action:intoolbar Copy shareable link for this user", "Copy Link")
icon.name: "username-copy-symbolic"
onTriggered: Clipboard.saveText(root.shareUrl)
},
Kirigami.Action {
text: i18nc("@action:intoolbar Search for this user's messages.", "Search Messages…")
icon.name: "search-symbolic"
onTriggered: {
((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RoomSearchPage'), {
room: root.room,
senderId: root.user.id
}, {
title: i18nc("@action:title", "Search")
});
root.close();
}
},
Kirigami.Action {
text: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report…")
icon.name: "dialog-warning-symbolic"
visible: root.connection.supportsMatrixSpecVersion("v1.13")
onTriggered: {
let dialog = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
title: i18nc("@title:dialog", "Report User"),
placeholder: i18nc("@info:placeholder", "Optionally give a reason for reporting this user"),
icon: "dialog-warning-symbolic",
actionText: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report"),
reporting: true,
connection: root.connection,
}, {
title: i18nc("@title", "Report User"),
width: Kirigami.Units.gridUnit * 25
}) as ReasonDialog;
dialog.accepted.connect(reason => {
root.connection.reportUser(root.user.id, reason);
});
}
},
Kirigami.Action {
visible: root.room
text: i18nc("@action:button", "View Main Profile")
icon.name: "user-properties-symbolic"
onTriggered: {
root.oldRoom = root.room;
root.room = null;
}
},
Kirigami.Action {
visible: !root.room && root.oldRoom
text: i18nc("@action:button", "View Room Profile")
icon.name: "user-properties-symbolic"
onTriggered: {
root.room = root.oldRoom;
root.oldRoom = null;
}
}
]
}
Kirigami.Heading {

View File

@@ -286,12 +286,12 @@ void RoomManager::maximizeMedia(const QString &eventId)
Q_EMIT showMaximizedMedia(index);
}
void RoomManager::maximizeCode(NeochatRoomMember *author, const NeoChatDateTime &dateTime, const QString &codeText, const QString &language)
void RoomManager::maximizeCode(NeochatRoomMember *author, const QDateTime &time, const QString &codeText, const QString &language)
{
if (codeText.isEmpty()) {
return;
}
Q_EMIT showMaximizedCode(author, dateTime, codeText, language);
Q_EMIT showMaximizedCode(author, time, codeText, language);
}
void RoomManager::requestFullScreenClose()
@@ -354,9 +354,7 @@ void RoomManager::loadInitialRoom()
void RoomManager::openRoomForActiveConnection()
{
if (!m_connection) {
return;
}
Q_ASSERT(m_connection);
auto lastSpace = m_lastRoomConfig.readEntry(u"lastSpace"_s, QString());
if (lastSpace == u"Home"_s) {

View File

@@ -23,7 +23,6 @@
#include "models/timelinemodel.h"
#include "models/userlistmodel.h"
#include "models/widgetmodel.h"
#include "neochatdatetime.h"
#include "neochatroommember.h"
class NeoChatRoom;
@@ -219,7 +218,7 @@ public:
*/
Q_INVOKABLE void maximizeMedia(const QString &eventId);
Q_INVOKABLE void maximizeCode(NeochatRoomMember *author, const NeoChatDateTime &time, const QString &codeText, const QString &language);
Q_INVOKABLE void maximizeCode(NeochatRoomMember *author, const QDateTime &time, const QString &codeText, const QString &language);
/**
* @brief Request that any full screen overlay currently open closes.
@@ -293,7 +292,7 @@ Q_SIGNALS:
/**
* @brief Request a block of code is shown maximized.
*/
void showMaximizedCode(NeochatRoomMember *author, const NeoChatDateTime &dateTime, const QString &codeText, const QString &language);
void showMaximizedCode(NeochatRoomMember *author, const QDateTime &time, const QString &codeText, const QString &language);
/**
* @brief Request that any full screen overlay closes.

View File

@@ -8,8 +8,6 @@
class SupportContact
{
Q_GADGET
QML_NAMED_ELEMENT(supportContact)
QML_UNCREATABLE("")
Q_PROPERTY(QString role MEMBER role)
Q_PROPERTY(QString matrixId MEMBER matrixId)

View File

@@ -412,6 +412,7 @@ QQC2.Control {
Component {
id: replyPane
Item {
implicitWidth: replyComponent.implicitWidth
implicitHeight: replyComponent.implicitHeight
ReplyComponent {
id: replyComponent

View File

@@ -30,7 +30,7 @@ QQC2.ItemDelegate {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.family: "emoji"
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 1.5
font.pointSize: Kirigami.Theme.defaultFont.pointSize * NeoChatConfig.fontScale
Kirigami.Icon {
width: Kirigami.Units.gridUnit * 0.5

View File

@@ -18,7 +18,6 @@ class StateFilterModel : public QSortFilterProxyModel
QML_ELEMENT
public:
using QSortFilterProxyModel::QSortFilterProxyModel;
/**
* @brief Custom filter function checking if an event type has been filtered out.
*

View File

@@ -28,7 +28,7 @@ public:
* @brief Defines the model roles.
*/
enum Roles {
TypeRole = Qt::UserRole, /**< The type of the state event. */
TypeRole = 0, /**< The type of the state event. */
EventCountRole, /**< Number of events of this type. */
StateKeyRole, /**<State key. Only valid if there's exactly one event of this type. */
};

View File

@@ -17,7 +17,6 @@ target_sources(LibNeoChat PRIVATE
filetransferpseudojob.cpp
filetype.cpp
linkpreviewer.cpp
neochatdatetime.cpp
roomlastmessageprovider.cpp
spacehierarchycache.cpp
texthandler.cpp

View File

@@ -28,7 +28,7 @@ class SyntaxHighlighter : public QSyntaxHighlighter
public:
QTextCharFormat mentionFormat;
QTextCharFormat errorFormat;
Sonnet::BackgroundChecker checker;
Sonnet::BackgroundChecker *checker = new Sonnet::BackgroundChecker;
Sonnet::Settings settings;
QList<QPair<int, QString>> errors;
QString previousText;
@@ -48,11 +48,11 @@ public:
errorFormat.setForeground(m_theme->negativeTextColor());
errorFormat.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
connect(&checker, &Sonnet::BackgroundChecker::misspelling, this, [this](const QString &word, int start) {
connect(checker, &Sonnet::BackgroundChecker::misspelling, this, [this](const QString &word, int start) {
errors += {start, word};
checker.continueChecking();
checker->continueChecking();
});
connect(&checker, &Sonnet::BackgroundChecker::done, this, [this]() {
connect(checker, &Sonnet::BackgroundChecker::done, this, [this]() {
rehighlightTimer.start();
});
rehighlightTimer.setInterval(100);
@@ -64,9 +64,9 @@ public:
if (settings.checkerEnabledByDefault()) {
if (text != previousText) {
previousText = text;
checker.stop();
checker->stop();
errors.clear();
checker.setText(text);
checker->setText(text);
}
for (const auto &error : errors) {
setFormat(error.first, error.second.size(), errorFormat);

View File

@@ -74,7 +74,7 @@ public:
*/
enum Roles {
NameRole = Qt::DisplayRole, /**< The power level name. */
ValueRole = Qt::UserRole, /**< The power level value. */
ValueRole, /**< The power level value. */
};
Q_ENUM(Roles)

View File

@@ -93,7 +93,7 @@ QString EventHandler::singleLineAuthorDisplayname(const NeoChatRoom *room, const
return displayName;
}
NeoChatDateTime EventHandler::dateTime(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isPending)
QDateTime EventHandler::time(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isPending)
{
if (room == nullptr) {
qCWarning(EventHandling) << "time called with room set to nullptr.";
@@ -114,6 +114,25 @@ NeoChatDateTime EventHandler::dateTime(const NeoChatRoom *room, const Quotient::
return event->originTimestamp();
}
QString EventHandler::timeString(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool relative, QLocale::FormatType format, bool isPending)
{
auto ts = time(room, event, isPending);
if (ts.isValid()) {
if (relative) {
KFormat formatter;
return formatter.formatRelativeDate(ts.toLocalTime().date(), format);
} else {
return QLocale().toString(ts.toLocalTime().time(), format);
}
}
return {};
}
QString EventHandler::timeString(const NeoChatRoom *room, const Quotient::RoomEvent *event, const QString &format, bool isPending)
{
return time(room, event, isPending).toLocalTime().toString(format);
}
bool EventHandler::isHighlighted(const NeoChatRoom *room, const Quotient::RoomEvent *event)
{
if (room == nullptr) {

View File

@@ -7,8 +7,6 @@
#include <QString>
#include <Quotient/events/eventcontent.h>
#include "neochatdatetime.h"
namespace Quotient
{
namespace EventContent
@@ -66,7 +64,41 @@ public:
/**
* @brief Return a QDateTime object for the event timestamp.
*/
static NeoChatDateTime dateTime(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isPending = false);
static QDateTime time(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isPending = false);
/**
* @brief Return a QString for the event timestamp.
*
* This is intended to return a string that is read for display in the UI without
* any further manipulation required.
*
* @param relative whether the string is realtive to the current date, i.e.
* Yesterday or Wednesday, etc.
* @param format the QLocale::FormatType to use.
* @param isPending whether the event is pending as this cannot be derived from
* just the event object.
* @param lastUpdated the time the event was last updated locally as this cannot be
* obtained from the event.
*/
static QString timeString(const NeoChatRoom *room,
const Quotient::RoomEvent *event,
bool relative,
QLocale::FormatType format = QLocale::ShortFormat,
bool isPending = false);
/**
* @brief Return a QString for the event timestamp.
*
* This is intended to return a string that is read for display in the UI without
* any further manipulation required.
*
* @param format the format to use as a string.
* @param isPending whether the event is pending as this cannot be derived from
* just the event object.
* @param lastUpdated the time the event was last updated locally as this cannot be
* obtained from the event.
*/
static QString timeString(const NeoChatRoom *room, const Quotient::RoomEvent *event, const QString &format, bool isPending = false);
/**
* @brief Whether the event should be highlighted in the timeline.

View File

@@ -13,7 +13,7 @@
CompletionModel::CompletionModel(QObject *parent)
: QAbstractListModel(parent)
, m_filterModel(new CompletionProxyModel(this))
, m_filterModel(new CompletionProxyModel())
, m_emojiModel(new QConcatenateTablesProxyModel(this))
{
connect(this, &CompletionModel::textChanged, this, &CompletionModel::updateCompletion);

View File

@@ -26,8 +26,6 @@ class CompletionProxyModel : public QSortFilterProxyModel
Q_OBJECT
public:
using QSortFilterProxyModel::QSortFilterProxyModel;
/**
* @brief Wether a row should be shown or not.
*

View File

@@ -178,19 +178,4 @@ void LiveLocationsModel::updateLocationData(LiveLocationData &&data)
Q_EMIT dataChanged(idx, idx);
}
NeoChatRoom *LiveLocationsModel::room() const
{
return m_room;
}
void LiveLocationsModel::setRoom(NeoChatRoom *room)
{
if (m_room == room) {
return;
}
m_room = room;
Q_EMIT roomChanged();
}
#include "moc_livelocationsmodel.cpp"

View File

@@ -27,7 +27,7 @@ class LiveLocationsModel : public QAbstractListModel
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
Q_PROPERTY(NeoChatRoom *room MEMBER m_room NOTIFY roomChanged)
/** The event id of the beacon start event, ie. the one all suspequent
* events use to relate to the same beacon.
* If this is set only this specific beacon will be coverd by this model,
@@ -57,9 +57,6 @@ public:
QRectF boundingBox() const;
NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
Q_SIGNALS:
void roomChanged();
void eventIdChanged();

View File

@@ -215,7 +215,7 @@ QVariant RoomListModel::data(const QModelIndex &index, int role) const
return QVariant();
}
NeoChatRoom *room = m_rooms.at(index.row());
if (role == DisplayNameRole || role == Qt::DisplayRole) {
if (role == DisplayNameRole) {
return room->displayName();
}
if (role == EscapedDisplayNameRole) {

View File

@@ -36,7 +36,7 @@ public:
* @brief Defines the model roles.
*/
enum EventRoles {
DisplayNameRole = Qt::UserRole, /**< The display name of the room. */
DisplayNameRole = Qt::DisplayRole, /**< The display name of the room. */
EscapedDisplayNameRole, /**< HTML-Escaped display name of the room. */
AvatarRole, /**< The source URL for the room's avatar. */
CanonicalAliasRole, /**< The room canonical alias. */

View File

@@ -11,9 +11,6 @@ bool UserFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceP
if (!m_allowEmpty && m_filterText.length() < 1) {
return false;
}
if (sourceModel()->data(sourceModel()->index(sourceRow, 0), UserListModel::MembershipRole).value<Quotient::Membership>() != Quotient::Membership::Join) {
return false;
}
return sourceModel()->data(sourceModel()->index(sourceRow, 0), UserListModel::DisplayNameRole).toString().contains(m_filterText, Qt::CaseInsensitive)
|| sourceModel()->data(sourceModel()->index(sourceRow, 0), UserListModel::UserIdRole).toString().contains(m_filterText, Qt::CaseInsensitive);
}

View File

@@ -27,8 +27,6 @@ class UserFilterModel : public QSortFilterProxyModel
Q_PROPERTY(bool allowEmpty READ allowEmpty WRITE setAllowEmpty NOTIFY allowEmptyChanged)
public:
using QSortFilterProxyModel::QSortFilterProxyModel;
/**
* @brief Custom filter function checking boith the display name and matrix ID.
*

View File

@@ -40,12 +40,17 @@ void UserListModel::setRoom(NeoChatRoom *room)
if (m_currentRoom) {
connect(m_currentRoom, &Room::memberJoined, this, &UserListModel::memberJoined);
connect(m_currentRoom, &Room::memberLeft, this, &UserListModel::memberLeft);
connect(m_currentRoom, &Room::memberNameUpdated, this, [this](RoomMember member) {
refreshMember(member, {DisplayNameRole});
});
connect(m_currentRoom, &Room::memberAvatarUpdated, this, [this](RoomMember member) {
refreshMember(member, {AvatarRole});
});
connect(m_currentRoom, &Room::memberListChanged, this, [this]() {
// this is slow
UserListModel::refreshAllMembers();
});
connect(m_currentRoom->connection(), &Connection::loggedOut, this, [this]() {
setRoom(nullptr);
});
@@ -74,7 +79,7 @@ QVariant UserListModel::data(const QModelIndex &index, int role) const
"users.count()";
return {};
}
const auto &memberId = m_members.at(index.row());
auto memberId = m_members.at(index.row());
if (role == DisplayNameRole) {
return m_currentRoom->member(memberId).disambiguatedName();
}
@@ -119,12 +124,6 @@ QVariant UserListModel::data(const QModelIndex &index, int role) const
if (role == IsCreatorRole) {
return m_currentRoom->isCreator(memberId);
}
if (role == MembershipRole) {
return QVariant::fromValue(m_currentRoom->member(memberId).membershipState());
}
if (role == ColorRole) {
return m_currentRoom->member(memberId).color();
}
return {};
}
@@ -140,8 +139,7 @@ int UserListModel::rowCount(const QModelIndex &parent) const
bool UserListModel::event(QEvent *event)
{
if (event->type() == QEvent::ApplicationPaletteChange) {
// Quotient::RoomMember::color needs to be recalculated for the new palette
Q_EMIT dataChanged(index(0, 0), index(m_members.size(), 0), {ColorRole});
refreshAllMembers();
}
return QObject::event(event);
}
@@ -158,6 +156,18 @@ void UserListModel::memberJoined(const Quotient::RoomMember &member)
endInsertRows();
}
void UserListModel::memberLeft(const Quotient::RoomMember &member)
{
auto pos = findUserPos(member);
if (pos != m_members.size()) {
beginRemoveRows(QModelIndex(), pos, pos);
m_members.removeAt(pos);
endRemoveRows();
} else {
qWarning() << "Trying to remove a room member not in the user list";
}
}
void UserListModel::refreshMember(const Quotient::RoomMember &member, const QList<int> &roles)
{
auto pos = findUserPos(member);
@@ -171,10 +181,21 @@ void UserListModel::refreshMember(const Quotient::RoomMember &member, const QLis
void UserListModel::refreshAllMembers()
{
beginResetModel();
if (m_currentRoom != nullptr) {
m_members = m_currentRoom->sortedMemberIds();
} else {
m_members.clear();
m_members = m_currentRoom->joinedMemberIds();
MemberSorter sorter;
std::sort(m_members.begin(), m_members.end(), [&sorter, this](const auto &left, const auto &right) {
const auto leftPl = m_currentRoom->memberEffectivePowerLevel(left);
const auto rightPl = m_currentRoom->memberEffectivePowerLevel(right);
if (leftPl > rightPl) {
return true;
} else if (rightPl > leftPl) {
return false;
}
return sorter(m_currentRoom->member(left), m_currentRoom->member(right));
});
}
endResetModel();
Q_EMIT usersRefreshed();
@@ -190,8 +211,8 @@ int UserListModel::findUserPos(const QString &userId) const
if (!m_currentRoom) {
return 0;
}
const auto pos = std::find_if(m_members.cbegin(), m_members.cend(), [&userId](const QString &member) {
return userId == member;
const auto pos = std::find_if(m_members.cbegin(), m_members.cend(), [&userId](const QString &memberId) {
return userId == memberId;
});
return pos - m_members.cbegin();
}
@@ -207,8 +228,6 @@ QHash<int, QByteArray> UserListModel::roleNames() const
roles[PowerLevelRole] = "powerLevel";
roles[PowerLevelStringRole] = "powerLevelString";
roles[IsCreatorRole] = "isCreator";
roles[MembershipRole] = "membership";
roles[ColorRole] = "color";
return roles;
}

View File

@@ -43,14 +43,12 @@ public:
*/
enum EventRoles {
DisplayNameRole = Qt::DisplayRole, /**< The user's display name in the current room. */
UserIdRole = Qt::UserRole, /**< Matrix ID of the user. */
UserIdRole, /**< Matrix ID of the user. */
AvatarRole, /**< The source URL for the user's avatar in the current room. */
ObjectRole, /**< The QObject for the user. */
PowerLevelRole, /**< The user's power level in the current room. */
PowerLevelStringRole, /**< The name of the user's power level in the current room. */
IsCreatorRole, /**< Whether this user is considered a creator of the current room. */
MembershipRole, /**< The membership state of this user. */
ColorRole, /**< The color of this user. */
};
Q_ENUM(EventRoles)
@@ -91,6 +89,7 @@ protected:
private Q_SLOTS:
void memberJoined(const Quotient::RoomMember &member);
void memberLeft(const Quotient::RoomMember &member);
void refreshMember(const Quotient::RoomMember &member, const QList<int> &roles = {});
void refreshAllMembers();

View File

@@ -104,7 +104,7 @@ public:
void handlePendingEvent(const Quotient::RoomEvent *event);
void buildJitsiIndex();
QPointer<NeoChatRoom> room;
NeoChatRoom *room = nullptr;
QMap<QString, const WidgetEvent *> state;
int jitsiIndex = -1;
};

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 "neochatdatetime.h"
#include <KFormat>
using namespace Qt::Literals::StringLiterals;
NeoChatDateTime::NeoChatDateTime(QDateTime dateTime)
: m_dateTime(dateTime)
{
}
QDateTime NeoChatDateTime::dateTime() const
{
return m_dateTime;
}
QString NeoChatDateTime::hourMinuteString() const
{
return m_dateTime.toLocalTime().toString(u"hh:mm"_s);
}
QString NeoChatDateTime::shortDateTime() const
{
return QLocale().toString(m_dateTime.toLocalTime(), QLocale::ShortFormat);
}
QString NeoChatDateTime::relativeDate() const
{
KFormat formatter;
return formatter.formatRelativeDate(m_dateTime.toLocalTime().date(), QLocale::ShortFormat);
}
QString NeoChatDateTime::relativeDateTime() const
{
KFormat formatter;
return formatter.formatRelativeDateTime(m_dateTime.toLocalTime(), QLocale::ShortFormat);
}
bool NeoChatDateTime::isValid() const
{
return m_dateTime.isValid();
}
bool NeoChatDateTime::operator==(const QDateTime &right) const
{
return m_dateTime == right;
}
#include "moc_neochatdatetime.cpp"

View File

@@ -1,93 +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
#pragma once
#include <QDateTime>
#include <QQmlEngine>
/**
* @class NeoChatDateTime
*
* This class is a helper for converting a QDateTime into the various format required in NeoChat.
*
* The intention is that this can be passed to QML and then the various Q_Properties
* can be called to get the date/time in the desired format reading for viewing in
* the UI.
*/
class NeoChatDateTime
{
Q_GADGET
QML_ELEMENT
/**
* @brief The base QDateTime used to generate the other values.
*/
Q_PROPERTY(QDateTime dateTime READ dateTime CONSTANT)
/**
* @brief The time formatted as "hh:mm".
*/
Q_PROPERTY(QString hourMinuteString READ hourMinuteString CONSTANT)
/**
* @brief The date and time formatted as per QLocale::ShortFormat for your locale.
*/
Q_PROPERTY(QString shortDateTime READ shortDateTime CONSTANT)
/**
* @brief The date formatted as relative to now.
*
* If the date falls within one week before or after the current date
* then a relative date string will be returned, such as:
* - Yesterday
* - Today
* - Tomorrow
* - Last Tuesday
* - Next Wednesday
*
* If the date falls outside this period then the format QLocale::ShortFormat
* for your locale is used.
*/
Q_PROPERTY(QString relativeDate READ relativeDate CONSTANT)
/**
* @brief The time and date formatted as relative to now.
*
* The format is "RelativeDate, hh::mm"
*
* If the date falls within one week before or after the current date
* then a relative date string will be returned, such as:
* - Yesterday
* - Today
* - Tomorrow
* - Last Tuesday
* - Next Wednesday
*
* If the date falls outside this period then the format QLocale::ShortFormat
* for your locale is used.
*/
Q_PROPERTY(QString relativeDateTime READ relativeDateTime CONSTANT)
/**
* @brief Whether this object has a valid date time.
*/
Q_PROPERTY(bool isValid READ isValid CONSTANT)
public:
NeoChatDateTime(QDateTime dateTime = {});
QDateTime dateTime() const;
QString hourMinuteString() const;
QString shortDateTime() const;
QString relativeDate() const;
QString relativeDateTime() const;
bool isValid() const;
bool operator==(const QDateTime &right) const;
private:
QDateTime m_dateTime;
};

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