Compare commits

..

1 Commits

Author SHA1 Message Date
Joshua Goins
de6e588981 Allow searching messages in all rooms 2025-08-23 17:31:45 -04:00
122 changed files with 15094 additions and 17300 deletions

View File

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

View File

@@ -44,22 +44,6 @@
}
]
},
{
"name": "opencv",
"config-opts": [
"-DBUILD_TESTS=OFF",
"-DWITH_GTK=OFF",
"-DBUILD_LIST=core,imgproc"
],
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "git",
"url": "https://github.com/opencv/opencv"
}
],
"builddir": true
},
{
"name": "kquickimageeditor",
"config-opts": [

View File

@@ -15,7 +15,7 @@ set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
set(KF_MIN_VERSION "6.17")
set(QT_MIN_VERSION "6.8")
set(QT_MIN_VERSION "6.5")
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)

View File

@@ -43,13 +43,13 @@ void MessageContentModelTest::missingEvent()
QCOMPARE(model1.rowCount(), 1);
QCOMPARE(model1.data(model1.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Loading);
QCOMPARE(model1.data(model1.index(0), MessageContentModel::DisplayRole), u"Loading"_s);
QCOMPARE(model1.data(model1.index(0), MessageContentModel::DisplayRole), u"Loading"_s);
auto model2 = EventMessageContentModel(room, u"$153456789:example.org"_s, true);
QCOMPARE(model2.rowCount(), 1);
QCOMPARE(model2.data(model2.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Loading);
QCOMPARE(model2.data(model2.index(0), MessageContentModel::DisplayRole), u"Loading reply"_s);
QCOMPARE(model2.data(model2.index(0), MessageContentModel::DisplayRole), u"Loading reply"_s);
room->syncNewEvents(u"test-min-sync.json"_s);
QCOMPARE(model1.rowCount(), 2);

View File

@@ -109,20 +109,113 @@ void Server::start()
m_server.route(u"/_matrix/client/v3/rooms/<arg>/invite"_s,
QHttpServerRequest::Method::Post,
[this](const QString &roomId, QHttpServerResponder &responder, const QHttpServerRequest &request) {
Changes changes;
changes.invitations += Changes::InviteUser{
.userId = QJsonDocument::fromJson(request.body()).object()[u"user_id"_s].toString(),
.roomId = roomId,
};
m_state += changes;
m_invitedUsers[roomId] += QJsonDocument::fromJson(request.body()).object()[u"user_id"_s].toString();
responder.write(QJsonDocument(QJsonObject{}), QHttpServerResponder::StatusCode::Ok);
});
m_server.route(u"/_matrix/client/r0/sync"_s, QHttpServerRequest::Method::Get, this, &Server::sync);
m_server.route(u"/_matrix/client/r0/sync"_s, QHttpServerRequest::Method::Get, [this](QHttpServerResponder &responder) {
QMap<QString, QJsonArray> stateEvents;
QMap<QString, QJsonArray> roomAccountData;
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, 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}}},
};
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()) {
const auto &values = m_invitedUsers[roomId];
for (const auto &value : values) {
stateEvents[roomId] += QJsonObject{
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"invite"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, roomId},
{u"sender"_s, u"@user:localhost:1234"_s},
{u"state_key"_s, value},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
}
}
m_invitedUsers.clear();
for (const auto &roomId : m_bannedUsers.keys()) {
const auto &values = m_bannedUsers[roomId];
for (const auto &value : values) {
stateEvents[roomId] += QJsonObject{
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"ban"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, roomId},
{u"sender"_s, u"@user:localhost:1234"_s},
{u"state_key"_s, value},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
}
}
m_bannedUsers.clear();
for (const auto &roomId : m_joinedUsers.keys()) {
const auto &values = m_joinedUsers[roomId];
for (const auto &value : values) {
stateEvents[roomId] += QJsonObject{
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"join"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, roomId},
{u"sender"_s, u"@user:localhost:1234"_s},
{u"state_key"_s, value},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
}
}
m_joinedUsers.clear();
QJsonObject rooms;
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();
auto json = QJsonObject{{u"rooms"_s, QJsonObject{{u"join"_s, rooms}}}};
responder.write(QJsonDocument(json), QHttpServerResponder::StatusCode::Ok);
});
QSslConfiguration config;
QFile key(QStringLiteral(DATA_DIR) + u"/localhost.key"_s);
void(key.open(QFile::ReadOnly));
key.open(QFile::ReadOnly);
config.setPrivateKey(QSslKey(&key, QSsl::Rsa));
config.setLocalCertificate(QSslCertificate::fromPath(QStringLiteral(DATA_DIR) + u"/localhost.crt"_s).front());
m_sslServer.setSslConfiguration(config);
@@ -136,239 +229,46 @@ void Server::start()
QString Server::createRoom(const QString &matrixId)
{
const auto roomId = generateRoomId();
Changes changes;
changes.newRooms += Changes::NewRoom{
.initialMembers = {matrixId},
.roomId = {roomId},
auto roomId = generateRoomId();
m_roomsToCreate += RoomData{
.members = {matrixId},
.id = roomId,
.tags = {},
};
m_state += changes;
return roomId;
}
void Server::inviteUser(const QString &roomId, const QString &matrixId)
{
Changes changes;
changes.invitations += Changes::InviteUser{
.userId = matrixId,
.roomId = roomId,
};
m_state += changes;
m_invitedUsers[roomId] += matrixId;
}
void Server::banUser(const QString &roomId, const QString &matrixId)
{
Changes changes;
changes.bans += Changes::BanUser{
.userId = matrixId,
.roomId = roomId,
};
m_state += changes;
m_bannedUsers[roomId] += matrixId;
}
void Server::joinUser(const QString &roomId, const QString &matrixId)
{
Changes changes;
changes.joins += Changes::JoinUser{
.userId = matrixId,
.roomId = roomId,
};
m_state += changes;
m_joinedUsers[roomId] += matrixId;
}
QString Server::createServerNoticesRoom(const QString &matrixId)
{
const auto roomId = generateRoomId();
Changes changes;
changes.newRooms += Changes::NewRoom{
.initialMembers = {matrixId},
.roomId = {roomId},
.tags = {u"m.server_notice"_s},
};
m_state += changes;
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)
{
Changes changes;
const auto eventId = generateEventId();
changes.events += Changes::Event{
.fullJson = QJsonObject{{u"type"_s, eventType},
{u"content"_s, content},
{u"sender"_s, u"@foo:server.com"_s},
{u"event_id"_s, eventId},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, roomId}},
m_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()},
};
m_state += changes;
return eventId;
}
void Server::sync(const QHttpServerRequest &request, QHttpServerResponder &responder)
{
QJsonObject joinRooms;
auto token = request.query().queryItemValue(u"since"_s).toInt();
for (const auto &change : m_state.mid(token)) {
for (const auto &newRoom : change.newRooms) {
QJsonArray stateEvents;
stateEvents += QJsonObject{
{u"content"_s, QJsonObject{{u"room_version"_s, u"11"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, newRoom.roomId},
{u"sender"_s, newRoom.initialMembers[0]},
{u"state_key"_s, QString()},
{u"type"_s, u"m.room.create"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
for (const auto &member : newRoom.initialMembers) {
stateEvents += QJsonObject{
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"join"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, newRoom.roomId},
{u"sender"_s, member},
{u"state_key"_s, member},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
}
auto room = QJsonObject{{u"state"_s, QJsonObject{{u"events"_s, stateEvents}}}};
QJsonArray roomAccountData;
QJsonObject tags;
for (const auto &tag : newRoom.tags) {
tags[tag] = QJsonObject();
}
if (!tags.empty()) {
roomAccountData += QJsonObject{{u"type"_s, u"m.tag"_s}, {u"content"_s, QJsonObject{{u"tags"_s, tags}}}};
}
if (roomAccountData.size() > 0) {
room[u"account_data"] = QJsonObject{{u"events"_s, roomAccountData}};
}
joinRooms[newRoom.roomId] = room;
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &invitation : change.invitations) {
// TODO: The invitation could be for a room we haven't joined yet. Shouldn't be necessary for now, though.
auto stateEvents = joinRooms[invitation.roomId][u"state"_s][u"events"_s].toArray();
stateEvents += QJsonObject{
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"invite"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, invitation.roomId},
{u"sender"_s, u"@user:localhost:1234"_s},
{u"state_key"_s, invitation.userId},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
if (joinRooms.contains(invitation.roomId)) {
auto room = joinRooms[invitation.roomId].toObject();
room[u"state"_s] = QJsonObject{{u"events"_s, stateEvents}};
joinRooms[invitation.roomId] = room;
} else {
joinRooms[invitation.roomId] = QJsonObject{{u"state"_s,
QJsonObject{
{u"events"_s, stateEvents},
}}};
}
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &ban : change.bans) {
// TODO: The ban could be for a room we haven't joined yet. Shouldn't be necessary for now, though.
auto stateEvents = joinRooms[ban.roomId][u"state"_s][u"events"_s].toArray();
stateEvents += QJsonObject{
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"ban"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, ban.roomId},
{u"sender"_s, u"@user:localhost:1234"_s},
{u"state_key"_s, ban.userId},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
if (joinRooms.contains(ban.roomId)) {
auto room = joinRooms[ban.roomId].toObject();
room[u"state"_s] = QJsonObject{{u"events"_s, stateEvents}};
joinRooms[ban.roomId] = room;
} else {
joinRooms[ban.roomId] = QJsonObject{{u"state"_s,
QJsonObject{
{u"events"_s, stateEvents},
}}};
}
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &join : change.joins) {
// TODO: The join could be for a room we haven't joined yet. Shouldn't be necessary for now, though.
auto stateEvents = joinRooms[join.roomId][u"state"_s][u"events"_s].toArray();
stateEvents += QJsonObject{
{u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"join"_s}}},
{u"event_id"_s, generateEventId()},
{u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()},
{u"room_id"_s, join.roomId},
{u"sender"_s, u"@user:localhost:1234"_s},
{u"state_key"_s, join.userId},
{u"type"_s, u"m.room.member"_s},
{u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}},
};
if (joinRooms.contains(join.roomId)) {
auto room = joinRooms[join.roomId].toObject();
room[u"state"_s] = QJsonObject{{u"events"_s, stateEvents}};
joinRooms[join.roomId] = room;
} else {
joinRooms[join.roomId] = QJsonObject{{u"state"_s,
QJsonObject{
{u"events"_s, stateEvents},
}}};
}
}
}
for (const auto &change : m_state.mid(token)) {
for (const auto &event : change.events) {
// TODO the room might be in a different join state.
auto timeline = joinRooms[event.fullJson[u"room_id"_s].toString()][u"timeline"_s][u"events"_s].toArray();
timeline += event.fullJson;
if (joinRooms.contains(event.fullJson[u"room_id"_s].toString())) {
auto room = joinRooms[event.fullJson[u"room_id"_s].toString()].toObject();
room[u"timeline"_s] = QJsonObject{{u"events"_s, timeline}};
joinRooms[event.fullJson[u"room_id"_s].toString()] = room;
} else {
joinRooms[event.fullJson[u"room_id"_s].toString()] = QJsonObject{
{u"timeline"_s, QJsonObject{{u"events"_s, timeline}}},
};
}
}
}
QJsonObject syncData = {
// {u"account_data"_s, QJsonObject {}},
// {u"presence"_s, QJsonObject {}},
{u"next_batch"_s, QString::number(m_state.size())},
};
QJsonObject rooms;
if (!joinRooms.isEmpty()) {
rooms[u"join"_s] = joinRooms;
}
if (!rooms.empty()) {
syncData[u"rooms"_s] = rooms;
}
qWarning() << syncData;
responder.write(QJsonDocument(syncData), QHttpServerResponder::StatusCode::Ok);
}

View File

@@ -2,51 +2,16 @@
// SPDX-License-Identifier: LGPL-2.0-or-later
#include <QHttpServer>
#include <QJsonObject>
#include <QSslServer>
struct Changes {
struct NewRoom {
QStringList initialMembers;
QString roomId;
QStringList tags;
};
QList<NewRoom> newRooms;
struct InviteUser {
QString userId;
QString roomId;
};
QList<InviteUser> invitations;
struct BanUser {
QString userId;
QString roomId;
};
QList<BanUser> bans;
struct JoinUser {
QString userId;
QString roomId;
};
QList<JoinUser> joins;
struct Event {
QJsonObject fullJson;
};
QList<Event> events;
};
struct RoomData {
QStringList members;
QString id;
QStringList tags;
};
class Server : public QObject
class Server
{
Q_OBJECT
public:
Server();
@@ -72,7 +37,10 @@ private:
QHttpServer m_server;
QSslServer m_sslServer;
void sync(const QHttpServerRequest &request, QHttpServerResponder &responder);
QHash<QString, QList<QString>> m_invitedUsers;
QHash<QString, QList<QString>> m_bannedUsers;
QHash<QString, QList<QString>> m_joinedUsers;
QList<Changes> m_state;
QList<RoomData> m_roomsToCreate;
QMap<QString, QJsonArray> m_events;
};

View File

@@ -1,7 +1,6 @@
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QTest>
#include <Quotient/events/event.h>
#include <Quotient/syncdata.h>
@@ -33,7 +32,7 @@ public:
if (!syncFileName.isEmpty()) {
QFile testSyncFile;
testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + syncFileName);
Q_UNUSED(testSyncFile.open(QIODevice::ReadOnly));
testSyncFile.open(QIODevice::ReadOnly);
const auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll());
Quotient::SyncRoomData roomData(id(), Quotient::JoinState::Join, testSyncJson.object());
update(std::move(roomData));
@@ -47,7 +46,7 @@ inline Quotient::event_ptr_tt<EventT> loadEventFromFile(const QString &eventFile
if (!eventFileName.isEmpty()) {
QFile testEventFile;
testEventFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + eventFileName);
Q_UNUSED(testEventFile.open(QIODevice::ReadOnly));
testEventFile.open(QIODevice::ReadOnly);
auto testSyncJson = QJsonDocument::fromJson(testEventFile.readAll()).object();
return Quotient::loadEvent<EventT>(testSyncJson);
}

View File

@@ -161,7 +161,7 @@ void TimelineMessageModelTest::pendingEvent()
// different every time.
QFile testSyncFile;
testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + u"test-pending-sync.json"_s);
QVERIFY(testSyncFile.open(QIODevice::ReadOnly));
testSyncFile.open(QIODevice::ReadOnly);
auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll());
auto root = testSyncJson.object();
auto timeline = root["timeline"_L1].toObject();

View File

@@ -3,12 +3,12 @@
add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}" )
qt_add_executable(timeline_memtest
qt_add_executable(timeline-memtest
main.cpp
)
target_link_libraries(timeline_memtest PRIVATE neochatplugin Timelineplugin)
target_link_libraries(timeline_memtest PUBLIC
target_link_libraries(timeline-memtest PRIVATE neochatplugin Timelineplugin)
target_link_libraries(timeline-memtest PUBLIC
Qt::Core
Qt::Quick
Qt::Qml
@@ -16,13 +16,14 @@ target_link_libraries(timeline_memtest PUBLIC
Qt::QuickControls2
Qt::Widgets
KF6::I18nQml
KF6::Kirigami
QuotientQt6
LibNeoChat
Timeline
)
ecm_add_qml_module(timeline_memtest URI org.kde.neochat.timeline_memtest GENERATE_PLUGIN_SOURCE
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/timeline_memtest
ecm_add_qml_module(timeline-memtest URI org.kde.neochat.timeline-memtest GENERATE_PLUGIN_SOURCE
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/timeline-memtest
QML_FILES
Main.qml
SOURCES

View File

@@ -28,7 +28,7 @@ int main(int argc, char **argv)
engine.rootContext()->setContextProperty(u"memTestTimelineModel"_s, memTestTimelineModel);
engine.rootContext()->setContextProperty(u"messageFilterModel"_s, messageFilterModel);
engine.loadFromModule("org.kde.neochat.timeline_memtest", "Main");
engine.loadFromModule("org.kde.neochat.timeline-memtest", "Main");
return app.exec();
}

View File

@@ -37,11 +37,7 @@ public:
if (!syncFileName.isEmpty()) {
QFile testSyncFile;
testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + syncFileName);
auto ok = testSyncFile.open(QIODevice::ReadOnly);
if (!ok) {
qWarning() << "Failed to open" << testSyncFile.fileName() << testSyncFile.errorString();
}
testSyncFile.open(QIODevice::ReadOnly);
auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll()).object();
auto timelineJson = testSyncJson["timeline"_L1].toObject();
timelineJson["events"_L1] = multiplyEvents(timelineJson["events"_L1].toArray(), 100);

View File

@@ -20,7 +20,7 @@ index 10fe66daa..cd063113d 100644
-set(KF_MIN_VERSION "6.17")
+set(KF_MIN_VERSION "6.12")
set(QT_MIN_VERSION "6.8")
set(QT_MIN_VERSION "6.5")
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
--

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -342,6 +342,9 @@ if(TARGET KF6::DBusAddons AND NOT WIN32)
endif()
if (TARGET KUnifiedPush)
target_compile_definitions(neochat PUBLIC -DHAVE_KUNIFIEDPUSH)
target_link_libraries(neochat PUBLIC KUnifiedPush)
if (NOT ANDROID)
configure_file(org.kde.neochat.service.in ${CMAKE_CURRENT_BINARY_DIR}/org.kde.neochat.service)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.neochat.service DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR})

View File

@@ -19,7 +19,6 @@
#include "accountmanager.h"
#include "enums/roomsortparameter.h"
#include "general_logging.h"
#include "mediasizehelper.h"
#include "models/actionsmodel.h"
#include "models/messagemodel.h"
@@ -38,6 +37,14 @@
#include "trayicon_sni.h"
#endif
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
#ifndef Q_OS_ANDROID
#include <QDBusConnection>
#include <QDBusInterface>
#include <QDBusMessage>
#endif
#endif
#ifdef HAVE_KUNIFIEDPUSH
#include <kunifiedpush/connector.h>
#endif
@@ -126,10 +133,8 @@ Controller::Controller(QObject *parent)
connect(NeoChatConfig::self(), &NeoChatConfig::SystemTrayChanged, this, &Controller::setQuitOnLastWindowClosed);
#endif
connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [this] {
#ifndef Q_OS_ANDROID
QObject::connect(QGuiApplication::instance(), &QCoreApplication::aboutToQuit, QGuiApplication::instance(), [this] {
delete m_trayIcon;
#endif
NeoChatConfig::self()->save();
});
@@ -195,15 +200,13 @@ void Controller::setAccountManager(AccountManager *manager)
m_accountManager = manager;
if (!m_accountManager) {
return;
if (m_accountManager) {
connect(m_accountManager, &AccountManager::errorOccured, this, &Controller::errorOccured);
connect(m_accountManager, &AccountManager::accountsLoadingChanged, this, &Controller::accountsLoadingChanged);
connect(m_accountManager, &AccountManager::connectionAdded, this, &Controller::initConnection);
connect(m_accountManager, &AccountManager::connectionDropped, this, &Controller::teardownConnection);
connect(m_accountManager, &AccountManager::activeConnectionChanged, this, &Controller::initActiveConnection);
}
connect(m_accountManager, &AccountManager::errorOccured, this, &Controller::errorOccured);
connect(m_accountManager, &AccountManager::accountsLoadingChanged, this, &Controller::accountsLoadingChanged);
connect(m_accountManager, &AccountManager::connectionAdded, this, &Controller::initConnection);
connect(m_accountManager, &AccountManager::connectionDropped, this, &Controller::teardownConnection);
connect(m_accountManager, &AccountManager::activeConnectionChanged, this, &Controller::initActiveConnection);
}
void Controller::initConnection(NeoChatConnection *connection)
@@ -259,8 +262,8 @@ bool Controller::supportSystemTray() const
#ifdef Q_OS_ANDROID
return false;
#else
QStringList unsupportedPlatforms{u"GNOME"_s, u"Pantheon"_s};
return !unsupportedPlatforms.contains(QString::fromLatin1(qgetenv("XDG_CURRENT_DESKTOP")));
auto de = QString::fromLatin1(qgetenv("XDG_CURRENT_DESKTOP"));
return de != u"GNOME"_s && de != u"Pantheon"_s;
#endif
}
@@ -270,8 +273,11 @@ void Controller::setQuitOnLastWindowClosed()
if (supportSystemTray() && NeoChatConfig::self()->systemTray()) {
m_trayIcon = new TrayIcon(this);
m_trayIcon->show();
} else if (m_trayIcon) {
delete m_trayIcon;
} else {
if (m_trayIcon) {
delete m_trayIcon;
m_trayIcon = nullptr;
}
}
#endif
}
@@ -328,7 +334,30 @@ void Controller::clearInvitationNotification(const QString &roomId)
void Controller::updateBadgeNotificationCount(int count)
{
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
#ifndef Q_OS_ANDROID
// copied from Telegram desktop
const auto launcherUrl = "application://org.kde.neochat.desktop"_L1;
// Gnome requires that count is a 64bit integer
const qint64 counterSlice = std::min(count, 9999);
QVariantMap dbusUnityProperties;
if (counterSlice > 0) {
dbusUnityProperties["count"_L1] = counterSlice;
dbusUnityProperties["count-visible"_L1] = true;
} else {
dbusUnityProperties["count-visible"_L1] = false;
}
auto signal = QDBusMessage::createSignal("/com/canonical/unity/launcherentry/neochat"_L1, "com.canonical.Unity.LauncherEntry"_L1, "Update"_L1);
signal.setArguments({launcherUrl, dbusUnityProperties});
QDBusConnection::sessionBus().send(signal);
#endif // Q_OS_ANDROID
#else
qGuiApp->setBadgeNumber(count);
#endif // QT_VERSION_CHECK(6, 6, 0)
}
bool Controller::isFlatpak() const
@@ -349,10 +378,7 @@ QString Controller::loadFileContent(const QString &path) const
{
QUrl url(path);
QFile file(url.isLocalFile() ? url.toLocalFile() : url.toString());
if (!file.open(QFile::ReadOnly)) {
qCWarning(GENERAL) << "Failed to open file" << path;
return {};
}
file.open(QFile::ReadOnly);
return QString::fromLatin1(file.readAll());
}

View File

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

View File

@@ -60,8 +60,9 @@ void NotificationsManager::startNotificationJob(QPointer<NeoChatConnection> conn
}
if (!m_connActiveJob.contains(connection->user()->id())) {
auto job = connection->callApi<GetNotificationsJob>();
m_connActiveJob.append(connection->user()->id());
connection->callApi<GetNotificationsJob>().onResult([this, connection](const auto &job) {
connect(job, &BaseJob::success, this, [this, job, connection]() {
m_connActiveJob.removeAll(connection->user()->id());
processNotificationJob(connection, job, !m_oldNotifications.contains(connection->user()->id()));
});

View File

@@ -20,7 +20,8 @@ RowLayout {
}
}
z: 99
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

View File

@@ -2,8 +2,6 @@
// SPDX-FileCopyrightText: 2025 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-2.0-or-later
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts

View File

@@ -120,7 +120,7 @@ Components.AlbumMaximizeComponent {
onOpened: forceActiveFocus()
onItemRightClicked: RoomManager.viewEventMenu(root.currentEventId, root.currentRoom)
onItemRightClicked: RoomManager.viewEventMenu(root.currentEventId, root.currentRoom, root.currentAuthor)
onSaveItem: {
var dialog = saveAsDialog.createObject(QQC2.Overlay.overlay);

View File

@@ -215,8 +215,21 @@ Kirigami.Page {
});
}
function onShowDelegateMenu(eventId: string, author, messageComponentType, plainText: string, richText: string, mimeType: string, progressInfo, isThread: bool, selectedText: string, hoveredLink: string) {
const contextMenu = delegateContextMenu.createObject(root, {
function onShowMessageMenu(eventId, author, messageComponentType, plainText, htmlText, selectedText, hoveredLink, isThread) {
const contextMenu = messageDelegateContextMenu.createObject(root, {
selectedText: selectedText,
hoveredLink: hoveredLink,
author: author,
eventId: eventId,
messageComponentType: messageComponentType,
plainText: plainText,
htmlText: htmlText,
});
contextMenu.popup();
}
function onShowFileMenu(eventId, author, messageComponentType, plainText, mimeType, progressInfo, isThread) {
const contextMenu = fileDelegateContextMenu.createObject(root, {
author: author,
eventId: eventId,
plainText: plainText,
@@ -249,8 +262,16 @@ Kirigami.Page {
}
Component {
id: delegateContextMenu
DelegateContextMenu {
id: messageDelegateContextMenu
MessageDelegateContextMenu {
room: root.currentRoom
connection: root.currentRoom.connection
}
}
Component {
id: fileDelegateContextMenu
FileDelegateContextMenu {
room: root.currentRoom
connection: root.currentRoom.connection
}

View File

@@ -1,14 +1,14 @@
// SPDX-FileCopyrightText: 2024 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
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.neochat
@@ -44,7 +44,7 @@ QQC2.ComboBox {
required property bool isHomeServer
required property bool isDeletable
text: isAddServerDelegate ? i18nc("@action:button", "Add New Server") : url
text: isAddServerDelegate ? i18n("Add New Server") : url
highlighted: index === root.highlightedIndex
topInset: index === 0 ? Kirigami.Units.smallSpacing : Math.round(Kirigami.Units.smallSpacing / 2)
@@ -60,7 +60,7 @@ QQC2.ComboBox {
Delegates.SubtitleContentItem {
itemDelegate: serverItem
subtitle: serverItem.isHomeServer ? i18nc("@info", "Home Server") : ""
subtitle: serverItem.isHomeServer ? i18n("Home Server") : ""
Layout.fillWidth: true
}
@@ -138,11 +138,11 @@ QQC2.ComboBox {
text: {
if (serverUrlField.length > 0) {
if (!serverUrlField.acceptableInput) {
return i18nc("@info", "The entered text is not a valid url");
return i18n("The entered text is not a valid url");
}
if (!serverUrlField.isValidServer) {
return i18nc("@info", "This server cannot be resolved or has already been added");
return i18n("This server cannot be resolved or has already been added");
}
}
@@ -153,7 +153,7 @@ QQC2.ComboBox {
QQC2.Label {
Layout.fillWidth: true
text: i18nc("@label", "Server URL:")
text: i18n("Server URL:")
}
QQC2.TextField {

View File

@@ -45,8 +45,6 @@ Kirigami.Action {
model: Purpose.PurposeAlternativesModel {
pluginType: "Export"
inputData: root.inputData
// We already have many better ways to copy events to the clipboard
disabledPlugins: ["clipboardplugin"]
}
delegate: Kirigami.Action {

View File

@@ -130,6 +130,6 @@ FormCard.FormCardPage {
property OpenFileDialog openFileDialog: OpenFileDialog {
id: openFileDialog
onChosen: path => securityKeyField.text = Controller.loadFileContent(path)
onChosen: securityKeyField.text = Controller.loadFileContent(path)
}
}

View File

@@ -63,7 +63,6 @@ Kirigami.Dialog {
level: 1
Layout.fillWidth: true
font.bold: true
clip: true // Intentional to limit insane Unicode in display names
elide: Text.ElideRight
wrapMode: Text.NoWrap
@@ -275,20 +274,6 @@ Kirigami.Dialog {
}
}
FormCard.FormButtonDelegate {
text: i18nc("@action:button %1 is the name of the user.", "Search room for %1's messages", root.room ? root.room.member(root.user.id).htmlSafeDisplayName : QmlUtils.escapeString(root.user.displayName))
icon.name: "search-symbolic"
onClicked: {
pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RoomSearchPage'), {
room: root.room,
senderId: root.user.id
}, {
title: i18nc("@action:title", "Search")
});
root.close();
}
}
FormCard.FormButtonDelegate {
text: i18n("Copy link")
icon.name: "username-copy"

View File

@@ -71,15 +71,15 @@ SearchPage {
}
QQC2.Label {
visible: userDelegate.directChatExists
text: i18nc("@info", "Friends")
text: i18n("Friends")
textFormat: Text.PlainText
color: Kirigami.Theme.positiveTextColor
}
}
}
searchFieldPlaceholder: i18nc("@info:placeholder", "Find your friends…")
noSearchPlaceholderMessage: i18nc("@info:placeholder", "Enter text to start searching for your friends")
searchFieldPlaceholder: i18n("Find your friends…")
noSearchPlaceholderMessage: i18n("Enter text to start searching for your friends")
noResultPlaceholderMessage: i18nc("@info:label", "No matches found")
noSearchHelpfulAction: noResultHelpfulAction
@@ -101,8 +101,8 @@ SearchPage {
function openManualUserDialog() {
let dialog = manualUserDialog.createObject(this, {
connection: root.connection
}) as ManualUserDialog;
dialog.parent = root.QQC2.Overlay.overlay;
});
dialog.parent = root.Window.window.overlay;
dialog.accepted.connect(() => {
root.closeDialog();
});

View File

@@ -266,7 +266,7 @@ void RoomManager::viewEventSource(const QString &eventId)
Q_EMIT showEventSource(eventId);
}
void RoomManager::viewEventMenu(const QString &eventId, NeoChatRoom *room, const QString &selectedText, const QString &hoveredLink)
void RoomManager::viewEventMenu(const QString &eventId, NeoChatRoom *room, NeochatRoomMember *sender, const QString &selectedText, const QString &hoveredLink)
{
if (eventId.isEmpty()) {
qWarning() << "Tried to open event menu with empty event id";
@@ -279,15 +279,23 @@ void RoomManager::viewEventMenu(const QString &eventId, NeoChatRoom *room, const
return;
}
const auto &event = **it;
Q_EMIT showDelegateMenu(eventId,
room->qmlSafeMember(event.senderId()),
if (EventHandler::mediaInfo(room, &event).contains("mimeType"_L1)) {
Q_EMIT showFileMenu(eventId,
sender,
MessageComponentType::typeForEvent(event),
EventHandler::plainBody(room, &event),
EventHandler::richBody(room, &event),
EventHandler::mediaInfo(room, &event)["mimeType"_L1].toString(),
room->fileTransferInfo(eventId),
selectedText,
hoveredLink);
room->fileTransferInfo(eventId));
return;
}
Q_EMIT showMessageMenu(eventId,
sender,
MessageComponentType::typeForEvent(event),
EventHandler::plainBody(room, &event),
EventHandler::richBody(room, &event),
selectedText,
hoveredLink);
}
bool RoomManager::hasOpenRoom() const

View File

@@ -222,7 +222,8 @@ public:
/**
* @brief Show a context menu for the given event.
*/
Q_INVOKABLE void viewEventMenu(const QString &eventId, NeoChatRoom *room, const QString &selectedText = {}, const QString &hoveredLink = {});
Q_INVOKABLE void
viewEventMenu(const QString &eventId, NeoChatRoom *room, NeochatRoomMember *sender, const QString &selectedText = {}, const QString &hoveredLink = {});
/**
* @brief Set a URL to be loaded as the initial room.
@@ -295,15 +296,23 @@ Q_SIGNALS:
/**
* @brief Request to show a menu for the given event.
*/
void showDelegateMenu(const QString &eventId,
const NeochatRoomMember *author,
MessageComponentType::Type messageComponentType,
const QString &plainText,
const QString &richtText,
const QString &mimeType,
const FileTransferInfo &progressInfo,
const QString &selectedText,
const QString &hoveredLink);
void showMessageMenu(const QString &eventId,
const NeochatRoomMember *author,
MessageComponentType::Type messageComponentType,
const QString &plainText,
const QString &htmlText,
const QString &selectedText,
const QString &hoveredLink);
/**
* @brief Request to show a menu for the given media event.
*/
void showFileMenu(const QString &eventId,
const NeochatRoomMember *author,
MessageComponentType::Type messageComponentType,
const QString &plainText,
const QString &mimeType,
const FileTransferInfo &progressInfo);
/**
* @brief Show the direct chat confirmation dialog.

View File

@@ -253,8 +253,7 @@ QQC2.Control {
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
// This has to stay PlainText or else formatting starts breaking in strange ways
textFormat: TextEdit.PlainText
textFormat: TextEdit.MarkdownText
Accessible.description: placeholderText

View File

@@ -92,7 +92,6 @@ QQC2.Popup {
Delegates.SubtitleContentItem {
itemDelegate: completionDelegate
labelItem.textFormat: Text.PlainText
labelItem.clip: true // Intentional to limit insane Unicode in display names
subtitle: completionDelegate.subtitle ?? ""
subtitleItem.textFormat: Text.PlainText
}

View File

@@ -15,7 +15,7 @@ QQC2.ScrollView {
readonly property int emojisPerRow: emojis.width / targetIconSize
required property bool withCustom
readonly property var searchCategory: withCustom ? EmojiModel.Search : EmojiModel.SearchNoCustom
required property Item header
required property QtObject header
property bool stickers: false
signal chosen(string unicode)
@@ -75,7 +75,7 @@ QQC2.ScrollView {
shortName: modelData.shortName,
unicode: modelData.unicode,
categoryIconSize: root.targetIconSize
}) as EmojiTonesPicker;
});
tones.open();
tones.forceActiveFocus();
}
@@ -85,14 +85,14 @@ QQC2.ScrollView {
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
icon.name: root.stickers ? "stickers" : "preferences-desktop-emoticons"
text: root.stickers ? i18nc("@info", "No stickers") : i18nc("@info", "No emojis")
text: root.stickers ? i18n("No stickers") : i18n("No emojis")
visible: emojis.count === 0
}
}
Component {
id: tonesPopupComponent
EmojiTonesPicker {
onChosen: emoji => root.chosen(emoji)
onChosen: root.chosen(emoji)
}
}
}

View File

@@ -1,8 +1,6 @@
// SPDX-FileCopyrightText: 2022 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
@@ -53,14 +51,14 @@ ColumnLayout {
Kirigami.Action {
id: emojis
icon.name: "smiley"
text: i18nc("@action:button", "Emojis")
text: i18n("Emojis")
checked: true
onTriggered: root.selectedType = 0
},
Kirigami.Action {
id: stickers
icon.name: "stickers"
text: i18nc("@action:button", "Stickers")
text: i18n("Stickers")
onTriggered: root.selectedType = 1
}
]
@@ -109,7 +107,7 @@ ColumnLayout {
id: searchField
Layout.margins: Kirigami.Units.smallSpacing
Layout.fillWidth: true
visible: root.selectedType === 0
visible: selectedType === 0
/**
* The focus is manged by the parent and we don't want to use the standard
@@ -129,17 +127,17 @@ ColumnLayout {
header: categories
Keys.forwardTo: searchField
stickers: root.selectedType === 1
onStickerChosen: index => stickerModel.postSticker(emoticonFilterModel.mapToSource(emoticonFilterModel.index(index, 0)).row)
onStickerChosen: stickerModel.postSticker(emoticonFilterModel.mapToSource(emoticonFilterModel.index(index, 0)).row)
}
Kirigami.Separator {
visible: root.showQuickReaction
visible: showQuickReaction
Layout.fillWidth: true
Layout.preferredHeight: 1
}
QQC2.ScrollView {
visible: root.showQuickReaction
visible: showQuickReaction
Layout.fillWidth: true
Layout.preferredHeight: root.categoryIconSize + QQC2.ScrollBar.horizontal.height
QQC2.ScrollBar.horizontal.height: QQC2.ScrollBar.horizontal.visible ? QQC2.ScrollBar.horizontal.implicitHeight : 0
@@ -151,7 +149,6 @@ ColumnLayout {
model: ["👍", "👎", "😄", "🎉", "😕", "❤", "🚀", "👀"]
delegate: EmojiDelegate {
required property string modelData
emoji: modelData
height: root.categoryIconSize
@@ -187,14 +184,11 @@ ColumnLayout {
Component {
id: emojiDelegate
Kirigami.NavigationTabButton {
required property string emoji
required property int index
required property string name
width: root.categoryIconSize
height: width
checked: categories.currentIndex === index
text: emoji
QQC2.ToolTip.text: name
checked: categories.currentIndex === model.index
text: modelData ? modelData.emoji : ""
QQC2.ToolTip.text: modelData ? modelData.name : ""
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered
onClicked: {
@@ -207,25 +201,21 @@ ColumnLayout {
Component {
id: stickerDelegate
Kirigami.NavigationTabButton {
id: sticker
required property string name
required property int index
required property string emoji
width: root.categoryIconSize
height: width
checked: stickerModel.packIndex === index
checked: stickerModel.packIndex === model.index
padding: Kirigami.Units.largeSpacing
contentItem: Image {
source: sticker.emoji
source: model.url
fillMode: Image.PreserveAspectFit
sourceSize.width: width
sourceSize.height: height
}
QQC2.ToolTip.text: name
QQC2.ToolTip.text: model.name
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.visible: hovered && !!name
onClicked: stickerModel.packIndex = index
QQC2.ToolTip.visible: hovered && !!model.name
onClicked: stickerModel.packIndex = model.index
}
}

View File

@@ -46,7 +46,6 @@ void StateModel::loadState()
beginResetModel();
m_stateEvents.clear();
if (!m_room) {
endResetModel();
return;
}
const auto keys = m_room->currentState().events().keys();

View File

@@ -66,13 +66,6 @@ ecm_add_qml_module(LibNeoChat GENERATE_PLUGIN_SOURCE
io.github.quotient_im.libquotient
)
ecm_qt_declare_logging_category(LibNeoChat
HEADER "general_logging.h"
IDENTIFIER "GENERAL"
CATEGORY_NAME "org.kde.neochat"
DEFAULT_SEVERITY Info
)
ecm_qt_declare_logging_category(LibNeoChat
HEADER "eventhandler_logging.h"
IDENTIFIER "EventHandling"
@@ -112,8 +105,3 @@ if(NOT ANDROID)
)
target_compile_definitions(LibNeoChat PUBLIC -DHAVE_ICU)
endif()
if (TARGET KUnifiedPush)
target_compile_definitions(LibNeoChat PUBLIC -DHAVE_KUNIFIEDPUSH)
target_link_libraries(LibNeoChat PUBLIC KUnifiedPush)
endif()

View File

@@ -12,8 +12,6 @@
#include "neochatroom.h"
#include "general_logging.h"
using namespace Qt::StringLiterals;
AccountManager::AccountManager(bool testMode, QObject *parent)
@@ -25,10 +23,10 @@ AccountManager::AccountManager(bool testMode, QObject *parent)
loadAccountsFromCache();
});
} else {
auto connection = new NeoChatConnection(QUrl(u"https://localhost:1234"_s), this);
connection->assumeIdentity(u"@user:localhost:1234"_s, u"device_1234"_s, u"token_1234"_s);
m_accountRegistry->add(connection);
connection->syncLoop();
auto c = new NeoChatConnection(QUrl(u"https://localhost:1234"_s), this);
c->assumeIdentity(u"@user:localhost:1234"_s, u"device_1234"_s, u"token_1234"_s);
m_accountRegistry->add(c);
c->syncLoop();
}
}
@@ -39,45 +37,48 @@ Quotient::AccountRegistry *AccountManager::accounts()
void AccountManager::loadAccountsFromCache()
{
for (const auto &accountId : Quotient::SettingsGroup("Accounts"_L1).childGroups()) {
const auto accounts = Quotient::SettingsGroup("Accounts"_L1).childGroups();
for (const auto &accountId : accounts) {
Quotient::AccountSettings account{accountId};
m_accountsLoading += accountId;
Q_EMIT accountsLoadingChanged();
if (account.homeserver().isEmpty()) {
continue;
}
auto accessTokenLoadingJob = loadAccessTokenFromKeyChain(account.userId());
connect(accessTokenLoadingJob, &QKeychain::Job::finished, this, [accountId, this, accessTokenLoadingJob](QKeychain::Job *) {
if (accessTokenLoadingJob->error() != QKeychain::Error::NoError) {
return;
}
Quotient::AccountSettings account{accountId};
auto connection = new NeoChatConnection(account.homeserver());
m_connectionsLoading[accountId] = connection;
connect(connection, &NeoChatConnection::connected, this, [this, connection, accountId] {
connection->loadState();
if (connection->allRooms().size() == 0 || connection->allRooms()[0]->currentState().get<Quotient::RoomCreateEvent>()) {
addConnection(connection);
m_accountsLoading.removeAll(connection->userId());
m_connectionsLoading.remove(accountId);
Q_EMIT accountsLoadingChanged();
if (!account.homeserver().isEmpty()) {
auto accessTokenLoadingJob = loadAccessTokenFromKeyChain(account.userId());
connect(accessTokenLoadingJob, &QKeychain::Job::finished, this, [accountId, this, accessTokenLoadingJob](QKeychain::Job *) {
Quotient::AccountSettings account{accountId};
QString accessToken;
if (accessTokenLoadingJob->error() == QKeychain::Error::NoError) {
accessToken = QString::fromLatin1(accessTokenLoadingJob->binaryData());
} else {
connect(
connection->allRooms()[0],
&NeoChatRoom::baseStateLoaded,
this,
[this, connection, accountId]() {
addConnection(connection);
m_accountsLoading.removeAll(connection->userId());
m_connectionsLoading.remove(accountId);
Q_EMIT accountsLoadingChanged();
},
Qt::SingleShotConnection);
return;
}
auto connection = new NeoChatConnection(account.homeserver());
m_connectionsLoading[accountId] = connection;
connect(connection, &NeoChatConnection::connected, this, [this, connection, accountId] {
connection->loadState();
if (connection->allRooms().size() == 0 || connection->allRooms()[0]->currentState().get<Quotient::RoomCreateEvent>()) {
addConnection(connection);
m_accountsLoading.removeAll(connection->userId());
m_connectionsLoading.remove(accountId);
Q_EMIT accountsLoadingChanged();
} else {
connect(
connection->allRooms()[0],
&NeoChatRoom::baseStateLoaded,
this,
[this, connection, accountId]() {
addConnection(connection);
m_accountsLoading.removeAll(connection->userId());
m_connectionsLoading.remove(accountId);
Q_EMIT accountsLoadingChanged();
},
Qt::SingleShotConnection);
}
});
connection->assumeIdentity(account.userId(), account.deviceId(), accessToken);
});
connection->assumeIdentity(account.userId(), account.deviceId(), QString::fromLatin1(accessTokenLoadingJob->binaryData()));
});
}
}
}
@@ -93,7 +94,7 @@ void AccountManager::saveAccessTokenToKeyChain(NeoChatConnection *connection)
}
const auto userId = connection->userId();
qCDebug(GENERAL) << "Save the access token to the keychain for " << userId;
qDebug() << "Save the access token to the keychain for " << userId;
auto job = new QKeychain::WritePasswordJob(qAppName());
job->setAutoDelete(true);
job->setKey(userId);
@@ -108,7 +109,7 @@ void AccountManager::saveAccessTokenToKeyChain(NeoChatConnection *connection)
QKeychain::ReadPasswordJob *AccountManager::loadAccessTokenFromKeyChain(const QString &userId)
{
qCDebug(GENERAL) << "Reading access token from the keychain for" << userId;
qDebug() << "Reading access token from the keychain for" << userId;
auto job = new QKeychain::ReadPasswordJob(qAppName(), this);
job->setKey(userId);
@@ -204,7 +205,8 @@ void AccountManager::dropConnection(const QString &userId)
if (dropConnectionLoading(m_connectionsLoading.value(userId, nullptr))) {
return;
}
if (const auto connection = dynamic_cast<NeoChatConnection *>(m_accountRegistry->get(userId))) {
const auto connection = dynamic_cast<NeoChatConnection *>(m_accountRegistry->get(userId));
if (connection) {
dropRegistry(connection);
}
}

View File

@@ -330,7 +330,7 @@ QString ChatDocumentHandler::getText() const
qCWarning(ChatDocumentHandling) << "getText called with no QQuickTextDocument available.";
return {};
}
return document()->toPlainText();
return document()->toRawText();
}
void ChatDocumentHandler::pushMention(const Mention mention) const

View File

@@ -116,14 +116,12 @@ Q_SIGNALS:
void textItemChanged();
void roomChanged();
public Q_SLOTS:
void updateCompletion() const;
private:
ChatBarType::Type m_type = ChatBarType::None;
QPointer<QQuickItem> m_textItem;
QTextDocument *document() const;
void updateCompletion() const;
int completionStartIndex() const;
QPointer<NeoChatRoom> m_room;

View File

@@ -186,10 +186,6 @@ int DelegateSizeHelper::availablePercentageWidth() const
qreal DelegateSizeHelper::availableWidth() const
{
qreal absoluteWidth = maxAvailableWidth() * availablePercentageWidth() * 0.01;
// We want to use all available space for a horizontal line.
if (m_startPercentWidth == m_endPercentWidth) {
return std::round(absoluteWidth);
}
return std::round(std::min(absoluteWidth, maxWidth()));
}

View File

@@ -61,7 +61,14 @@ void LinkPreviewer::loadUrlPreview()
return;
}
auto onSuccess = [this, conn](const auto &job) {
BaseJob *job = nullptr;
if (conn->supportedMatrixSpecVersions().contains("v1.11"_L1)) {
job = conn->callApi<GetUrlPreviewAuthedJob>(m_url);
} else {
QT_IGNORE_DEPRECATIONS(job = conn->callApi<GetUrlPreviewJob>(m_url);)
}
connect(job, &BaseJob::success, this, [this, job, conn]() {
const auto json = job->jsonData();
m_title = json["og:title"_L1].toString().trimmed();
m_description = json["og:description"_L1].toString().trimmed().replace("\n"_L1, " "_L1);
@@ -78,13 +85,7 @@ void LinkPreviewer::loadUrlPreview()
Q_EMIT descriptionChanged();
Q_EMIT imageSourceChanged();
Q_EMIT loadedChanged();
};
if (conn->supportedMatrixSpecVersions().contains("v1.11"_L1)) {
conn->callApi<GetUrlPreviewAuthedJob>(m_url);
} else {
QT_IGNORE_DEPRECATIONS(conn->callApi<GetUrlPreviewJob>(m_url).onResult(onSuccess);)
}
});
}
}

View File

@@ -66,7 +66,9 @@ void CustomEmojiModel::addEmoji(const QString &name, const QUrl &location)
{
using namespace Quotient;
m_connection->uploadFile(location.toLocalFile()).onResult([name, location, this](const auto &job) {
auto job = m_connection->uploadFile(location.toLocalFile());
connect(job, &BaseJob::success, this, [name, location, job, this] {
const auto &data = m_connection->accountData("im.ponies.user_emotes"_L1);
auto json = data != nullptr ? data->contentJson() : QJsonObject();
auto emojiData = json["images"_L1].toObject();

View File

@@ -44,8 +44,8 @@ QVariant ImagePacksModel::data(const QModelIndex &index, int role) const
QHash<int, QByteArray> ImagePacksModel::roleNames() const
{
return {
{DisplayNameRole, "name"},
{AvatarUrlRole, "emoji"},
{DisplayNameRole, "displayName"},
{AvatarUrlRole, "avatarUrl"},
{AttributionRole, "attribution"},
{IdRole, "id"},
};

View File

@@ -12,27 +12,33 @@ LocationsModel::LocationsModel(QObject *parent)
{
connect(this, &LocationsModel::roomChanged, this, [this]() {
for (const auto &event : m_room->messageEvents()) {
if (const auto &roomMessageEvent = event.viewAs<RoomMessageEvent>()) {
if (roomMessageEvent->msgtype() == RoomMessageEvent::MsgType::Location) {
addLocation(roomMessageEvent);
}
if (!is<RoomMessageEvent>(*event)) {
continue;
}
if (event->contentJson()["msgtype"_L1] == "m.location"_L1) {
const auto &e = *event;
addLocation(eventCast<const RoomMessageEvent>(&e));
}
}
connect(m_room, &NeoChatRoom::aboutToAddHistoricalMessages, this, [this](const auto &events) {
for (const auto &event : events) {
if (const auto &roomMessageEvent = eventCast<const RoomMessageEvent>(event)) {
if (roomMessageEvent->msgtype() == RoomMessageEvent::MsgType::Location) {
addLocation(roomMessageEvent);
}
if (!is<RoomMessageEvent>(*event)) {
continue;
}
if (event->contentJson()["msgtype"_L1] == "m.location"_L1) {
const auto &e = *event;
addLocation(eventCast<const RoomMessageEvent>(&e));
}
}
});
connect(m_room, &NeoChatRoom::aboutToAddNewMessages, this, [this](const auto &events) {
for (const auto &event : events) {
if (const auto &roomMessageEvent = eventCast<const RoomMessageEvent>(event)) {
if (roomMessageEvent->msgtype() == RoomMessageEvent::MsgType::Location) {
addLocation(roomMessageEvent);
}
if (!is<RoomMessageEvent>(*event)) {
continue;
}
if (event->contentJson()["msgtype"_L1] == "m.location"_L1) {
const auto &e = *event;
addLocation(eventCast<const RoomMessageEvent>(&e));
}
}
});

View File

@@ -133,7 +133,8 @@ void NeoChatConnection::connectSignals()
&Connection::connected,
this,
[this] {
callApi<GetVersionsJob>(BackgroundRequest).onResult([this](const auto &job) {
auto job = callApi<GetVersionsJob>(BackgroundRequest);
connect(job, &GetVersionsJob::success, this, [this, job] {
m_canCheckMutualRooms = job->unstableFeatures().contains("uk.half-shot.msc2666.query_mutual_rooms"_L1);
Q_EMIT canCheckMutualRoomsChanged();
m_canEraseData = job->unstableFeatures().contains("org.matrix.msc4025"_L1) || job->versions().count("v1.10"_L1);
@@ -236,22 +237,24 @@ bool NeoChatConnection::canCheckMutualRooms() const
void NeoChatConnection::changePassword(const QString &currentPassword, const QString &newPassword)
{
callApi<ChangePasswordJob>(newPassword, false).onFailure([this, currentPassword, newPassword](const auto &job) {
QJsonObject replyData = job->jsonData();
AuthenticationData authData;
authData.session = replyData["session"_L1].toString();
authData.type = "m.login.password"_L1;
authData.authInfo["password"_L1] = currentPassword;
authData.authInfo["user"_L1] = user()->id();
authData.authInfo["identifier"_L1] = QJsonObject{{"type"_L1, "m.id.user"_L1}, {"user"_L1, user()->id()}};
auto innerJob = callApi<ChangePasswordJob>(newPassword, false, authData)
.then(
[this]() {
Q_EMIT passwordStatus(PasswordStatus::Success);
},
[this](const auto &job) {
Q_EMIT passwordStatus(job->jsonData()["errcode"_L1] == "M_FORBIDDEN"_L1 ? PasswordStatus::Wrong : PasswordStatus::Other);
});
auto job = callApi<ChangePasswordJob>(newPassword, false);
connect(job, &BaseJob::result, this, [this, job, currentPassword, newPassword] {
if (job->error() == 103) {
QJsonObject replyData = job->jsonData();
AuthenticationData authData;
authData.session = replyData["session"_L1].toString();
authData.type = "m.login.password"_L1;
authData.authInfo["password"_L1] = currentPassword;
authData.authInfo["user"_L1] = user()->id();
authData.authInfo["identifier"_L1] = QJsonObject{{"type"_L1, "m.id.user"_L1}, {"user"_L1, user()->id()}};
auto innerJob = callApi<ChangePasswordJob>(newPassword, false, authData);
connect(innerJob, &BaseJob::success, this, [this]() {
Q_EMIT passwordStatus(PasswordStatus::Success);
});
connect(innerJob, &BaseJob::failure, this, [innerJob, this]() {
Q_EMIT passwordStatus(innerJob->jsonData()["errcode"_L1] == "M_FORBIDDEN"_L1 ? PasswordStatus::Wrong : PasswordStatus::Other);
});
}
});
}
@@ -271,18 +274,22 @@ QString NeoChatConnection::label() const
void NeoChatConnection::deactivateAccount(const QString &password, const bool erase)
{
callApi<DeactivateAccountJob>().onFailure([password, erase, this](const auto &job) {
QJsonObject replyData = job->jsonData();
AuthenticationData authData;
authData.session = replyData["session"_L1].toString();
authData.authInfo["password"_L1] = password;
authData.type = "m.login.password"_L1;
authData.authInfo["user"_L1] = user()->id();
QJsonObject identifier = {{"type"_L1, "m.id.user"_L1}, {"user"_L1, user()->id()}};
authData.authInfo["identifier"_L1] = identifier;
callApi<DeactivateAccountJob>(authData, QString{}, erase).onResult([this]() {
logout(false);
});
auto job = callApi<DeactivateAccountJob>();
connect(job, &BaseJob::result, this, [this, job, password, erase] {
if (job->error() == 103) {
QJsonObject replyData = job->jsonData();
AuthenticationData authData;
authData.session = replyData["session"_L1].toString();
authData.authInfo["password"_L1] = password;
authData.type = "m.login.password"_L1;
authData.authInfo["user"_L1] = user()->id();
QJsonObject identifier = {{"type"_L1, "m.id.user"_L1}, {"user"_L1, user()->id()}};
authData.authInfo["identifier"_L1] = identifier;
auto innerJob = callApi<DeactivateAccountJob>(authData, QString{}, erase);
connect(innerJob, &BaseJob::success, this, [this]() {
logout(false);
});
}
});
}
@@ -335,19 +342,19 @@ void NeoChatConnection::createRoom(const QString &name, const QString &topic, co
});
}
Connection::createRoom(Connection::PublishRoom, QString(), name, topic, QStringList(), {}, {}, {}, initialStateEvents)
.then(
[parent, setChildParent, this](const auto &job) {
if (parent.isEmpty() || !setChildParent) {
return;
}
const auto job = Connection::createRoom(Connection::PublishRoom, QString(), name, topic, QStringList(), {}, {}, {}, initialStateEvents);
if (!parent.isEmpty()) {
connect(job, &Quotient::CreateRoomJob::success, this, [this, parent, setChildParent, job]() {
if (setChildParent) {
if (auto parentRoom = room(parent)) {
parentRoom->setState(u"m.space.child"_s, job->roomId(), QJsonObject{{"via"_L1, QJsonArray{domain()}}});
}
},
[this](const auto &job) {
Q_EMIT errorOccured(i18n("Room creation failed: %1", job->errorString()));
});
}
});
}
connect(job, &CreateRoomJob::failure, this, [this, job] {
Q_EMIT errorOccured(i18n("Room creation failed: %1", job->errorString()));
});
}
void NeoChatConnection::createSpace(const QString &name, const QString &topic, const QString &parent, bool setChildParent)
@@ -364,19 +371,20 @@ void NeoChatConnection::createSpace(const QString &name, const QString &topic, c
});
}
Connection::createRoom(Connection::UnpublishRoom, {}, name, topic, {}, {}, {}, false, initialStateEvents, {}, QJsonObject{{"type"_L1, "m.space"_L1}})
.then(
[parent, setChildParent, this](const auto &job) {
if (parent.isEmpty() || !setChildParent) {
return;
}
const auto job =
Connection::createRoom(Connection::UnpublishRoom, {}, name, topic, {}, {}, {}, false, initialStateEvents, {}, QJsonObject{{"type"_L1, "m.space"_L1}});
if (!parent.isEmpty()) {
connect(job, &Quotient::CreateRoomJob::success, this, [this, parent, setChildParent, job]() {
if (setChildParent) {
if (auto parentRoom = room(parent)) {
parentRoom->setState(u"m.space.child"_s, job->roomId(), QJsonObject{{"via"_L1, QJsonArray{domain()}}});
}
},
[this](const auto &job) {
Q_EMIT errorOccured(i18n("Space creation failed: %1", job->errorString()));
});
}
});
}
connect(job, &CreateRoomJob::failure, this, [this, job] {
Q_EMIT errorOccured(i18n("Space creation failed: %1", job->errorString()));
});
}
Quotient::ForgetRoomJob *NeoChatConnection::forgetRoom(const QString &id)
@@ -525,11 +533,7 @@ KeyImport::Error NeoChatConnection::exportMegolmSessions(const QString &passphra
}
QUrl url(path);
QFile file(url.toLocalFile());
auto ok = file.open(QFile::WriteOnly);
if (!ok) {
qWarning() << "Failed to open" << file.fileName() << file.errorString();
return KeyImport::OtherError;
}
file.open(QFile::WriteOnly);
file.write(result.value());
file.close();
return KeyImport::Success;

View File

@@ -261,10 +261,7 @@ QCoro::Task<void> NeoChatRoom::doUploadFile(QUrl url, QString body, std::optiona
QTemporaryFile file;
file.setFileTemplate(QStringLiteral("XXXXXX.jpg"));
auto ok = file.open();
if (!ok) {
qWarning() << "Failed to open" << file.fileName() << file.errorString();
}
file.open();
const auto thumbnailImage = sink.videoFrame().toImage();
Q_UNUSED(thumbnailImage.save(file.fileName()))
@@ -513,9 +510,12 @@ QUrl NeoChatRoom::avatarMediaUrl() const
void NeoChatRoom::changeAvatar(const QUrl &localFile)
{
connection()->uploadFile(localFile.toLocalFile()).onResult([this](const auto &job) {
connection()->callApi<SetRoomStateWithKeyJob>(id(), "m.room.avatar"_L1, QString(), QJsonObject{{"url"_L1, job->contentUri().toString()}});
});
const auto job = connection()->uploadFile(localFile.toLocalFile());
if (isJobPending(job)) {
connect(job, &BaseJob::success, this, [this, job] {
connection()->callApi<SetRoomStateWithKeyJob>(id(), "m.room.avatar"_L1, QString(), QJsonObject{{"url"_L1, job->contentUri().toString()}});
});
}
}
void NeoChatRoom::toggleReaction(const QString &eventId, const QString &reaction)
@@ -625,8 +625,8 @@ void NeoChatRoom::deleteMessagesByUser(const QString &user, const QString &reaso
QString NeoChatRoom::historyVisibility() const
{
if (const auto stateEvent = currentState().get("m.room.history_visibility"_L1)) {
return stateEvent->contentPart<QString>("history_visibility"_L1);
if (auto stateEvent = currentState().get("m.room.history_visibility"_L1)) {
return stateEvent->contentJson()["history_visibility"_L1].toString();
}
return {};
}
@@ -883,11 +883,10 @@ void NeoChatRoom::addParent(const QString &parentId, bool canonical, bool setPar
setState("m.space.parent"_L1, parentId, QJsonObject{{"canonical"_L1, canonical}, {"via"_L1, QJsonArray{connection()->domain()}}});
if (!setParentChild) {
return;
}
if (auto parent = static_cast<NeoChatRoom *>(connection()->room(parentId))) {
parent->setState("m.space.child"_L1, id(), QJsonObject{{"via"_L1, QJsonArray{connection()->domain()}}});
if (setParentChild) {
if (auto parent = static_cast<NeoChatRoom *>(connection()->room(parentId))) {
parent->setState("m.space.child"_L1, id(), QJsonObject{{"via"_L1, QJsonArray{connection()->domain()}}});
}
}
}
@@ -938,27 +937,23 @@ void NeoChatRoom::addChild(const QString &childId, bool setChildParent, bool can
}
setState("m.space.child"_L1, childId, QJsonObject{{"via"_L1, QJsonArray{connection()->domain()}}, {"suggested"_L1, suggested}, {"order"_L1, order}});
if (!setChildParent) {
return;
}
if (auto child = static_cast<NeoChatRoom *>(connection()->room(childId))) {
if (!child->canSendState("m.space.parent"_L1)) {
return;
}
child->setState("m.space.parent"_L1, id(), QJsonObject{{"canonical"_L1, canonical}, {"via"_L1, QJsonArray{connection()->domain()}}});
if (setChildParent) {
if (auto child = static_cast<NeoChatRoom *>(connection()->room(childId))) {
if (child->canSendState("m.space.parent"_L1)) {
child->setState("m.space.parent"_L1, id(), QJsonObject{{"canonical"_L1, canonical}, {"via"_L1, QJsonArray{connection()->domain()}}});
if (!canonical) {
return;
}
// Only one canonical parent can exist so make sure others are set to false.
auto parentEvents = child->currentState().eventsOfType("m.space.parent"_L1);
for (const auto &parentEvent : parentEvents) {
if (!parentEvent->contentPart<bool>("canonical"_L1)) {
continue;
if (canonical) {
// Only one canonical parent can exist so make sure others are set to false.
auto parentEvents = child->currentState().eventsOfType("m.space.parent"_L1);
for (const auto &parentEvent : parentEvents) {
if (parentEvent->contentPart<bool>("canonical"_L1)) {
auto content = parentEvent->contentJson();
content.insert("canonical"_L1, false);
setState("m.space.parent"_L1, parentEvent->stateKey(), content);
}
}
}
}
auto content = parentEvent->contentJson();
content.insert("canonical"_L1, false);
setState("m.space.parent"_L1, parentEvent->stateKey(), content);
}
}
}
@@ -973,14 +968,12 @@ void NeoChatRoom::removeChild(const QString &childId, bool unsetChildParent)
}
setState("m.space.child"_L1, childId, {});
if (!unsetChildParent) {
return;
}
if (auto child = static_cast<NeoChatRoom *>(connection()->room(childId))) {
if (!child->canSendState("m.space.parent"_L1) || !child->currentState().contains("m.space.parent"_L1, id())) {
return;
if (unsetChildParent) {
if (auto child = static_cast<NeoChatRoom *>(connection()->room(childId))) {
if (child->canSendState("m.space.parent"_L1) && child->currentState().contains("m.space.parent"_L1, id())) {
child->setState("m.space.parent"_L1, id(), {});
}
}
child->setState("m.space.parent"_L1, id(), {});
}
}
@@ -1112,8 +1105,10 @@ void NeoChatRoom::setPushNotificationState(PushNotificationState::State state)
const QList<PushCondition> conditions = {pushCondition};
// Add new override rule and make sure it's enabled
connection()->callApi<SetPushRuleJob>("override"_L1, id(), actions, QString(), QString(), conditions, QString()).onResult([this]() {
connection()->callApi<SetPushRuleEnabledJob>("override"_L1, id(), true).onResult([this]() {
auto job = connection()->callApi<SetPushRuleJob>("override"_L1, id(), actions, QString(), QString(), conditions, QString());
connect(job, &BaseJob::success, this, [this]() {
auto enableJob = connection()->callApi<SetPushRuleEnabledJob>("override"_L1, id(), true);
connect(enableJob, &BaseJob::success, this, [this]() {
m_pushNotificationStateUpdating = false;
});
});
@@ -1136,8 +1131,10 @@ void NeoChatRoom::setPushNotificationState(PushNotificationState::State state)
// No conditions for a room rule
const QList<PushCondition> conditions;
connection()->callApi<SetPushRuleJob>("room"_L1, id(), actions, QString(), QString(), conditions, QString()).onResult([this]() {
connection()->callApi<SetPushRuleEnabledJob>("room"_L1, id(), true).onResult([this]() {
auto setJob = connection()->callApi<SetPushRuleJob>("room"_L1, id(), actions, QString(), QString(), conditions, QString());
connect(setJob, &BaseJob::success, this, [this]() {
auto enableJob = connection()->callApi<SetPushRuleEnabledJob>("room"_L1, id(), true);
connect(enableJob, &BaseJob::success, this, [this]() {
m_pushNotificationStateUpdating = false;
});
});
@@ -1165,8 +1162,10 @@ void NeoChatRoom::setPushNotificationState(PushNotificationState::State state)
const QList<PushCondition> conditions;
// Add new room rule and make sure enabled
connection()->callApi<SetPushRuleJob>("room"_L1, id(), actions, QString(), QString(), conditions, QString()).onResult([this]() {
connection()->callApi<SetPushRuleEnabledJob>("room"_L1, id(), true).onResult([this]() {
auto setJob = connection()->callApi<SetPushRuleJob>("room"_L1, id(), actions, QString(), QString(), conditions, QString());
connect(setJob, &BaseJob::success, this, [this]() {
auto enableJob = connection()->callApi<SetPushRuleEnabledJob>("room"_L1, id(), true);
connect(enableJob, &BaseJob::success, this, [this]() {
m_pushNotificationStateUpdating = false;
});
});
@@ -1233,15 +1232,20 @@ void NeoChatRoom::updatePushNotificationState(QString type)
void NeoChatRoom::reportEvent(const QString &eventId, const QString &reason)
{
auto job = connection()->callApi<ReportContentJob>(id(), eventId, -50, reason).onResult([this]() {
Q_EMIT showMessage(MessageType::Positive, i18n("Report sent successfully."));
auto job = connection()->callApi<ReportContentJob>(id(), eventId, -50, reason);
connect(job, &BaseJob::finished, this, [this, job]() {
if (job->error() == BaseJob::Success) {
Q_EMIT showMessage(MessageType::Positive, i18n("Report sent successfully."));
}
});
}
QByteArray NeoChatRoom::getEventJsonSource(const QString &eventId)
{
if (const auto evtIt = findInTimeline(eventId); evtIt != messageEvents().rend() && is<RoomEvent>(**evtIt)) {
return QJsonDocument(evtIt->viewAs<RoomEvent>()->fullJson()).toJson();
auto evtIt = findInTimeline(eventId);
if (evtIt != messageEvents().rend() && is<RoomEvent>(**evtIt)) {
const auto event = evtIt->viewAs<RoomEvent>();
return QJsonDocument(event->fullJson()).toJson();
}
return {};
}
@@ -1529,11 +1533,13 @@ void NeoChatRoom::download(const QString &eventId, const QUrl &localFilename)
void NeoChatRoom::mapAlias(const QString &alias)
{
connection()->callApi<GetLocalAliasesJob>(id()).onResult([this, alias](const auto &job) {
if (job->aliases().contains(alias)) {
auto getLocalAliasesJob = connection()->callApi<GetLocalAliasesJob>(id());
connect(getLocalAliasesJob, &BaseJob::success, this, [this, getLocalAliasesJob, alias] {
if (getLocalAliasesJob->aliases().contains(alias)) {
return;
} else {
connection()->callApi<SetRoomAliasJob>(alias, id()).onResult([this, alias] {
auto setRoomAliasJob = connection()->callApi<SetRoomAliasJob>(alias, id());
connect(setRoomAliasJob, &BaseJob::success, this, [this, alias] {
auto newAltAliases = altAliases();
newAltAliases.append(alias);
setLocalAliases(newAltAliases);
@@ -1625,25 +1631,23 @@ void NeoChatRoom::downloadEventFromServer(const QString &eventId)
Q_EMIT extraEventLoaded(eventId);
return;
}
connection()
->callApi<GetOneRoomEventJob>(id(), eventId)
.then(
[this, eventId](const auto &job) {
// The event may have arrived in the meantime so check it's not in the timeline.
if (findInTimeline(eventId) != historyEdge()) {
Q_EMIT extraEventLoaded(eventId);
return;
}
auto job = connection()->callApi<GetOneRoomEventJob>(id(), eventId);
connect(job, &BaseJob::success, this, [this, job, eventId] {
// The event may have arrived in the meantime so check it's not in the timeline.
if (findInTimeline(eventId) != historyEdge()) {
Q_EMIT extraEventLoaded(eventId);
return;
}
event_ptr_tt<RoomEvent> event = fromJson<event_ptr_tt<RoomEvent>>(job->jsonData());
m_extraEvents.push_back(std::move(event));
Q_EMIT extraEventLoaded(eventId);
},
[this, eventId](const auto &job) {
if (job->error() == BaseJob::NotFound) {
Q_EMIT extraEventNotFound(eventId);
}
});
event_ptr_tt<RoomEvent> event = fromJson<event_ptr_tt<RoomEvent>>(job->jsonData());
m_extraEvents.push_back(std::move(event));
Q_EMIT extraEventLoaded(eventId);
});
connect(job, &BaseJob::failure, this, [this, job, eventId] {
if (job->error() == BaseJob::NotFound) {
Q_EMIT extraEventNotFound(eventId);
}
});
}
std::pair<const Quotient::RoomEvent *, bool> NeoChatRoom::getEvent(const QString &eventId) const

View File

@@ -112,7 +112,7 @@ Kirigami.ScrollablePage {
* @brief Force the search to be updated if the model has a valid search function.
*/
function updateSearch() {
root.model.search();
searchTimer.restart();
}
header: QQC2.Control {
@@ -142,13 +142,10 @@ Kirigami.ScrollablePage {
Layout.fillWidth: true
Keys.onEnterPressed: searchButton.clicked()
Keys.onReturnPressed: searchButton.clicked()
onTextChanged: root.model.searchText = text
onAccepted: {
// If the text is empty, call the search model immediately because it will early-return.
if (root.model.searchText.length === 0) {
root.model.search();
} else {
searchTimer.restart();
onTextChanged: {
searchTimer.restart();
if (root.model) {
root.model.searchText = text;
}
}
}
@@ -161,7 +158,6 @@ Kirigami.ScrollablePage {
onClicked: {
if (typeof root.model.search === 'function') {
searchTimer.stop();
root.model.search();
}
}
@@ -191,7 +187,6 @@ Kirigami.ScrollablePage {
Kirigami.PlaceholderMessage {
id: noSearchMessage
icon.name: "search"
anchors.centerIn: parent
visible: searchField.text.length === 0 && listView.count === 0 && customPlaceholder.text.length === 0
helpfulAction: root.noSearchHelpfulAction
@@ -199,9 +194,8 @@ Kirigami.ScrollablePage {
Kirigami.PlaceholderMessage {
id: noResultMessage
icon.name: "search"
anchors.centerIn: parent
visible: searchField.text.length > 0 && listView.count === 0 && (!root.model.searching && !searchTimer.running) && customPlaceholder.text.length === 0
visible: searchField.text.length > 0 && listView.count === 0 && !root.model.searching && customPlaceholder.text.length === 0
helpfulAction: root.noResultHelpfulAction
}
@@ -214,7 +208,7 @@ Kirigami.ScrollablePage {
Kirigami.LoadingPlaceholder {
anchors.centerIn: parent
visible: searchField.text.length > 0 && listView.count === 0 && (root.model.searching || searchTimer.running) && customPlaceholder.text.length === 0
visible: searchField.text.length > 0 && listView.count === 0 && root.model.searching && customPlaceholder.text.length === 0
}
Keys.onUpPressed: {

View File

@@ -61,12 +61,13 @@ void SpaceHierarchyCache::populateSpaceHierarchy(const QString &spaceId)
}
m_nextBatchTokens[spaceId] = QString();
m_connection->callApi<GetSpaceHierarchyJob>(spaceId, std::nullopt, std::nullopt, std::nullopt, *m_nextBatchTokens[spaceId])
.onResult([this, spaceId](const auto &job) {
addBatch(spaceId, job);
});
auto job = m_connection->callApi<GetSpaceHierarchyJob>(spaceId, std::nullopt, std::nullopt, std::nullopt, *m_nextBatchTokens[spaceId]);
auto group = KConfigGroup(KSharedConfig::openStateConfig("SpaceHierarchy"_L1), "Cache"_L1);
m_spaceHierarchy.insert(spaceId, group.readEntry(spaceId, QStringList()));
connect(job, &BaseJob::success, this, [this, job, spaceId]() {
addBatch(spaceId, job);
});
}
void SpaceHierarchyCache::addBatch(const QString &spaceId, Quotient::GetSpaceHierarchyJob *job)
@@ -89,10 +90,10 @@ void SpaceHierarchyCache::addBatch(const QString &spaceId, Quotient::GetSpaceHie
const auto nextBatchToken = job->nextBatch();
if (!nextBatchToken.isEmpty() && nextBatchToken != *m_nextBatchTokens[spaceId] && m_connection) {
*m_nextBatchTokens[spaceId] = nextBatchToken;
m_connection->callApi<GetSpaceHierarchyJob>(spaceId, std::nullopt, std::nullopt, std::nullopt, *m_nextBatchTokens[spaceId])
.onResult([this, spaceId](const auto &nextJob) {
addBatch(spaceId, nextJob);
});
auto nextJob = m_connection->callApi<GetSpaceHierarchyJob>(spaceId, std::nullopt, std::nullopt, std::nullopt, *m_nextBatchTokens[spaceId]);
connect(nextJob, &BaseJob::success, this, [this, nextJob, spaceId]() {
addBatch(spaceId, nextJob);
});
} else {
m_nextBatchTokens[spaceId].reset();
}

View File

@@ -49,7 +49,6 @@ RowLayout {
textFormat: Text.PlainText
font.weight: Font.Bold
elide: Text.ElideRight
clip: true // Intentional to limit insane Unicode in display names
function openUserMenu(): void {
const menu = Qt.createComponent("org.kde.neochat", "UserMenu").createObject(root, {

View File

@@ -26,7 +26,6 @@ QQC2.Control {
Layout.fillWidth: true
Layout.maximumWidth: Message.maxContentWidth
implicitWidth: Message.maxContentWidth
contentItem: ColumnLayout {
Loader {

View File

@@ -129,7 +129,7 @@ QQC2.Control {
TapHandler {
acceptedDevices: PointerDevice.TouchScreen
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.author, root.Message.selectedText, root.Message.hoveredLink);
}
background: null

View File

@@ -27,15 +27,6 @@ ColumnLayout {
*/
required property string display
/**
* @brief The attributes of the component.
*/
required property var componentAttributes
required property int index
required property NeochatRoomMember author
Layout.fillWidth: true
Layout.maximumWidth: Message.maxContentWidth
@@ -92,9 +83,5 @@ ColumnLayout {
TextComponent {
display: root.display
visible: root.display !== ""
componentAttributes: root.componentAttributes
index: root.index
eventId: root.eventId
author: root.author
}
}

View File

@@ -19,7 +19,7 @@ RowLayout {
Layout.fillWidth: true
Layout.maximumWidth: Message.maxContentWidth
spacing: 0
spacing: Kirigami.Units.smallSpacing
QQC2.BusyIndicator {}
Kirigami.Heading {
@@ -27,7 +27,7 @@ RowLayout {
Layout.fillWidth: true
verticalAlignment: Text.AlignVCenter
level: 2
text: root.display.length > 0 ? root.display : i18nc("@info Loading this message", "Loading…")
text: root.display.length > 0 ? root.display : i18n("Loading")
}
}

View File

@@ -41,8 +41,6 @@ ColumnLayout {
*/
required property var componentAttributes
required property int index
Layout.fillWidth: true
Layout.maximumWidth: Message.maxContentWidth
@@ -126,7 +124,5 @@ ColumnLayout {
author: root.author
display: root.display
visible: root.display !== ""
index: root.index
componentAttributes: root.componentAttributes
}
}

View File

@@ -54,8 +54,6 @@ QQC2.Control {
textFormat: TextEdit.RichText
wrapMode: TextEdit.Wrap
color: Kirigami.Theme.textColor
selectedTextColor: Kirigami.Theme.highlightedTextColor
selectionColor: Kirigami.Theme.highlightColor
font.italic: true
@@ -65,7 +63,7 @@ QQC2.Control {
enabled: !quoteText.hoveredLink
acceptedDevices: PointerDevice.TouchScreen
acceptedButtons: Qt.LeftButton
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.author, root.Message.selectedText, root.Message.hoveredLink);
}
}

View File

@@ -95,12 +95,12 @@ TextEdit {
enabled: !root.hoveredLink
acceptedButtons: Qt.LeftButton
acceptedDevices: PointerDevice.TouchScreen
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
onLongPressed: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.author, root.Message.selectedText, root.Message.hoveredLink);
}
TapHandler {
acceptedButtons: Qt.RightButton
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus
gesturePolicy: TapHandler.WithinBounds
onTapped: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.Message.selectedText, root.Message.hoveredLink);
onTapped: RoomManager.viewEventMenu(root.eventId, root.Message.room, root.author, root.Message.selectedText, root.Message.hoveredLink);
}
}

View File

@@ -256,8 +256,7 @@ void EventMessageContentModel::resetModel()
const auto event = m_room->getEvent(m_eventId);
if (event.first == nullptr) {
m_components +=
MessageComponent{MessageComponentType::Loading, m_isReply ? i18nc("@info", "Loading reply…") : i18nc("@info Loading this message", "Loading…"), {}};
m_components += MessageComponent{MessageComponentType::Loading, m_isReply ? i18n("Loading reply") : i18n("Loading"), {}};
endResetModel();
return;
}
@@ -449,7 +448,7 @@ QList<MessageComponent> EventMessageContentModel::componentsForType(MessageCompo
}
case MessageComponentType::Location:
return {MessageComponent{type,
EventHandler::plainBody(m_room, event.first),
QString(),
{
{u"latitude"_s, EventHandler::latitude(event.first)},
{u"longitude"_s, EventHandler::longitude(event.first)},

View File

@@ -259,7 +259,7 @@ MessageComponent MessageContentModel::linkPreviewComponent(const QUrl &link)
it->type = MessageComponentType::LinkPreview;
Q_EMIT dataChanged(index(it - m_components.begin()), index(it - m_components.begin()), {ComponentTypeRole});
}
return ++it;
return it;
});
}
});

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