Compare commits
94 Commits
work/tobia
...
work/redst
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de6e588981 | ||
|
|
306d75a9b0 | ||
|
|
ee33a70bb2 | ||
|
|
bd80390daa | ||
|
|
0fa490f532 | ||
|
|
1c7cc45d8e | ||
|
|
f3288c2e34 | ||
|
|
7eff1eec56 | ||
|
|
85c7b1a3fc | ||
|
|
5c82c07f06 | ||
|
|
fcf125f9ca | ||
|
|
8ae13adbcd | ||
|
|
1e1ad389e3 | ||
|
|
f58212e8de | ||
|
|
8622087e51 | ||
|
|
5fc59b0d66 | ||
|
|
5aeefea5c0 | ||
|
|
52e9c9e8e2 | ||
|
|
ae69401d57 | ||
|
|
2339cad49f | ||
|
|
8f6683fd1d | ||
|
|
74deb684e1 | ||
|
|
54a918b0cf | ||
|
|
55c9b09b24 | ||
|
|
0d63fce59a | ||
|
|
4498d4457b | ||
|
|
83415d202a | ||
|
|
ed65855cdd | ||
|
|
1477159376 | ||
|
|
53a88708d6 | ||
|
|
8eb8803afd | ||
|
|
20e30982cf | ||
|
|
e13b82f66a | ||
|
|
8e50388031 | ||
|
|
fb21686894 | ||
|
|
62faf0f784 | ||
|
|
be377d9ad8 | ||
|
|
ab8e2f7573 | ||
|
|
7991429ef4 | ||
|
|
e3b70a14be | ||
|
|
39abf6b5f3 | ||
|
|
096842bd3a | ||
|
|
c69db9d375 | ||
|
|
ec36d519b1 | ||
|
|
45b02ae34e | ||
|
|
9b763daf52 | ||
|
|
6e8ed5b341 | ||
|
|
63a3c3e58a | ||
|
|
aadd9b0189 | ||
|
|
39f595e45d | ||
|
|
823b2d3747 | ||
|
|
eb268576da | ||
|
|
8e51f3ec8e | ||
|
|
de03e1ce2b | ||
|
|
21b1258b8d | ||
|
|
becad8c127 | ||
|
|
4044048352 | ||
|
|
7d6bd7ab4c | ||
|
|
209ae00f8f | ||
|
|
f64c860453 | ||
|
|
36fccaffe6 | ||
|
|
13deb2d928 | ||
|
|
14fe71b556 | ||
|
|
ecb900994b | ||
|
|
55b97b469e | ||
|
|
1d594b492d | ||
|
|
3902293de7 | ||
|
|
a8bc51667c | ||
|
|
0bcf6e74c0 | ||
|
|
78b218fef3 | ||
|
|
964bcfd5f5 | ||
|
|
fc733f9ba1 | ||
|
|
e7e83fa789 | ||
|
|
ef4f11546f | ||
|
|
648796b9e0 | ||
|
|
3b5da2473d | ||
|
|
9a04ae3e02 | ||
|
|
9ed5224470 | ||
|
|
bc82ceeb5f | ||
|
|
5f7ff209d3 | ||
|
|
35b363fdce | ||
|
|
a74931e794 | ||
|
|
6698bbcf79 | ||
|
|
8c78992b1a | ||
|
|
04e3b88e8c | ||
|
|
9b8b13e98e | ||
|
|
e6dd6aec7f | ||
|
|
c6f0879c9c | ||
|
|
9e7ae37add | ||
|
|
0c727237ee | ||
|
|
bf66118355 | ||
|
|
aacb097650 | ||
|
|
ce5d60fc5d | ||
|
|
96a0b86c33 |
2
.contextProperties.ini
Normal file
2
.contextProperties.ini
Normal file
@@ -0,0 +1,2 @@
|
||||
[General]
|
||||
disableUnqualifiedAccess = "i18nc,xi18nc,i18ncp"
|
||||
@@ -161,11 +161,15 @@
|
||||
"name": "kunifiedpush",
|
||||
"buildsystem": "cmake-ninja",
|
||||
"builddir": true,
|
||||
"config-opts": [
|
||||
"-DENABLE_TESTING=OFF",
|
||||
"-DKUNIFIEDPUSH_CLIENT_ONLY=ON"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://download.kde.org/stable/release-service/25.04.3/src/kunifiedpush-25.04.3.tar.xz",
|
||||
"sha256": "a16ffe4117b14baa02f3b8ae7de9e509a17359c1b67dcd851aef4f3c3661a1df",
|
||||
"url": "https://download.kde.org/stable/release-service/25.08.0/src/kunifiedpush-25.08.0.tar.xz",
|
||||
"sha256": "846db6ffc7d93f6afea7ce0d5a9f10b52792157ceb593856542279f4197f3518",
|
||||
"x-checker-data": {
|
||||
"type": "anitya",
|
||||
"project-id": 8763,
|
||||
@@ -186,6 +190,14 @@
|
||||
{
|
||||
"type": "dir",
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"type": "patch",
|
||||
"path": "patches/0001-Revert-Bump-KF6-dependency-version.patch"
|
||||
},
|
||||
{
|
||||
"type": "patch",
|
||||
"path": "patches/0001-Revert-Use-new-Kirigami-builtin-column-resize-handle.patch"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -43,3 +43,4 @@ Dependencies:
|
||||
Options:
|
||||
per-test-timeout: 90
|
||||
require-passing-tests-on: ['Linux', 'Android', 'FreeBSD', 'Windows']
|
||||
run-qmllint: True
|
||||
|
||||
@@ -14,7 +14,7 @@ set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_
|
||||
|
||||
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
|
||||
|
||||
set(KF_MIN_VERSION "6.12")
|
||||
set(KF_MIN_VERSION "6.17")
|
||||
set(QT_MIN_VERSION "6.5")
|
||||
|
||||
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
|
||||
|
||||
@@ -88,3 +88,9 @@ path = "memorytests/memtest-sync.json"
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "2024 James Graham <james.h.graham@protonmail.com>"
|
||||
SPDX-License-Identifier = "BSD-2-Clause"
|
||||
|
||||
[[annotations]]
|
||||
path = ".contextProperties.ini"
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "2025 Tobias Fella <tobias.fella@kde.org>"
|
||||
SPDX-License-Identifier = "BSD-2-Clause"
|
||||
|
||||
@@ -92,3 +92,15 @@ ecm_add_test(
|
||||
LINK_LIBRARIES neochat Qt::Test neochat_server
|
||||
TEST_NAME actionstest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
servernoticestest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test neochat_server
|
||||
TEST_NAME servernoticestest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
roommanagertest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test neochat_server
|
||||
TEST_NAME roommanagertest
|
||||
)
|
||||
|
||||
132
autotests/roommanagertest.cpp
Normal file
132
autotests/roommanagertest.cpp
Normal file
@@ -0,0 +1,132 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include <QObject>
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
#include <QVariantList>
|
||||
|
||||
#include "accountmanager.h"
|
||||
#include "models/actionsmodel.h"
|
||||
#include "roommanager.h"
|
||||
|
||||
#include "server.h"
|
||||
#include "testutils.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class RoomManagerTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
NeoChatConnection *connection = nullptr;
|
||||
NeoChatRoom *room = nullptr;
|
||||
|
||||
Server server;
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
void testMaximizeMedia();
|
||||
};
|
||||
|
||||
void RoomManagerTest::initTestCase()
|
||||
{
|
||||
Connection::setRoomType<NeoChatRoom>();
|
||||
server.start();
|
||||
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
|
||||
auto accountManager = new AccountManager(true);
|
||||
QSignalSpy spy(accountManager, &AccountManager::connectionAdded);
|
||||
connection = dynamic_cast<NeoChatConnection *>(accountManager->accounts()->front());
|
||||
QVERIFY(connection);
|
||||
auto roomId = server.createRoom(u"@user:localhost:1234"_s);
|
||||
|
||||
QSignalSpy syncSpy(connection, &Connection::syncDone);
|
||||
// We need to wait for two syncs, as the next one won't have the changes yet
|
||||
QVERIFY(syncSpy.wait());
|
||||
QVERIFY(syncSpy.wait());
|
||||
room = dynamic_cast<NeoChatRoom *>(connection->room(roomId));
|
||||
QVERIFY(room);
|
||||
RoomManager::instance().setConnection(connection);
|
||||
QSignalSpy roomSpy(&RoomManager::instance(), &RoomManager::currentRoomChanged);
|
||||
RoomManager::instance().resolveResource(room->id());
|
||||
QVERIFY(roomSpy.size() > 0);
|
||||
}
|
||||
|
||||
void RoomManagerTest::testMaximizeMedia()
|
||||
{
|
||||
QSignalSpy spy(&RoomManager::instance(), &RoomManager::showMaximizedMedia);
|
||||
QSignalSpy syncSpy(connection, &Connection::syncDone);
|
||||
|
||||
QTest::ignoreMessage(QtMsgType::QtWarningMsg, "Tried to open media for empty event id");
|
||||
RoomManager::instance().maximizeMedia(QString());
|
||||
QVERIFY(!spy.wait(10));
|
||||
|
||||
QTest::ignoreMessage(QtMsgType::QtWarningMsg, "Tried to open media for unknown event id \"Doesn't exist\"");
|
||||
RoomManager::instance().maximizeMedia(u"Doesn't exist"_s);
|
||||
QVERIFY(!spy.wait(10));
|
||||
|
||||
const auto eventWithoutMedia = server.sendEvent(room->id(),
|
||||
u"m.room.message"_s,
|
||||
QJsonObject({
|
||||
{u"body"_s, u"Foo"_s},
|
||||
{u"format"_s, u"org.matrix.custom.html"_s},
|
||||
{u"formatted_body"_s, u"Foo"_s},
|
||||
{u"msgtype"_s, u"m.text"_s},
|
||||
}));
|
||||
QVERIFY(syncSpy.wait());
|
||||
QVERIFY(syncSpy.wait());
|
||||
QTest::ignoreMessage(QtMsgType::QtWarningMsg, u"Tried to open media for unknown event id \"%1\""_s.arg(eventWithoutMedia).toLatin1().data());
|
||||
RoomManager::instance().maximizeMedia(eventWithoutMedia);
|
||||
QVERIFY(!spy.wait(10));
|
||||
|
||||
// NOTE: This is supposed to test that maximizing pending media works correctly. This probably doesn't work in the UI yet, but at least the backend supports
|
||||
// it. If the server ever learns how to process events, this becomes pointless and we need to find a way of preventing *these* events from arriving
|
||||
auto pendingEventWithoutMedia = room->postText(u"Hello"_s);
|
||||
QTest::ignoreMessage(QtMsgType::QtWarningMsg, u"Tried to open media for unknown event id \"%1\""_s.arg(pendingEventWithoutMedia).toLatin1().data());
|
||||
RoomManager::instance().maximizeMedia(pendingEventWithoutMedia);
|
||||
QVERIFY(!spy.wait(10));
|
||||
|
||||
const auto eventWithMedia = server.sendEvent(room->id(),
|
||||
u"m.room.message"_s,
|
||||
QJsonObject({
|
||||
{u"body"_s, u"Foo"_s},
|
||||
{u"filename"_s, u"foo.jpg"_s},
|
||||
{u"info"_s,
|
||||
QJsonObject{
|
||||
{u"h"_s, 1000},
|
||||
{u"w"_s, 2000},
|
||||
{u"size"_s, 10000},
|
||||
{u"mimetype"_s, u"image/png"_s},
|
||||
}},
|
||||
{u"msgtype"_s, u"m.image"_s},
|
||||
{u"url"_s, u"mxc://foo.bar/asdf"_s},
|
||||
}));
|
||||
QVERIFY(syncSpy.wait());
|
||||
QVERIFY(syncSpy.wait());
|
||||
QVERIFY(syncSpy.wait());
|
||||
RoomManager::instance().maximizeMedia(eventWithMedia);
|
||||
QVERIFY(spy.size() == 1);
|
||||
QVERIFY(spy[0][0] == 0);
|
||||
|
||||
auto pendingEventWithMedia = room->postJson(u"m.room.message"_s,
|
||||
QJsonObject({
|
||||
{u"body"_s, u"Foo"_s},
|
||||
{u"filename"_s, u"foo.jpg"_s},
|
||||
{u"info"_s,
|
||||
QJsonObject{
|
||||
{u"h"_s, 1000},
|
||||
{u"w"_s, 2000},
|
||||
{u"size"_s, 10000},
|
||||
{u"mimetype"_s, u"image/png"_s},
|
||||
}},
|
||||
{u"msgtype"_s, u"m.image"_s},
|
||||
{u"url"_s, u"mxc://foo.bar/asdf"_s},
|
||||
}));
|
||||
RoomManager::instance().maximizeMedia(pendingEventWithMedia);
|
||||
QVERIFY(spy.size() == 2);
|
||||
QVERIFY(spy[1][0] == 0);
|
||||
}
|
||||
|
||||
QTEST_MAIN(RoomManagerTest)
|
||||
#include "roommanagertest.moc"
|
||||
@@ -115,28 +115,36 @@ void Server::start()
|
||||
|
||||
m_server.route(u"/_matrix/client/r0/sync"_s, QHttpServerRequest::Method::Get, [this](QHttpServerResponder &responder) {
|
||||
QMap<QString, QJsonArray> stateEvents;
|
||||
QMap<QString, QJsonArray> roomAccountData;
|
||||
|
||||
for (const auto &[roomId, matrixId] : m_roomsToCreate) {
|
||||
stateEvents[roomId] += QJsonObject{
|
||||
for (const auto &roomData : m_roomsToCreate) {
|
||||
stateEvents[roomData.id] += QJsonObject{
|
||||
{u"content"_s, QJsonObject{{u"room_version"_s, u"11"_s}}},
|
||||
{u"event_id"_s, generateEventId()},
|
||||
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
|
||||
{u"room_id"_s, roomId},
|
||||
{u"sender"_s, matrixId},
|
||||
{u"room_id"_s, roomData.id},
|
||||
{u"sender"_s, roomData.members[0]},
|
||||
{u"state_key"_s, QString()},
|
||||
{u"type"_s, u"m.room.create"_s},
|
||||
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
|
||||
};
|
||||
stateEvents[roomId] += QJsonObject{
|
||||
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"join"_s}}},
|
||||
{u"event_id"_s, generateEventId()},
|
||||
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
|
||||
{u"room_id"_s, roomId},
|
||||
{u"sender"_s, matrixId},
|
||||
{u"state_key"_s, matrixId},
|
||||
{u"type"_s, u"m.room.member"_s},
|
||||
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
|
||||
};
|
||||
for (const auto &member : roomData.members) {
|
||||
stateEvents[roomData.id] += QJsonObject{
|
||||
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"join"_s}}},
|
||||
{u"event_id"_s, generateEventId()},
|
||||
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
|
||||
{u"room_id"_s, roomData.id},
|
||||
{u"sender"_s, member},
|
||||
{u"state_key"_s, member},
|
||||
{u"type"_s, u"m.room.member"_s},
|
||||
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
|
||||
};
|
||||
}
|
||||
QJsonObject tags;
|
||||
for (const auto &tag : roomData.tags) {
|
||||
tags[tag] = QJsonObject();
|
||||
}
|
||||
roomAccountData[roomData.id] += QJsonObject{{u"type"_s, u"m.tag"_s}, {u"content"_s, QJsonObject{{u"tags"_s, tags}}}};
|
||||
}
|
||||
m_roomsToCreate.clear();
|
||||
for (const auto &roomId : m_invitedUsers.keys()) {
|
||||
@@ -191,11 +199,18 @@ void Server::start()
|
||||
m_joinedUsers.clear();
|
||||
|
||||
QJsonObject rooms;
|
||||
for (const auto &roomId : stateEvents.keys()) {
|
||||
rooms[roomId] = QJsonObject{{u"state"_s, QJsonObject{{u"events"_s, stateEvents[roomId]}}}};
|
||||
auto keys = stateEvents.keys() + m_events.keys();
|
||||
for (const auto &roomId : QSet(keys.begin(), keys.end())) {
|
||||
rooms[roomId] = QJsonObject{
|
||||
{u"state"_s, QJsonObject{{u"events"_s, stateEvents[roomId]}}},
|
||||
{u"account_data"_s, QJsonObject{{u"events"_s, roomAccountData[roomId]}}},
|
||||
{u"timeline"_s, QJsonObject{{u"events"_s, m_events[roomId]}}},
|
||||
};
|
||||
}
|
||||
m_events.clear();
|
||||
|
||||
responder.write(QJsonDocument(QJsonObject{{u"rooms"_s, QJsonObject{{u"join"_s, rooms}}}}), QHttpServerResponder::StatusCode::Ok);
|
||||
auto json = QJsonObject{{u"rooms"_s, QJsonObject{{u"join"_s, rooms}}}};
|
||||
responder.write(QJsonDocument(json), QHttpServerResponder::StatusCode::Ok);
|
||||
});
|
||||
|
||||
QSslConfiguration config;
|
||||
@@ -215,7 +230,11 @@ void Server::start()
|
||||
QString Server::createRoom(const QString &matrixId)
|
||||
{
|
||||
auto roomId = generateRoomId();
|
||||
m_roomsToCreate += {roomId, matrixId};
|
||||
m_roomsToCreate += RoomData{
|
||||
.members = {matrixId},
|
||||
.id = roomId,
|
||||
.tags = {},
|
||||
};
|
||||
return roomId;
|
||||
}
|
||||
|
||||
@@ -233,3 +252,23 @@ void Server::joinUser(const QString &roomId, const QString &matrixId)
|
||||
{
|
||||
m_joinedUsers[roomId] += matrixId;
|
||||
}
|
||||
|
||||
QString Server::createServerNoticesRoom(const QString &matrixId)
|
||||
{
|
||||
auto roomId = createRoom(matrixId);
|
||||
m_roomsToCreate.last().tags = {u"m.server_notice"_s};
|
||||
return roomId;
|
||||
}
|
||||
|
||||
QString Server::sendEvent(const QString &roomId, const QString &eventType, const QJsonObject &content)
|
||||
{
|
||||
const auto eventId = generateEventId();
|
||||
m_events[roomId] += 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()},
|
||||
};
|
||||
return eventId;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
#include <QHttpServer>
|
||||
#include <QSslServer>
|
||||
|
||||
struct RoomData {
|
||||
QStringList members;
|
||||
QString id;
|
||||
QStringList tags;
|
||||
};
|
||||
|
||||
class Server
|
||||
{
|
||||
public:
|
||||
@@ -21,6 +27,12 @@ public:
|
||||
void banUser(const QString &roomId, const QString &matrixId);
|
||||
void joinUser(const QString &roomId, const QString &matrixId);
|
||||
|
||||
/**
|
||||
* Create a server notices room.
|
||||
*/
|
||||
QString createServerNoticesRoom(const QString &matrixId);
|
||||
QString sendEvent(const QString &roomId, const QString &eventType, const QJsonObject &content);
|
||||
|
||||
private:
|
||||
QHttpServer m_server;
|
||||
QSslServer m_sslServer;
|
||||
@@ -29,5 +41,6 @@ private:
|
||||
QHash<QString, QList<QString>> m_bannedUsers;
|
||||
QHash<QString, QList<QString>> m_joinedUsers;
|
||||
|
||||
QList<std::pair<QString, QString>> m_roomsToCreate;
|
||||
QList<RoomData> m_roomsToCreate;
|
||||
QMap<QString, QJsonArray> m_events;
|
||||
};
|
||||
|
||||
87
autotests/servernoticestest.cpp
Normal file
87
autotests/servernoticestest.cpp
Normal file
@@ -0,0 +1,87 @@
|
||||
// SPDX-FileCopyrightText: 2025 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include <QObject>
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include <Quotient/connection.h>
|
||||
#include <Quotient/eventstats.h>
|
||||
#include <Quotient/quotient_common.h>
|
||||
#include <Quotient/syncdata.h>
|
||||
|
||||
#include "accountmanager.h"
|
||||
#include "neochatroom.h"
|
||||
#include "roommanager.h"
|
||||
#include "server.h"
|
||||
|
||||
#include "testutils.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class ServerNoticesTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
NeoChatConnection *connection = nullptr;
|
||||
Server server;
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
void test();
|
||||
};
|
||||
|
||||
void ServerNoticesTest::initTestCase()
|
||||
{
|
||||
Connection::setRoomType<NeoChatRoom>();
|
||||
server.start();
|
||||
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
|
||||
auto accountManager = new AccountManager(true);
|
||||
QSignalSpy spy(accountManager, &AccountManager::connectionAdded);
|
||||
connection = dynamic_cast<NeoChatConnection *>(accountManager->accounts()->front());
|
||||
QVERIFY(connection);
|
||||
auto roomId = server.createRoom(u"@user:localhost:1234"_s);
|
||||
RoomManager::instance().setConnection(connection);
|
||||
|
||||
QSignalSpy syncSpy(connection, &Connection::syncDone);
|
||||
// We need to wait for two syncs, as the next one won't have the changes yet
|
||||
QVERIFY(syncSpy.wait());
|
||||
QVERIFY(syncSpy.wait());
|
||||
auto room = dynamic_cast<NeoChatRoom *>(connection->room(roomId));
|
||||
QVERIFY(room);
|
||||
}
|
||||
|
||||
void ServerNoticesTest::test()
|
||||
{
|
||||
auto roomTreeModel = RoomManager::instance().roomTreeModel();
|
||||
QCOMPARE(roomTreeModel->rowCount(roomTreeModel->index(NeoChatRoomType::ServerNotice, 0)), 0);
|
||||
auto sortFilterRoomTreeModel = RoomManager::instance().sortFilterRoomTreeModel();
|
||||
const auto roomId = server.createServerNoticesRoom(u"@user:localhost:1234"_s);
|
||||
QSignalSpy syncSpy(connection, &Connection::syncDone);
|
||||
QVERIFY(syncSpy.wait());
|
||||
QVERIFY(syncSpy.wait());
|
||||
const auto room = dynamic_cast<NeoChatRoom *>(connection->room(roomId));
|
||||
QVERIFY(connection->room(roomId)->isServerNoticeRoom());
|
||||
QCOMPARE(roomTreeModel->rowCount(roomTreeModel->index(NeoChatRoomType::ServerNotice, 0)), 1);
|
||||
QCOMPARE(sortFilterRoomTreeModel->mapFromSource(roomTreeModel->indexForRoom(room)).parent().row(), 1 /* Below the normal room */);
|
||||
server.sendEvent(roomId,
|
||||
u"m.room.message"_s,
|
||||
QJsonObject{
|
||||
{u"body"_s, u"Foo"_s},
|
||||
{u"format"_s, u"org.matrix.custom.html"_s},
|
||||
{u"formatted_body"_s, u"Foo"_s},
|
||||
{u"msgtype"_s, u"m.text"_s},
|
||||
});
|
||||
QVERIFY(syncSpy.wait());
|
||||
QVERIFY(syncSpy.wait());
|
||||
sortFilterRoomTreeModel->invalidate();
|
||||
QCOMPARE(sortFilterRoomTreeModel->mapFromSource(roomTreeModel->indexForRoom(room)).parent().row(), 0);
|
||||
room->markAllMessagesAsRead();
|
||||
QCOMPARE(sortFilterRoomTreeModel->mapFromSource(roomTreeModel->indexForRoom(room)).parent().row(), 1 /* Below the normal room */);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(ServerNoticesTest)
|
||||
#include "servernoticestest.moc"
|
||||
@@ -34,6 +34,10 @@ private Q_SLOTS:
|
||||
void stripDisallowedTags();
|
||||
void stripDisallowedAttributes();
|
||||
void emptyCodeTags();
|
||||
void addStyle_data();
|
||||
void addStyle();
|
||||
void dontAddStyle_data();
|
||||
void dontAddStyle();
|
||||
|
||||
void sendSimpleStringCase();
|
||||
void sendSingleParaMarkup();
|
||||
@@ -71,6 +75,9 @@ private Q_SLOTS:
|
||||
|
||||
void componentOutput_data();
|
||||
void componentOutput();
|
||||
|
||||
void updateSpoiler_data();
|
||||
void updateSpoiler();
|
||||
};
|
||||
|
||||
void TextHandlerTest::initTestCase()
|
||||
@@ -89,21 +96,26 @@ void TextHandlerTest::initTestCase()
|
||||
|
||||
void TextHandlerTest::allowedAttributes()
|
||||
{
|
||||
auto theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
|
||||
const QString testInputString1 = u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s;
|
||||
const QString testOutputString1 = u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s;
|
||||
const QString testOutputString1S = u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s;
|
||||
const QString testOutputString1R = u"<span data-mx-spoiler style=\"color: transparent; background: %1;\"><font color=#FFFFFF>Test</font><span>"_s.arg(
|
||||
theme->alternateBackgroundColor().name());
|
||||
// Handle urls where the href has either single (') or double (") quotes.
|
||||
const QString testInputString2 = u"<a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a>"_s;
|
||||
const QString testOutputString2 = u"<a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a>"_s;
|
||||
const QString testOutputString2S = u"<a href=\"https://kde.org\">link</a><a href='https://kde.org'>link</a>"_s;
|
||||
const QString testOutputString2R =
|
||||
u"<a href=\"https://kde.org\" style=\"text-decoration: none;\">link</a><a href='https://kde.org' style=\"text-decoration: none;\">link</a>"_s;
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString1);
|
||||
|
||||
QCOMPARE(testTextHandler.handleSendText(), testOutputString1);
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString1);
|
||||
QCOMPARE(testTextHandler.handleSendText(), testOutputString1S);
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString1R);
|
||||
|
||||
testTextHandler.setData(testInputString2);
|
||||
QCOMPARE(testTextHandler.handleSendText(), testOutputString2);
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString2);
|
||||
QCOMPARE(testTextHandler.handleSendText(), testOutputString2S);
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString2R);
|
||||
}
|
||||
|
||||
void TextHandlerTest::stripDisallowedTags()
|
||||
@@ -146,6 +158,56 @@ void TextHandlerTest::emptyCodeTags()
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::addStyle_data()
|
||||
{
|
||||
QTest::addColumn<QString>("testInputString");
|
||||
QTest::addColumn<QString>("testOutputString");
|
||||
|
||||
QTest::newRow("link") << u"<a href=\"https://kde.org\">link</a>"_s << u"<a href=\"https://kde.org\" style=\"text-decoration: none;\">link</a>"_s;
|
||||
QTest::newRow("table")
|
||||
<< u"<table><tr><th>Company</th><th>Contact</th><th>Country</th></tr><tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr><tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr></table>"_s
|
||||
<< u"<table style=\"width: 100%; border-collapse: collapse; border: 1px; border-style: solid;\"><tr><th style=\"border: 1px solid black; padding: 3px;\">Company</th><th style=\"border: 1px solid black; padding: 3px;\">Contact</th><th style=\"border: 1px solid black; padding: 3px;\">Country</th></tr><tr><td style=\"border: 1px solid black; padding: 3px;\">Alfreds Futterkiste</td><td style=\"border: 1px solid black; padding: 3px;\">Maria Anders</td><td style=\"border: 1px solid black; padding: 3px;\">Germany</td></tr><tr><td style=\"border: 1px solid black; padding: 3px;\">Centro comercial Moctezuma</td><td style=\"border: 1px solid black; padding: 3px;\">Francisco Chang</td><td style=\"border: 1px solid black; padding: 3px;\">Mexico</td></tr></table>"_s;
|
||||
auto theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
|
||||
QTest::newRow("spoiler") << u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s
|
||||
<< u"<span data-mx-spoiler style=\"color: transparent; background: %1;\"><font color=#FFFFFF>Test</font><span>"_s.arg(
|
||||
theme->alternateBackgroundColor().name());
|
||||
}
|
||||
|
||||
void TextHandlerTest::addStyle()
|
||||
{
|
||||
QFETCH(QString, testInputString);
|
||||
QFETCH(QString, testOutputString);
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::dontAddStyle_data()
|
||||
{
|
||||
QTest::addColumn<QString>("testInputString");
|
||||
QTest::addColumn<QString>("testOutputString");
|
||||
|
||||
QTest::newRow("link") << u"<a href=\"https://kde.org\">link</a>"_s << u"<a href=\"https://kde.org\">link</a>"_s;
|
||||
QTest::newRow("table")
|
||||
<< u"<table><tr><th>Company</th><th>Contact</th><th>Country</th></tr><tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr><tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr></table>"_s
|
||||
<< u"<table><tr><th>Company</th><th>Contact</th><th>Country</th></tr><tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr><tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr></table>"_s;
|
||||
QTest::newRow("spoiler") << u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s
|
||||
<< u"<span data-mx-spoiler><font color=#FFFFFF>Test</font><span>"_s;
|
||||
}
|
||||
|
||||
void TextHandlerTest::dontAddStyle()
|
||||
{
|
||||
QFETCH(QString, testInputString);
|
||||
QFETCH(QString, testOutputString);
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
|
||||
QCOMPARE(testTextHandler.handleSendText(), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::sendSimpleStringCase()
|
||||
{
|
||||
const QString testInputString = u"This data should just be left alone."_s;
|
||||
@@ -338,7 +400,8 @@ void TextHandlerTest::receiveRichInPlainOut()
|
||||
void TextHandlerTest::receivePlainTextIn()
|
||||
{
|
||||
const QString testInputString = u"<plain text in tag bracket>\nTest link https://kde.org."_s;
|
||||
const QString testOutputStringRich = u"<plain text in tag bracket><br>Test link <a href=\"https://kde.org\">https://kde.org</a>."_s;
|
||||
const QString testOutputStringRich =
|
||||
u"<plain text in tag bracket><br>Test link <a href=\"https://kde.org\" style=\"text-decoration: none;\">https://kde.org</a>."_s;
|
||||
QString testOutputStringPlain = u"<plain text in tag bracket>\nTest link https://kde.org."_s;
|
||||
|
||||
// Make sure quotes are maintained in a plain string.
|
||||
@@ -408,7 +471,7 @@ void TextHandlerTest::receivePlainStripMarkup()
|
||||
void TextHandlerTest::receiveRichUserPill()
|
||||
{
|
||||
const QString testInputString = u"<p><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></p>"_s;
|
||||
const QString testOutputString = u"<b><a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a></b>"_s;
|
||||
const QString testOutputString = u"<b><a href=\"https://matrix.to/#/@alice:example.org\" style=\"text-decoration: none;\">@alice:example.org</a></b>"_s;
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
@@ -460,21 +523,23 @@ void TextHandlerTest::receiveRichPlainUrl_data()
|
||||
// so we can confirm consistent behaviour for complex urls.
|
||||
QTest::addRow("link 1")
|
||||
<< u"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im\">Link already rich</a>"_s
|
||||
<< u"<a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im\">https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im</a> <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im\">Link already rich</a>"_s;
|
||||
<< u"<a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im\" style=\"text-decoration: none;\">https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im</a> <a href=\"https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im\" style=\"text-decoration: none;\">Link already rich</a>"_s;
|
||||
|
||||
// Another real case. The linkification wasn't handling it when a single link
|
||||
// contains what looks like and email. It was broken into 3 but needs to
|
||||
// be just single link.
|
||||
QTest::addRow("link 2")
|
||||
<< u"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/"_s
|
||||
<< u"<a href=\"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/\">https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/</a>"_s;
|
||||
<< u"<a href=\"https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/\" style=\"text-decoration: none;\">https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/</a>"_s;
|
||||
|
||||
QTest::addRow("email") << uR"(email@example.com <a href="mailto:email@example.com">Link already rich</a>)"_s
|
||||
<< uR"(<a href="mailto:email@example.com">email@example.com</a> <a href="mailto:email@example.com">Link already rich</a>)"_s;
|
||||
QTest::addRow("email")
|
||||
<< uR"(email@example.com <a href="mailto:email@example.com">Link already rich</a>)"_s
|
||||
<< uR"(<a href="mailto:email@example.com" style="text-decoration: none;">email@example.com</a> <a href="mailto:email@example.com" style="text-decoration: none;">Link already rich</a>)"_s;
|
||||
QTest::addRow("mxid")
|
||||
<< u"@user:kde.org <a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a>"_s
|
||||
<< u"<b><a href=\"https://matrix.to/#/@user:kde.org\">@user:kde.org</a></b> <b><a href=\"https://matrix.to/#/@user:kde.org\">Link already rich</a></b>"_s;
|
||||
QTest::addRow("mxid with prefix") << u"a @user:kde.org b"_s << u"a <b><a href=\"https://matrix.to/#/@user:kde.org\">@user:kde.org</a></b> b"_s;
|
||||
<< u"<b><a href=\"https://matrix.to/#/@user:kde.org\" style=\"text-decoration: none;\">@user:kde.org</a></b> <b><a href=\"https://matrix.to/#/@user:kde.org\" style=\"text-decoration: none;\">Link already rich</a></b>"_s;
|
||||
QTest::addRow("mxid with prefix") << u"a @user:kde.org b"_s
|
||||
<< u"a <b><a href=\"https://matrix.to/#/@user:kde.org\" style=\"text-decoration: none;\">@user:kde.org</a></b> b"_s;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -596,5 +661,35 @@ void TextHandlerTest::componentOutput()
|
||||
QCOMPARE(testTextHandler.textComponents(testInputString), testOutputComponents);
|
||||
}
|
||||
|
||||
void TextHandlerTest::updateSpoiler_data()
|
||||
{
|
||||
QTest::addColumn<QString>("testInputString");
|
||||
QTest::addColumn<QString>("testOutputString");
|
||||
QTest::addColumn<bool>("spoilerRevealed");
|
||||
|
||||
auto theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
|
||||
QTest::newRow("same length") << u"<span data-mx-spoiler style=\"color: #123456; background: #123456;\">Test<span>"_s
|
||||
<< u"<span data-mx-spoiler style=\"color: transparent; background: %1;\">Test<span>"_s.arg(
|
||||
theme->alternateBackgroundColor().name())
|
||||
<< false;
|
||||
QTest::newRow("different length") << u"<span data-mx-spoiler style=\"color: short; background: looooooooooong;\">Test<span>"_s
|
||||
<< u"<span data-mx-spoiler style=\"color: transparent; background: %1;\">Test<span>"_s.arg(
|
||||
theme->alternateBackgroundColor().name())
|
||||
<< false;
|
||||
QTest::newRow("spoiler revealed")
|
||||
<< u"<span data-mx-spoiler style=\"color: transparent; background: %1;\">Test<span>"_s.arg(theme->alternateBackgroundColor().name())
|
||||
<< u"<span data-mx-spoiler style=\"color: %1; background: %2;\">Test<span>"_s.arg(theme->textColor().name(), theme->alternateBackgroundColor().name())
|
||||
<< true;
|
||||
}
|
||||
|
||||
void TextHandlerTest::updateSpoiler()
|
||||
{
|
||||
QFETCH(QString, testInputString);
|
||||
QFETCH(QString, testOutputString);
|
||||
QFETCH(bool, spoilerRevealed);
|
||||
|
||||
QCOMPARE(TextHandler::updateSpoilerText(this, testInputString, spoilerRevealed), testOutputString);
|
||||
}
|
||||
|
||||
QTEST_MAIN(TextHandlerTest)
|
||||
#include "texthandlertest.moc"
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
<p xml:lang="x-test">xxNeoChat is a chat app that lets you take full advantage of the Matrix network. It provides you with a secure way to send text messages, videos and audio files to your family, colleagues and friends.xx</p>
|
||||
<p xml:lang="zh-TW">NeoChat 是一個讓您能夠完全利用 Matrix 網路的聊天應用程式。它讓您安全地傳送文字訊息、影片或音訊檔給家人、同事或朋友等等。</p>
|
||||
<p>NeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly evolving but the aim remains to provide eventual support for the entire spec.</p>
|
||||
<p xml:lang="ar">يهدف نيوتشات إلى أن يكون تطبيقًا كامل الميزات لمواصفات ماتركس. على هذا النحو يتم دعم كل شيء في المواصفات المستقرة الحالية مع الاستثناءات الملحوظة لـ VoIP والخيوط وبعض جوانب التشفير من طرف إلى طرف. هناك عدد قليل من الإغفالات الصغيرة الأخرى بسبب حقيقة أن مواصفات ماتركس تتطور باستمرار ، ولكن يبقى الهدف توفير الدعم النهائي للمواصفات بأكملها.</p>
|
||||
<p xml:lang="ar">يهدف نيوتشات إلى أن يكون تطبيقًا كامل الميزات لمواصفات ماتركس. يوفر نيوتشات كل شيء في المواصفات المستقرة الحالية مع الاستثناءات الملحوظة لـ VoIP و تعدد الخيوط وبعض جوانب التعمية من طرف إلى طرف. هناك عدد قليل من الإغفالات الصغيرة الأخرى بسبب حقيقة أن مواصفات ماتركس تتطور باستمرار، ولكن يبقى الهدف توفير تطبيق للمواصفات بأكملها.</p>
|
||||
<p xml:lang="ca">NeoChat pretén ser una aplicació amb totes les característiques per a l'especificació de Matrix. Com a tal, s'ha implementat tota l'especificació actual estable amb les notables excepcions de la VoIP, fils i alguns aspectes de l'encriptatge d'extrem a extrem. Hi ha algunes altres omissions més petites a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu segueix sent proporcionar suport eventual per a tota l'especificació.</p>
|
||||
<p xml:lang="ca-valencia">NeoChat pretén ser una aplicació amb totes les característiques per a l'especificació de Matrix. Com a tal, s'ha implementat tota l'especificació actual estable amb les notables excepcions de la VoIP, fils i alguns aspectes de l'encriptació d'extrem a extrem. Hi ha algunes altres omissions més xicotetes a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu seguix sent proporcionar suport eventual per a tota l'especificació.</p>
|
||||
<p xml:lang="de">NeoChat versucht eine vollumfängliche Anwendung für die Spezifikation von Matrix zu sein. Damit wird alles der aktuellen stabilen Spezifikation mit den erwähnenswerten Ausnahmen von VoIP, Diskussionsfäden und ein paar Teilen der Ende-zu-Ende-Verschlüsselung unterstützt. Zudem sind andere kleinere Auslassungen vorhanden, da sich die Matrixspezifikation ständig weiterentwickelt. Nichtsdestotrotz soll letztendlich die gesamte Spezifikation unterstützt werden.</p>
|
||||
@@ -154,7 +154,7 @@
|
||||
<p xml:lang="x-test">xxNeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly evolving but the aim remains to provide eventual support for the entire spec.xx</p>
|
||||
<p xml:lang="zh-TW">NeoChat 以完整支援 Matrix 標準為目標,因此目前穩定版標準除了 VoIP、對話串與端對端加密的某些部分以外的所有部分都有支援。其他部分還有一些較小的不支援的部分,這是因為 Matrix 標準隨時都在改進,但目標仍然時最終提供整個標準的完整支援。</p>
|
||||
<p>Due to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:</p>
|
||||
<p xml:lang="ar">نظرًا لطبيعة تطوير مواصفات ماتركس، يدعم نيوتشات أيضًا العديد من الميزات غير المستقرة وهي:</p>
|
||||
<p xml:lang="ar">نظرًا لطبيعة تطوير مواصفات ماتركس، يوفر نيوتشات أيضًا العديد من الميزات غير المستقرة وهي:</p>
|
||||
<p xml:lang="ca">A causa de la naturalesa del desenvolupament de l'especificació de Matrix, el NeoChat també implementa nombroses característiques inestables. Actualment són:</p>
|
||||
<p xml:lang="ca-valencia">A causa de la naturalea del desenvolupament de l'especificació de Matrix, NeoChat també implementa nombroses característiques inestables. Actualment són:</p>
|
||||
<p xml:lang="de">Durch die Weiterentwicklung der Matrix-Spezifikation unterstützt auch NeoChat einige als noch instabil gekennzeichnete Funktionen. Derzeit sind das:</p>
|
||||
@@ -306,8 +306,8 @@
|
||||
<keyword>Matrix</keyword>
|
||||
<keyword>Kirigami</keyword>
|
||||
</keywords>
|
||||
<developer id="kde.org">
|
||||
<name>The KDE Community</name>
|
||||
<developer id="org.kde">
|
||||
<name translate="no">KDE</name>
|
||||
<url>https://kde.org</url>
|
||||
</developer>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
|
||||
28
patches/0001-Revert-Bump-KF6-dependency-version.patch
Normal file
28
patches/0001-Revert-Bump-KF6-dependency-version.patch
Normal file
@@ -0,0 +1,28 @@
|
||||
SPDX-FileCopyrightText: 2025 Tobias Fella <tobias.fella@kde.org>
|
||||
SPDX-License-Identifier: BSD-2-Clause
|
||||
From dbd1cefd0f07a6942aef450f8f3e082aa3b1cc25 Mon Sep 17 00:00:00 2001
|
||||
From: Tobias Fella <tobias.fella@kde.org>
|
||||
Date: Sun, 17 Aug 2025 20:04:04 +0200
|
||||
Subject: [PATCH] Revert "Bump KF6 dependency version"
|
||||
|
||||
This reverts commit 18a6ea98232b3a734905fb18eebba9cf39bf5325.
|
||||
---
|
||||
CMakeLists.txt | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index 10fe66daa..cd063113d 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -14,7 +14,7 @@ set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_
|
||||
|
||||
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
|
||||
|
||||
-set(KF_MIN_VERSION "6.17")
|
||||
+set(KF_MIN_VERSION "6.12")
|
||||
set(QT_MIN_VERSION "6.5")
|
||||
|
||||
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
|
||||
--
|
||||
2.50.1
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
SPDX-FileCopyrightText: 2025 Tobias Fella <tobias.fella@kde.org>
|
||||
SPDX-License-Identifier: BSD-2-Clause
|
||||
From ca72345b8ee550be2172d8ac5e5dc9e4c2b508c9 Mon Sep 17 00:00:00 2001
|
||||
From: Tobias Fella <tobias.fella@kde.org>
|
||||
Date: Sun, 17 Aug 2025 20:00:08 +0200
|
||||
Subject: [PATCH] Revert "Use new Kirigami builtin column resize handle"
|
||||
|
||||
This reverts commit de97275a387abcbca6fcb185bcbd1b69c30f5c66.
|
||||
---
|
||||
src/app/qml/Main.qml | 1 -
|
||||
src/rooms/RoomListPage.qml | 70 +++++++++++++++++++++++++++++---------
|
||||
2 files changed, 54 insertions(+), 17 deletions(-)
|
||||
|
||||
diff --git a/src/app/qml/Main.qml b/src/app/qml/Main.qml
|
||||
index ea8955674..6eed271c1 100644
|
||||
--- a/src/app/qml/Main.qml
|
||||
+++ b/src/app/qml/Main.qml
|
||||
@@ -45,7 +45,6 @@ Kirigami.ApplicationWindow {
|
||||
showExisting: true
|
||||
onConnectionChosen: root.load()
|
||||
}
|
||||
- columnView.columnResizeMode: pageStack.wideMode ? Kirigami.ColumnView.DynamicColumns : Kirigami.ColumnView.SingleColumn
|
||||
globalToolBar.canContainHandles: true
|
||||
globalToolBar {
|
||||
style: Kirigami.ApplicationHeaderStyle.ToolBar
|
||||
diff --git a/src/rooms/RoomListPage.qml b/src/rooms/RoomListPage.qml
|
||||
index 2ac211fd5..f5586d789 100644
|
||||
--- a/src/rooms/RoomListPage.qml
|
||||
+++ b/src/rooms/RoomListPage.qml
|
||||
@@ -17,22 +17,13 @@ import org.kde.neochat
|
||||
Kirigami.Page {
|
||||
id: root
|
||||
|
||||
- Kirigami.ColumnView.interactiveResizeEnabled: true
|
||||
- Kirigami.ColumnView.minimumWidth: _private.collapsedSize + spaceDrawer.width + 1
|
||||
- Kirigami.ColumnView.maximumWidth: _private.defaultWidth + spaceDrawer.width + 1
|
||||
- Kirigami.ColumnView.onInteractiveResizingChanged: {
|
||||
- if (!Kirigami.ColumnView.interactiveResizing && collapsed) {
|
||||
- Kirigami.ColumnView.preferredWidth = root.Kirigami.ColumnView.minimumWidth;
|
||||
- }
|
||||
- }
|
||||
- Kirigami.ColumnView.preferredWidth: _private.currentWidth + spaceDrawer.width + 1
|
||||
- Kirigami.ColumnView.onPreferredWidthChanged: {
|
||||
- if (width > _private.collapseWidth) {
|
||||
- NeoChatConfig.collapsed = false;
|
||||
- } else if (Kirigami.ColumnView.interactiveResizing) {
|
||||
- NeoChatConfig.collapsed = true;
|
||||
- }
|
||||
- }
|
||||
+ /**
|
||||
+ * @brief The current width of the room list.
|
||||
+ *
|
||||
+ * @note Other objects can access the value but the private function makes sure
|
||||
+ * that only the internal members can modify it.
|
||||
+ */
|
||||
+ readonly property int currentWidth: _private.currentWidth + spaceDrawer.width + 1
|
||||
|
||||
required property NeoChatConnection connection
|
||||
|
||||
@@ -40,6 +31,10 @@ Kirigami.Page {
|
||||
|
||||
signal search
|
||||
|
||||
+ onCurrentWidthChanged: pageStack.defaultColumnWidth = root.currentWidth
|
||||
+ Component.onCompleted: pageStack.defaultColumnWidth = root.currentWidth
|
||||
+
|
||||
+
|
||||
onCollapsedChanged: {
|
||||
if (collapsed) {
|
||||
RoomManager.sortFilterRoomTreeModel.filterText = "";
|
||||
@@ -252,6 +247,49 @@ Kirigami.Page {
|
||||
sourceComponent: Kirigami.Settings.isMobile ? exploreComponentMobile : userInfoDesktop
|
||||
}
|
||||
|
||||
+ MouseArea {
|
||||
+ anchors.top: parent.top
|
||||
+ anchors.bottom: parent.bottom
|
||||
+ parent: applicationWindow().overlay.parent
|
||||
+
|
||||
+ x: root.currentWidth - width / 2
|
||||
+ width: Kirigami.Units.smallSpacing * 2
|
||||
+ z: root.z + 1
|
||||
+ enabled: RoomManager.hasOpenRoom && applicationWindow().width >= Kirigami.Units.gridUnit * 35
|
||||
+ visible: enabled
|
||||
+ cursorShape: Qt.SplitHCursor
|
||||
+
|
||||
+ property int _lastX
|
||||
+
|
||||
+ onPressed: mouse => {
|
||||
+ _lastX = mouse.x;
|
||||
+ }
|
||||
+ onPositionChanged: mouse => {
|
||||
+ if (_lastX == -1) {
|
||||
+ return;
|
||||
+ }
|
||||
+ if (mouse.x > _lastX) {
|
||||
+ // we moved to the right
|
||||
+ if (_private.currentWidth < _private.collapseWidth && _private.currentWidth + (mouse.x - _lastX) >= _private.collapseWidth) {
|
||||
+ // Here we get back directly to a more wide mode.
|
||||
+ _private.currentWidth = _private.defaultWidth;
|
||||
+ NeoChatConfig.collapsed = false;
|
||||
+ } else if (_private.currentWidth >= _private.collapseWidth) {
|
||||
+ // Increase page width
|
||||
+ _private.currentWidth = Math.min(_private.defaultWidth, _private.currentWidth + (mouse.x - _lastX));
|
||||
+ }
|
||||
+ } else if (mouse.x < _lastX) {
|
||||
+ const tmpWidth = _private.currentWidth - (_lastX - mouse.x);
|
||||
+ if (tmpWidth < _private.collapseWidth) {
|
||||
+ _private.currentWidth = Qt.binding(() => _private.collapsedSize);
|
||||
+ NeoChatConfig.collapsed = true;
|
||||
+ } else {
|
||||
+ _private.currentWidth = tmpWidth;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
Component {
|
||||
id: userInfo
|
||||
UserInfo {
|
||||
--
|
||||
2.50.1
|
||||
|
||||
1065
po/ar/neochat.po
1065
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1019
po/az/neochat.po
1019
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
865
po/ca/neochat.po
865
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1070
po/cs/neochat.po
1070
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
948
po/da/neochat.po
948
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
1103
po/de/neochat.po
1103
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
1035
po/el/neochat.po
1035
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
1108
po/en_GB/neochat.po
1108
po/en_GB/neochat.po
File diff suppressed because it is too large
Load Diff
1122
po/eo/neochat.po
1122
po/eo/neochat.po
File diff suppressed because it is too large
Load Diff
925
po/es/neochat.po
925
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
905
po/eu/neochat.po
905
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
1122
po/fi/neochat.po
1122
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
1124
po/fr/neochat.po
1124
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
1122
po/gl/neochat.po
1122
po/gl/neochat.po
File diff suppressed because it is too large
Load Diff
984
po/he/neochat.po
984
po/he/neochat.po
File diff suppressed because it is too large
Load Diff
1109
po/hi/neochat.po
1109
po/hi/neochat.po
File diff suppressed because it is too large
Load Diff
1122
po/hu/neochat.po
1122
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
1122
po/ia/neochat.po
1122
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
1071
po/id/neochat.po
1071
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
990
po/ie/neochat.po
990
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
894
po/it/neochat.po
894
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
795
po/ja/neochat.po
795
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
875
po/ka/neochat.po
875
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
1122
po/ko/neochat.po
1122
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
795
po/lt/neochat.po
795
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
1122
po/lv/neochat.po
1122
po/lv/neochat.po
File diff suppressed because it is too large
Load Diff
865
po/nl/neochat.po
865
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
1036
po/nn/neochat.po
1036
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
1003
po/pa/neochat.po
1003
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
1118
po/pl/neochat.po
1118
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
1071
po/pt/neochat.po
1071
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
1085
po/pt_BR/neochat.po
1085
po/pt_BR/neochat.po
File diff suppressed because it is too large
Load Diff
1122
po/ru/neochat.po
1122
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
1109
po/sa/neochat.po
1109
po/sa/neochat.po
File diff suppressed because it is too large
Load Diff
1037
po/sk/neochat.po
1037
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
905
po/sl/neochat.po
905
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
877
po/sv/neochat.po
877
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
1156
po/ta/neochat.po
1156
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1012
po/tr/neochat.po
1012
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
866
po/uk/neochat.po
866
po/uk/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1122
po/zh_TW/neochat.po
1122
po/zh_TW/neochat.po
File diff suppressed because it is too large
Load Diff
@@ -104,6 +104,7 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
||||
DEPENDENCIES
|
||||
QtCore
|
||||
QtQuick
|
||||
io.github.quotient_im.libquotient
|
||||
IMPORTS
|
||||
org.kde.neochat.libneochat
|
||||
org.kde.neochat.rooms
|
||||
@@ -115,13 +116,15 @@ ecm_add_qml_module(neochat URI org.kde.neochat GENERATE_PLUGIN_SOURCE
|
||||
org.kde.neochat.devtools
|
||||
org.kde.neochat.login
|
||||
org.kde.neochat.chatbar
|
||||
org.kde.config
|
||||
org.kde.purpose
|
||||
org.kde.syntaxhighlighting
|
||||
)
|
||||
|
||||
if(NOT ANDROID AND NOT WIN32)
|
||||
qt_target_qml_sources(neochat QML_FILES
|
||||
qml/ShareAction.qml
|
||||
qml/GlobalMenu.qml
|
||||
qml/EditMenu.qml
|
||||
)
|
||||
else()
|
||||
qt_target_qml_sources(neochat QML_FILES
|
||||
@@ -338,10 +341,6 @@ if(TARGET KF6::DBusAddons AND NOT WIN32)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_KDBUSADDONS)
|
||||
endif()
|
||||
|
||||
if (TARGET KF6::KIOWidgets)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_KIO)
|
||||
endif()
|
||||
|
||||
if (TARGET KUnifiedPush)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_KUNIFIEDPUSH)
|
||||
target_link_libraries(neochat PUBLIC KUnifiedPush)
|
||||
|
||||
@@ -433,7 +433,7 @@ QPixmap NotificationsManager::createNotificationImage(const QImage &icon, NeoCha
|
||||
|
||||
if (room != nullptr) {
|
||||
const QImage roomAvatar = room->avatar(imageRect.width(), imageRect.height());
|
||||
if (icon != roomAvatar) {
|
||||
if (!roomAvatar.isNull() && icon != roomAvatar) {
|
||||
const QRect lowerQuarter{imageRect.center(), imageRect.size() / 2};
|
||||
|
||||
painter.setBrush(Qt::white);
|
||||
|
||||
@@ -11,7 +11,6 @@ import org.kde.kirigamiaddons.components as KirigamiComponents
|
||||
|
||||
import org.kde.neochat
|
||||
import org.kde.neochat.settings
|
||||
import org.kde.neochat.devtools
|
||||
|
||||
KirigamiComponents.ConvergentContextMenu {
|
||||
id: root
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
@@ -53,7 +55,7 @@ Kirigami.Dialog {
|
||||
text: i18nc("@button: login to or register a new account.", "Add Account")
|
||||
contentItem: Delegates.SubtitleContentItem {
|
||||
itemDelegate: addDelegate
|
||||
subtitle: i18n("Log in or create a new account")
|
||||
subtitle: i18nc("@info", "Log in or create a new account")
|
||||
labelItem.textFormat: Text.PlainText
|
||||
subtitleItem.textFormat: Text.PlainText
|
||||
}
|
||||
@@ -93,8 +95,8 @@ Kirigami.Dialog {
|
||||
accountView.decrementCurrentIndex();
|
||||
}
|
||||
}
|
||||
Keys.onEnterPressed: accountView.currentItem.clicked()
|
||||
Keys.onReturnPressed: accountView.currentItem.clicked()
|
||||
Keys.onEnterPressed: (accountView.currentItem as Delegates.RoundedItemDelegate).clicked()
|
||||
Keys.onReturnPressed: (accountView.currentItem as Delegates.RoundedItemDelegate).clicked()
|
||||
|
||||
onVisibleChanged: {
|
||||
for (let i = 0; i < accountView.count; i++) {
|
||||
|
||||
@@ -6,8 +6,6 @@ import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Dialog {
|
||||
id: root
|
||||
|
||||
@@ -20,7 +18,7 @@ Kirigami.Dialog {
|
||||
title: i18nc("@title:dialog", "Start a chat")
|
||||
|
||||
contentItem: QQC2.Label {
|
||||
text: i18n("Do you want to start a chat with %1?", root.user.displayName)
|
||||
text: i18nc("@info", "Do you want to start a chat with %1?", root.user.displayName)
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.Wrap
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
|
||||
@@ -58,14 +58,18 @@ ColumnLayout {
|
||||
QQC2.ToolButton {
|
||||
id: cancelAttachmentButton
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Cancel sending attachment")
|
||||
icon.name: "dialog-close"
|
||||
onTriggered: root.attachmentCancelled()
|
||||
shortcut: "Escape"
|
||||
}
|
||||
text: i18nc("@action:button", "Cancel sending attachment")
|
||||
icon.name: "dialog-close"
|
||||
onClicked: root.attachmentCancelled()
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
Kirigami.Action {
|
||||
shortcut: "Escape"
|
||||
onTriggered: cancelAttachmentButton.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carson Black <uhhadd@gmail.com>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import Qt.labs.platform as Labs
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Labs.Menu {
|
||||
id: root
|
||||
|
||||
required property Item field
|
||||
|
||||
Labs.MenuItem {
|
||||
enabled: root.field !== null && root.field.canUndo
|
||||
text: i18nc("text editing menu action", "Undo")
|
||||
shortcut: StandardKey.Undo
|
||||
onTriggered: {
|
||||
root.field.undo();
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
Labs.MenuItem {
|
||||
enabled: root.field !== null && root.field.canRedo
|
||||
text: i18nc("text editing menu action", "Redo")
|
||||
shortcut: StandardKey.Redo
|
||||
onTriggered: {
|
||||
root.field.undo();
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
Labs.MenuSeparator {}
|
||||
|
||||
Labs.MenuItem {
|
||||
enabled: root.field !== null && root.field.selectedText
|
||||
text: i18nc("text editing menu action", "Cut")
|
||||
shortcut: StandardKey.Cut
|
||||
onTriggered: {
|
||||
root.field.cut();
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
Labs.MenuItem {
|
||||
enabled: root.field !== null && root.field.selectedText
|
||||
text: i18nc("text editing menu action", "Copy")
|
||||
shortcut: StandardKey.Copy
|
||||
onTriggered: {
|
||||
root.field.copy();
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
Labs.MenuItem {
|
||||
enabled: root.field !== null && root.field.canPaste
|
||||
text: i18nc("text editing menu action", "Paste")
|
||||
shortcut: StandardKey.Paste
|
||||
onTriggered: {
|
||||
root.field.paste();
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
Labs.MenuItem {
|
||||
enabled: root.field !== null && root.field.selectedText !== ""
|
||||
text: i18nc("text editing menu action", "Delete")
|
||||
shortcut: ""
|
||||
onTriggered: {
|
||||
root.field.remove(root.field.selectionStart, root.field.selectionEnd);
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
Labs.MenuSeparator {}
|
||||
|
||||
Labs.MenuItem {
|
||||
enabled: root.field !== null
|
||||
text: i18nc("text editing menu action", "Select All")
|
||||
shortcut: StandardKey.SelectAll
|
||||
onTriggered: {
|
||||
root.field.selectAll();
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import Qt.labs.platform as Labs
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
@@ -16,6 +15,7 @@ Labs.MenuBar {
|
||||
id: root
|
||||
|
||||
required property NeoChatConnection connection
|
||||
required property Kirigami.ApplicationWindow appWindow
|
||||
|
||||
Labs.Menu {
|
||||
title: i18nc("menu", "File")
|
||||
@@ -23,8 +23,8 @@ Labs.MenuBar {
|
||||
Labs.MenuItem {
|
||||
icon.name: "list-add-user"
|
||||
text: i18nc("@action:inmenu", "Find your Friends")
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("Find your friends") && AccountRegistry.accountCount > 0
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), {
|
||||
enabled: root.connection
|
||||
onTriggered: root.appWindow.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'UserSearchPage'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Find your friends")
|
||||
@@ -33,21 +33,22 @@ Labs.MenuBar {
|
||||
Labs.MenuItem {
|
||||
icon.name: "system-users-symbolic"
|
||||
text: i18nc("@action:inmenu", "Create a Room…")
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("Find your friends") && AccountRegistry.accountCount > 0
|
||||
enabled: root.connection
|
||||
shortcut: StandardKey.New
|
||||
onTriggered: {
|
||||
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'CreateRoomDialog'), {
|
||||
Qt.createComponent('org.kde.neochat', 'CreateRoomDialog').createObject(root.appWindow, {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Create a Room")
|
||||
});
|
||||
}).open();
|
||||
}
|
||||
}
|
||||
Labs.MenuItem {
|
||||
icon.name: "compass-symbolic"
|
||||
text: i18nc("@action:inmenu", "Explore Rooms")
|
||||
enabled: root.connection
|
||||
onTriggered: {
|
||||
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
|
||||
let dialog = root.appWindow.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Explore Rooms")
|
||||
@@ -58,7 +59,6 @@ Labs.MenuBar {
|
||||
}
|
||||
}
|
||||
Labs.MenuItem {
|
||||
enabled: pageStack.layers.currentItem.title !== i18n("Configure NeoChat…")
|
||||
text: i18nc("menu", "Configure NeoChat…")
|
||||
|
||||
shortcut: StandardKey.Preferences
|
||||
@@ -71,17 +71,15 @@ Labs.MenuBar {
|
||||
onTriggered: Qt.quit()
|
||||
}
|
||||
}
|
||||
EditMenu {
|
||||
title: i18nc("menu", "Edit")
|
||||
field: (root.activeFocusItem instanceof TextEdit || root.activeFocusItem instanceof TextInput) ? root.activeFocusItem : null
|
||||
}
|
||||
|
||||
Labs.Menu {
|
||||
title: i18nc("menu", "View")
|
||||
|
||||
Labs.MenuItem {
|
||||
icon.name: "search-symbolic"
|
||||
enabled: root.connection
|
||||
text: i18nc("@action:inmenu opens a UI element called the 'Quick Switcher', which offers a fast keyboard-based interface for switching in between chats.", "Search Rooms")
|
||||
onTriggered: quickSwitcher.open()
|
||||
onTriggered: (root.appWindow as Main).quickSwitcher.open()
|
||||
}
|
||||
}
|
||||
Labs.Menu {
|
||||
@@ -89,8 +87,8 @@ Labs.MenuBar {
|
||||
|
||||
Labs.MenuItem {
|
||||
icon.name: "view-fullscreen-symbolic"
|
||||
text: root.visibility === Window.FullScreen ? i18nc("menu", "Exit Full Screen") : i18nc("menu", "Enter Full Screen")
|
||||
onTriggered: root.visibility === Window.FullScreen ? root.showNormal() : root.showFullScreen()
|
||||
text: root.appWindow.visibility === Window.FullScreen ? i18nc("menu", "Exit Full Screen") : i18nc("menu", "Enter Full Screen")
|
||||
onTriggered: root.appWindow.visibility === Window.FullScreen ? root.appWindow.showNormal() : root.appWindow.showFullScreen()
|
||||
}
|
||||
}
|
||||
Labs.Menu {
|
||||
@@ -99,12 +97,12 @@ Labs.MenuBar {
|
||||
Labs.MenuItem {
|
||||
icon.name: "help-about-symbolic"
|
||||
text: i18nc("menu", "About NeoChat")
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.kirigamiaddons.formcard", "AboutPage"))
|
||||
onTriggered: root.appWindow.pageStack.pushDialogLayer(Qt.createComponent("org.kde.kirigamiaddons.formcard", "AboutPage"))
|
||||
}
|
||||
Labs.MenuItem {
|
||||
icon.name: "kde-symbolic"
|
||||
text: i18nc("menu", "About KDE")
|
||||
onTriggered: pageStack.pushDialogLayer(Qt.createComponent("org.kde.kirigamiaddons.formcard", "AboutKDEPage"))
|
||||
onTriggered: root.appWindow.pageStack.pushDialogLayer(Qt.createComponent("org.kde.kirigamiaddons.formcard", "AboutKDEPage"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,29 +3,55 @@
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
QQC2.Control {
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property string text
|
||||
|
||||
visible: !root.text.startsWith("https://matrix.to/") && root.text.length > 0
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
|
||||
z: 20
|
||||
|
||||
Accessible.ignored: true
|
||||
|
||||
contentItem: QQC2.Label {
|
||||
text: root.text.startsWith("https://matrix.to/") ? "" : root.text
|
||||
elide: Text.ElideRight
|
||||
Accessible.description: i18nc("@info screenreader", "The currently selected link")
|
||||
onTextChanged: {
|
||||
// This is done so the text doesn't disappear for a split second while in the opacity transition
|
||||
if (root.text.length > 0) {
|
||||
urlLabel.text = root.text
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
Kirigami.OverlayZStacking.layer: Kirigami.OverlayZStacking.ToolTip
|
||||
z: Kirigami.OverlayZStacking.z
|
||||
spacing: 0
|
||||
|
||||
opacity: (!root.text.startsWith("https://matrix.to/") && root.text.length > 0) ? 1 : 0
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
OpacityAnimator {
|
||||
duration: Kirigami.Units.shortDuration
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.Control {
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
|
||||
Accessible.ignored: true
|
||||
|
||||
contentItem: QQC2.Label {
|
||||
id: urlLabel
|
||||
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
background: Kirigami.ShadowedRectangle {
|
||||
corners.topRightRadius: Kirigami.Units.cornerRadius
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
border {
|
||||
color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast)
|
||||
width: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,22 @@ Kirigami.Page {
|
||||
sourceComponent: message
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "waitingForKey"
|
||||
when: root.session.state === KeyVerificationSession.WAITINGFORKEY
|
||||
PropertyChanges {
|
||||
target: stateLoader
|
||||
sourceComponent: message
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "waitingForAccept"
|
||||
when: root.session.state === KeyVerificationSession.WAITINGFORACCEPT
|
||||
PropertyChanges {
|
||||
target: stateLoader
|
||||
sourceComponent: message
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "waitingForMac"
|
||||
when: root.session.state === KeyVerificationSession.WAITINGFORMAC
|
||||
@@ -127,7 +143,9 @@ Kirigami.Page {
|
||||
case KeyVerificationSession.WAITINGFORREADY:
|
||||
case KeyVerificationSession.INCOMING:
|
||||
case KeyVerificationSession.WAITINGFORMAC:
|
||||
return "security-medium-symbolic";
|
||||
case KeyVerificationSession.WAITINGFORKEY:
|
||||
case KeyVerificationSession.WAITINGFORACCEPT:
|
||||
return "security-medium-symbolic";
|
||||
case KeyVerificationSession.DONE:
|
||||
return "security-high";
|
||||
default:
|
||||
@@ -141,9 +159,13 @@ Kirigami.Page {
|
||||
case KeyVerificationSession.INCOMING:
|
||||
return i18n("Incoming key verification request from device **%1**", root.session.remoteDeviceId);
|
||||
case KeyVerificationSession.WAITINGFORMAC:
|
||||
return i18n("Waiting for other party to send us keys.");
|
||||
case KeyVerificationSession.WAITINGFORKEY:
|
||||
return i18n("Waiting for other party to confirm our keys.");
|
||||
case KeyVerificationSession.WAITINGFORACCEPT:
|
||||
return i18n("Waiting for other party to verify.");
|
||||
case KeyVerificationSession.DONE:
|
||||
return i18n("Successfully verified device **%1**", root.session.remoteDeviceId)
|
||||
return i18n("Successfully verified device **%1**", root.session.remoteDeviceId);
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.config as KConfig
|
||||
@@ -20,6 +19,11 @@ Kirigami.ApplicationWindow {
|
||||
|
||||
property bool initialized: false
|
||||
|
||||
readonly property QuickSwitcher quickSwitcher: QuickSwitcher {
|
||||
connection: root.connection
|
||||
window: root
|
||||
}
|
||||
|
||||
title: {
|
||||
if (NeoChatConfig.windowTitleFocus) {
|
||||
return activeFocusItem + " " + (activeFocusItem ? activeFocusItem.Accessible.name : "");
|
||||
@@ -83,6 +87,7 @@ Kirigami.ApplicationWindow {
|
||||
active: Kirigami.Settings.hasPlatformMenuBar && !Kirigami.Settings.isMobile
|
||||
sourceComponent: GlobalMenu {
|
||||
connection: root.connection
|
||||
appWindow: root
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,17 +95,12 @@ Kirigami.ApplicationWindow {
|
||||
configGroupName: "MainWindow"
|
||||
}
|
||||
|
||||
QuickSwitcher {
|
||||
id: quickSwitcher
|
||||
connection: root.connection
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: RoomManager
|
||||
|
||||
function onCurrentRoomChanged() {
|
||||
if (RoomManager.currentRoom && pageStack.depth <= 1 && root.initialized && Kirigami.Settings.isMobile) {
|
||||
let roomPage = pageStack.layers.push(Qt.createComponent('org.kde.neochat', 'RoomPage'));
|
||||
if (RoomManager.currentRoom && root.pageStack.depth <= 1 && root.initialized && Kirigami.Settings.isMobile) {
|
||||
let roomPage = root.pageStack.layers.push(Qt.createComponent('org.kde.neochat', 'RoomPage'));
|
||||
roomPage.backRequested.connect(event => {
|
||||
RoomManager.clearCurrentRoom();
|
||||
});
|
||||
@@ -108,33 +108,26 @@ Kirigami.ApplicationWindow {
|
||||
}
|
||||
|
||||
function onAskJoinRoom(room) {
|
||||
Qt.createComponent("org.kde.neochat", "JoinRoomDialog").createObject(root, {
|
||||
(Qt.createComponent("org.kde.neochat", "JoinRoomDialog").createObject(root, {
|
||||
room: room,
|
||||
connection: root.connection
|
||||
}).open();
|
||||
}) as JoinRoomDialog).open();
|
||||
}
|
||||
|
||||
function onShowUserDetail(user, room) {
|
||||
root.showUserDetail(user, room);
|
||||
}
|
||||
|
||||
function goToEvent(event) {
|
||||
if (event.length > 0) {
|
||||
roomItem.goToEvent(event);
|
||||
}
|
||||
roomItem.forceActiveFocus();
|
||||
}
|
||||
|
||||
function onAskDirectChatConfirmation(user) {
|
||||
Qt.createComponent("org.kde.neochat", "AskDirectChatConfirmation").createObject(this, {
|
||||
(Qt.createComponent("org.kde.neochat", "AskDirectChatConfirmation").createObject(this, {
|
||||
user: user
|
||||
}).open();
|
||||
}) as AskDirectChatConfirmation).open();
|
||||
}
|
||||
|
||||
function onExternalUrl(url) {
|
||||
let dialog = Qt.createComponent("org.kde.neochat", "ConfirmUrlDialog").createObject(this);
|
||||
dialog.link = url;
|
||||
dialog.open();
|
||||
(Qt.createComponent("org.kde.neochat", "ConfirmUrlDialog").createObject(this, {
|
||||
link: url
|
||||
}) as ConfirmUrlDialog).open();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +191,7 @@ Kirigami.ApplicationWindow {
|
||||
dim = false;
|
||||
}
|
||||
}
|
||||
enabled: RoomManager.hasOpenRoom && pageStack.layers.depth < 2 && pageStack.depth < 3 && (pageStack.visibleItems.length > 1 || pageStack.currentIndex > 0) && !Kirigami.Settings.isMobile && root.pageStack.wideMode
|
||||
enabled: RoomManager.hasOpenRoom && root.pageStack.layers.depth < 2 && root.pageStack.depth < 3 && (root.pageStack.visibleItems.length > 1 || root.pageStack.currentIndex > 0) && !Kirigami.Settings.isMobile && root.pageStack.wideMode
|
||||
handleVisible: enabled
|
||||
}
|
||||
|
||||
@@ -220,10 +213,10 @@ Kirigami.ApplicationWindow {
|
||||
Connections {
|
||||
target: NeoChatConfig
|
||||
function onBlurChanged() {
|
||||
WindowController.setBlur(pageStack, NeoChatConfig.blur && !NeoChatConfig.compactLayout);
|
||||
WindowController.setBlur(root.pageStack, NeoChatConfig.blur && !NeoChatConfig.compactLayout);
|
||||
}
|
||||
function onCompactLayoutChanged() {
|
||||
WindowController.setBlur(pageStack, NeoChatConfig.blur && !NeoChatConfig.compactLayout);
|
||||
WindowController.setBlur(root.pageStack, NeoChatConfig.blur && !NeoChatConfig.compactLayout);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,8 +271,8 @@ Kirigami.ApplicationWindow {
|
||||
target: AccountRegistry
|
||||
function onRowsRemoved() {
|
||||
if (AccountRegistry.rowCount() === 0) {
|
||||
pageStack.clear();
|
||||
pageStack.push(Qt.createComponent('org.kde.neochat.login', 'WelcomePage'));
|
||||
root.pageStack.clear();
|
||||
root.pageStack.push(Qt.createComponent('org.kde.neochat.login', 'WelcomePage'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,7 +281,7 @@ Kirigami.ApplicationWindow {
|
||||
target: Controller
|
||||
|
||||
function onErrorOccured(error) {
|
||||
showPassiveNotification(error, "short");
|
||||
root.showPassiveNotification(error, "short");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,9 +296,9 @@ Kirigami.ApplicationWindow {
|
||||
});
|
||||
}
|
||||
function onUserConsentRequired(url) {
|
||||
Qt.createComponent("org.kde.neochat", "ConsentDialog").createObject(this, {
|
||||
(Qt.createComponent("org.kde.neochat", "ConsentDialog").createObject(this, {
|
||||
url: url
|
||||
}).open();
|
||||
}) as ConsentDialog).open();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +346,7 @@ Kirigami.ApplicationWindow {
|
||||
room: room,
|
||||
user: user,
|
||||
connection: root.connection,
|
||||
});
|
||||
}) as UserDetailDialog;
|
||||
dialog.parent = QmlUtils.focusedWindowItem(); // Kirigami Dialogs overwrite the parent, so we need to set it again
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ Kirigami.Dialog {
|
||||
/**
|
||||
* @brief Thrown when a user is selected.
|
||||
*/
|
||||
signal userSelected
|
||||
signal userSelected(string userId)
|
||||
|
||||
title: i18nc("@title", "User ID")
|
||||
|
||||
@@ -38,7 +38,7 @@ Kirigami.Dialog {
|
||||
text: i18n("OK")
|
||||
icon.name: "dialog-ok"
|
||||
onTriggered: {
|
||||
root.connection.requestDirectChat(userIdText.text);
|
||||
root.userSelected(userIdText.text)
|
||||
root.accept();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
@@ -8,11 +10,8 @@ import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
|
||||
import Quotient
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Dialog {
|
||||
@@ -43,22 +42,22 @@ Kirigami.Dialog {
|
||||
FormCard.FormComboBoxDelegate {
|
||||
id: pollTypeCombo
|
||||
|
||||
text: i18n("Poll type:")
|
||||
text: i18nc("@label", "Poll type:")
|
||||
currentIndex: 0
|
||||
textRole: "text"
|
||||
valueRole: "value"
|
||||
model: [
|
||||
{ value: PollKind.Disclosed, text: i18n("Open poll") },
|
||||
{ value: PollKind.Undisclosed, text: i18n("Closed poll") }
|
||||
{ value: PollKind.Disclosed, text: i18nc("@item:inlistbox", "Open poll") },
|
||||
{ value: PollKind.Undisclosed, text: i18nc("@item:inlistbox", "Closed poll") }
|
||||
]
|
||||
}
|
||||
FormCard.FormTextDelegate {
|
||||
verticalPadding: 0
|
||||
text: pollTypeCombo.currentValue == 0 ? i18n("Voters can see the result as soon as they have voted") : i18n("Results are revealed only after the poll has closed")
|
||||
text: pollTypeCombo.currentValue == 0 ? i18nc("@info", "Voters can see the result as soon as they have voted") : i18nc("@info", "Results are revealed only after the poll has closed")
|
||||
}
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: questionTextField
|
||||
label: i18n("Question:")
|
||||
label: i18nc("@label", "Question:")
|
||||
}
|
||||
Repeater {
|
||||
id: optionRepeater
|
||||
@@ -117,20 +116,16 @@ Kirigami.Dialog {
|
||||
optionModel.set(optionDelegate.index, {optionText: text})
|
||||
optionModel.allValuesSetChanged()
|
||||
}
|
||||
placeholderText: i18n("Enter option")
|
||||
placeholderText: i18nc("@placeholder", "Enter option")
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
action: Kirigami.Action {
|
||||
id: removeOptionAction
|
||||
text: i18nc("@action:button", "Remove option")
|
||||
icon.name: "edit-delete-remove"
|
||||
onTriggered: optionModel.remove(optionDelegate.index)
|
||||
}
|
||||
QQC2.ToolTip {
|
||||
text: removeOptionAction.text
|
||||
delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
text: i18nc("@action:button", "Remove option")
|
||||
icon.name: "edit-delete-remove"
|
||||
onClicked: optionModel.remove(optionDelegate.index)
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.visible: hovered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,6 @@ FileDialog {
|
||||
|
||||
signal chosen(string path)
|
||||
|
||||
title: i18n("Select a File")
|
||||
title: i18nc("@title:dialog", "Select a File")
|
||||
onAccepted: root.chosen(selectedFile)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
@@ -20,7 +19,7 @@ QQC2.Popup {
|
||||
contentItem: Flow {
|
||||
QQC2.ToolButton {
|
||||
icon.name: "format-text-bold"
|
||||
text: i18n("Bold")
|
||||
text: i18nc("@action:button", "Bold")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: {
|
||||
@@ -29,7 +28,7 @@ QQC2.Popup {
|
||||
end: "**",
|
||||
extra: ""
|
||||
};
|
||||
formattingSelected(format, selectionStart, selectionEnd);
|
||||
root.formattingSelected(format, root.selectionStart, root.selectionEnd);
|
||||
root.close();
|
||||
}
|
||||
|
||||
@@ -39,7 +38,7 @@ QQC2.Popup {
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "format-text-italic"
|
||||
text: i18n("Italic")
|
||||
text: i18nc("@action:button", "Italic")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: {
|
||||
@@ -48,7 +47,7 @@ QQC2.Popup {
|
||||
end: "*",
|
||||
extra: ""
|
||||
};
|
||||
formattingSelected(format, selectionStart, selectionEnd);
|
||||
root.formattingSelected(format, root.selectionStart, root.selectionEnd);
|
||||
root.close();
|
||||
}
|
||||
|
||||
@@ -58,7 +57,7 @@ QQC2.Popup {
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "format-text-strikethrough"
|
||||
text: i18n("Strikethrough")
|
||||
text: i18nc("@action:button", "Strikethrough")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: {
|
||||
@@ -67,7 +66,7 @@ QQC2.Popup {
|
||||
end: "~~",
|
||||
extra: ""
|
||||
};
|
||||
formattingSelected(format, selectionStart, selectionEnd);
|
||||
root.formattingSelected(format, root.selectionStart, root.selectionEnd);
|
||||
root.close();
|
||||
}
|
||||
|
||||
@@ -77,7 +76,7 @@ QQC2.Popup {
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "view-hidden-symbolic"
|
||||
text: i18n("Spoiler")
|
||||
text: i18nc("@action:button", "Spoiler")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: {
|
||||
@@ -86,7 +85,7 @@ QQC2.Popup {
|
||||
end: "||",
|
||||
extra: ""
|
||||
};
|
||||
formattingSelected(format, selectionStart, selectionEnd);
|
||||
root.formattingSelected(format, root.selectionStart, root.selectionEnd);
|
||||
root.close();
|
||||
}
|
||||
|
||||
@@ -96,7 +95,7 @@ QQC2.Popup {
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "format-text-code"
|
||||
text: i18n("Code block")
|
||||
text: i18nc("@action:button", "Code block")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: {
|
||||
@@ -105,7 +104,7 @@ QQC2.Popup {
|
||||
end: "`",
|
||||
extra: ""
|
||||
};
|
||||
formattingSelected(format, selectionStart, selectionEnd);
|
||||
root.formattingSelected(format, root.selectionStart, root.selectionEnd);
|
||||
root.close();
|
||||
}
|
||||
|
||||
@@ -115,16 +114,16 @@ QQC2.Popup {
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "format-text-blockquote"
|
||||
text: i18n("Quote")
|
||||
text: i18nc("@action:button", "Quote")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: {
|
||||
const format = {
|
||||
start: selectionStart == 0 ? ">" : "\n>",
|
||||
start: root.selectionStart == 0 ? ">" : "\n>",
|
||||
end: "\n\n",
|
||||
extra: ""
|
||||
};
|
||||
formattingSelected(format, selectionStart, selectionEnd);
|
||||
root.formattingSelected(format, root.selectionStart, root.selectionEnd);
|
||||
root.close();
|
||||
}
|
||||
|
||||
@@ -134,7 +133,7 @@ QQC2.Popup {
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "link"
|
||||
text: i18n("Insert link")
|
||||
text: i18nc("@action:button", "Insert link")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onClicked: {
|
||||
@@ -143,7 +142,7 @@ QQC2.Popup {
|
||||
end: "](",
|
||||
extra: ")"
|
||||
};
|
||||
formattingSelected(format, selectionStart, selectionEnd);
|
||||
root.formattingSelected(format, root.selectionStart, root.selectionEnd);
|
||||
root.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// SPDX-FileCopyrightText: 2023 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 org.kde.kirigami as Kirigami
|
||||
|
||||
@@ -13,14 +14,15 @@ Kirigami.SearchDialog {
|
||||
id: root
|
||||
|
||||
required property NeoChatConnection connection
|
||||
required property Kirigami.ApplicationWindow window
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+K"
|
||||
onActivated: root.open()
|
||||
onActivated: if (root.connection) root.open()
|
||||
}
|
||||
|
||||
onAccepted: if (currentItem) {
|
||||
currentItem.clicked();
|
||||
(currentItem as QQC2.ItemDelegate).clicked();
|
||||
}
|
||||
|
||||
onTextChanged: RoomManager.sortFilterRoomListModel.filterText = text
|
||||
@@ -32,7 +34,7 @@ Kirigami.SearchDialog {
|
||||
icon.name: "compass"
|
||||
onTriggered: {
|
||||
root.close()
|
||||
let dialog = pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
|
||||
let dialog = root.window.pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ExploreRoomsPage'), {
|
||||
connection: root.connection
|
||||
}, {
|
||||
title: i18nc("@title", "Explore Rooms")
|
||||
|
||||
@@ -7,8 +7,6 @@ import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Page {
|
||||
id: root
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
@@ -63,18 +62,24 @@ Kirigami.Page {
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
visible: Kirigami.Settings.isMobile || !root.Kirigami.PageStack.pageStack.wideMode
|
||||
visible: Kirigami.Settings.isMobile || !(root.Kirigami.PageStack.pageStack as Kirigami.PageRow).wideMode
|
||||
icon.name: "view-right-new"
|
||||
onTriggered: (root.QQC2.ApplicationWindow.window as Main).openRoomDrawer()
|
||||
}
|
||||
]
|
||||
|
||||
KeyNavigation.left: pageStack.get(0)
|
||||
KeyNavigation.left: (root.Kirigami.PageStack.pageStack as Kirigami.PageRow).get(0)
|
||||
|
||||
onCurrentRoomChanged: {
|
||||
banner.visible = false;
|
||||
if (!Kirigami.Settings.isMobile && chatBarLoader.item) {
|
||||
chatBarLoader.item.forceActiveFocus();
|
||||
(chatBarLoader.item as ChatBar).forceActiveFocus();
|
||||
}
|
||||
|
||||
if (root.currentRoom.tagNames.includes("m.server_notice")) {
|
||||
banner.text = i18nc("@info", "This room contains official messages from your homeserver.")
|
||||
banner.visible = true;
|
||||
} else {
|
||||
banner.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,10 +190,10 @@ Kirigami.Page {
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_PageUp) {
|
||||
event.accepted = true;
|
||||
timelineViewLoader.item.pageUp();
|
||||
(timelineViewLoader.item as TimelineView).pageUp();
|
||||
} else if (event.key === Qt.Key_PageDown) {
|
||||
event.accepted = true;
|
||||
timelineViewLoader.item.pageDown();
|
||||
(timelineViewLoader.item as TimelineView).pageDown();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ QQC2.ComboBox {
|
||||
}
|
||||
|
||||
customFooterActions: Kirigami.Action {
|
||||
text: i18nc("@action:button", "Ok")
|
||||
text: i18nc("@action:button", "OK")
|
||||
enabled: serverUrlField.acceptableInput && serverUrlField.isValidServer
|
||||
onTriggered: {
|
||||
serverListModel.addServer(serverUrlField.text);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.purpose as Purpose
|
||||
@@ -20,8 +21,8 @@ Kirigami.Action {
|
||||
id: root
|
||||
|
||||
icon.name: "emblem-shared-symbolic"
|
||||
text: i18n("Share")
|
||||
tooltip: i18n("Share the selected media")
|
||||
text: i18nc("@action:button", "Share")
|
||||
tooltip: i18nc("@info:tooltip", "Share the selected media")
|
||||
|
||||
/**
|
||||
* This property holds the input data for purpose.
|
||||
@@ -47,14 +48,17 @@ Kirigami.Action {
|
||||
}
|
||||
|
||||
delegate: Kirigami.Action {
|
||||
property int index
|
||||
text: model.display
|
||||
icon.name: model.iconName
|
||||
required property int index
|
||||
required property string display
|
||||
required property string iconName
|
||||
|
||||
text: display
|
||||
icon.name: iconName
|
||||
onTriggered: {
|
||||
root.room.download(root.eventId, root.inputData.urls[0]);
|
||||
root.room.fileTransferCompleted.connect(share);
|
||||
}
|
||||
function share(id) {
|
||||
function share(id: string): void {
|
||||
if (id != root.eventId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls as QQC2
|
||||
|
||||
import org.kde.purpose as Purpose
|
||||
@@ -24,7 +23,7 @@ Kirigami.Page {
|
||||
bottomPadding: 0
|
||||
|
||||
property alias index: jobView.index
|
||||
property alias model: jobView.model
|
||||
required property var model
|
||||
|
||||
QQC2.Action {
|
||||
shortcut: 'Escape'
|
||||
@@ -34,7 +33,7 @@ Kirigami.Page {
|
||||
Notification {
|
||||
id: sharingFailed
|
||||
eventId: "Share"
|
||||
text: i18n("Sharing failed")
|
||||
text: i18nc("@info:status", "Sharing failed")
|
||||
urgency: Notification.NormalUrgency
|
||||
}
|
||||
|
||||
@@ -51,11 +50,12 @@ Kirigami.Page {
|
||||
Purpose.JobView {
|
||||
id: jobView
|
||||
|
||||
model: root.model
|
||||
anchors.fill: parent
|
||||
onStateChanged: {
|
||||
if (state === Purpose.PurposeJobController.Finished) {
|
||||
if (jobView.job?.output?.url?.length > 0) {
|
||||
sharingSuccess.text = i18n("Shared url for image is <a href='%1'>%1</a>", jobView.job.output.url);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
|
||||
@@ -142,107 +142,95 @@ Kirigami.Dialog {
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
visible: root.user.id !== root.connection.localUserId && !!root.user
|
||||
action: Kirigami.Action {
|
||||
text: !!root.user && root.connection.isIgnored(root.user.id) ? i18n("Unignore this user") : i18n("Ignore this user")
|
||||
icon.name: "im-invisible-user"
|
||||
onTriggered: {
|
||||
root.close();
|
||||
root.connection.isIgnored(root.user.id) ? root.connection.removeFromIgnoredUsers(root.user.id) : root.connection.addToIgnoredUsers(root.user.id);
|
||||
}
|
||||
text: !!root.user && root.connection.isIgnored(root.user.id) ? i18n("Unignore this user") : i18n("Ignore this user")
|
||||
icon.name: "im-invisible-user"
|
||||
onClicked: {
|
||||
root.close();
|
||||
root.connection.isIgnored(root.user.id) ? root.connection.removeFromIgnoredUsers(root.user.id) : root.connection.addToIgnoredUsers(root.user.id);
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
visible: root.room && root.user.id !== root.connection.localUserId && room.canSendState("kick") && room.containsUser(root.user.id) && room.memberEffectivePowerLevel(root.user.id) < room.memberEffectivePowerLevel(root.connection.localUserId)
|
||||
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Kick this user")
|
||||
icon.name: "im-kick-user"
|
||||
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"),
|
||||
actionText: i18nc("@action:button 'Kick' as in 'Kick this user from the room'", "Kick"),
|
||||
icon: "im-kick-user"
|
||||
}, {
|
||||
title: i18nc("@title:dialog", "Kick User"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
});
|
||||
dialog.accepted.connect(reason => {
|
||||
root.room.kickMember(root.user.id, reason);
|
||||
});
|
||||
root.close();
|
||||
}
|
||||
text: i18nc("@action:button", "Kick this user")
|
||||
icon.name: "im-kick-user"
|
||||
onClicked: {
|
||||
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"),
|
||||
actionText: i18nc("@action:button 'Kick' as in 'Kick this user from the room'", "Kick"),
|
||||
icon: "im-kick-user"
|
||||
}, {
|
||||
title: i18nc("@title:dialog", "Kick User"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
});
|
||||
dialog.accepted.connect(reason => {
|
||||
root.room.kickMember(root.user.id, reason);
|
||||
});
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
visible: root.room && root.user.id !== root.connection.localUserId && room.canSendState("invite") && !room.containsUser(root.user.id)
|
||||
|
||||
action: Kirigami.Action {
|
||||
enabled: root.room && !root.room.isUserBanned(root.user.id)
|
||||
text: i18n("Invite this user")
|
||||
icon.name: "list-add-user"
|
||||
onTriggered: {
|
||||
root.room.inviteToRoom(root.user.id);
|
||||
root.close();
|
||||
}
|
||||
enabled: root.room && !root.room.isUserBanned(root.user.id)
|
||||
text: i18nc("@action:button", "Invite this user")
|
||||
icon.name: "list-add-user"
|
||||
onClicked: {
|
||||
root.room.inviteToRoom(root.user.id);
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
visible: root.room && root.user.id !== root.connection.localUserId && room.canSendState("ban") && !room.isUserBanned(root.user.id) && room.memberEffectivePowerLevel(root.user.id) < room.memberEffectivePowerLevel(root.connection.localUserId)
|
||||
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Ban this user")
|
||||
icon.name: "im-ban-user"
|
||||
icon.color: Kirigami.Theme.negativeTextColor
|
||||
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"),
|
||||
actionText: i18nc("@action:button 'Ban' as in 'Ban this user'", "Ban"),
|
||||
icon: "im-ban-user"
|
||||
}, {
|
||||
title: i18nc("@title:dialog", "Ban User"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
});
|
||||
dialog.accepted.connect(reason => {
|
||||
root.room.ban(root.user.id, reason);
|
||||
});
|
||||
root.close();
|
||||
}
|
||||
text: i18nc("@action:button", "Ban this user")
|
||||
icon.name: "im-ban-user"
|
||||
icon.color: Kirigami.Theme.negativeTextColor
|
||||
onClicked: {
|
||||
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"),
|
||||
actionText: i18nc("@action:button 'Ban' as in 'Ban this user'", "Ban"),
|
||||
icon: "im-ban-user"
|
||||
}, {
|
||||
title: i18nc("@title:dialog", "Ban User"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
});
|
||||
dialog.accepted.connect(reason => {
|
||||
root.room.ban(root.user.id, reason);
|
||||
});
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
visible: root.room && root.user.id !== root.connection.localUserId && room.canSendState("ban") && room.isUserBanned(root.user.id)
|
||||
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Unban this user")
|
||||
icon.name: "im-irc"
|
||||
icon.color: Kirigami.Theme.negativeTextColor
|
||||
onTriggered: {
|
||||
root.room.unban(root.user.id);
|
||||
root.close();
|
||||
}
|
||||
text: i18nc("@action:button", "Unban this user")
|
||||
icon.name: "im-irc"
|
||||
icon.color: Kirigami.Theme.negativeTextColor
|
||||
onClicked: {
|
||||
root.room.unban(root.user.id);
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
visible: root.room && root.room.canSendState("m.room.power_levels")
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Set user power level")
|
||||
icon.name: "visibility"
|
||||
onTriggered: {
|
||||
let dialog = powerLevelDialog.createObject(this, {
|
||||
room: root.room,
|
||||
userId: root.user.id,
|
||||
powerLevel: root.room.memberEffectivePowerLevel(root.user.id)
|
||||
});
|
||||
dialog.open();
|
||||
root.close();
|
||||
}
|
||||
text: i18nc("@action:button", "Set user power level")
|
||||
icon.name: "visibility"
|
||||
onClicked: {
|
||||
let dialog = powerLevelDialog.createObject(this, {
|
||||
room: root.room,
|
||||
userId: root.user.id,
|
||||
powerLevel: root.room.memberEffectivePowerLevel(root.user.id)
|
||||
});
|
||||
dialog.open();
|
||||
root.close();
|
||||
}
|
||||
|
||||
Component {
|
||||
@@ -256,48 +244,40 @@ Kirigami.Dialog {
|
||||
FormCard.FormButtonDelegate {
|
||||
visible: root.room && (root.user.id === root.connection.localUserId || room.canSendState("redact"))
|
||||
|
||||
action: Kirigami.Action {
|
||||
text: i18nc("@action:button", "Remove recent messages by this user")
|
||||
icon.name: "delete"
|
||||
icon.color: Kirigami.Theme.negativeTextColor
|
||||
onTriggered: {
|
||||
let dialog = pageStack.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"),
|
||||
actionText: i18nc("@action:button 'Remove' as in 'Remove these messages'", "Remove"),
|
||||
icon: "delete"
|
||||
}, {
|
||||
title: i18nc("@title", "Remove Messages"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
});
|
||||
dialog.accepted.connect(reason => {
|
||||
root.room.deleteMessagesByUser(root.user.id, reason);
|
||||
});
|
||||
root.close();
|
||||
}
|
||||
text: i18nc("@action:button", "Remove recent messages by this user")
|
||||
icon.name: "delete"
|
||||
icon.color: Kirigami.Theme.negativeTextColor
|
||||
onClicked: {
|
||||
let dialog = pageStack.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"),
|
||||
actionText: i18nc("@action:button 'Remove' as in 'Remove these messages'", "Remove"),
|
||||
icon: "delete"
|
||||
}, {
|
||||
title: i18nc("@title", "Remove Messages"),
|
||||
width: Kirigami.Units.gridUnit * 25
|
||||
});
|
||||
dialog.accepted.connect(reason => {
|
||||
root.room.deleteMessagesByUser(root.user.id, reason);
|
||||
});
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
visible: root.user.id !== root.connection.localUserId
|
||||
action: Kirigami.Action {
|
||||
text: root.connection.directChatExists(root.user) ? i18nc("%1 is the name of the user.", "Chat with %1", root.room ? root.room.member(root.user.id).htmlSafeDisplayName : QmlUtils.escapeString(root.user.displayName)) : i18n("Invite to private chat")
|
||||
icon.name: "document-send"
|
||||
onTriggered: {
|
||||
root.connection.requestDirectChat(root.user.id);
|
||||
root.close();
|
||||
}
|
||||
text: root.connection.directChatExists(root.user) ? i18nc("%1 is the name of the user.", "Chat with %1", root.room ? root.room.member(root.user.id).htmlSafeDisplayName : QmlUtils.escapeString(root.user.displayName)) : i18n("Invite to private chat")
|
||||
icon.name: "document-send"
|
||||
onClicked: {
|
||||
root.connection.requestDirectChat(root.user.id);
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
action: Kirigami.Action {
|
||||
text: i18n("Copy link")
|
||||
icon.name: "username-copy"
|
||||
onTriggered: {
|
||||
Clipboard.saveText("https://matrix.to/#/" + root.user.id);
|
||||
}
|
||||
}
|
||||
text: i18n("Copy link")
|
||||
icon.name: "username-copy"
|
||||
onClicked: Clipboard.saveText("https://matrix.to/#/" + root.user.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,16 +39,6 @@ SearchPage {
|
||||
connection: root.connection
|
||||
}
|
||||
|
||||
listHeaderDelegate: Delegates.RoundedItemDelegate {
|
||||
onClicked: _private.openManualUserDialog()
|
||||
|
||||
activeFocusOnTab: false // We handle moving to this item via up/down arrows, otherwise the tab order is wacky
|
||||
text: i18n("Enter a user ID")
|
||||
icon.name: "list-add-user"
|
||||
icon.width: Kirigami.Units.gridUnit * 2
|
||||
icon.height: Kirigami.Units.gridUnit * 2
|
||||
}
|
||||
|
||||
modelDelegate: Delegates.RoundedItemDelegate {
|
||||
id: userDelegate
|
||||
required property string userId
|
||||
@@ -92,6 +82,15 @@ SearchPage {
|
||||
noSearchPlaceholderMessage: i18n("Enter text to start searching for your friends")
|
||||
noResultPlaceholderMessage: i18nc("@info:label", "No matches found")
|
||||
|
||||
noSearchHelpfulAction: noResultHelpfulAction
|
||||
|
||||
noResultHelpfulAction: Kirigami.Action {
|
||||
icon.name: "list-add-user"
|
||||
text: i18nc("@action:button", "Enter a User ID")
|
||||
onTriggered: _private.openManualUserDialog()
|
||||
tooltip: text
|
||||
}
|
||||
|
||||
Component {
|
||||
id: manualUserDialog
|
||||
ManualUserDialog {}
|
||||
@@ -107,6 +106,9 @@ SearchPage {
|
||||
dialog.accepted.connect(() => {
|
||||
root.closeDialog();
|
||||
});
|
||||
dialog.userSelected.connect(userId => {
|
||||
root.connection.requestDirectChat(userId);
|
||||
});
|
||||
dialog.open();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,10 +54,6 @@ RoomManager::RoomManager(QObject *parent)
|
||||
}
|
||||
#endif
|
||||
|
||||
m_lastRoomConfig = m_config->group(u"LastOpenRoom"_s);
|
||||
m_lastSpaceConfig = m_config->group(u"LastOpenSpace"_s);
|
||||
m_directChatsConfig = m_config->group(u"DirectChatsActive"_s);
|
||||
|
||||
connect(this, &RoomManager::currentRoomChanged, this, [this]() {
|
||||
m_userListModel->setRoom(m_currentRoom);
|
||||
m_timelineModel->setRoom(m_currentRoom);
|
||||
@@ -245,6 +241,7 @@ void RoomManager::maximizeMedia(const QString &eventId)
|
||||
|
||||
const auto index = m_mediaMessageFilterModel->getRowForEventId(eventId);
|
||||
if (index == -1) {
|
||||
qWarning() << "Tried to open media for unknown event id" << eventId;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -320,7 +317,7 @@ void RoomManager::loadInitialRoom()
|
||||
}
|
||||
|
||||
if (m_isMobile) {
|
||||
QString lastSpace = m_lastSpaceConfig.readEntry(m_connection->userId(), QString());
|
||||
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();
|
||||
@@ -347,7 +344,11 @@ void RoomManager::openRoomForActiveConnection()
|
||||
setCurrentSpace({}, false);
|
||||
return;
|
||||
}
|
||||
setCurrentSpace(m_lastSpaceConfig.readEntry(m_connection->userId(), QString()), true);
|
||||
auto lastSpace = m_lastRoomConfig.readEntry(u"lastSpace"_s, QString());
|
||||
if (lastSpace == u"Home"_s) {
|
||||
lastSpace.clear();
|
||||
}
|
||||
setCurrentSpace(lastSpace, true);
|
||||
}
|
||||
|
||||
UriResolveResult RoomManager::visitUser(User *user, const QString &action)
|
||||
@@ -485,6 +486,8 @@ void RoomManager::setConnection(NeoChatConnection *connection)
|
||||
|
||||
m_connection = connection;
|
||||
|
||||
m_lastRoomConfig = m_config->group(m_connection->userId()).group(u"LastOpenRoom"_s);
|
||||
|
||||
if (m_connection != nullptr) {
|
||||
connect(m_connection, &NeoChatConnection::showMessage, this, &RoomManager::showMessage);
|
||||
connect(m_connection, &NeoChatConnection::createdRoom, this, [this](Quotient::Room *room) {
|
||||
@@ -513,11 +516,7 @@ void RoomManager::setCurrentSpace(const QString &spaceId, bool setRoom)
|
||||
|
||||
Q_EMIT currentSpaceChanged();
|
||||
if (m_connection) {
|
||||
if (spaceId.isEmpty()) {
|
||||
m_lastSpaceConfig.writeEntry(m_connection->userId(), u"Home"_s);
|
||||
} else {
|
||||
m_lastSpaceConfig.writeEntry(m_connection->userId(), spaceId);
|
||||
}
|
||||
m_lastRoomConfig.writeEntry(u"lastSpace"_s, spaceId.isEmpty() ? u"Home"_s : spaceId);
|
||||
}
|
||||
|
||||
if (!setRoom) {
|
||||
@@ -525,22 +524,20 @@ void RoomManager::setCurrentSpace(const QString &spaceId, bool setRoom)
|
||||
}
|
||||
|
||||
// We intentionally don't want to open the last room on mobile
|
||||
if (!m_isMobile) {
|
||||
QString configSpaceId = spaceId;
|
||||
// We can't have empty keys in KConfig, so it's stored as "Home"
|
||||
if (spaceId.isEmpty()) {
|
||||
configSpaceId = u"Home"_s;
|
||||
}
|
||||
|
||||
const auto &lastRoom = m_lastRoomConfig.readEntry(configSpaceId, QString());
|
||||
if (lastRoom.isEmpty()) {
|
||||
if (spaceId != u"DM"_s && spaceId != u"Home"_s) {
|
||||
resolveResource(spaceId, "no_join"_L1);
|
||||
}
|
||||
} else {
|
||||
resolveResource(lastRoom, "no_join"_L1);
|
||||
}
|
||||
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;
|
||||
}
|
||||
if (!spaceId.isEmpty() && spaceId != u"DM"_s) {
|
||||
resolveResource(spaceId, "no_join"_L1);
|
||||
return;
|
||||
}
|
||||
setCurrentRoom({});
|
||||
}
|
||||
|
||||
void RoomManager::setCurrentRoom(const QString &roomId)
|
||||
|
||||
@@ -353,8 +353,6 @@ private:
|
||||
QString m_arg;
|
||||
KSharedConfig::Ptr m_config;
|
||||
KConfigGroup m_lastRoomConfig;
|
||||
KConfigGroup m_lastSpaceConfig;
|
||||
KConfigGroup m_directChatsConfig;
|
||||
|
||||
RoomListModel *m_roomListModel;
|
||||
SortFilterRoomListModel *m_sortFilterRoomListModel;
|
||||
|
||||
@@ -26,11 +26,11 @@ QQC2.Popup {
|
||||
|
||||
icon.name: 'mail-attachment'
|
||||
|
||||
text: i18n("Choose local file")
|
||||
text: i18nc("@action:button", "Choose local file")
|
||||
|
||||
onClicked: {
|
||||
root.close();
|
||||
var fileDialog = openFileDialog.createObject(QQC2.Overlay.overlay);
|
||||
var fileDialog = openFileDialog.createObject(QQC2.Overlay.overlay) as OpenFileDialog;
|
||||
fileDialog.chosen.connect(path => root.chosen(path));
|
||||
fileDialog.open();
|
||||
}
|
||||
@@ -42,7 +42,7 @@ QQC2.Popup {
|
||||
Layout.fillHeight: true
|
||||
|
||||
icon.name: 'insert-image'
|
||||
text: i18n("Clipboard image")
|
||||
text: i18nc("@action:button", "Clipboard image")
|
||||
onClicked: {
|
||||
const path = StandardPaths.standardLocations(StandardPaths.CacheLocation)[0] + "/screenshots/" + (new Date()).getTime() + ".png";
|
||||
if (!Clipboard.saveImage(path)) {
|
||||
|
||||
@@ -56,7 +56,7 @@ QQC2.Control {
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: currentRoom.mainCache
|
||||
target: root.currentRoom.mainCache
|
||||
|
||||
function onMentionAdded(mention: string): void {
|
||||
// add mention text
|
||||
@@ -74,16 +74,16 @@ QQC2.Control {
|
||||
* Each of these will be visualised in the ChatBar so new actions can be added
|
||||
* by appending to this list.
|
||||
*/
|
||||
property list<Kirigami.Action> actions: [
|
||||
Kirigami.Action {
|
||||
property list<BusyAction> actions: [
|
||||
BusyAction {
|
||||
id: attachmentAction
|
||||
|
||||
property bool isBusy: root.currentRoom && root.currentRoom.hasFileUploading
|
||||
isBusy: root.currentRoom && root.currentRoom.hasFileUploading
|
||||
|
||||
// Matrix does not allow sending attachments in replies
|
||||
visible: _private.chatBarCache.replyId.length === 0 && _private.chatBarCache.attachmentPath.length === 0
|
||||
icon.name: "mail-attachment"
|
||||
text: i18n("Attach an image or file")
|
||||
text: i18nc("@action:button", "Attach an image or file")
|
||||
displayHint: Kirigami.DisplayHint.IconOnly
|
||||
|
||||
onTriggered: {
|
||||
@@ -94,14 +94,14 @@ QQC2.Control {
|
||||
|
||||
tooltip: text
|
||||
},
|
||||
Kirigami.Action {
|
||||
BusyAction {
|
||||
id: emojiAction
|
||||
|
||||
property bool isBusy: false
|
||||
isBusy: false
|
||||
|
||||
visible: !Kirigami.Settings.isMobile
|
||||
icon.name: "smiley"
|
||||
text: i18n("Emojis & Stickers")
|
||||
text: i18nc("@action:button", "Emojis & Stickers")
|
||||
displayHint: Kirigami.DisplayHint.IconOnly
|
||||
checkable: true
|
||||
|
||||
@@ -114,11 +114,11 @@ QQC2.Control {
|
||||
}
|
||||
tooltip: text
|
||||
},
|
||||
Kirigami.Action {
|
||||
BusyAction {
|
||||
id: mapButton
|
||||
icon.name: "mark-location-symbolic"
|
||||
property bool isBusy: false
|
||||
text: i18n("Send a Location")
|
||||
isBusy: false
|
||||
text: i18nc("@action:button", "Send a Location")
|
||||
displayHint: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onTriggered: {
|
||||
@@ -128,10 +128,10 @@ QQC2.Control {
|
||||
}
|
||||
tooltip: text
|
||||
},
|
||||
Kirigami.Action {
|
||||
BusyAction {
|
||||
id: pollButton
|
||||
icon.name: "amarok_playcount"
|
||||
property bool isBusy: false
|
||||
isBusy: false
|
||||
text: i18nc("@action:button", "Create a Poll")
|
||||
displayHint: QQC2.AbstractButton.IconOnly
|
||||
|
||||
@@ -142,13 +142,13 @@ QQC2.Control {
|
||||
}
|
||||
tooltip: text
|
||||
},
|
||||
Kirigami.Action {
|
||||
BusyAction {
|
||||
id: sendAction
|
||||
|
||||
property bool isBusy: false
|
||||
isBusy: false
|
||||
|
||||
icon.name: "document-send"
|
||||
text: i18n("Send message")
|
||||
text: i18nc("@action:button", "Send message")
|
||||
displayHint: Kirigami.DisplayHint.IconOnly
|
||||
checkable: true
|
||||
|
||||
@@ -191,7 +191,7 @@ QQC2.Control {
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Kirigami.Units.largeSpacing
|
||||
Layout.preferredHeight: active ? item.implicitHeight : 0
|
||||
Layout.preferredHeight: active ? (item as Item).implicitHeight : 0
|
||||
|
||||
active: visible
|
||||
visible: root.currentRoom.mainCache.replyId.length > 0
|
||||
@@ -218,7 +218,7 @@ QQC2.Control {
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Kirigami.Units.largeSpacing
|
||||
Layout.preferredHeight: active ? item.implicitHeight : 0
|
||||
Layout.preferredHeight: active ? (item as Item).implicitHeight : 0
|
||||
|
||||
active: visible
|
||||
visible: root.currentRoom.mainCache.attachmentPath.length > 0
|
||||
@@ -250,9 +250,10 @@ QQC2.Control {
|
||||
QQC2.TextArea {
|
||||
id: textField
|
||||
|
||||
placeholderText: root.currentRoom.usesEncryption ? i18n("Send an encrypted message…") : root.currentRoom.mainCache.attachmentPath.length > 0 ? i18n("Set an attachment caption…") : i18n("Send a message…")
|
||||
placeholderText: root.currentRoom.usesEncryption ? i18nc("@placeholder", "Send an encrypted message…") : root.currentRoom.mainCache.attachmentPath.length > 0 ? i18nc("@placeholder", "Set an attachment caption…") : i18nc("@placeholder", "Send a message…")
|
||||
verticalAlignment: TextEdit.AlignVCenter
|
||||
wrapMode: TextEdit.Wrap
|
||||
textFormat: TextEdit.MarkdownText
|
||||
|
||||
Accessible.description: placeholderText
|
||||
|
||||
@@ -269,7 +270,6 @@ QQC2.Control {
|
||||
root.currentRoom.sendTypingNotification(textExists);
|
||||
textExists ? repeatTimer.start() : repeatTimer.stop();
|
||||
}
|
||||
_private.chatBarCache.text = text;
|
||||
}
|
||||
onSelectedTextChanged: {
|
||||
if (selectedText.length > 0) {
|
||||
@@ -285,7 +285,7 @@ QQC2.Control {
|
||||
x: textField.cursorRectangle.x
|
||||
y: textField.cursorRectangle.y - height
|
||||
|
||||
onFormattingSelected: _private.formatText(format, selectionStart, selectionEnd)
|
||||
onFormattingSelected: (format, selectionStart, selectionEnd) => _private.formatText(format, selectionStart, selectionEnd)
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: event => {
|
||||
@@ -363,6 +363,8 @@ QQC2.Control {
|
||||
Repeater {
|
||||
model: root.actions
|
||||
delegate: QQC2.ToolButton {
|
||||
id: actionDelegate
|
||||
required property BusyAction modelData
|
||||
icon.name: modelData.isBusy ? "" : (modelData.icon.name.length > 0 ? modelData.icon.name : modelData.icon.source)
|
||||
onClicked: modelData.trigger()
|
||||
|
||||
@@ -373,7 +375,7 @@ QQC2.Control {
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
|
||||
contentItem: PieProgressBar {
|
||||
visible: modelData.isBusy
|
||||
visible: actionDelegate.modelData.isBusy
|
||||
progress: root.currentRoom.fileUploadingProgress
|
||||
}
|
||||
}
|
||||
@@ -500,13 +502,8 @@ QQC2.Control {
|
||||
ChatDocumentHandler {
|
||||
id: documentHandler
|
||||
type: ChatBarType.Room
|
||||
textItem: textField
|
||||
room: root.currentRoom
|
||||
document: textField.textDocument
|
||||
cursorPosition: textField.cursorPosition
|
||||
selectionStart: textField.selectionStart
|
||||
selectionEnd: textField.selectionEnd
|
||||
mentionColor: Kirigami.Theme.linkColor
|
||||
errorColor: Kirigami.Theme.negativeTextColor
|
||||
}
|
||||
|
||||
Component {
|
||||
@@ -565,7 +562,7 @@ QQC2.Control {
|
||||
|
||||
currentRoom: root.currentRoom
|
||||
|
||||
onChosen: emoji => insertText(emoji)
|
||||
onChosen: emoji => root.insertText(emoji)
|
||||
onClosed: if (emojiAction.checked) {
|
||||
emojiAction.checked = false;
|
||||
}
|
||||
@@ -576,4 +573,8 @@ QQC2.Control {
|
||||
textField.text = textField.text.substr(0, initialCursorPosition) + text + textField.text.substr(initialCursorPosition);
|
||||
textField.cursorPosition = initialCursorPosition + text.length;
|
||||
}
|
||||
|
||||
component BusyAction : Kirigami.Action {
|
||||
required property bool isBusy
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ target_sources(LibNeoChat PRIVATE
|
||||
models/userlistmodel.cpp
|
||||
)
|
||||
|
||||
if (TARGET KF6::KIOWidgets)
|
||||
target_compile_definitions(LibNeoChat PUBLIC -DHAVE_KIO)
|
||||
endif()
|
||||
|
||||
ecm_add_qml_module(LibNeoChat GENERATE_PLUGIN_SOURCE
|
||||
URI org.kde.neochat.libneochat
|
||||
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/libneochat
|
||||
@@ -58,6 +62,8 @@ ecm_add_qml_module(LibNeoChat GENERATE_PLUGIN_SOURCE
|
||||
qml/SearchPage.qml
|
||||
qml/CreateRoomDialog.qml
|
||||
qml/CreateSpaceDialog.qml
|
||||
DEPENDENCIES
|
||||
io.github.quotient_im.libquotient
|
||||
)
|
||||
|
||||
ecm_qt_declare_logging_category(LibNeoChat
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org>
|
||||
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "chatdocumenthandler.h"
|
||||
|
||||
#include <QQmlFile>
|
||||
#include <QQmlFileSelector>
|
||||
#include <QQuickTextDocument>
|
||||
#include <QStringBuilder>
|
||||
#include <QSyntaxHighlighter>
|
||||
#include <QTextBlock>
|
||||
#include <QTextDocument>
|
||||
#include <QTimer>
|
||||
|
||||
#include <Kirigami/Platform/PlatformTheme>
|
||||
#include <Sonnet/BackgroundChecker>
|
||||
#include <Sonnet/Settings>
|
||||
|
||||
@@ -33,10 +36,16 @@ public:
|
||||
SyntaxHighlighter(QObject *parent)
|
||||
: QSyntaxHighlighter(parent)
|
||||
{
|
||||
mentionFormat.setFontWeight(QFont::Bold);
|
||||
mentionFormat.setForeground(Qt::blue);
|
||||
m_theme = static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
|
||||
connect(m_theme, &Kirigami::Platform::PlatformTheme::colorsChanged, this, [this]() {
|
||||
mentionFormat.setForeground(m_theme->linkColor());
|
||||
errorFormat.setForeground(m_theme->negativeTextColor());
|
||||
});
|
||||
|
||||
errorFormat.setForeground(Qt::red);
|
||||
mentionFormat.setFontWeight(QFont::Bold);
|
||||
mentionFormat.setForeground(m_theme->linkColor());
|
||||
|
||||
errorFormat.setForeground(m_theme->negativeTextColor());
|
||||
errorFormat.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
|
||||
|
||||
connect(checker, &Sonnet::BackgroundChecker::misspelling, this, [this](const QString &word, int start) {
|
||||
@@ -101,29 +110,22 @@ public:
|
||||
}),
|
||||
mentions->end());
|
||||
}
|
||||
|
||||
private:
|
||||
Kirigami::Platform::PlatformTheme *m_theme = nullptr;
|
||||
};
|
||||
|
||||
ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_document(nullptr)
|
||||
, m_cursorPosition(-1)
|
||||
, m_highlighter(new SyntaxHighlighter(this))
|
||||
, m_completionModel(new CompletionModel(this))
|
||||
{
|
||||
connect(this, &ChatDocumentHandler::documentChanged, this, [this]() {
|
||||
if (!m_document) {
|
||||
m_highlighter->setDocument(nullptr);
|
||||
return;
|
||||
}
|
||||
m_highlighter->setDocument(m_document->textDocument());
|
||||
});
|
||||
connect(this, &ChatDocumentHandler::cursorPositionChanged, this, [this]() {
|
||||
if (!m_room) {
|
||||
return;
|
||||
}
|
||||
int start = completionStartIndex();
|
||||
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
|
||||
});
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::updateCompletion() const
|
||||
{
|
||||
int start = completionStartIndex();
|
||||
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
|
||||
}
|
||||
|
||||
int ChatDocumentHandler::completionStartIndex() const
|
||||
@@ -160,38 +162,58 @@ void ChatDocumentHandler::setType(ChatBarType::Type type)
|
||||
Q_EMIT typeChanged();
|
||||
}
|
||||
|
||||
QQuickTextDocument *ChatDocumentHandler::document() const
|
||||
QQuickItem *ChatDocumentHandler::textItem() const
|
||||
{
|
||||
return m_document;
|
||||
return m_textItem;
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::setDocument(QQuickTextDocument *document)
|
||||
void ChatDocumentHandler::setTextItem(QQuickItem *textItem)
|
||||
{
|
||||
if (document == m_document) {
|
||||
if (textItem == m_textItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_document) {
|
||||
m_document->textDocument()->disconnect(this);
|
||||
if (m_textItem) {
|
||||
m_textItem->disconnect(this);
|
||||
if (const auto textDoc = document()) {
|
||||
textDoc->disconnect(this);
|
||||
}
|
||||
}
|
||||
m_document = document;
|
||||
Q_EMIT documentChanged();
|
||||
|
||||
m_textItem = textItem;
|
||||
|
||||
m_highlighter->setDocument(document());
|
||||
if (m_textItem) {
|
||||
connect(m_textItem, SIGNAL(cursorPositionChanged()), this, SLOT(updateCompletion()));
|
||||
if (document()) {
|
||||
connect(document(), &QTextDocument::contentsChanged, this, [this]() {
|
||||
if (m_room) {
|
||||
m_room->cacheForType(m_type)->setText(getText());
|
||||
int start = completionStartIndex();
|
||||
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Q_EMIT textItemChanged();
|
||||
}
|
||||
|
||||
QTextDocument *ChatDocumentHandler::document() const
|
||||
{
|
||||
if (!m_textItem) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto quickDocument = qvariant_cast<QQuickTextDocument *>(m_textItem->property("textDocument"));
|
||||
return quickDocument ? quickDocument->textDocument() : nullptr;
|
||||
}
|
||||
|
||||
int ChatDocumentHandler::cursorPosition() const
|
||||
{
|
||||
return m_cursorPosition;
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::setCursorPosition(int position)
|
||||
{
|
||||
if (position == m_cursorPosition) {
|
||||
return;
|
||||
if (!m_textItem) {
|
||||
return -1;
|
||||
}
|
||||
if (m_room) {
|
||||
m_cursorPosition = position;
|
||||
}
|
||||
Q_EMIT cursorPositionChanged();
|
||||
return m_textItem->property("cursorPosition").toInt();
|
||||
}
|
||||
|
||||
NeoChatRoom *ChatDocumentHandler::room() const
|
||||
@@ -207,8 +229,8 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room)
|
||||
|
||||
if (m_room && m_type != ChatBarType::None) {
|
||||
m_room->cacheForType(m_type)->disconnect(this);
|
||||
if (!m_room->isSpace() && m_document && m_type == ChatBarType::Room) {
|
||||
m_room->mainCache()->setSavedText(document()->textDocument()->toPlainText());
|
||||
if (!m_room->isSpace() && document() && m_type == ChatBarType::Room) {
|
||||
m_room->mainCache()->setSavedText(document()->toPlainText());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,8 +242,8 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room)
|
||||
int start = completionStartIndex();
|
||||
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
|
||||
});
|
||||
if (!m_room->isSpace() && m_document && m_type == ChatBarType::Room) {
|
||||
document()->textDocument()->setPlainText(room->mainCache()->savedText());
|
||||
if (!m_room->isSpace() && document() && m_type == ChatBarType::Room) {
|
||||
document()->setPlainText(room->mainCache()->savedText());
|
||||
m_room->mainCache()->setText(room->mainCache()->savedText());
|
||||
}
|
||||
}
|
||||
@@ -239,7 +261,7 @@ ChatBarCache *ChatDocumentHandler::chatBarCache() const
|
||||
|
||||
void ChatDocumentHandler::complete(int index)
|
||||
{
|
||||
if (m_document == nullptr) {
|
||||
if (document() == nullptr) {
|
||||
qCWarning(ChatDocumentHandling) << "complete called with m_document set to nullptr.";
|
||||
return;
|
||||
}
|
||||
@@ -256,7 +278,7 @@ void ChatDocumentHandler::complete(int index)
|
||||
auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
|
||||
auto text = getText();
|
||||
auto at = text.indexOf(QLatin1Char('@'), fromIndex);
|
||||
QTextCursor cursor(document()->textDocument());
|
||||
QTextCursor cursor(document());
|
||||
cursor.setPosition(at);
|
||||
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
|
||||
cursor.insertText(name + u" "_s);
|
||||
@@ -269,7 +291,7 @@ void ChatDocumentHandler::complete(int index)
|
||||
auto command = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedTextRole).toString();
|
||||
auto text = getText();
|
||||
auto at = text.indexOf(QLatin1Char('/'), fromIndex);
|
||||
QTextCursor cursor(document()->textDocument());
|
||||
QTextCursor cursor(document());
|
||||
cursor.setPosition(at);
|
||||
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
|
||||
cursor.insertText(u"/%1 "_s.arg(command));
|
||||
@@ -277,7 +299,7 @@ void ChatDocumentHandler::complete(int index)
|
||||
auto alias = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
|
||||
auto text = getText();
|
||||
auto at = text.indexOf(QLatin1Char('#'), fromIndex);
|
||||
QTextCursor cursor(document()->textDocument());
|
||||
QTextCursor cursor(document());
|
||||
cursor.setPosition(at);
|
||||
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
|
||||
cursor.insertText(alias + u" "_s);
|
||||
@@ -290,7 +312,7 @@ void ChatDocumentHandler::complete(int index)
|
||||
auto shortcode = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedTextRole).toString();
|
||||
auto text = getText();
|
||||
auto at = text.indexOf(QLatin1Char(':'), fromIndex);
|
||||
QTextCursor cursor(document()->textDocument());
|
||||
QTextCursor cursor(document());
|
||||
cursor.setPosition(at);
|
||||
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
|
||||
cursor.insertText(shortcode);
|
||||
@@ -302,43 +324,13 @@ CompletionModel *ChatDocumentHandler::completionModel() const
|
||||
return m_completionModel;
|
||||
}
|
||||
|
||||
int ChatDocumentHandler::selectionStart() const
|
||||
{
|
||||
return m_selectionStart;
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::setSelectionStart(int position)
|
||||
{
|
||||
if (position == m_selectionStart) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_selectionStart = position;
|
||||
Q_EMIT selectionStartChanged();
|
||||
}
|
||||
|
||||
int ChatDocumentHandler::selectionEnd() const
|
||||
{
|
||||
return m_selectionEnd;
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::setSelectionEnd(int position)
|
||||
{
|
||||
if (position == m_selectionEnd) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_selectionEnd = position;
|
||||
Q_EMIT selectionEndChanged();
|
||||
}
|
||||
|
||||
QString ChatDocumentHandler::getText() const
|
||||
{
|
||||
if (!m_room || m_type == ChatBarType::None) {
|
||||
qCWarning(ChatDocumentHandling) << "getText called with no ChatBarCache available. ChatBarType: " << m_type << " Room: " << m_room;
|
||||
if (!document()) {
|
||||
qCWarning(ChatDocumentHandling) << "getText called with no QQuickTextDocument available.";
|
||||
return {};
|
||||
}
|
||||
return m_room->cacheForType(m_type)->text();
|
||||
return document()->toRawText();
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::pushMention(const Mention mention) const
|
||||
@@ -350,42 +342,8 @@ void ChatDocumentHandler::pushMention(const Mention mention) const
|
||||
m_room->cacheForType(m_type)->mentions()->push_back(mention);
|
||||
}
|
||||
|
||||
QColor ChatDocumentHandler::mentionColor() const
|
||||
void ChatDocumentHandler::updateMentions(const QString &editId)
|
||||
{
|
||||
return m_mentionColor;
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::setMentionColor(const QColor &color)
|
||||
{
|
||||
if (m_mentionColor == color) {
|
||||
return;
|
||||
}
|
||||
m_mentionColor = color;
|
||||
m_highlighter->mentionFormat.setForeground(m_mentionColor);
|
||||
m_highlighter->rehighlight();
|
||||
Q_EMIT mentionColorChanged();
|
||||
}
|
||||
|
||||
QColor ChatDocumentHandler::errorColor() const
|
||||
{
|
||||
return m_errorColor;
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::setErrorColor(const QColor &color)
|
||||
{
|
||||
if (m_errorColor == color) {
|
||||
return;
|
||||
}
|
||||
m_errorColor = color;
|
||||
m_highlighter->errorFormat.setForeground(m_errorColor);
|
||||
m_highlighter->rehighlight();
|
||||
Q_EMIT errorColorChanged();
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::updateMentions(QQuickTextDocument *document, const QString &editId)
|
||||
{
|
||||
setDocument(document);
|
||||
|
||||
if (editId.isEmpty() || m_type == ChatBarType::None || !m_room) {
|
||||
return;
|
||||
}
|
||||
@@ -409,7 +367,7 @@ void ChatDocumentHandler::updateMentions(QQuickTextDocument *document, const QSt
|
||||
const int end = position + name.length();
|
||||
linkSize += match.capturedLength(0) - name.length();
|
||||
|
||||
QTextCursor cursor(this->document()->textDocument());
|
||||
QTextCursor cursor(document());
|
||||
cursor.setPosition(position);
|
||||
cursor.setPosition(end, QTextCursor::KeepAnchor);
|
||||
cursor.setKeepPositionOnInsert(true);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
#include <QQuickTextDocument>
|
||||
#include <QTextCursor>
|
||||
|
||||
#include "chatbarcache.h"
|
||||
@@ -13,6 +13,8 @@
|
||||
#include "models/completionmodel.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
class QTextDocument;
|
||||
|
||||
class NeoChatRoom;
|
||||
class SyntaxHighlighter;
|
||||
|
||||
@@ -69,24 +71,9 @@ class ChatDocumentHandler : public QObject
|
||||
Q_PROPERTY(ChatBarType::Type type READ type WRITE setType NOTIFY typeChanged)
|
||||
|
||||
/**
|
||||
* @brief The QQuickTextDocument that is being handled.
|
||||
* @brief The QML text Item the ChatDocumentHandler is handling.
|
||||
*/
|
||||
Q_PROPERTY(QQuickTextDocument *document READ document WRITE setDocument NOTIFY documentChanged)
|
||||
|
||||
/**
|
||||
* @brief The current saved cursor position.
|
||||
*/
|
||||
Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged)
|
||||
|
||||
/**
|
||||
* @brief The start position of any currently selected text.
|
||||
*/
|
||||
Q_PROPERTY(int selectionStart READ selectionStart WRITE setSelectionStart NOTIFY selectionStartChanged)
|
||||
|
||||
/**
|
||||
* @brief The end position of any currently selected text.
|
||||
*/
|
||||
Q_PROPERTY(int selectionEnd READ selectionEnd WRITE setSelectionEnd NOTIFY selectionEndChanged)
|
||||
Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged)
|
||||
|
||||
/**
|
||||
* @brief The current CompletionModel.
|
||||
@@ -101,33 +88,14 @@ class ChatDocumentHandler : public QObject
|
||||
*/
|
||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||
|
||||
/**
|
||||
* @brief The color to highlight user mentions.
|
||||
*/
|
||||
Q_PROPERTY(QColor mentionColor READ mentionColor WRITE setMentionColor NOTIFY mentionColorChanged)
|
||||
|
||||
/**
|
||||
* @brief The color to highlight spelling errors.
|
||||
*/
|
||||
Q_PROPERTY(QColor errorColor READ errorColor WRITE setErrorColor NOTIFY errorColorChanged)
|
||||
|
||||
public:
|
||||
explicit ChatDocumentHandler(QObject *parent = nullptr);
|
||||
|
||||
ChatBarType::Type type() const;
|
||||
void setType(ChatBarType::Type type);
|
||||
|
||||
[[nodiscard]] QQuickTextDocument *document() const;
|
||||
void setDocument(QQuickTextDocument *document);
|
||||
|
||||
[[nodiscard]] int cursorPosition() const;
|
||||
void setCursorPosition(int position);
|
||||
|
||||
[[nodiscard]] int selectionStart() const;
|
||||
void setSelectionStart(int position);
|
||||
|
||||
[[nodiscard]] int selectionEnd() const;
|
||||
void setSelectionEnd(int position);
|
||||
QQuickItem *textItem() const;
|
||||
void setTextItem(QQuickItem *textItem);
|
||||
|
||||
[[nodiscard]] NeoChatRoom *room() const;
|
||||
void setRoom(NeoChatRoom *room);
|
||||
@@ -138,41 +106,27 @@ public:
|
||||
|
||||
CompletionModel *completionModel() const;
|
||||
|
||||
[[nodiscard]] QColor mentionColor() const;
|
||||
void setMentionColor(const QColor &color);
|
||||
|
||||
[[nodiscard]] QColor errorColor() const;
|
||||
void setErrorColor(const QColor &color);
|
||||
|
||||
/**
|
||||
* @brief Update the mentions in @p document when editing a message.
|
||||
*/
|
||||
Q_INVOKABLE void updateMentions(QQuickTextDocument *document, const QString &editId);
|
||||
Q_INVOKABLE void updateMentions(const QString &editId);
|
||||
|
||||
Q_SIGNALS:
|
||||
void typeChanged();
|
||||
void documentChanged();
|
||||
void cursorPositionChanged();
|
||||
void textItemChanged();
|
||||
void roomChanged();
|
||||
void selectionStartChanged();
|
||||
void selectionEndChanged();
|
||||
void errorColorChanged();
|
||||
void mentionColorChanged();
|
||||
|
||||
private:
|
||||
int completionStartIndex() const;
|
||||
|
||||
ChatBarType::Type m_type = ChatBarType::None;
|
||||
QPointer<QQuickTextDocument> m_document;
|
||||
QPointer<QQuickItem> m_textItem;
|
||||
QTextDocument *document() const;
|
||||
|
||||
void updateCompletion() const;
|
||||
int completionStartIndex() const;
|
||||
|
||||
QPointer<NeoChatRoom> m_room;
|
||||
|
||||
QColor m_mentionColor;
|
||||
QColor m_errorColor;
|
||||
|
||||
int m_cursorPosition;
|
||||
int m_selectionStart;
|
||||
int m_selectionEnd;
|
||||
int cursorPosition() const;
|
||||
|
||||
QString getText() const;
|
||||
void pushMention(const Mention mention) const;
|
||||
|
||||
@@ -27,6 +27,7 @@ public:
|
||||
Favorite, /**< The room is set as a favourite. */
|
||||
Direct, /**< The room is a direct chat. */
|
||||
Normal, /**< The default category for a joined room. */
|
||||
ServerNotice, /**< Official messages from the server. */
|
||||
Deprioritized, /**< The room is set as low priority. */
|
||||
Space, /**< The room is a space. */
|
||||
AddDirect, /**< So we can show the add friend delegate. */
|
||||
@@ -36,6 +37,9 @@ public:
|
||||
|
||||
static NeoChatRoomType::Types typeForRoom(const NeoChatRoom *room)
|
||||
{
|
||||
if (room->isServerNoticeRoom()) {
|
||||
return NeoChatRoomType::ServerNotice;
|
||||
}
|
||||
if (room->isSpace()) {
|
||||
return NeoChatRoomType::Space;
|
||||
}
|
||||
@@ -69,6 +73,8 @@ public:
|
||||
return i18n("Low priority");
|
||||
case NeoChatRoomType::Space:
|
||||
return i18n("Spaces");
|
||||
case NeoChatRoomType::ServerNotice:
|
||||
return i18nc("@info Meaning: official information messages from your server", "Server Notices");
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -1269,7 +1269,7 @@ void NeoChatRoom::openEventMediaExternally(const QString &eventId)
|
||||
return;
|
||||
}
|
||||
downloadFile(eventId,
|
||||
QUrl(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u'/'
|
||||
QUrl(u"file:"_s + QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u'/'
|
||||
+ evtIt->event()->id().replace(u':', u'_').replace(u'/', u'_').replace(u'+', u'_') + fileNameToDownload(eventId)));
|
||||
connect(
|
||||
this,
|
||||
@@ -1290,33 +1290,36 @@ void NeoChatRoom::openEventMediaExternally(const QString &eventId)
|
||||
void NeoChatRoom::copyEventMedia(const QString &eventId)
|
||||
{
|
||||
const auto evtIt = findInTimeline(eventId);
|
||||
if (evtIt != messageEvents().rend() && is<RoomMessageEvent>(**evtIt)) {
|
||||
const auto event = evtIt->viewAs<RoomMessageEvent>();
|
||||
if (event->has<EventContent::FileContent>()) {
|
||||
const auto transferInfo = fileTransferInfo(eventId);
|
||||
if (transferInfo.completed()) {
|
||||
Clipboard clipboard;
|
||||
clipboard.setImage(transferInfo.localPath);
|
||||
} else {
|
||||
downloadFile(eventId,
|
||||
QUrl(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u'/'
|
||||
+ event->id().replace(u':', u'_').replace(u'/', u'_').replace(u'+', u'_') + fileNameToDownload(eventId)));
|
||||
connect(
|
||||
this,
|
||||
&Room::fileTransferCompleted,
|
||||
this,
|
||||
[this, eventId](QString id, QUrl localFile, FileSourceInfo fileMetadata) {
|
||||
Q_UNUSED(localFile);
|
||||
Q_UNUSED(fileMetadata);
|
||||
if (id == eventId) {
|
||||
auto transferInfo = fileTransferInfo(eventId);
|
||||
Clipboard clipboard;
|
||||
clipboard.setImage(transferInfo.localPath);
|
||||
}
|
||||
},
|
||||
static_cast<Qt::ConnectionType>(Qt::SingleShotConnection));
|
||||
}
|
||||
}
|
||||
|
||||
if (evtIt == messageEvents().rend() || !is<RoomMessageEvent>(**evtIt)) {
|
||||
return;
|
||||
}
|
||||
const auto event = evtIt->viewAs<RoomMessageEvent>();
|
||||
if (!event->has<EventContent::FileContentBase>()) {
|
||||
return;
|
||||
}
|
||||
const auto transferInfo = fileTransferInfo(eventId);
|
||||
if (transferInfo.completed()) {
|
||||
Clipboard clipboard;
|
||||
clipboard.setImage(transferInfo.localPath);
|
||||
} else {
|
||||
downloadFile(eventId,
|
||||
QUrl(u"file:"_s + QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + u'/'
|
||||
+ event->id().replace(u':', u'_').replace(u'/', u'_').replace(u'+', u'_') + fileNameToDownload(eventId)));
|
||||
connect(
|
||||
this,
|
||||
&Room::fileTransferCompleted,
|
||||
this,
|
||||
[this, eventId](QString id, QUrl localFile, FileSourceInfo fileMetadata) {
|
||||
Q_UNUSED(localFile);
|
||||
Q_UNUSED(fileMetadata);
|
||||
if (id == eventId) {
|
||||
auto transferInfo = fileTransferInfo(eventId);
|
||||
Clipboard clipboard;
|
||||
clipboard.setImage(transferInfo.localPath);
|
||||
}
|
||||
},
|
||||
Qt::SingleShotConnection);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
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.kirigamiaddons.labs.components as Components
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
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.kirigamiaddons.labs.components as Components
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ SearchPage {
|
||||
searchFieldPlaceholder: i18nc("@info:placeholder", "Find a user…")
|
||||
noResultPlaceholderMessage: i18nc("@info:placeholder", "No users found")
|
||||
|
||||
noSearchPlaceholderMessage: i18nc("@placeholder", "Enter text to start searching for users")
|
||||
|
||||
headerTrailing: QQC2.Button {
|
||||
icon.name: "list-add"
|
||||
display: QQC2.Button.IconOnly
|
||||
@@ -37,6 +39,15 @@ SearchPage {
|
||||
onClicked: root.room.inviteToRoom(root.model.searchText);
|
||||
}
|
||||
|
||||
noSearchHelpfulAction: noResultHelpfulAction
|
||||
|
||||
noResultHelpfulAction: Kirigami.Action {
|
||||
icon.name: "list-add-user"
|
||||
text: i18nc("@action:button", "Enter a User ID")
|
||||
onTriggered: _private.openManualUserDialog()
|
||||
tooltip: text
|
||||
}
|
||||
|
||||
model: UserDirectoryListModel {
|
||||
id: userDictListModel
|
||||
|
||||
@@ -87,4 +98,26 @@ SearchPage {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: manualUserDialog
|
||||
ManualUserDialog {}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: _private
|
||||
function openManualUserDialog(): void {
|
||||
let dialog = manualUserDialog.createObject(this, {
|
||||
connection: root.connection
|
||||
});
|
||||
dialog.parent = root.Window.window.overlay;
|
||||
dialog.accepted.connect(() => {
|
||||
root.closeDialog();
|
||||
});
|
||||
dialog.userSelected.connect(userId => {
|
||||
root.room.inviteToRoom(userId)
|
||||
});
|
||||
dialog.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,12 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtLocation
|
||||
import QtPositioning
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
/** Location marker for any of the shared location maps. */
|
||||
MapQuickItem {
|
||||
id: root
|
||||
|
||||
@@ -91,6 +91,16 @@ Kirigami.ScrollablePage {
|
||||
*/
|
||||
property string customPlaceholderIcon: ""
|
||||
|
||||
/**
|
||||
* @brief Action to be shown in the "no search" placeholder
|
||||
*/
|
||||
property Kirigami.Action noSearchHelpfulAction
|
||||
|
||||
/**
|
||||
* @brief Action to be shown in the "no result" placeholder
|
||||
*/
|
||||
property Kirigami.Action noResultHelpfulAction
|
||||
|
||||
/**
|
||||
* @brief Force the search field to be focussed.
|
||||
*/
|
||||
@@ -134,8 +144,8 @@ Kirigami.ScrollablePage {
|
||||
Keys.onReturnPressed: searchButton.clicked()
|
||||
onTextChanged: {
|
||||
searchTimer.restart();
|
||||
if (model) {
|
||||
model.searchText = text;
|
||||
if (root.model) {
|
||||
root.model.searchText = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,8 +157,8 @@ Kirigami.ScrollablePage {
|
||||
text: i18nc("@action:button", "Search")
|
||||
|
||||
onClicked: {
|
||||
if (typeof model.search === 'function') {
|
||||
model.search();
|
||||
if (typeof root.model.search === 'function') {
|
||||
root.model.search();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,8 +170,8 @@ Kirigami.ScrollablePage {
|
||||
id: searchTimer
|
||||
interval: 500
|
||||
running: true
|
||||
onTriggered: if (typeof model.search === 'function') {
|
||||
model.search();
|
||||
onTriggered: if (typeof root.model.search === 'function') {
|
||||
root.model.search();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,12 +189,14 @@ Kirigami.ScrollablePage {
|
||||
id: noSearchMessage
|
||||
anchors.centerIn: parent
|
||||
visible: searchField.text.length === 0 && listView.count === 0 && customPlaceholder.text.length === 0
|
||||
helpfulAction: root.noSearchHelpfulAction
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
id: noResultMessage
|
||||
anchors.centerIn: parent
|
||||
visible: searchField.text.length > 0 && listView.count === 0 && !root.model.searching && customPlaceholder.text.length === 0
|
||||
helpfulAction: root.noResultHelpfulAction
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
|
||||
@@ -93,8 +93,12 @@ QString TextHandler::handleSendText()
|
||||
return outputString;
|
||||
}
|
||||
|
||||
QString
|
||||
TextHandler::handleRecieveRichText(Qt::TextFormat inputFormat, const NeoChatRoom *room, const Quotient::RoomEvent *event, bool stripNewlines, bool isEdited)
|
||||
QString TextHandler::handleRecieveRichText(Qt::TextFormat inputFormat,
|
||||
const NeoChatRoom *room,
|
||||
const Quotient::RoomEvent *event,
|
||||
bool stripNewlines,
|
||||
bool isEdited,
|
||||
bool spoilerRevealed)
|
||||
{
|
||||
m_pos = 0;
|
||||
m_dataBuffer = m_data;
|
||||
@@ -151,7 +155,7 @@ TextHandler::handleRecieveRichText(Qt::TextFormat inputFormat, const NeoChatRoom
|
||||
} else if ((getTagType(m_nextToken) == u"br"_s && stripNewlines)) {
|
||||
nextTokenBuffer = u' ';
|
||||
}
|
||||
nextTokenBuffer = cleanAttributes(getTagType(m_nextToken), nextTokenBuffer);
|
||||
nextTokenBuffer = cleanAttributes(getTagType(m_nextToken), nextTokenBuffer, true, spoilerRevealed);
|
||||
}
|
||||
|
||||
outputString.append(nextTokenBuffer);
|
||||
@@ -333,7 +337,8 @@ MessageComponent TextHandler::nextBlock(const QString &string,
|
||||
Qt::TextFormat inputFormat,
|
||||
const NeoChatRoom *room,
|
||||
const Quotient::RoomEvent *event,
|
||||
bool isEdited)
|
||||
bool isEdited,
|
||||
bool spoilerRevealed)
|
||||
{
|
||||
if (string.isEmpty()) {
|
||||
return {};
|
||||
@@ -355,7 +360,11 @@ MessageComponent TextHandler::nextBlock(const QString &string,
|
||||
content = unescapeHtml(content);
|
||||
break;
|
||||
default:
|
||||
content = handleRecieveRichText(inputFormat, room, event, false, isEdited);
|
||||
content = handleRecieveRichText(inputFormat, room, event, false, isEdited, spoilerRevealed);
|
||||
}
|
||||
|
||||
if (content.contains(u"data-mx-spoiler"_s)) {
|
||||
attributes[u"hasSpoiler"_s] = true;
|
||||
}
|
||||
return MessageComponent{messageComponentType, content, attributes};
|
||||
}
|
||||
@@ -462,8 +471,11 @@ bool TextHandler::isAllowedLink(const QString &link, bool isImg)
|
||||
}
|
||||
}
|
||||
|
||||
QString TextHandler::cleanAttributes(const QString &tag, const QString &tagString)
|
||||
QString TextHandler::cleanAttributes(const QString &tag, const QString &tagString, bool addStyle, bool spoilerRevealed)
|
||||
{
|
||||
if (!tagString.contains(u'<') || !tagString.contains(u'>')) {
|
||||
return tagString;
|
||||
}
|
||||
int nextAttributeIndex = tagString.indexOf(u' ', 1);
|
||||
|
||||
if (nextAttributeIndex != -1) {
|
||||
@@ -518,11 +530,33 @@ QString TextHandler::cleanAttributes(const QString &tag, const QString &tagStrin
|
||||
nextAttributeIndex = nextSpaceIndex + 1;
|
||||
}
|
||||
|
||||
outputString += u'>';
|
||||
return outputString;
|
||||
return addStyle ? this->addStyle(tag, outputString, spoilerRevealed) : outputString + u'>';
|
||||
}
|
||||
|
||||
return tagString;
|
||||
return addStyle ? this->addStyle(tag, tagString) : tagString;
|
||||
}
|
||||
|
||||
QString TextHandler::addStyle(const QString &tag, QString cleanTagString, bool spoilerRevealed)
|
||||
{
|
||||
if (cleanTagString.endsWith(u'>')) {
|
||||
cleanTagString.removeLast();
|
||||
}
|
||||
|
||||
if (!cleanTagString.startsWith(u"</"_s)) {
|
||||
if (tag == u"a"_s) {
|
||||
cleanTagString += u" style=\"text-decoration: none;\""_s;
|
||||
} else if (tag == u"table"_s) {
|
||||
cleanTagString += u" style=\"width: 100%; border-collapse: collapse; border: 1px; border-style: solid;\""_s;
|
||||
} else if (tag == u"th"_s || tag == u"td"_s) {
|
||||
cleanTagString += u" style=\"border: 1px solid black; padding: 3px;\""_s;
|
||||
} else if (tag == u"span"_s && cleanTagString.contains(u"data-mx-spoiler"_s)) {
|
||||
Kirigami::Platform::PlatformTheme *theme =
|
||||
static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
|
||||
cleanTagString += u" style=\"color: %1; background: %2;\""_s.arg(spoilerRevealed ? theme->highlightedTextColor().name() : u"transparent"_s,
|
||||
theme->alternateBackgroundColor().name());
|
||||
}
|
||||
}
|
||||
return cleanTagString + u'>';
|
||||
}
|
||||
|
||||
QVariantMap TextHandler::getAttributes(const QString &tag, const QString &tagString)
|
||||
@@ -567,8 +601,12 @@ QVariantMap TextHandler::getAttributes(const QString &tag, const QString &tagStr
|
||||
return attributes;
|
||||
}
|
||||
|
||||
QList<MessageComponent>
|
||||
TextHandler::textComponents(QString string, Qt::TextFormat inputFormat, const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isEdited)
|
||||
QList<MessageComponent> TextHandler::textComponents(QString string,
|
||||
Qt::TextFormat inputFormat,
|
||||
const NeoChatRoom *room,
|
||||
const Quotient::RoomEvent *event,
|
||||
bool isEdited,
|
||||
bool spoilerRevealed)
|
||||
{
|
||||
if (string.trimmed().isEmpty()) {
|
||||
return {MessageComponent{MessageComponentType::Text, i18n("<i>This event does not have any content.</i>"), {}}};
|
||||
@@ -580,7 +618,8 @@ TextHandler::textComponents(QString string, Qt::TextFormat inputFormat, const Ne
|
||||
QList<MessageComponent> components;
|
||||
while (!string.isEmpty()) {
|
||||
const auto nextBlockPos = this->nextBlockPos(string);
|
||||
const auto nextBlock = this->nextBlock(string, nextBlockPos, inputFormat, room, event, nextBlockPos == string.size() ? isEdited : false);
|
||||
const auto nextBlock =
|
||||
this->nextBlock(string, nextBlockPos, inputFormat, room, event, nextBlockPos == string.size() ? isEdited : false, spoilerRevealed);
|
||||
components += nextBlock;
|
||||
string.remove(0, nextBlockPos);
|
||||
|
||||
@@ -798,4 +837,20 @@ QString TextHandler::convertCodeLanguageString(const QString &languageString)
|
||||
return languageString.right(languageString.length() - equalsPos - 1);
|
||||
}
|
||||
|
||||
QString TextHandler::updateSpoilerText(QObject *object, QString string, bool spoilerRevealed)
|
||||
{
|
||||
auto it = QRegularExpression(u"<span[^>]*data-mx-spoiler[^>]*style=\"color: (.*?); background: (.*?);\">"_s).globalMatch(string);
|
||||
Kirigami::Platform::PlatformTheme *theme =
|
||||
static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(object, true));
|
||||
int offset = 0;
|
||||
while (it.hasNext()) {
|
||||
const QRegularExpressionMatch match = it.next();
|
||||
const auto newColor = spoilerRevealed ? theme->textColor().name() : u"transparent"_s;
|
||||
string.replace(match.capturedStart(2) + offset, match.capturedLength(2), theme->alternateBackgroundColor().name());
|
||||
string.replace(match.capturedStart(1) + offset, match.capturedLength(1), newColor);
|
||||
offset = newColor.length() - match.capturedLength(1);
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
#include "moc_texthandler.cpp"
|
||||
|
||||
@@ -75,7 +75,8 @@ public:
|
||||
const NeoChatRoom *room = nullptr,
|
||||
const Quotient::RoomEvent *event = nullptr,
|
||||
bool stripNewlines = false,
|
||||
bool isEdited = false);
|
||||
bool isEdited = false,
|
||||
bool spoilerRevealed = false);
|
||||
|
||||
/**
|
||||
* @brief Handle the text as a plain output for a message being received.
|
||||
@@ -104,7 +105,13 @@ public:
|
||||
Qt::TextFormat inputFormat = Qt::RichText,
|
||||
const NeoChatRoom *room = nullptr,
|
||||
const Quotient::RoomEvent *event = nullptr,
|
||||
bool isEdited = false);
|
||||
bool isEdited = false,
|
||||
bool spoilerRevealed = false);
|
||||
|
||||
/**
|
||||
* @brief Modify the style parameters of the spoilers to reveal or hide the text.
|
||||
*/
|
||||
static QString updateSpoilerText(QObject *object, QString string, bool spoilerRevealed);
|
||||
|
||||
private:
|
||||
QString m_data;
|
||||
@@ -123,7 +130,8 @@ private:
|
||||
Qt::TextFormat inputFormat = Qt::RichText,
|
||||
const NeoChatRoom *room = nullptr,
|
||||
const Quotient::RoomEvent *event = nullptr,
|
||||
bool isEdited = false);
|
||||
bool isEdited = false,
|
||||
bool spoilerRevealed = false);
|
||||
QString stripBlockTags(QString string, const QString &tagType) const;
|
||||
|
||||
QString getTagType(const QString &tagToken) const;
|
||||
@@ -133,7 +141,8 @@ private:
|
||||
bool isAllowedTag(const QString &type);
|
||||
bool isAllowedAttribute(const QString &tag, const QString &attribute);
|
||||
bool isAllowedLink(const QString &link, bool isImg = false);
|
||||
QString cleanAttributes(const QString &tag, const QString &tagString);
|
||||
QString cleanAttributes(const QString &tag, const QString &tagString, bool addStyle = false, bool spoilerRevealed = false);
|
||||
QString addStyle(const QString &tag, QString cleanTagString, bool spoilerRevealed = false);
|
||||
QVariantMap getAttributes(const QString &tag, const QString &tagString);
|
||||
|
||||
QString markdownToHTML(const QString &markdown);
|
||||
|
||||
@@ -125,13 +125,8 @@ QQC2.Control {
|
||||
ChatDocumentHandler {
|
||||
id: documentHandler
|
||||
type: root.chatBarCache.isEditing ? ChatBarType.Edit : ChatBarType.Thread
|
||||
document: textArea.textDocument
|
||||
cursorPosition: textArea.cursorPosition
|
||||
selectionStart: textArea.selectionStart
|
||||
selectionEnd: textArea.selectionEnd
|
||||
textItem: textArea
|
||||
room: root.Message.room
|
||||
mentionColor: Kirigami.Theme.linkColor
|
||||
errorColor: Kirigami.Theme.negativeTextColor
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
@@ -166,42 +161,45 @@ QQC2.Control {
|
||||
QQC2.ToolButton {
|
||||
visible: !root.isBusy
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
action: Kirigami.Action {
|
||||
text: i18nc("@action:button", "Attach an image or file")
|
||||
icon.name: "mail-attachment"
|
||||
onTriggered: {
|
||||
let dialog = (Clipboard.hasImage ? attachDialog : openFileDialog).createObject(QQC2.Overlay.overlay);
|
||||
dialog.chosen.connect(path => root.chatBarCache.attachmentPath = path);
|
||||
dialog.open();
|
||||
}
|
||||
text: i18nc("@action:button", "Attach an image or file")
|
||||
icon.name: "mail-attachment"
|
||||
onClicked: {
|
||||
let dialog = (Clipboard.hasImage ? attachDialog : openFileDialog).createObject(QQC2.Overlay.overlay);
|
||||
dialog.chosen.connect(path => root.chatBarCache.attachmentPath = path);
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
action: Kirigami.Action {
|
||||
text: root.chatBarCache.isEditing ? i18nc("@action:button", "Confirm edit") : i18nc("@action:button", "Post message in thread")
|
||||
icon.name: "document-send"
|
||||
onTriggered: {
|
||||
_private.post();
|
||||
}
|
||||
}
|
||||
text: root.chatBarCache.isEditing ? i18nc("@action:button", "Confirm edit") : i18nc("@action:button", "Post message in thread")
|
||||
icon.name: "document-send"
|
||||
onClicked: _private.post()
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: cancelButton
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
action: Kirigami.Action {
|
||||
text: i18nc("@action:button", "Cancel")
|
||||
icon.name: "dialog-close"
|
||||
onTriggered: {
|
||||
root.chatBarCache.clearRelations();
|
||||
}
|
||||
text: i18nc("@action:button", "Cancel")
|
||||
icon.name: "dialog-close"
|
||||
onClicked: {
|
||||
root.chatBarCache.clearRelations();
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
shortcut: "Escape"
|
||||
onTriggered: cancelButton.clicked()
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -264,7 +262,7 @@ QQC2.Control {
|
||||
documentHandler.document;
|
||||
if (chatBarCache?.isEditing && chatBarCache.relationMessage.length > 0) {
|
||||
textArea.text = chatBarCache.relationMessage;
|
||||
documentHandler.updateMentions(textArea.textDocument, chatBarCache.editId);
|
||||
documentHandler.updateMentions(chatBarCache.editId);
|
||||
textArea.forceActiveFocus();
|
||||
textArea.cursorPosition = textArea.text.length;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user