Compare commits
155 Commits
work/redst
...
work/redst
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f3c542d04 | ||
|
|
4e616d53b2 | ||
|
|
57b6dbcbde | ||
|
|
49646c63f8 | ||
|
|
9d2a427619 | ||
|
|
593ad27e8c | ||
|
|
3d07f723c8 | ||
|
|
d47a4eb0de | ||
|
|
11f8407ef7 | ||
|
|
e4c9230c09 | ||
|
|
318432d561 | ||
|
|
afa699381b | ||
|
|
244ef77c4a | ||
|
|
847db41fb3 | ||
|
|
115d4e7466 | ||
|
|
346d311909 | ||
|
|
cff27ca7db | ||
|
|
35daae6b5d | ||
|
|
94fb429c47 | ||
|
|
9f4146e5b1 | ||
|
|
a9b4a900c9 | ||
|
|
18d7d2f736 | ||
|
|
be65d506c4 | ||
|
|
f5d726989f | ||
|
|
0f634ff795 | ||
|
|
1289194b3f | ||
|
|
8ca1b8b1d3 | ||
|
|
793f81e733 | ||
|
|
4c31f42144 | ||
|
|
6b664b0547 | ||
|
|
44f78353ab | ||
|
|
3b2df95df7 | ||
|
|
8edb248647 | ||
|
|
07bc06f4ff | ||
|
|
0d9988013b | ||
|
|
c1720bbaa7 | ||
|
|
692ce82717 | ||
|
|
ac04fa6a13 | ||
|
|
39aaced0f9 | ||
|
|
9887665560 | ||
|
|
c590cb76a0 | ||
|
|
f9b0c56fa0 | ||
|
|
aa7ab6b2ec | ||
|
|
ac6a5663a1 | ||
|
|
f09dc79995 | ||
|
|
693b2e74fe | ||
|
|
3a54d58516 | ||
|
|
6abb117989 | ||
|
|
b2dee6a96b | ||
|
|
1e1ba1dca3 | ||
|
|
55676b06c2 | ||
|
|
513496cf85 | ||
|
|
69215401dd | ||
|
|
d5ec37e1af | ||
|
|
6a45e2532c | ||
|
|
322926e31c | ||
|
|
29592a7f92 | ||
|
|
8fc8baa2d2 | ||
|
|
de7f2654f4 | ||
|
|
ccf9ed66f8 | ||
|
|
259c60f669 | ||
|
|
2ca121ede9 | ||
|
|
fba01c17ae | ||
|
|
5de394b4b7 | ||
|
|
edd64d9b8f | ||
|
|
5c614e59f2 | ||
|
|
853d48e768 | ||
|
|
295ecf0f18 | ||
|
|
7c0ab697d7 | ||
|
|
75fceaccea | ||
|
|
275d221f75 | ||
|
|
72416884d4 | ||
|
|
537ce772af | ||
|
|
37b8d8d813 | ||
|
|
d64e22c270 | ||
|
|
fa4533e757 | ||
|
|
8b6f5447e1 | ||
|
|
6dce1564b7 | ||
|
|
7313386903 | ||
|
|
c0f5db7fd2 | ||
|
|
551d827dee | ||
|
|
332a822996 | ||
|
|
f145bbe8db | ||
|
|
381b119ad1 | ||
|
|
b64dcdb004 | ||
|
|
b33f1cf5e1 | ||
|
|
f22cafbce1 | ||
|
|
92c58b0ea0 | ||
|
|
c797ecea3d | ||
|
|
b65590a9b9 | ||
|
|
2bacbe7ac7 | ||
|
|
74c12e89ea | ||
|
|
02b95c921d | ||
|
|
7f58ee3793 | ||
|
|
7312bf183d | ||
|
|
40b7853338 | ||
|
|
ade5750550 | ||
|
|
fb8ee02e3b | ||
|
|
8b27323488 | ||
|
|
96d24f5c3a | ||
|
|
45cee495a5 | ||
|
|
53dc9c1944 | ||
|
|
1fb215dae7 | ||
|
|
971875c8a2 | ||
|
|
9ad64b990d | ||
|
|
1ceffe6a2e | ||
|
|
9810b3dee0 | ||
|
|
76954c162a | ||
|
|
0a7978f4f5 | ||
|
|
bf41e1083d | ||
|
|
593f772845 | ||
|
|
98a277ac63 | ||
|
|
cfe5182a65 | ||
|
|
9ace01f74a | ||
|
|
4632a9f9bb | ||
|
|
a92587cc50 | ||
|
|
889b7dd2e6 | ||
|
|
44fa196a26 | ||
|
|
2bc8c6a379 | ||
|
|
3f1ba8d067 | ||
|
|
a1c9b63d1e | ||
|
|
73fdc72ce7 | ||
|
|
08fc8be09c | ||
|
|
e5a48bae01 | ||
|
|
581f5be410 | ||
|
|
f4e857519b | ||
|
|
93e932c09c | ||
|
|
3b8930c2bc | ||
|
|
7e34570a05 | ||
|
|
0b5de13c36 | ||
|
|
6eb2b2e739 | ||
|
|
be89362fdd | ||
|
|
e53c84d30c | ||
|
|
a90c26f566 | ||
|
|
c2ae5afa73 | ||
|
|
5f20a86b62 | ||
|
|
f2d3c9706e | ||
|
|
39de4d10e4 | ||
|
|
4c37dcf518 | ||
|
|
3c77711417 | ||
|
|
136063bd37 | ||
|
|
730a9e97fd | ||
|
|
d5260376d2 | ||
|
|
dc935e09b7 | ||
|
|
5759f7d82b | ||
|
|
ed4b77c184 | ||
|
|
716ee2e494 | ||
|
|
c15860cac3 | ||
|
|
f5c991c55c | ||
|
|
41609749d8 | ||
|
|
644df80090 | ||
|
|
e3307326ef | ||
|
|
74d4e786d3 | ||
|
|
1e461658b8 | ||
|
|
f305cb849f |
@@ -43,3 +43,4 @@ Options:
|
||||
per-test-timeout: 90
|
||||
require-passing-tests-on: ['Linux', 'Android', 'FreeBSD', 'Windows']
|
||||
run-qmllint: True
|
||||
enable-lsan: True
|
||||
|
||||
19
README.md
19
README.md
@@ -25,15 +25,10 @@ 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 fact that the Matrix spec is 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 Matrix spec 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).
|
||||
@@ -48,12 +43,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
|
||||
|
||||
Just start the executable in your preferred way - either from the build directory or from the installed location.
|
||||
Start the executable in your preferred way – either from the build directory or from the installed location.
|
||||
|
||||
## Tests
|
||||
|
||||
@@ -66,12 +61,12 @@ be complete.
|
||||
|
||||

|
||||
|
||||
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)
|
||||
@@ -86,7 +81,7 @@ The best place to reach the maintainers is on the KDE Matrix instance in the Neo
|
||||
|
||||
## Acknowledgement
|
||||
|
||||
NeoChat utilizes [libQuotient](https://github.com/quotient-im/libQuotient/) as its Matrix SDK.
|
||||
NeoChat uses [libQuotient](https://github.com/quotient-im/libQuotient/) as its Matrix SDK.
|
||||
|
||||
NeoChat is a fork of [Spectral](https://gitlab.com/spectral-im/spectral/).
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" )
|
||||
|
||||
ecm_add_test(
|
||||
neochatroomtest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
LINK_LIBRARIES neochat Qt::Test Qt::HttpServer neochat_server
|
||||
TEST_NAME neochatroomtest
|
||||
)
|
||||
|
||||
@@ -41,7 +41,7 @@ ecm_add_test(
|
||||
|
||||
ecm_add_test(
|
||||
chatbarcachetest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
LINK_LIBRARIES neochat Qt::Test Qt::HttpServer neochat_server
|
||||
TEST_NAME chatbarcachetest
|
||||
)
|
||||
|
||||
@@ -104,3 +104,9 @@ 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
|
||||
)
|
||||
|
||||
@@ -88,7 +88,7 @@ void ActionsTest::testActions()
|
||||
QFETCH(std::optional<QString>, resultText);
|
||||
QFETCH(std::optional<Quotient::RoomMessageEvent::MsgType>, type);
|
||||
|
||||
auto cache = new ChatBarCache();
|
||||
auto cache = new ChatBarCache(this);
|
||||
cache->setText(command);
|
||||
auto result = ActionsModel::handleAction(room, cache);
|
||||
QCOMPARE(resultText, std::get<std::optional<QString>>(result));
|
||||
|
||||
@@ -11,9 +11,13 @@
|
||||
#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;
|
||||
@@ -24,7 +28,9 @@ class ChatBarCacheTest : public QObject
|
||||
|
||||
private:
|
||||
Connection *connection = nullptr;
|
||||
TestUtils::TestRoom *room = nullptr;
|
||||
NeoChatRoom *room = nullptr;
|
||||
Server server;
|
||||
QString eventId;
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
@@ -40,8 +46,31 @@ private Q_SLOTS:
|
||||
|
||||
void ChatBarCacheTest::initTestCase()
|
||||
{
|
||||
connection = Connection::makeMockConnection(u"@bob:kde.org"_s);
|
||||
room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, "test-min-sync.json"_L1);
|
||||
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());
|
||||
}
|
||||
|
||||
void ChatBarCacheTest::empty()
|
||||
@@ -60,8 +89,9 @@ 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(u"$153456789:example.org"_s);
|
||||
chatBarCache->setReplyId(eventId);
|
||||
|
||||
// These should return empty even though a reply ID has been set because the
|
||||
// ChatBarCache has no parent.
|
||||
@@ -75,9 +105,10 @@ 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(u"$153456789:example.org"_s);
|
||||
chatBarCache->setReplyId(eventId);
|
||||
|
||||
// These should return empty even though a reply ID has been set because the
|
||||
// ChatBarCache has no parent.
|
||||
@@ -94,15 +125,15 @@ void ChatBarCacheTest::reply()
|
||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
||||
chatBarCache->setText(u"some text"_s);
|
||||
chatBarCache->setAttachmentPath(u"some/path"_s);
|
||||
chatBarCache->setReplyId(u"$153456789:example.org"_s);
|
||||
chatBarCache->setReplyId(eventId);
|
||||
|
||||
QCOMPARE(chatBarCache->text(), u"some text"_s);
|
||||
QCOMPARE(chatBarCache->isReplying(), true);
|
||||
QCOMPARE(chatBarCache->replyId(), u"$153456789:example.org"_s);
|
||||
QCOMPARE(chatBarCache->replyId(), eventId);
|
||||
QCOMPARE(chatBarCache->isEditing(), false);
|
||||
QCOMPARE(chatBarCache->editId(), QString());
|
||||
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@example:example.org"_s));
|
||||
QCOMPARE(chatBarCache->relationMessage(), u"This is an example\ntext message"_s);
|
||||
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@foo:server.com"_s));
|
||||
QCOMPARE(chatBarCache->relationMessage(), u"foo"_s);
|
||||
QCOMPARE(chatBarCache->attachmentPath(), QString());
|
||||
QCOMPARE(chatBarCache->relationAuthorIsPresent(), true);
|
||||
}
|
||||
@@ -112,22 +143,26 @@ void ChatBarCacheTest::replyMissingUser()
|
||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
||||
chatBarCache->setText(u"some text"_s);
|
||||
chatBarCache->setAttachmentPath(u"some/path"_s);
|
||||
chatBarCache->setReplyId(u"$153456789:example.org"_s);
|
||||
chatBarCache->setReplyId(eventId);
|
||||
|
||||
QCOMPARE(chatBarCache->text(), u"some text"_s);
|
||||
QCOMPARE(chatBarCache->isReplying(), true);
|
||||
QCOMPARE(chatBarCache->replyId(), u"$153456789:example.org"_s);
|
||||
QCOMPARE(chatBarCache->replyId(), eventId);
|
||||
QCOMPARE(chatBarCache->isEditing(), false);
|
||||
QCOMPARE(chatBarCache->editId(), QString());
|
||||
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@example:example.org"_s));
|
||||
QCOMPARE(chatBarCache->relationMessage(), u"This is an example\ntext message"_s);
|
||||
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@foo:server.com"_s));
|
||||
QCOMPARE(chatBarCache->relationMessage(), u"foo"_s);
|
||||
QCOMPARE(chatBarCache->attachmentPath(), QString());
|
||||
QCOMPARE(chatBarCache->relationAuthorIsPresent(), true);
|
||||
|
||||
QSignalSpy relationAuthorIsPresentSpy(chatBarCache.get(), &ChatBarCache::relationAuthorIsPresentChanged);
|
||||
|
||||
// sync again, which will simulate the reply user leaving the room
|
||||
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);
|
||||
QCOMPARE(chatBarCache->relationAuthorIsPresent(), false);
|
||||
@@ -139,19 +174,19 @@ void ChatBarCacheTest::edit()
|
||||
|
||||
chatBarCache->setText(u"some text"_s);
|
||||
chatBarCache->setAttachmentPath(u"some/path"_s);
|
||||
connect(chatBarCache.get(), &ChatBarCache::relationIdChanged, this, [](const QString &oldEventId, const QString &newEventId) {
|
||||
connect(chatBarCache.get(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) {
|
||||
QCOMPARE(oldEventId, QString());
|
||||
QCOMPARE(newEventId, QString(u"$153456789:example.org"_s));
|
||||
QCOMPARE(newEventId, eventId);
|
||||
});
|
||||
chatBarCache->setEditId(u"$153456789:example.org"_s);
|
||||
chatBarCache->setEditId(eventId);
|
||||
|
||||
QCOMPARE(chatBarCache->text(), u"some text"_s);
|
||||
QCOMPARE(chatBarCache->isReplying(), false);
|
||||
QCOMPARE(chatBarCache->replyId(), QString());
|
||||
QCOMPARE(chatBarCache->isEditing(), true);
|
||||
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->editId(), eventId);
|
||||
QCOMPARE(chatBarCache->relationAuthor(), room->member(u"@foo:server.com"_s));
|
||||
QCOMPARE(chatBarCache->relationMessage(), u"foo"_s);
|
||||
QCOMPARE(chatBarCache->attachmentPath(), QString());
|
||||
}
|
||||
|
||||
@@ -159,7 +194,7 @@ void ChatBarCacheTest::attachment()
|
||||
{
|
||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
||||
chatBarCache->setText(u"some text"_s);
|
||||
chatBarCache->setEditId(u"$153456789:example.org"_s);
|
||||
chatBarCache->setEditId(eventId);
|
||||
chatBarCache->setAttachmentPath(u"some/path"_s);
|
||||
|
||||
QCOMPARE(chatBarCache->text(), u"some text"_s);
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,6 @@ private Q_SLOTS:
|
||||
void nullSingleLineDisplayName();
|
||||
void time();
|
||||
void nullTime();
|
||||
void timeString();
|
||||
void highlighted();
|
||||
void nullHighlighted();
|
||||
void hidden();
|
||||
@@ -100,12 +99,12 @@ void EventHandlerTest::time()
|
||||
{
|
||||
const auto event = room->messageEvents().at(0).get();
|
||||
|
||||
QCOMPARE(EventHandler::time(room, event), QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)));
|
||||
QCOMPARE(EventHandler::dateTime(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::time(room, pendingIt->event(), true), pendingIt->lastUpdated());
|
||||
QCOMPARE(EventHandler::dateTime(room, pendingIt->event(), true), pendingIt->lastUpdated());
|
||||
|
||||
room->discardMessage(txID);
|
||||
QCOMPARE(room->pendingEvents().size(), 0);
|
||||
@@ -114,40 +113,10 @@ void EventHandlerTest::time()
|
||||
void EventHandlerTest::nullTime()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "time called with room set to nullptr.");
|
||||
QCOMPARE(EventHandler::time(nullptr, nullptr), QDateTime());
|
||||
QCOMPARE(EventHandler::dateTime(nullptr, nullptr), QDateTime());
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "time called with event set to nullptr.");
|
||||
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);
|
||||
QCOMPARE(EventHandler::dateTime(room, nullptr), QDateTime());
|
||||
}
|
||||
|
||||
void EventHandlerTest::highlighted()
|
||||
|
||||
@@ -19,13 +19,7 @@ class LinkPreviewerTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
Connection *connection = nullptr;
|
||||
TestUtils::TestRoom *room = nullptr;
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
|
||||
void linkPreviewsMatch_data();
|
||||
void linkPreviewsMatch();
|
||||
|
||||
@@ -36,12 +30,6 @@ 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");
|
||||
|
||||
620
autotests/modeltest.cpp
Normal file
620
autotests/modeltest.cpp
Normal file
@@ -0,0 +1,620 @@
|
||||
// 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"
|
||||
@@ -9,6 +9,10 @@
|
||||
#include <Quotient/quotient_common.h>
|
||||
#include <Quotient/syncdata.h>
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include "accountmanager.h"
|
||||
#include "server.h"
|
||||
#include "testutils.h"
|
||||
|
||||
using namespace Quotient;
|
||||
@@ -18,7 +22,8 @@ class NeoChatRoomTest : public QObject {
|
||||
|
||||
private:
|
||||
Connection *connection = nullptr;
|
||||
TestUtils::TestRoom *room = nullptr;
|
||||
NeoChatRoom *room = nullptr;
|
||||
Server server;
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
@@ -27,8 +32,27 @@ private Q_SLOTS:
|
||||
|
||||
void NeoChatRoomTest::initTestCase()
|
||||
{
|
||||
connection = Connection::makeMockConnection(u"@bob:kde.org"_s);
|
||||
room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-min-sync.json"_s);
|
||||
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);
|
||||
}
|
||||
|
||||
void NeoChatRoomTest::eventTest()
|
||||
|
||||
@@ -127,7 +127,7 @@ void Server::start()
|
||||
qFatal() << "Server failed to listen on a port.";
|
||||
return;
|
||||
} else {
|
||||
qWarning() << "Server listening";
|
||||
qInfo() << "Server listening";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +203,25 @@ 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;
|
||||
@@ -334,6 +353,18 @@ 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.
|
||||
@@ -366,6 +397,5 @@ void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &respo
|
||||
syncData[u"rooms"_s] = rooms;
|
||||
}
|
||||
|
||||
qWarning() << syncData;
|
||||
responder.write(QJsonDocument(syncData), QHttpServerResponder::StatusCode::Ok);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ struct Changes {
|
||||
QJsonObject fullJson;
|
||||
};
|
||||
QList<Event> events;
|
||||
QList<Event> stateEvents;
|
||||
};
|
||||
|
||||
struct RoomData {
|
||||
@@ -67,6 +68,7 @@ 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;
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
<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>
|
||||
@@ -227,6 +228,7 @@
|
||||
<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>
|
||||
@@ -487,6 +489,7 @@
|
||||
<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"/>
|
||||
|
||||
1354
po/ar/neochat.po
1354
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
1135
po/ast/neochat.po
1135
po/ast/neochat.po
File diff suppressed because it is too large
Load Diff
1293
po/az/neochat.po
1293
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
1241
po/ca/neochat.po
1241
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1245
po/cs/neochat.po
1245
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
1260
po/da/neochat.po
1260
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
1451
po/de/neochat.po
1451
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
1330
po/el/neochat.po
1330
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
1351
po/en_GB/neochat.po
1351
po/en_GB/neochat.po
File diff suppressed because it is too large
Load Diff
1348
po/eo/neochat.po
1348
po/eo/neochat.po
File diff suppressed because it is too large
Load Diff
1298
po/es/neochat.po
1298
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
1334
po/eu/neochat.po
1334
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
1417
po/fi/neochat.po
1417
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
1346
po/fr/neochat.po
1346
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
1140
po/ga/neochat.po
1140
po/ga/neochat.po
File diff suppressed because it is too large
Load Diff
1336
po/gl/neochat.po
1336
po/gl/neochat.po
File diff suppressed because it is too large
Load Diff
1327
po/he/neochat.po
1327
po/he/neochat.po
File diff suppressed because it is too large
Load Diff
1349
po/hi/neochat.po
1349
po/hi/neochat.po
File diff suppressed because it is too large
Load Diff
1340
po/hu/neochat.po
1340
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
2103
po/ia/neochat.po
2103
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
1300
po/id/neochat.po
1300
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
1285
po/ie/neochat.po
1285
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
1429
po/it/neochat.po
1429
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
1135
po/ja/neochat.po
1135
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
1340
po/ka/neochat.po
1340
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
1333
po/ko/neochat.po
1333
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
1271
po/lt/neochat.po
1271
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
1326
po/lv/neochat.po
1326
po/lv/neochat.po
File diff suppressed because it is too large
Load Diff
1338
po/nl/neochat.po
1338
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
1360
po/nn/neochat.po
1360
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
1293
po/pa/neochat.po
1293
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
1446
po/pl/neochat.po
1446
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
1299
po/pt/neochat.po
1299
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
1248
po/pt_BR/neochat.po
1248
po/pt_BR/neochat.po
File diff suppressed because it is too large
Load Diff
1305
po/ro/neochat.po
1305
po/ro/neochat.po
File diff suppressed because it is too large
Load Diff
1334
po/ru/neochat.po
1334
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
1349
po/sa/neochat.po
1349
po/sa/neochat.po
File diff suppressed because it is too large
Load Diff
1299
po/sk/neochat.po
1299
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
1241
po/sl/neochat.po
1241
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
1329
po/sv/neochat.po
1329
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
1331
po/ta/neochat.po
1331
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
1247
po/tok/neochat.po
1247
po/tok/neochat.po
File diff suppressed because it is too large
Load Diff
1248
po/tr/neochat.po
1248
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
1244
po/uk/neochat.po
1244
po/uk/neochat.po
File diff suppressed because it is too large
Load Diff
1243
po/zh_CN/neochat.po
1243
po/zh_CN/neochat.po
File diff suppressed because it is too large
Load Diff
1421
po/zh_TW/neochat.po
1421
po/zh_TW/neochat.po
File diff suppressed because it is too large
Load Diff
@@ -37,6 +37,8 @@ qt_add_library(neochat STATIC
|
||||
texttospeechhelper.cpp
|
||||
models/limitermodel.cpp
|
||||
models/limitermodel.h
|
||||
supportcontroller.cpp
|
||||
supportcontroller.h
|
||||
)
|
||||
|
||||
set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES
|
||||
@@ -107,6 +109,7 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
||||
qml/UserMenu.qml
|
||||
qml/MeetingDialog.qml
|
||||
qml/SeenByDialog.qml
|
||||
qml/SupportDialog.qml
|
||||
DEPENDENCIES
|
||||
QtCore
|
||||
QtQuick
|
||||
|
||||
@@ -92,7 +92,9 @@ void NotificationsModel::setConnection(NeoChatConnection *connection)
|
||||
|
||||
void NotificationsModel::loadData()
|
||||
{
|
||||
Q_ASSERT(m_connection);
|
||||
if (!m_connection) {
|
||||
return;
|
||||
}
|
||||
if (m_job || (m_notifications.size() && m_nextToken.isEmpty())) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ NotificationsManager::NotificationsManager(QObject *parent)
|
||||
{
|
||||
}
|
||||
|
||||
void NotificationsManager::handleNotifications(QPointer<NeoChatConnection> connection)
|
||||
void NotificationsManager::handleNotifications(const QPointer<NeoChatConnection> &connection)
|
||||
{
|
||||
if (KNotificationPermission::checkPermission() == Qt::PermissionStatus::Granted) {
|
||||
startNotificationJob(connection);
|
||||
@@ -68,7 +68,7 @@ void NotificationsManager::startNotificationJob(QPointer<NeoChatConnection> conn
|
||||
}
|
||||
}
|
||||
|
||||
void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> connection, Quotient::GetNotificationsJob *job, bool initialization)
|
||||
void NotificationsManager::processNotificationJob(const QPointer<NeoChatConnection> &connection, const GetNotificationsJob *job, const bool initialization)
|
||||
{
|
||||
if (!job || !connection || !connection->isLoggedIn()) {
|
||||
return;
|
||||
@@ -82,8 +82,7 @@ void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> co
|
||||
if (!m_initialTimestamp.contains(connectionId)) {
|
||||
m_initialTimestamp[connectionId] = notification["ts"_L1].toVariant().toLongLong();
|
||||
} else {
|
||||
qint64 timestamp = notification["ts"_L1].toVariant().toLongLong();
|
||||
if (timestamp > m_initialTimestamp[connectionId]) {
|
||||
if (const auto timestamp = notification["ts"_L1].toVariant().toLongLong(); timestamp > m_initialTimestamp[connectionId]) {
|
||||
m_initialTimestamp[connectionId] = timestamp;
|
||||
}
|
||||
}
|
||||
@@ -160,29 +159,29 @@ void NotificationsManager::processNotificationJob(QPointer<NeoChatConnection> co
|
||||
}
|
||||
}
|
||||
|
||||
bool NotificationsManager::shouldPostNotification(QPointer<NeoChatConnection> connection, const QJsonValue ¬ification)
|
||||
bool NotificationsManager::shouldPostNotification(const QPointer<NeoChatConnection> &connection, const QJsonValue ¬ification)
|
||||
{
|
||||
if (connection == nullptr || !connection->isLoggedIn()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto room = connection->room(notification["room_id"_L1].toString());
|
||||
const 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 setup so that if the application is inactive the notification will
|
||||
// This is set up so that if the application is inactive, the notification will
|
||||
// always be posted, even if the room is the current room.
|
||||
bool isCurrentRoom = RoomManager::instance().currentRoom() && room->id() == RoomManager::instance().currentRoom()->id();
|
||||
if (isCurrentRoom && QGuiApplication::applicationState() == Qt::ApplicationActive) {
|
||||
if (RoomManager::instance().currentRoom() && room->id() == RoomManager::instance().currentRoom()->id()
|
||||
&& 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.
|
||||
qint64 timestamp = notification["ts"_L1].toDouble();
|
||||
const auto timestamp = notification["ts"_L1].toDouble();
|
||||
if (timestamp < m_initialTimestamp[connection->user()->id()]) {
|
||||
return false;
|
||||
}
|
||||
@@ -199,7 +198,7 @@ void NotificationsManager::postNotification(NeoChatRoom *room,
|
||||
const QString &text,
|
||||
const QImage &icon,
|
||||
const QString &replyEventId,
|
||||
bool canReply,
|
||||
const bool canReply,
|
||||
qint64 timestamp)
|
||||
{
|
||||
const QString roomId = room->id();
|
||||
@@ -271,10 +270,8 @@ void NotificationsManager::postInviteNotification(NeoChatRoom *rawRoom)
|
||||
if (NeoChatConfig::rejectUnknownInvites()) {
|
||||
auto job = room->connection()->callApi<NeochatGetCommonRoomsJob>(roomMemberEvent->senderId());
|
||||
connect(job, &BaseJob::result, this, [this, job, room] {
|
||||
QJsonObject replyData = job->jsonData();
|
||||
if (replyData.contains(u"joined"_s)) {
|
||||
const bool inAnyOfOurRooms = !replyData["joined"_L1].toArray().isEmpty();
|
||||
if (inAnyOfOurRooms) {
|
||||
if (QJsonObject replyData = job->jsonData(); replyData.contains(u"joined"_s)) {
|
||||
if (!replyData["joined"_L1].toArray().isEmpty()) {
|
||||
doPostInviteNotification(room);
|
||||
} else {
|
||||
room->forget();
|
||||
@@ -286,7 +283,7 @@ void NotificationsManager::postInviteNotification(NeoChatRoom *rawRoom)
|
||||
}
|
||||
}
|
||||
|
||||
void NotificationsManager::doPostInviteNotification(QPointer<NeoChatRoom> room)
|
||||
void NotificationsManager::doPostInviteNotification(const QPointer<NeoChatRoom> &room)
|
||||
{
|
||||
const auto roomMemberEvent = room->currentState().get<RoomMemberEvent>(room->localMember().id());
|
||||
if (roomMemberEvent == nullptr) {
|
||||
@@ -295,18 +292,18 @@ void NotificationsManager::doPostInviteNotification(QPointer<NeoChatRoom> room)
|
||||
const auto sender = room->member(roomMemberEvent->senderId());
|
||||
|
||||
QImage avatar_image;
|
||||
if (roomMemberEvent && !room->member(roomMemberEvent->senderId()).avatarUrl().isEmpty()) {
|
||||
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);
|
||||
}
|
||||
|
||||
KNotification *notification = new KNotification(u"invite"_s);
|
||||
const auto 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));
|
||||
auto defaultAction = notification->addDefaultAction(i18n("Open this invitation in NeoChat"));
|
||||
const auto defaultAction = notification->addDefaultAction(i18n("Open this invitation in NeoChat"));
|
||||
connect(defaultAction, &KNotificationAction::activated, this, [notification, room]() {
|
||||
if (!room) {
|
||||
return;
|
||||
@@ -367,11 +364,9 @@ 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 (type == u"m.room.message"_s || type == u"m.room.encrypted"_s) {
|
||||
auto notification = new KNotification("message"_L1);
|
||||
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);
|
||||
|
||||
const auto sender = json["notification"_L1]["sender_display_name"_L1].toString();
|
||||
const auto roomName = json["notification"_L1]["room_name"_L1].toString();
|
||||
@@ -391,13 +386,13 @@ void NotificationsManager::postPushNotification(const QByteArray &message)
|
||||
}
|
||||
|
||||
#ifdef HAVE_KIO
|
||||
auto openAction = notification->addAction(i18n("Open NeoChat"));
|
||||
const 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());
|
||||
|
||||
auto *job = new KIO::ApplicationLauncherJob(KService::serviceByDesktopName(u"org.kde.neochat"_s));
|
||||
const 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();
|
||||
});
|
||||
@@ -428,13 +423,12 @@ QPixmap NotificationsManager::createNotificationImage(const QImage &icon, NeoCha
|
||||
painter.setBrush(Qt::white);
|
||||
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
|
||||
|
||||
QBrush brush(icon.scaledToHeight(biggestDimension));
|
||||
const QBrush brush(icon.scaledToHeight(biggestDimension));
|
||||
painter.setBrush(brush);
|
||||
painter.drawRoundedRect(imageRect, imageRect.width(), imageRect.height());
|
||||
|
||||
if (room != nullptr) {
|
||||
const QImage roomAvatar = room->avatar(imageRect.width(), imageRect.height());
|
||||
if (!roomAvatar.isNull() && icon != roomAvatar) {
|
||||
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);
|
||||
|
||||
@@ -58,7 +58,7 @@ public:
|
||||
/**
|
||||
* @brief Handle the notifications for the given connection.
|
||||
*/
|
||||
void handleNotifications(QPointer<NeoChatConnection> connection);
|
||||
void handleNotifications(const QPointer<NeoChatConnection> &connection);
|
||||
|
||||
private:
|
||||
QHash<QString, qint64> m_initialTimestamp;
|
||||
@@ -67,8 +67,8 @@ private:
|
||||
QStringList m_connActiveJob;
|
||||
void startNotificationJob(QPointer<NeoChatConnection> connection);
|
||||
|
||||
QPixmap createNotificationImage(const QImage &icon, NeoChatRoom *room);
|
||||
bool shouldPostNotification(QPointer<NeoChatConnection> connection, const QJsonValue ¬ification);
|
||||
static QPixmap createNotificationImage(const QImage &icon, NeoChatRoom *room);
|
||||
bool shouldPostNotification(const QPointer<NeoChatConnection> &connection, const QJsonValue ¬ification);
|
||||
void postNotification(NeoChatRoom *room,
|
||||
const QString &sender,
|
||||
const QString &text,
|
||||
@@ -77,7 +77,7 @@ private:
|
||||
bool canReply,
|
||||
qint64 timestamp);
|
||||
|
||||
void doPostInviteNotification(QPointer<NeoChatRoom> room);
|
||||
void doPostInviteNotification(const QPointer<NeoChatRoom> &room);
|
||||
|
||||
QHash<QString, std::pair<qint64, KNotification *>> m_notifications;
|
||||
QHash<QString, QPointer<KNotification>> m_invitations;
|
||||
@@ -85,5 +85,5 @@ private:
|
||||
bool permissionAsked = false;
|
||||
|
||||
private Q_SLOTS:
|
||||
void processNotificationJob(QPointer<NeoChatConnection> connection, Quotient::GetNotificationsJob *job, bool initialization);
|
||||
void processNotificationJob(const QPointer<NeoChatConnection> &connection, const Quotient::GetNotificationsJob *job, bool initialization);
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtMultimedia
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.components as KirigamiComponents
|
||||
@@ -18,27 +19,25 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
required property NeoChatConnection connection
|
||||
required property Kirigami.ApplicationWindow window
|
||||
|
||||
Kirigami.Action {
|
||||
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();
|
||||
}
|
||||
data: MediaDevices {
|
||||
id: devices
|
||||
}
|
||||
|
||||
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, {
|
||||
text: i18nc("@action:button", "Open Profile")
|
||||
icon.name: "im-user-symbolic"
|
||||
onTriggered: RoomManager.resolveResource(root.connection.localUserId, "qr") // Use "qr" action to make sure a room isn't passed, see RoomManager::visitUser
|
||||
}
|
||||
|
||||
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
|
||||
}) as Kirigami.Dialog).open();
|
||||
}, {
|
||||
title: i18nc("@title", "Scan a QR Code")
|
||||
})
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
@@ -55,14 +54,6 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:inmenu", "Devices")
|
||||
icon.name: "computer-symbolic"
|
||||
onTriggered: {
|
||||
NeoChatSettingsView.open('devices');
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:inmenu", "Open Developer Tools")
|
||||
icon.name: "tools"
|
||||
@@ -76,14 +67,6 @@ 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 {
|
||||
text: i18nc("@action:inmenu", "Verify This Device")
|
||||
icon.name: "security-low"
|
||||
@@ -103,10 +86,25 @@ KirigamiComponents.ConvergentContextMenu {
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:inmenu", "Logout…")
|
||||
icon.name: "im-kick-user"
|
||||
onTriggered: (Qt.createComponent("org.kde.neochat", "ConfirmLogoutDialog").createObject(QQC2.Overlay.overlay, {
|
||||
text: i18nc("@action:inmenu Open support dialog", "Support")
|
||||
icon.name: "help-contents-symbolic"
|
||||
onTriggered: {
|
||||
(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
|
||||
}) as Kirigami.Dialog).open()
|
||||
}) as Kirigami.Dialog).open();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ Components.AbstractMaximizeComponent {
|
||||
property NeochatRoomMember author
|
||||
|
||||
/**
|
||||
* @brief The timestamp of the message.
|
||||
* @brief The timestamp of the event as a neoChatDateTime.
|
||||
*/
|
||||
property var time
|
||||
required property neoChatDateTime dateTime
|
||||
|
||||
/**
|
||||
* @brief The code text to show.
|
||||
@@ -64,7 +64,7 @@ Components.AbstractMaximizeComponent {
|
||||
}
|
||||
QQC2.Label {
|
||||
id: dateTimeLabel
|
||||
text: root.time.toLocaleString(Qt.locale(), Locale.ShortFormat)
|
||||
text: root.dateTime.relativeDateTime
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
@@ -79,7 +79,7 @@ Components.AbstractMaximizeComponent {
|
||||
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
|
||||
|
||||
QQC2.TextArea {
|
||||
id: codeText
|
||||
id: codeTextEdit
|
||||
topPadding: Kirigami.Units.smallSpacing
|
||||
bottomPadding: Kirigami.Units.smallSpacing
|
||||
leftPadding: lineNumberColumn.width + lineNumberColumn.anchors.leftMargin + Kirigami.Units.smallSpacing * 2
|
||||
@@ -100,15 +100,15 @@ Components.AbstractMaximizeComponent {
|
||||
|
||||
SyntaxHighlighter {
|
||||
property string definitionName: Repository.definitionForName(root.language).name
|
||||
textEdit: definitionName == "None" ? null : codeText
|
||||
textEdit: definitionName == "None" ? null : codeTextEdit
|
||||
definition: definitionName
|
||||
}
|
||||
ColumnLayout {
|
||||
id: lineNumberColumn
|
||||
anchors {
|
||||
top: codeText.top
|
||||
topMargin: codeText.topPadding + 1
|
||||
left: codeText.left
|
||||
top: codeTextEdit.top
|
||||
topMargin: codeTextEdit.topPadding + 1
|
||||
left: codeTextEdit.left
|
||||
leftMargin: Kirigami.Units.smallSpacing
|
||||
}
|
||||
spacing: 0
|
||||
@@ -116,7 +116,7 @@ Components.AbstractMaximizeComponent {
|
||||
id: repeater
|
||||
model: LineModel {
|
||||
id: lineModel
|
||||
document: codeText.textDocument
|
||||
Component.onCompleted: setDocument(codeTextEdit.textDocument)
|
||||
}
|
||||
delegate: QQC2.Label {
|
||||
id: label
|
||||
@@ -150,4 +150,6 @@ Components.AbstractMaximizeComponent {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: forceActiveFocus()
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtQml.Models
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||
|
||||
@@ -8,13 +8,31 @@ import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
import Quotient
|
||||
|
||||
Kirigami.PromptDialog {
|
||||
id: root
|
||||
|
||||
required property NeoChatRoom 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) : ""
|
||||
title: root.room.isSpace ? i18nc("@title:dialog", "Confirm Leaving Space") : i18nc("@title:dialog", "Confirm Leaving Room")
|
||||
subtitle: {
|
||||
if (root.room) {
|
||||
let message = xi18nc("@info Do you really want to leave <room name>?", "Do you really want to leave %1?", root.room.displayNameForHtml)
|
||||
|
||||
// List any possible side-effects the user needs to be made aware of.
|
||||
if (root.room.historyVisibility !== "world_readable" && root.room.historyVisibility !== "shared") {
|
||||
message += xi18nc("@info", "<br><strong>This room's history is limited to when you rejoin the room.</strong>")
|
||||
}
|
||||
|
||||
if (root.room.joinRule === JoinRule.JoinRule.Invite) {
|
||||
message += xi18nc("@info", "<br><strong>This room can only be rejoined with an invite.</strong>");
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
dialogType: Kirigami.PromptDialog.Warning
|
||||
|
||||
onRejected: {
|
||||
@@ -28,7 +46,7 @@ Kirigami.PromptDialog {
|
||||
text: i18nc("@action:button", "Leave Room")
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
||||
icon.name: "arrow-left-symbolic"
|
||||
onClicked: root.room.forget();
|
||||
//onClicked: root.room.forget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ Kirigami.Page {
|
||||
icon.name: "document-edit"
|
||||
onTriggered: {
|
||||
root.room.setRoomState(root.type, root.stateKey, sourceTextArea.text);
|
||||
root.closeDialog();
|
||||
root.Kirigami.PageStack.closeDialog();
|
||||
}
|
||||
enabled: QmlUtils.isValidJson(sourceTextArea.text)
|
||||
}
|
||||
@@ -85,7 +85,7 @@ Kirigami.Page {
|
||||
id: repeater
|
||||
model: LineModel {
|
||||
id: lineModel
|
||||
document: sourceTextArea.textDocument
|
||||
Component.onCompleted: setDocument(sourceTextArea.textDocument)
|
||||
}
|
||||
delegate: QQC2.Label {
|
||||
id: label
|
||||
|
||||
@@ -41,13 +41,11 @@ ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
QQC2.Button {
|
||||
anchors.bottom: parent.bottom
|
||||
text: i18n("They match")
|
||||
icon.name: "dialog-ok"
|
||||
onClicked: root.accept()
|
||||
}
|
||||
QQC2.Button {
|
||||
anchors.bottom: parent.bottom
|
||||
text: i18n("They don't match")
|
||||
icon.name: "dialog-cancel"
|
||||
onClicked: root.reject()
|
||||
|
||||
@@ -9,8 +9,6 @@ import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
import org.kde.kirigamiaddons.labs.components as Components
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Delegates.RoundedItemDelegate {
|
||||
id: root
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: root.currentRoom.connection.canCheckMutualRooms
|
||||
visible: (root.currentRoom.connection as NeoChatConnection).canCheckMutualRooms
|
||||
spacing: 0
|
||||
|
||||
Layout.topMargin: Kirigami.Units.largeSpacing * 2
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
@@ -8,7 +10,6 @@ import QtQuick.Window
|
||||
import QtQml
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Page {
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtLocation
|
||||
import QtPositioning
|
||||
|
||||
@@ -45,6 +43,8 @@ Components.AbstractMaximizeComponent {
|
||||
}
|
||||
]
|
||||
|
||||
onOpened: forceActiveFocus()
|
||||
|
||||
PositionSource {
|
||||
id: positionSource
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ Kirigami.ApplicationWindow {
|
||||
|
||||
function onCurrentRoomChanged() {
|
||||
if (RoomManager.currentRoom && root.pageStack.depth <= 1 && root.initialized && Kirigami.Settings.isMobile) {
|
||||
let roomPage = pageStack.push(Qt.createComponent('org.kde.neochat', 'RoomPage'));
|
||||
let roomPage = root.pageStack.push(Qt.createComponent('org.kde.neochat', 'RoomPage'));
|
||||
roomPage.forceActiveFocus();
|
||||
roomPage.backRequested.connect(event => {
|
||||
RoomManager.clearCurrentRoom();
|
||||
@@ -151,8 +151,6 @@ Kirigami.ApplicationWindow {
|
||||
}
|
||||
|
||||
contextDrawer: RoomDrawer {
|
||||
id: contextDrawer
|
||||
|
||||
// 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
|
||||
property bool drawerUserState: NeoChatConfig.autoRoomInfoDrawer
|
||||
@@ -178,9 +176,9 @@ Kirigami.ApplicationWindow {
|
||||
|
||||
// Connect to the onClicked function of the RoomDrawer handle button
|
||||
Connections {
|
||||
target: contextDrawer.handle.children[0]
|
||||
target: root.contextDrawer.handle.children[0]
|
||||
function onClicked() {
|
||||
contextDrawer.drawerUserState = contextDrawer.drawerOpen;
|
||||
root.contextDrawer.drawerUserState = root.contextDrawer.drawerOpen;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,9 +72,9 @@ Kirigami.Dialog {
|
||||
return null;
|
||||
}
|
||||
if (isAlias()) {
|
||||
return root.connection.roomByAlias(text);
|
||||
return root.connection.roomByAlias(text) as NeoChatRoom;
|
||||
} else {
|
||||
return root.connection.room(text);
|
||||
return root.connection.room(text) as NeoChatRoom;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
@@ -16,7 +18,7 @@ Kirigami.PromptDialog {
|
||||
|
||||
customFooterActions: Kirigami.Action {
|
||||
icon.name: "camera-video-symbolic"
|
||||
text: hasExistingMeeting ? i18nc("@action:button Join the Jitsi meeting", "Join") : i18nc("@action:button Start a new Jitsi meeting", "Start")
|
||||
text: root.hasExistingMeeting ? i18nc("@action:button Join the Jitsi meeting", "Join") : i18nc("@action:button Start a new Jitsi meeting", "Start")
|
||||
onTriggered: root.accept()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ Kirigami.Page {
|
||||
id: repeater
|
||||
model: LineModel {
|
||||
id: lineModel
|
||||
document: sourceTextArea.textDocument
|
||||
Component.onCompleted: setDocument(sourceTextArea.textDocument)
|
||||
}
|
||||
delegate: QQC2.Label {
|
||||
id: label
|
||||
|
||||
@@ -66,7 +66,7 @@ Kirigami.Dialog {
|
||||
id: optionModel
|
||||
|
||||
readonly property bool allValuesSet: {
|
||||
for( var i = 0; i < optionModel.rowCount(); i++ ) {
|
||||
for (let i = 0; i < optionModel.rowCount(); i++) {
|
||||
if (optionModel.get(i).optionText.length <= 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -83,7 +83,7 @@ Kirigami.Dialog {
|
||||
|
||||
function values() {
|
||||
let textValues = []
|
||||
for( var i = 0; i < optionModel.rowCount(); i++ ) {
|
||||
for(let i = 0; i < optionModel.rowCount(); i++) {
|
||||
textValues.push(optionModel.get(i).optionText);
|
||||
}
|
||||
return textValues;
|
||||
|
||||
@@ -18,7 +18,7 @@ Kirigami.Page {
|
||||
required property NeoChatConnection connection
|
||||
padding: 0
|
||||
|
||||
Component.onCompleted: camera.start()
|
||||
Component.onCompleted: session.camera.start()
|
||||
|
||||
Connections {
|
||||
target: root.QQC2.ApplicationWindow.window
|
||||
@@ -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
|
||||
}
|
||||
@@ -66,12 +66,8 @@ Kirigami.Page {
|
||||
CaptureSession {
|
||||
id: session
|
||||
|
||||
camera: Camera {
|
||||
id: camera
|
||||
}
|
||||
imageCapture: ImageCapture {
|
||||
id: imageCapture
|
||||
}
|
||||
camera: Camera {}
|
||||
imageCapture: ImageCapture {}
|
||||
videoOutput: viewFinder
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ Kirigami.SearchDialog {
|
||||
}
|
||||
|
||||
onAccepted: if (currentItem) {
|
||||
(currentItem as QQC2.ItemDelegate).clicked();
|
||||
(root.currentItem as RoomDelegate).clicked();
|
||||
}
|
||||
|
||||
onTextChanged: RoomManager.sortFilterRoomListModel.filterText = text
|
||||
|
||||
@@ -6,6 +6,7 @@ import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Page {
|
||||
id: root
|
||||
@@ -13,6 +14,8 @@ Kirigami.Page {
|
||||
required property string placeholder
|
||||
required property string actionText
|
||||
required property string icon
|
||||
required property bool reporting
|
||||
required property NeoChatConnection connection
|
||||
|
||||
signal accepted(reason: string)
|
||||
|
||||
@@ -21,6 +24,15 @@ Kirigami.Page {
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
|
||||
header: Kirigami.InlineMessage {
|
||||
showCloseButton: false
|
||||
visible: root.reporting
|
||||
type: Kirigami.MessageType.Information
|
||||
position: Kirigami.InlineMessage.Position.Header
|
||||
|
||||
text: xi18nc("@info", "This report will <strong>only</strong> be sent to the administrators of <link>%1</link> (your server).", root.connection.domain)
|
||||
}
|
||||
|
||||
QQC2.TextArea {
|
||||
id: reason
|
||||
placeholderText: root.placeholder
|
||||
|
||||
@@ -75,6 +75,20 @@ 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.
|
||||
resetViewSettling();
|
||||
}
|
||||
|
||||
// Resets the view settling of the timeline.
|
||||
// This should be called whenever the apparent height of the timeline changes, or else the view will scroll on its own!
|
||||
function resetViewSettling(): void {
|
||||
(timelineViewLoader.item as TimelineView).resetViewSettling();
|
||||
}
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
id: jitsiMeetingAction
|
||||
@@ -96,8 +110,8 @@ Kirigami.Page {
|
||||
enabled: hasExistingMeeting || canStartNewMeeting
|
||||
visible: root.currentRoom && !root.currentRoom.isSpace
|
||||
onTriggered: {
|
||||
const dialog = Qt.createComponent("org.kde.neochat", "MeetingDialog").createObject(QQC2.Overlay.overlay, { hasExistingMeeting });
|
||||
dialog.onAccepted.connect(doAction);
|
||||
const dialog = Qt.createComponent("org.kde.neochat", "MeetingDialog").createObject(QQC2.Overlay.overlay, { hasExistingMeeting }) as MeetingDialog;
|
||||
dialog.accepted.connect(doAction);
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
@@ -207,7 +221,7 @@ Kirigami.Page {
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RoomPinnedMessagesPage'), {
|
||||
onTapped: (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RoomPinnedMessagesPage'), {
|
||||
room: root.currentRoom
|
||||
}, {
|
||||
title: i18nc("@title", "Pinned Messages")
|
||||
@@ -221,6 +235,58 @@ Kirigami.Page {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Kirigami.InlineMessage {
|
||||
id: selectedMessagesControl
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
showCloseButton: false
|
||||
visible: root.currentRoom?.selectedMessageCount > 0
|
||||
position: Kirigami.InlineMessage.Position.Header
|
||||
type: Kirigami.MessageType.Positive
|
||||
icon.name: "edit-select-all-symbolic"
|
||||
|
||||
text: i18nc("@info", "Selected Messages: %1", root.currentRoom?.selectedMessageCount)
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:button", "Copy Conversation")
|
||||
icon.name: "edit-copy"
|
||||
onTriggered: {
|
||||
Clipboard.saveText(root.currentRoom.getFormattedSelectedMessages())
|
||||
showPassiveNotification(i18nc("@info", "Conversation copied to clipboard"));
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:button", "Delete Messages")
|
||||
icon.name: "trash-empty-symbolic"
|
||||
icon.color: Kirigami.Theme.negativeTextColor
|
||||
enabled: root.currentRoom?.canDeleteSelectedMessages
|
||||
onTriggered: {
|
||||
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
||||
title: i18nc("@title:dialog", "Remove Messages"),
|
||||
placeholder: i18nc("@info:placeholder", "Optionally give a reason for removing these messages"),
|
||||
actionText: i18nc("@action:button 'Remove' as in 'Remove these messages'", "Remove"),
|
||||
icon: "delete",
|
||||
reporting: false,
|
||||
connection: root.currentRoom.connection,
|
||||
}, {
|
||||
title: i18nc("@title:dialog", "Remove Messages"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
}) as ReasonDialog;
|
||||
dialog.accepted.connect(reason => {
|
||||
root.currentRoom.deleteSelectedMessages(reason);
|
||||
});
|
||||
}
|
||||
},
|
||||
Kirigami.Action {
|
||||
icon.name: "dialog-close"
|
||||
icon.color: Kirigami.Theme.negativeTextColor
|
||||
onTriggered: root.currentRoom.clearSelectedMessages()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Kirigami.InlineMessage {
|
||||
id: banner
|
||||
|
||||
@@ -296,6 +362,9 @@ Kirigami.Page {
|
||||
width: parent.width
|
||||
currentRoom: root.currentRoom
|
||||
connection: root.currentRoom.connection as NeoChatConnection
|
||||
|
||||
// Creating a reply (or doing anything in the chat bar) can change the height, but this isn't picked up on the root's onHeightChanged.
|
||||
onHeightChanged: root.resetViewSettling()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,8 +418,9 @@ Kirigami.Page {
|
||||
});
|
||||
}
|
||||
|
||||
function onShowDelegateMenu(parent: QtObject, eventId: string, author, messageComponentType, plainText: string, richText: string, mimeType: string, progressInfo, selectedText: string, hoveredLink: string) {
|
||||
function onShowDelegateMenu(parent: QtObject, room: NeoChatRoom, eventId: string, author, messageComponentType, plainText: string, richText: string, mimeType: string, progressInfo, selectedText: string, hoveredLink: string) {
|
||||
(delegateContextMenu.createObject(parent, {
|
||||
room: room,
|
||||
author: author,
|
||||
eventId: eventId,
|
||||
plainText: plainText,
|
||||
@@ -373,10 +443,10 @@ Kirigami.Page {
|
||||
popup.open();
|
||||
}
|
||||
|
||||
function onShowMaximizedCode(author, time, codeText, language) {
|
||||
function onShowMaximizedCode(author, dateTime, codeText, language) {
|
||||
(Qt.createComponent('org.kde.neochat', 'CodeMaximizeComponent').createObject(QQC2.Overlay.overlay, {
|
||||
author: author,
|
||||
time: time,
|
||||
dateTime: dateTime,
|
||||
codeText: codeText,
|
||||
language: language
|
||||
}) as CodeMaximizeComponent).open();
|
||||
|
||||
@@ -27,7 +27,7 @@ Kirigami.Page {
|
||||
|
||||
QQC2.Action {
|
||||
shortcut: 'Escape'
|
||||
onTriggered: root.closeDialog()
|
||||
onTriggered: Kirigami.PageStack.closeDialog()
|
||||
}
|
||||
|
||||
Notification {
|
||||
@@ -53,20 +53,16 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
123
src/app/qml/SupportDialog.qml
Normal file
123
src/app/qml/SupportDialog.qml
Normal file
@@ -0,0 +1,123 @@
|
||||
// SPDX-FileCopyrightText: 2026 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Dialog {
|
||||
id: root
|
||||
|
||||
required property NeoChatConnection connection
|
||||
|
||||
readonly property SupportController supportController: SupportController {
|
||||
connection: root.connection
|
||||
}
|
||||
readonly property bool hasSupportResources: supportController.supportPage.length > 0 && supportController.contacts.length > 0
|
||||
|
||||
title: i18nc("@title Support information", "Support")
|
||||
width: Math.min(Kirigami.Units.gridUnit * 30, QQC2.ApplicationWindow.window.width)
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
FormCard.FormTextDelegate {
|
||||
id: explanationTextDelegate
|
||||
|
||||
text: root.hasSupportResources ?
|
||||
i18nc("@info:label %1 is the domain of the server", "Official support resources provided by %1:", root.connection.domain)
|
||||
: i18nc("@info:label %1 is the domain of the server", "%1 has no support resources.", root.connection.domain)
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {
|
||||
above: explanationTextDelegate
|
||||
below: openSupportPageDelegate
|
||||
visible: openSupportPageDelegate.visible
|
||||
}
|
||||
|
||||
FormCard.FormLinkDelegate {
|
||||
id: openSupportPageDelegate
|
||||
|
||||
icon.name: "help-contents-symbolic"
|
||||
text: i18nc("@action:button Open support webpage", "Open Support")
|
||||
url: root.supportController.supportPage
|
||||
visible: root.supportController.supportPage.length > 0
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {
|
||||
above: openSupportPageDelegate
|
||||
visible: root.supportController.contacts.length > 0
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.supportController.contacts
|
||||
|
||||
delegate: FormCard.AbstractFormDelegate {
|
||||
id: contactDelegate
|
||||
|
||||
required property string role
|
||||
required property string matrixId
|
||||
required property string emailAddress
|
||||
|
||||
background: null
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: Kirigami.Units.largeSpacing
|
||||
|
||||
Kirigami.Icon {
|
||||
source: "user"
|
||||
}
|
||||
|
||||
QQC2.Label {
|
||||
text: {
|
||||
// Translate known keys
|
||||
if (contactDelegate.role === "m.role.admin") {
|
||||
return i18nc("@info:label Adminstrator contact", "Admin")
|
||||
} else if (contactDelegate.role === "m.role.security") {
|
||||
return i18nc("@info:label Security contact", "Security")
|
||||
}
|
||||
return contactDelegate.role;
|
||||
}
|
||||
elide: Text.ElideRight
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
visible: contactDelegate.matrixId.length > 0
|
||||
icon.name: "document-send-symbolic"
|
||||
|
||||
onClicked: {
|
||||
root.close();
|
||||
root.connection.requestDirectChat(contactDelegate.matrixId);
|
||||
}
|
||||
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: i18nc("@info:tooltip %1 is a Matrix ID", "Contact via Matrix (%1)", contactDelegate.matrixId)
|
||||
}
|
||||
|
||||
QQC2.ToolButton {
|
||||
visible: contactDelegate.emailAddress.length > 0
|
||||
icon.name: "mail-sent-symbolic"
|
||||
|
||||
onClicked: Qt.openUrlExternally("mailto:%1".arg(contactDelegate.emailAddress))
|
||||
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.text: i18nc("@info:tooltip %1 is an e-mail address", "Contact via e-mail (%1)", contactDelegate.emailAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ FormCard.FormCardPage {
|
||||
|
||||
property bool processing: false
|
||||
|
||||
title: i18nc("@title:window", "Load your encrypted messages")
|
||||
title: i18nc("@title:window", "Manage Secret Backup")
|
||||
|
||||
topPadding: Kirigami.Units.gridUnit
|
||||
leftPadding: 0
|
||||
|
||||
@@ -8,7 +8,6 @@ import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.components as KirigamiComponents
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
import org.kde.prison
|
||||
|
||||
import org.kde.neochat
|
||||
@@ -39,6 +38,7 @@ 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
|
||||
@@ -64,10 +64,11 @@ Kirigami.Dialog {
|
||||
|
||||
KirigamiComponents.Avatar {
|
||||
id: avatar
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.huge
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.huge
|
||||
|
||||
name: root.room ? root.room.member(root.user.id).displayName : root.user.displayName
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.large
|
||||
Layout.preferredHeight: Kirigami.Units.iconSizes.large
|
||||
|
||||
name: root.displayName
|
||||
source: {
|
||||
if (root.room) {
|
||||
return root.room.member(root.user.id).avatarUrl;
|
||||
@@ -80,27 +81,29 @@ 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.room ? root.room.member(root.user.id).displayName : root.user.displayName
|
||||
text: root.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
|
||||
@@ -109,108 +112,121 @@ 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", "Reason for reporting this user"),
|
||||
icon: "dialog-warning-symbolic",
|
||||
actionText: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report")
|
||||
}, {
|
||||
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 {
|
||||
@@ -244,9 +260,11 @@ Kirigami.Dialog {
|
||||
onTriggered: {
|
||||
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
||||
title: i18nc("@title:dialog", "Kick User"),
|
||||
placeholder: i18nc("@info:placeholder", "Reason for kicking this user"),
|
||||
placeholder: i18nc("@info:placeholder", "Optionally give a reason for kicking this user"),
|
||||
actionText: i18nc("@action:button 'Kick' as in 'Kick this user from the room'", "Kick"),
|
||||
icon: "im-kick-user"
|
||||
icon: "im-kick-user",
|
||||
reporting: false,
|
||||
connection: root.connection,
|
||||
}, {
|
||||
title: i18nc("@title:dialog", "Kick User"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
@@ -271,9 +289,11 @@ Kirigami.Dialog {
|
||||
onTriggered: {
|
||||
let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), {
|
||||
title: i18nc("@title:dialog", "Ban User"),
|
||||
placeholder: i18nc("@info:placeholder", "Reason for banning this user"),
|
||||
placeholder: i18nc("@info:placeholder", "Optionally give a reason for banning this user"),
|
||||
actionText: i18nc("@action:button 'Ban' as in 'Ban this user'", "Ban"),
|
||||
icon: "im-ban-user"
|
||||
icon: "im-ban-user",
|
||||
reporting: false,
|
||||
connection: root.connection,
|
||||
}, {
|
||||
title: i18nc("@title:dialog", "Ban User"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
@@ -309,9 +329,11 @@ Kirigami.Dialog {
|
||||
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", "Remove Messages"),
|
||||
placeholder: i18nc("@info:placeholder", "Reason for removing this user's recent messages"),
|
||||
placeholder: i18nc("@info:placeholder", "Optionally give a reason for removing this user's recent messages"),
|
||||
actionText: i18nc("@action:button 'Remove' as in 'Remove these messages'", "Remove"),
|
||||
icon: "delete"
|
||||
icon: "delete",
|
||||
reporting: false,
|
||||
connection: root.connection,
|
||||
}, {
|
||||
title: i18nc("@title", "Remove Messages"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
|
||||
@@ -29,6 +29,28 @@
|
||||
#include <KIO/OpenUrlJob>
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Stops RoomManager from updating the last room and space config.
|
||||
*/
|
||||
class LastRoomBlocker
|
||||
{
|
||||
public:
|
||||
explicit LastRoomBlocker(RoomManager *manager)
|
||||
: m_manager(manager)
|
||||
{
|
||||
Q_ASSERT(manager);
|
||||
|
||||
m_manager->m_dontUpdateLastRoom = true;
|
||||
}
|
||||
~LastRoomBlocker()
|
||||
{
|
||||
m_manager->m_dontUpdateLastRoom = false;
|
||||
}
|
||||
|
||||
private:
|
||||
RoomManager *m_manager;
|
||||
};
|
||||
|
||||
RoomManager::RoomManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_config(KSharedConfig::openStateConfig())
|
||||
@@ -264,12 +286,12 @@ void RoomManager::maximizeMedia(const QString &eventId)
|
||||
Q_EMIT showMaximizedMedia(index);
|
||||
}
|
||||
|
||||
void RoomManager::maximizeCode(NeochatRoomMember *author, const QDateTime &time, const QString &codeText, const QString &language)
|
||||
void RoomManager::maximizeCode(NeochatRoomMember *author, const NeoChatDateTime &dateTime, const QString &codeText, const QString &language)
|
||||
{
|
||||
if (codeText.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Q_EMIT showMaximizedCode(author, time, codeText, language);
|
||||
Q_EMIT showMaximizedCode(author, dateTime, codeText, language);
|
||||
}
|
||||
|
||||
void RoomManager::requestFullScreenClose()
|
||||
@@ -290,6 +312,7 @@ void RoomManager::viewEventMenu(QObject *parent, const RoomEvent *event, NeoChat
|
||||
}
|
||||
|
||||
Q_EMIT showDelegateMenu(parent,
|
||||
room,
|
||||
event->id(),
|
||||
room->qmlSafeMember(event->senderId()),
|
||||
MessageComponentType::typeForEvent(*event),
|
||||
@@ -319,17 +342,6 @@ void RoomManager::loadInitialRoom()
|
||||
resolveResource(m_arg);
|
||||
}
|
||||
|
||||
if (m_isMobile) {
|
||||
QString lastSpace = m_lastRoomConfig.readEntry(u"lastSpace"_s, QString());
|
||||
// We can't have empty keys in KConfig, so we stored it as "Home"
|
||||
if (lastSpace == u"Home"_s) {
|
||||
lastSpace.clear();
|
||||
}
|
||||
setCurrentSpace(lastSpace, false);
|
||||
// We don't want to open a room on startup on mobile
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_currentRoom) {
|
||||
// we opened a room with the arg parsing already
|
||||
return;
|
||||
@@ -343,15 +355,15 @@ void RoomManager::loadInitialRoom()
|
||||
void RoomManager::openRoomForActiveConnection()
|
||||
{
|
||||
if (!m_connection) {
|
||||
setCurrentRoom({});
|
||||
setCurrentSpace({}, false);
|
||||
return;
|
||||
}
|
||||
|
||||
auto lastSpace = m_lastRoomConfig.readEntry(u"lastSpace"_s, QString());
|
||||
if (lastSpace == u"Home"_s) {
|
||||
lastSpace.clear();
|
||||
}
|
||||
setCurrentSpace(lastSpace, true);
|
||||
// We don't want to open a room on startup on mobile
|
||||
setCurrentSpace(lastSpace, !m_isMobile);
|
||||
}
|
||||
|
||||
UriResolveResult RoomManager::visitUser(User *user, const QString &action)
|
||||
@@ -508,7 +520,7 @@ void RoomManager::setConnection(NeoChatConnection *connection)
|
||||
Q_EMIT connectionChanged();
|
||||
}
|
||||
|
||||
void RoomManager::setCurrentSpace(const QString &spaceId, bool setRoom)
|
||||
void RoomManager::setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom)
|
||||
{
|
||||
m_currentSpaceId = spaceId;
|
||||
|
||||
@@ -528,25 +540,26 @@ void RoomManager::setCurrentSpace(const QString &spaceId, bool setRoom)
|
||||
m_lastRoomConfig.writeEntry(u"lastSpace"_s, spaceId.isEmpty() ? u"Home"_s : spaceId);
|
||||
}
|
||||
|
||||
if (!setRoom) {
|
||||
return;
|
||||
}
|
||||
// If we requested to change to the last opened room, do so:
|
||||
if (goToLastUsedRoom) {
|
||||
// We don't want to needlessly update the last room config here, that should only be done during explicit user action.
|
||||
LastRoomBlocker blocker(this);
|
||||
|
||||
// We intentionally don't want to open the last room on mobile
|
||||
if (m_isMobile) {
|
||||
return;
|
||||
}
|
||||
// We can't have empty keys in KConfig, so it's stored as "Home":
|
||||
if (const auto &lastRoom = m_lastRoomConfig.readEntry(spaceId.isEmpty() ? u"Home"_s : spaceId, QString()); !lastRoom.isEmpty()) {
|
||||
resolveResource(lastRoom, "no_join"_L1);
|
||||
return;
|
||||
}
|
||||
|
||||
// We can't have empty keys in KConfig, so it's stored as "Home"
|
||||
if (const auto &lastRoom = m_lastRoomConfig.readEntry(spaceId.isEmpty() ? u"Home"_s : spaceId, QString()); !lastRoom.isEmpty()) {
|
||||
resolveResource(lastRoom, "no_join"_L1);
|
||||
return;
|
||||
// If no last room was opened, go to the space home:
|
||||
if (!spaceId.isEmpty() && spaceId != u"DM"_s) {
|
||||
resolveResource(spaceId, "no_join"_L1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to no room opened:
|
||||
setCurrentRoom({});
|
||||
}
|
||||
if (!spaceId.isEmpty() && spaceId != u"DM"_s) {
|
||||
resolveResource(spaceId, "no_join"_L1);
|
||||
return;
|
||||
}
|
||||
setCurrentRoom({});
|
||||
}
|
||||
|
||||
QString RoomManager::findSpaceIdForCurrentRoom() const
|
||||
@@ -591,6 +604,7 @@ QString RoomManager::findSpaceIdForCurrentRoom() const
|
||||
void RoomManager::setCurrentRoom(const QString &roomId)
|
||||
{
|
||||
if (m_currentRoom != nullptr) {
|
||||
m_currentRoom->clearSelectedMessages();
|
||||
m_currentRoom->disconnect(this);
|
||||
}
|
||||
|
||||
@@ -606,21 +620,23 @@ void RoomManager::setCurrentRoom(const QString &roomId)
|
||||
|
||||
Q_EMIT currentRoomChanged();
|
||||
|
||||
if (roomId.isEmpty()) {
|
||||
m_lastRoomConfig.deleteEntry(m_currentSpaceId);
|
||||
return;
|
||||
}
|
||||
if (!m_dontUpdateLastRoom) {
|
||||
if (roomId.isEmpty()) {
|
||||
m_lastRoomConfig.deleteEntry(m_currentSpaceId);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto spaceIdForRoom = findSpaceIdForCurrentRoom();
|
||||
// We can't have empty keys in KConfig, so name it "Home"
|
||||
if (spaceIdForRoom.isEmpty()) {
|
||||
m_lastRoomConfig.writeEntry(u"Home"_s, roomId);
|
||||
} else {
|
||||
m_lastRoomConfig.writeEntry(spaceIdForRoom, roomId);
|
||||
}
|
||||
const auto spaceIdForRoom = findSpaceIdForCurrentRoom();
|
||||
// We can't have empty keys in KConfig, so name it "Home"
|
||||
if (spaceIdForRoom.isEmpty()) {
|
||||
m_lastRoomConfig.writeEntry(u"Home"_s, roomId);
|
||||
} else {
|
||||
m_lastRoomConfig.writeEntry(spaceIdForRoom, roomId);
|
||||
}
|
||||
|
||||
if (m_currentSpaceId != spaceIdForRoom) {
|
||||
setCurrentSpace(spaceIdForRoom, false);
|
||||
if (m_currentSpaceId != spaceIdForRoom) {
|
||||
setCurrentSpace(spaceIdForRoom, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "models/timelinemodel.h"
|
||||
#include "models/userlistmodel.h"
|
||||
#include "models/widgetmodel.h"
|
||||
#include "neochatdatetime.h"
|
||||
#include "neochatroommember.h"
|
||||
|
||||
class NeoChatRoom;
|
||||
@@ -218,7 +219,7 @@ public:
|
||||
*/
|
||||
Q_INVOKABLE void maximizeMedia(const QString &eventId);
|
||||
|
||||
Q_INVOKABLE void maximizeCode(NeochatRoomMember *author, const QDateTime &time, const QString &codeText, const QString &language);
|
||||
Q_INVOKABLE void maximizeCode(NeochatRoomMember *author, const NeoChatDateTime &time, const QString &codeText, const QString &language);
|
||||
|
||||
/**
|
||||
* @brief Request that any full screen overlay currently open closes.
|
||||
@@ -292,7 +293,7 @@ Q_SIGNALS:
|
||||
/**
|
||||
* @brief Request a block of code is shown maximized.
|
||||
*/
|
||||
void showMaximizedCode(NeochatRoomMember *author, const QDateTime &time, const QString &codeText, const QString &language);
|
||||
void showMaximizedCode(NeochatRoomMember *author, const NeoChatDateTime &dateTime, const QString &codeText, const QString &language);
|
||||
|
||||
/**
|
||||
* @brief Request that any full screen overlay closes.
|
||||
@@ -308,6 +309,7 @@ Q_SIGNALS:
|
||||
* @brief Request to show a menu for the given event.
|
||||
*/
|
||||
void showDelegateMenu(QObject *parent,
|
||||
NeoChatRoom *room,
|
||||
const QString &eventId,
|
||||
const NeochatRoomMember *author,
|
||||
MessageComponentType::Type messageComponentType,
|
||||
@@ -339,6 +341,11 @@ Q_SIGNALS:
|
||||
|
||||
void currentSpaceChanged();
|
||||
|
||||
protected:
|
||||
bool m_dontUpdateLastRoom = false; // Don't set directly, use LastRoomBlocker.
|
||||
|
||||
friend class LastRoomBlocker;
|
||||
|
||||
private:
|
||||
bool m_isMobile = false;
|
||||
|
||||
@@ -384,8 +391,13 @@ private:
|
||||
*/
|
||||
QString findSpaceIdForCurrentRoom() const;
|
||||
|
||||
// Space ID, "DM", or empty string
|
||||
void setCurrentSpace(const QString &spaceId, bool setRoom = true);
|
||||
/**
|
||||
* @brief Sets the current space.
|
||||
*
|
||||
* @param spaceId The ID of the space, "DM" for direct messages or an empty string for Home.
|
||||
* @param goToLastUsedRoom If true, we will navigate to the last opened room in this space.
|
||||
*/
|
||||
void setCurrentSpace(const QString &spaceId, bool goToLastUsedRoom = true);
|
||||
|
||||
/**
|
||||
* @brief Resolve a user URI.
|
||||
|
||||
66
src/app/supportcontroller.cpp
Normal file
66
src/app/supportcontroller.cpp
Normal file
@@ -0,0 +1,66 @@
|
||||
// SPDX-FileCopyrightText: 2026 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "supportcontroller.h"
|
||||
|
||||
#include <Quotient/csapi/support.h>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
void SupportController::setConnection(NeoChatConnection *connection)
|
||||
{
|
||||
if (m_connection != connection) {
|
||||
m_connection = connection;
|
||||
Q_EMIT connectionChanged();
|
||||
|
||||
load();
|
||||
}
|
||||
}
|
||||
|
||||
NeoChatConnection *SupportController::connection() const
|
||||
{
|
||||
return m_connection;
|
||||
}
|
||||
|
||||
QString SupportController::supportPage() const
|
||||
{
|
||||
return m_supportPage;
|
||||
}
|
||||
|
||||
QList<SupportContact> SupportController::contacts() const
|
||||
{
|
||||
return m_contacts;
|
||||
}
|
||||
|
||||
void SupportController::load()
|
||||
{
|
||||
if (!m_connection) {
|
||||
qWarning() << "Tried to load support information without a valid connection?";
|
||||
return;
|
||||
}
|
||||
|
||||
m_connection->callApi<GetWellknownSupportJob>()
|
||||
.onResult([this](const auto &job) {
|
||||
m_supportPage = job->supportPage();
|
||||
m_contacts.reserve(job->contacts().size());
|
||||
for (const auto &contact : job->contacts()) {
|
||||
m_contacts.push_back(SupportContact{
|
||||
.role = contact.role,
|
||||
.matrixId = contact.matrixId,
|
||||
.emailAddress = contact.emailAddress,
|
||||
});
|
||||
}
|
||||
|
||||
Q_EMIT loaded();
|
||||
})
|
||||
.onFailure([this](const auto &job) {
|
||||
Q_UNUSED(job)
|
||||
|
||||
// Just do nothing, our properties will be empty.
|
||||
Q_EMIT loaded();
|
||||
});
|
||||
}
|
||||
|
||||
#include "moc_supportcontroller.cpp"
|
||||
50
src/app/supportcontroller.h
Normal file
50
src/app/supportcontroller.h
Normal file
@@ -0,0 +1,50 @@
|
||||
// SPDX-FileCopyrightText: 2026 Joshua Goins <josh@redstrate.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "neochatconnection.h"
|
||||
|
||||
class SupportContact
|
||||
{
|
||||
Q_GADGET
|
||||
QML_NAMED_ELEMENT(supportContact)
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
Q_PROPERTY(QString role MEMBER role)
|
||||
Q_PROPERTY(QString matrixId MEMBER matrixId)
|
||||
Q_PROPERTY(QString emailAddress MEMBER emailAddress)
|
||||
|
||||
public:
|
||||
QString role;
|
||||
QString matrixId;
|
||||
QString emailAddress;
|
||||
};
|
||||
|
||||
class SupportController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged REQUIRED)
|
||||
Q_PROPERTY(QString supportPage READ supportPage NOTIFY loaded)
|
||||
Q_PROPERTY(QList<SupportContact> contacts READ contacts NOTIFY loaded)
|
||||
|
||||
public:
|
||||
void setConnection(NeoChatConnection *connection);
|
||||
NeoChatConnection *connection() const;
|
||||
|
||||
QString supportPage() const;
|
||||
QList<SupportContact> contacts() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void connectionChanged();
|
||||
void loaded();
|
||||
|
||||
private:
|
||||
void load();
|
||||
|
||||
QPointer<NeoChatConnection> m_connection = nullptr;
|
||||
QList<SupportContact> m_contacts;
|
||||
QString m_supportPage;
|
||||
};
|
||||
@@ -16,4 +16,5 @@ ecm_add_qml_module(Chatbar GENERATE_PLUGIN_SOURCE
|
||||
EmojiDialog.qml
|
||||
EmojiTonesPicker.qml
|
||||
ImageEditorPage.qml
|
||||
VoiceMessageDialog.qml
|
||||
)
|
||||
|
||||
@@ -150,6 +150,19 @@ QQC2.Control {
|
||||
}
|
||||
tooltip: text
|
||||
},
|
||||
BusyAction {
|
||||
icon.name: "microphone"
|
||||
isBusy: false
|
||||
text: i18nc("@action:button", "Send a Voice Message")
|
||||
displayHint: QQC2.AbstractButton.IconOnly
|
||||
onTriggered: {
|
||||
let dialog = voiceMessageDialog.createObject(root, {
|
||||
room: root.currentRoom
|
||||
}) as VoiceMessageDialog;
|
||||
dialog.open();
|
||||
}
|
||||
tooltip: text
|
||||
},
|
||||
BusyAction {
|
||||
id: sendAction
|
||||
|
||||
@@ -412,7 +425,6 @@ QQC2.Control {
|
||||
Component {
|
||||
id: replyPane
|
||||
Item {
|
||||
implicitWidth: replyComponent.implicitWidth
|
||||
implicitHeight: replyComponent.implicitHeight
|
||||
ReplyComponent {
|
||||
id: replyComponent
|
||||
@@ -549,6 +561,11 @@ QQC2.Control {
|
||||
NewPollDialog {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: voiceMessageDialog
|
||||
VoiceMessageDialog {}
|
||||
}
|
||||
|
||||
CompletionMenu {
|
||||
id: completionMenu
|
||||
chatDocumentHandler: documentHandler
|
||||
|
||||
@@ -30,7 +30,7 @@ QQC2.ItemDelegate {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: "emoji"
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * NeoChatConfig.fontScale
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 1.5
|
||||
|
||||
Kirigami.Icon {
|
||||
width: Kirigami.Units.gridUnit * 0.5
|
||||
|
||||
68
src/chatbar/VoiceMessageDialog.qml
Normal file
68
src/chatbar/VoiceMessageDialog.qml
Normal file
@@ -0,0 +1,68 @@
|
||||
// SPDX-FileCopyrightText: 2026 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtMultimedia
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.coreaddons
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
QQC2.Dialog {
|
||||
id: root
|
||||
|
||||
required property NeoChatRoom room
|
||||
|
||||
VoiceRecorder {
|
||||
id: voiceRecorder
|
||||
readonly property bool recording: recorder.recorderState == MediaRecorder.RecordingState
|
||||
room: root.room
|
||||
}
|
||||
|
||||
width: Kirigami.Units.gridUnit * 24
|
||||
|
||||
standardButtons: QQC2.DialogButtonBox.Cancel
|
||||
|
||||
title: i18nc("@title:dialog", "Record Voice Message")
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
QQC2.RoundButton {
|
||||
icon.name: voiceRecorder.recording ? "media-playback-stop" : "media-record"
|
||||
text: voiceRecorder.recording ? i18nc("@action:button Stop audio recording", "Stop Recording") : i18nc("@action:button Start audio recording", "Start Recording")
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 4
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 4
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
display: QQC2.RoundButton.IconOnly
|
||||
enabled: voiceRecorder.isSupported
|
||||
|
||||
onClicked: voiceRecorder.recording ? voiceRecorder.stopRecording() : voiceRecorder.startRecording()
|
||||
}
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
QQC2.Label {
|
||||
text: i18nc("@info Duration being the length of an audio recording", "Duration: %1", Format.formatDuration(voiceRecorder.recorder.duration))
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.InlineMessage {
|
||||
Layout.fillWidth: true
|
||||
text: i18nc("@info", "Voice message recording requires a newer Qt version than is currently installed on this system.")
|
||||
visible: !voiceRecorder.isSupported
|
||||
}
|
||||
}
|
||||
|
||||
footer: QQC2.DialogButtonBox {
|
||||
QQC2.Button {
|
||||
text: i18nc("@action:button Send the voice message", "Send")
|
||||
icon.name: "document-send"
|
||||
QQC2.DialogButtonBox.buttonRole: QQC2.DialogButtonBox.AcceptRole
|
||||
onClicked: voiceRecorder.send()
|
||||
enabled: !voiceRecorder.recording && voiceRecorder.recorder.duration > 0 && voiceRecorder.isSupported
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,9 +40,11 @@ ColumnLayout {
|
||||
model: root.connection.getSupportedRoomVersions()
|
||||
|
||||
delegate: FormCard.FormTextDelegate {
|
||||
id: versionDelegate
|
||||
required property var modelData
|
||||
text: modelData.id
|
||||
contentItem.children: QQC2.Label {
|
||||
text: modelData.status
|
||||
text: versionDelegate.modelData.status
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ class StateFilterModel : public QSortFilterProxyModel
|
||||
QML_ELEMENT
|
||||
|
||||
public:
|
||||
using QSortFilterProxyModel::QSortFilterProxyModel;
|
||||
/**
|
||||
* @brief Custom filter function checking if an event type has been filtered out.
|
||||
*
|
||||
|
||||
@@ -28,7 +28,7 @@ public:
|
||||
* @brief Defines the model roles.
|
||||
*/
|
||||
enum Roles {
|
||||
TypeRole = 0, /**< The type of the state event. */
|
||||
TypeRole = Qt::UserRole, /**< 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. */
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user