Event Handler
Similar to text handler, pull out the disparate array of functions which format information from an event ready for display in the UI and put in a handler class with a test suite. requires https://github.com/quotient-im/libQuotient/pull/686
This commit is contained in:
@@ -26,3 +26,9 @@ ecm_add_test(
|
|||||||
LINK_LIBRARIES neochat Qt::Test
|
LINK_LIBRARIES neochat Qt::Test
|
||||||
TEST_NAME mediasizehelpertest
|
TEST_NAME mediasizehelpertest
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ecm_add_test(
|
||||||
|
eventhandlertest.cpp
|
||||||
|
LINK_LIBRARIES neochat Qt::Test
|
||||||
|
TEST_NAME eventhandlertest
|
||||||
|
)
|
||||||
|
|||||||
729
autotests/eventhandlertest.cpp
Normal file
729
autotests/eventhandlertest.cpp
Normal file
@@ -0,0 +1,729 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QTest>
|
||||||
|
|
||||||
|
#include "eventhandler.h"
|
||||||
|
|
||||||
|
#include <KFormat>
|
||||||
|
|
||||||
|
#include <Quotient/connection.h>
|
||||||
|
#include <Quotient/quotient_common.h>
|
||||||
|
#include <Quotient/syncdata.h>
|
||||||
|
|
||||||
|
#include "enums/delegatetype.h"
|
||||||
|
#include "linkpreviewer.h"
|
||||||
|
#include "models/reactionmodel.h"
|
||||||
|
#include "neochatroom.h"
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
|
using namespace Quotient;
|
||||||
|
|
||||||
|
class TestRoom : public NeoChatRoom
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using NeoChatRoom::NeoChatRoom;
|
||||||
|
|
||||||
|
void update(SyncRoomData &&data, bool fromCache = false)
|
||||||
|
{
|
||||||
|
Room::updateData(std::move(data), fromCache);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class EventHandlerTest : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private:
|
||||||
|
Connection *connection = nullptr;
|
||||||
|
TestRoom *room = nullptr;
|
||||||
|
EventHandler eventHandler;
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void initTestCase();
|
||||||
|
|
||||||
|
void eventId();
|
||||||
|
void delegateType_data();
|
||||||
|
void delegateType();
|
||||||
|
void author();
|
||||||
|
void authorDisplayName();
|
||||||
|
void time();
|
||||||
|
void timeString();
|
||||||
|
void highlighted();
|
||||||
|
void hidden();
|
||||||
|
void body();
|
||||||
|
void genericBody_data();
|
||||||
|
void genericBody();
|
||||||
|
void mediaInfo();
|
||||||
|
void linkPreviewer();
|
||||||
|
void reactions();
|
||||||
|
void hasReply();
|
||||||
|
void replyId();
|
||||||
|
void replyDelegateType();
|
||||||
|
void replyAuthor();
|
||||||
|
void replyBody();
|
||||||
|
void replyMediaInfo();
|
||||||
|
void location();
|
||||||
|
void readMarkers();
|
||||||
|
};
|
||||||
|
|
||||||
|
void EventHandlerTest::initTestCase()
|
||||||
|
{
|
||||||
|
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
|
||||||
|
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
|
||||||
|
|
||||||
|
const auto json = QJsonDocument::fromJson(R"EVENT({
|
||||||
|
"account_data": {
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"tags": {
|
||||||
|
"u.work": {
|
||||||
|
"order": 0.9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "m.tag"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"custom_config_key": "custom_config_value"
|
||||||
|
},
|
||||||
|
"type": "org.example.custom.room.config"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ephemeral": {
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"user_ids": [
|
||||||
|
"@alice:matrix.org",
|
||||||
|
"@bob:example.com"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||||
|
"type": "m.typing"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"$153456789:example.org": {
|
||||||
|
"m.read": {
|
||||||
|
"@alice:matrix.org": {
|
||||||
|
"ts": 1436451550453
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "m.receipt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"$1532735824654:example.org": {
|
||||||
|
"m.read": {
|
||||||
|
"@bob:example.com": {
|
||||||
|
"ts": 1436451550453
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "m.receipt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"$1532735824654:example.org": {
|
||||||
|
"m.read": {
|
||||||
|
"@tim:example.com": {
|
||||||
|
"ts": 1436451550454
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "m.receipt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"$1532735824654:example.org": {
|
||||||
|
"m.read": {
|
||||||
|
"@jeff:example.com": {
|
||||||
|
"ts": 1436451550455
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "m.receipt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"$1532735824654:example.org": {
|
||||||
|
"m.read": {
|
||||||
|
"@tina:example.com": {
|
||||||
|
"ts": 1436451550456
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "m.receipt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"$1532735824654:example.org": {
|
||||||
|
"m.read": {
|
||||||
|
"@sally:example.com": {
|
||||||
|
"ts": 1436451550457
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "m.receipt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"$1532735824654:example.org": {
|
||||||
|
"m.read": {
|
||||||
|
"@fred:example.com": {
|
||||||
|
"ts": 1436451550458
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "m.receipt"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||||
|
"displayname": "Alice Margatroid",
|
||||||
|
"membership": "join",
|
||||||
|
"reason": "Looking for support"
|
||||||
|
},
|
||||||
|
"event_id": "$143273582443PhrSn:example.org",
|
||||||
|
"origin_server_ts": 1432735824653,
|
||||||
|
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"state_key": "@alice:example.org",
|
||||||
|
"type": "m.room.member",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1234
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"m.heroes": [
|
||||||
|
"@alice:example.com",
|
||||||
|
"@bob:example.com"
|
||||||
|
],
|
||||||
|
"m.invited_member_count": 0,
|
||||||
|
"m.joined_member_count": 2
|
||||||
|
},
|
||||||
|
"timeline": {
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"body": "This is an example\ntext message",
|
||||||
|
"format": "org.matrix.custom.html",
|
||||||
|
"formatted_body": "<b>This is an example<br>text message</b>",
|
||||||
|
"msgtype": "m.text"
|
||||||
|
},
|
||||||
|
"event_id": "$153456789:example.org",
|
||||||
|
"origin_server_ts": 1432735824654,
|
||||||
|
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"type": "m.room.message",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1232
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"avatar_url": "mxc://kde.org/123456",
|
||||||
|
"displayname": "after",
|
||||||
|
"membership": "join"
|
||||||
|
},
|
||||||
|
"origin_server_ts": 1690651134736,
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"state_key": "@example:example.org",
|
||||||
|
"type": "m.room.member",
|
||||||
|
"unsigned": {
|
||||||
|
"replaces_state": "$1234567890:example.org",
|
||||||
|
"prev_content": {
|
||||||
|
"avatar_url": "mxc://kde.org/12345",
|
||||||
|
"displayname": "before",
|
||||||
|
"membership": "join"
|
||||||
|
},
|
||||||
|
"prev_sender": "@example:example.orgg",
|
||||||
|
"age": 1234
|
||||||
|
},
|
||||||
|
"event_id": "$143273583553PhrSn:example.org",
|
||||||
|
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"body": "This is a highlight @bob:kde.org and this is a link https://kde.org",
|
||||||
|
"format": "org.matrix.custom.html",
|
||||||
|
"msgtype": "m.text"
|
||||||
|
},
|
||||||
|
"event_id": "$1532735824654:example.org",
|
||||||
|
"origin_server_ts": 1532735824654,
|
||||||
|
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"type": "m.room.message",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 1233
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"m.relates_to": {
|
||||||
|
"event_id": "$153456789:example.org",
|
||||||
|
"key": "👍",
|
||||||
|
"rel_type": "m.annotation"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"origin_server_ts": 1690322545182,
|
||||||
|
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||||
|
"sender": "@alice:matrix.org",
|
||||||
|
"type": "m.reaction",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 390159120
|
||||||
|
},
|
||||||
|
"event_id": "$163456789:example.org",
|
||||||
|
"age": 390159120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"age": 4926305285,
|
||||||
|
"content": {
|
||||||
|
"body": "video caption",
|
||||||
|
"filename": "video.mp4",
|
||||||
|
"info": {
|
||||||
|
"duration": 10,
|
||||||
|
"h": 1080,
|
||||||
|
"mimetype": "video/mp4",
|
||||||
|
"size": 62650636,
|
||||||
|
"w": 1920,
|
||||||
|
"thumbnail_info": {
|
||||||
|
"h": 450,
|
||||||
|
"mimetype": "image/jpeg",
|
||||||
|
"size": 382249,
|
||||||
|
"w": 800
|
||||||
|
},
|
||||||
|
"thumbnail_url": "mxc://kde.org/2234567"
|
||||||
|
},
|
||||||
|
"msgtype": "m.video",
|
||||||
|
"url": "mxc://kde.org/1234567"
|
||||||
|
},
|
||||||
|
"event_id": "$263456789:example.org",
|
||||||
|
"origin_server_ts": 1685793783330,
|
||||||
|
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"type": "m.room.message",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 4926305285
|
||||||
|
},
|
||||||
|
"user_id": "@example:example.org"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"body": "> <@example:example.org> This is an example\ntext message\n\nreply",
|
||||||
|
"format": "org.matrix.custom.html",
|
||||||
|
"formatted_body": "<mx-reply><blockquote><a href=\"https://matrix.to/#/!jEsUZKDJdhlrceRyVU:example.org/$153456789:example.org?via=kde.org&via=matrix.org\">In reply to</a> <a href=\"https://matrix.to/#/@example:example.org\">@example:example.org</a><br><b>This is an example<br>text message</b></blockquote></mx-reply>reply",
|
||||||
|
"m.relates_to": {
|
||||||
|
"m.in_reply_to": {
|
||||||
|
"event_id": "$153456789:example.org"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"msgtype": "m.text"
|
||||||
|
},
|
||||||
|
"origin_server_ts": 1690725965572,
|
||||||
|
"sender": "@alice:matrix.org",
|
||||||
|
"type": "m.room.message",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 98
|
||||||
|
},
|
||||||
|
"event_id": "$154456789:example.org",
|
||||||
|
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"body": "> <@example:example.org> video caption\n\nreply",
|
||||||
|
"m.relates_to": {
|
||||||
|
"m.in_reply_to": {
|
||||||
|
"event_id": "$263456789:example.org"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"msgtype": "m.text"
|
||||||
|
},
|
||||||
|
"origin_server_ts": 1690725965573,
|
||||||
|
"sender": "@alice:matrix.org",
|
||||||
|
"type": "m.room.message",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 98
|
||||||
|
},
|
||||||
|
"event_id": "$154456799:example.org",
|
||||||
|
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"age": 96845207,
|
||||||
|
"content": {
|
||||||
|
"body": "Lat: 51.7035, Lon: -1.14394",
|
||||||
|
"geo_uri": "geo:51.7035,-1.14394",
|
||||||
|
"msgtype": "m.location",
|
||||||
|
"org.matrix.msc1767.text": "Lat: 51.7035, Lon: -1.14394",
|
||||||
|
"org.matrix.msc3488.asset": {
|
||||||
|
"type": "m.pin"
|
||||||
|
},
|
||||||
|
"org.matrix.msc3488.location": {
|
||||||
|
"uri": "geo:51.7035,-1.14394"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"event_id": "$1544567999:example.org",
|
||||||
|
"origin_server_ts": 1690821582876,
|
||||||
|
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||||
|
"sender": "@example:example.org",
|
||||||
|
"type": "m.room.message",
|
||||||
|
"unsigned": {
|
||||||
|
"age": 96845207
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"limited": true,
|
||||||
|
"prev_batch": "t34-23535_0_0"
|
||||||
|
}
|
||||||
|
})EVENT");
|
||||||
|
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object());
|
||||||
|
room->update(std::move(roomData));
|
||||||
|
|
||||||
|
eventHandler.setRoom(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::eventId()
|
||||||
|
{
|
||||||
|
eventHandler.setEvent(room->messageEvents().at(0).get());
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.getId(), QStringLiteral("$153456789:example.org"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::delegateType_data()
|
||||||
|
{
|
||||||
|
QTest::addColumn<int>("eventNum");
|
||||||
|
QTest::addColumn<DelegateType::Type>("delegateType");
|
||||||
|
|
||||||
|
QTest::newRow("message") << 0 << DelegateType::Message;
|
||||||
|
QTest::newRow("state") << 1 << DelegateType::State;
|
||||||
|
QTest::newRow("message 2") << 2 << DelegateType::Message;
|
||||||
|
QTest::newRow("reaction") << 3 << DelegateType::Other;
|
||||||
|
QTest::newRow("video") << 4 << DelegateType::Video;
|
||||||
|
QTest::newRow("location") << 7 << DelegateType::Location;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::delegateType()
|
||||||
|
{
|
||||||
|
QFETCH(int, eventNum);
|
||||||
|
QFETCH(DelegateType::Type, delegateType);
|
||||||
|
|
||||||
|
eventHandler.setEvent(room->messageEvents().at(eventNum).get());
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.getDelegateType(), delegateType);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::author()
|
||||||
|
{
|
||||||
|
auto event = room->messageEvents().at(0).get();
|
||||||
|
auto author = room->user(event->senderId());
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
auto eventHandlerAuthor = eventHandler.getAuthor();
|
||||||
|
|
||||||
|
QCOMPARE(eventHandlerAuthor["isLocalUser"_ls], author->id() == room->localUser()->id());
|
||||||
|
QCOMPARE(eventHandlerAuthor["id"_ls], author->id());
|
||||||
|
QCOMPARE(eventHandlerAuthor["displayName"_ls], author->displayname(room));
|
||||||
|
QCOMPARE(eventHandlerAuthor["avatarSource"_ls], room->avatarForMember(author));
|
||||||
|
QCOMPARE(eventHandlerAuthor["avatarMediaId"_ls], author->avatarMediaId(room));
|
||||||
|
QCOMPARE(eventHandlerAuthor["color"_ls], Utils::getUserColor(author->hueF()));
|
||||||
|
QCOMPARE(eventHandlerAuthor["object"_ls], QVariant::fromValue(author));
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::authorDisplayName()
|
||||||
|
{
|
||||||
|
auto event = room->messageEvents().at(1).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.getAuthorDisplayName(), QStringLiteral("before"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::time()
|
||||||
|
{
|
||||||
|
auto event = room->messageEvents().at(0).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.getTime(), QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC));
|
||||||
|
QCOMPARE(eventHandler.getTime(true, QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC)), QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC));
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::timeString()
|
||||||
|
{
|
||||||
|
auto event = room->messageEvents().at(0).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
KFormat format;
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.getTimeString(false),
|
||||||
|
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toLocalTime().time(), QLocale::ShortFormat));
|
||||||
|
QCOMPARE(eventHandler.getTimeString(true),
|
||||||
|
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toLocalTime().date(), QLocale::ShortFormat));
|
||||||
|
QCOMPARE(eventHandler.getTimeString(false, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
|
||||||
|
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().time(), QLocale::ShortFormat));
|
||||||
|
QCOMPARE(eventHandler.getTimeString(true, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
|
||||||
|
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::ShortFormat));
|
||||||
|
QCOMPARE(eventHandler.getTimeString(false, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
|
||||||
|
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().time(), QLocale::LongFormat));
|
||||||
|
QCOMPARE(eventHandler.getTimeString(true, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
|
||||||
|
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::LongFormat));
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::highlighted()
|
||||||
|
{
|
||||||
|
auto event = room->messageEvents().at(2).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.isHighlighted(), true);
|
||||||
|
|
||||||
|
event = room->messageEvents().at(0).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.isHighlighted(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::hidden()
|
||||||
|
{
|
||||||
|
auto event = room->messageEvents().at(3).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.isHidden(), true);
|
||||||
|
|
||||||
|
event = room->messageEvents().at(0).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.isHidden(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::body()
|
||||||
|
{
|
||||||
|
auto event = room->messageEvents().at(0).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.getRichBody(), QStringLiteral("<b>This is an example<br>text message</b>"));
|
||||||
|
QCOMPARE(eventHandler.getRichBody(true), QStringLiteral("<b>This is an example text message</b>"));
|
||||||
|
QCOMPARE(eventHandler.getPlainBody(), QStringLiteral("This is an example\ntext message"));
|
||||||
|
QCOMPARE(eventHandler.getPlainBody(true), QStringLiteral("This is an example text message"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::genericBody_data()
|
||||||
|
{
|
||||||
|
QTest::addColumn<int>("eventNum");
|
||||||
|
QTest::addColumn<QString>("output");
|
||||||
|
|
||||||
|
QTest::newRow("message") << 0 << QStringLiteral("sent a message");
|
||||||
|
QTest::newRow("member") << 1 << QStringLiteral("changed their display name and updated their avatar");
|
||||||
|
QTest::newRow("message 2") << 2 << QStringLiteral("sent a message");
|
||||||
|
QTest::newRow("reaction") << 3 << QStringLiteral("Unknown event");
|
||||||
|
QTest::newRow("video") << 4 << QStringLiteral("sent a message");
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::genericBody()
|
||||||
|
{
|
||||||
|
QFETCH(int, eventNum);
|
||||||
|
QFETCH(QString, output);
|
||||||
|
|
||||||
|
eventHandler.setEvent(room->messageEvents().at(eventNum).get());
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.getGenericBody(), output);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::mediaInfo()
|
||||||
|
{
|
||||||
|
auto event = room->messageEvents().at(4).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
auto mediaInfo = eventHandler.getMediaInfo();
|
||||||
|
auto thumbnailInfo = mediaInfo["tempInfo"_ls].toMap();
|
||||||
|
|
||||||
|
QCOMPARE(mediaInfo["source"_ls], room->makeMediaUrl(event->id(), QUrl("mxc://kde.org/1234567"_ls)));
|
||||||
|
QCOMPARE(mediaInfo["mimeType"_ls], QStringLiteral("video/mp4"));
|
||||||
|
QCOMPARE(mediaInfo["mimeIcon"_ls], QStringLiteral("video-mp4"));
|
||||||
|
QCOMPARE(mediaInfo["size"_ls], 62650636);
|
||||||
|
QCOMPARE(mediaInfo["duration"_ls], 10);
|
||||||
|
QCOMPARE(mediaInfo["width"_ls], 1920);
|
||||||
|
QCOMPARE(mediaInfo["height"_ls], 1080);
|
||||||
|
QCOMPARE(thumbnailInfo["source"_ls], room->makeMediaUrl(event->id(), QUrl("mxc://kde.org/2234567"_ls)));
|
||||||
|
QCOMPARE(thumbnailInfo["mimeType"_ls], QStringLiteral("image/jpeg"));
|
||||||
|
QCOMPARE(thumbnailInfo["mimeIcon"_ls], QStringLiteral("image-jpeg"));
|
||||||
|
QCOMPARE(thumbnailInfo["size"_ls], 382249);
|
||||||
|
QCOMPARE(thumbnailInfo["width"_ls], 800);
|
||||||
|
QCOMPARE(thumbnailInfo["height"_ls], 450);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::linkPreviewer()
|
||||||
|
{
|
||||||
|
auto event = room->messageEvents().at(2).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.getLinkPreviewer()->url(), QUrl("https://kde.org"_ls));
|
||||||
|
|
||||||
|
event = room->messageEvents().at(0).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.getLinkPreviewer(), nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::reactions()
|
||||||
|
{
|
||||||
|
auto event = room->messageEvents().at(0).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.getReactions()->rowCount(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::hasReply()
|
||||||
|
{
|
||||||
|
auto event = room->messageEvents().at(5).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.hasReply(), true);
|
||||||
|
|
||||||
|
event = room->messageEvents().at(0).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.hasReply(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::replyId()
|
||||||
|
{
|
||||||
|
auto event = room->messageEvents().at(5).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.getReplyId(), QStringLiteral("$153456789:example.org"));
|
||||||
|
|
||||||
|
event = room->messageEvents().at(0).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.getReplyId(), QStringLiteral(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::replyDelegateType()
|
||||||
|
{
|
||||||
|
auto event = room->messageEvents().at(5).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.getReplyDelegateType(), DelegateType::Message);
|
||||||
|
|
||||||
|
event = room->messageEvents().at(0).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.getReplyDelegateType(), DelegateType::Other);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::replyAuthor()
|
||||||
|
{
|
||||||
|
auto event = room->messageEvents().at(5).get();
|
||||||
|
auto replyEvent = room->messageEvents().at(0).get();
|
||||||
|
auto replyAuthor = room->user(replyEvent->senderId());
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
auto eventHandlerReplyAuthor = eventHandler.getReplyAuthor();
|
||||||
|
|
||||||
|
QCOMPARE(eventHandlerReplyAuthor["isLocalUser"_ls], replyAuthor->id() == room->localUser()->id());
|
||||||
|
QCOMPARE(eventHandlerReplyAuthor["id"_ls], replyAuthor->id());
|
||||||
|
QCOMPARE(eventHandlerReplyAuthor["displayName"_ls], replyAuthor->displayname(room));
|
||||||
|
QCOMPARE(eventHandlerReplyAuthor["avatarSource"_ls], room->avatarForMember(replyAuthor));
|
||||||
|
QCOMPARE(eventHandlerReplyAuthor["avatarMediaId"_ls], replyAuthor->avatarMediaId(room));
|
||||||
|
QCOMPARE(eventHandlerReplyAuthor["color"_ls], Utils::getUserColor(replyAuthor->hueF()));
|
||||||
|
QCOMPARE(eventHandlerReplyAuthor["object"_ls], QVariant::fromValue(replyAuthor));
|
||||||
|
|
||||||
|
event = room->messageEvents().at(0).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.getReplyAuthor(), room->getUser(nullptr));
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::replyBody()
|
||||||
|
{
|
||||||
|
auto event = room->messageEvents().at(5).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.getReplyRichBody(), QStringLiteral("<b>This is an example<br>text message</b>"));
|
||||||
|
QCOMPARE(eventHandler.getReplyRichBody(true), QStringLiteral("<b>This is an example text message</b>"));
|
||||||
|
QCOMPARE(eventHandler.getReplyPlainBody(), QStringLiteral("This is an example\ntext message"));
|
||||||
|
QCOMPARE(eventHandler.getReplyPlainBody(true), QStringLiteral("This is an example text message"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::replyMediaInfo()
|
||||||
|
{
|
||||||
|
auto event = room->messageEvents().at(6).get();
|
||||||
|
auto replyEvent = room->messageEvents().at(4).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
auto mediaInfo = eventHandler.getReplyMediaInfo();
|
||||||
|
auto thumbnailInfo = mediaInfo["tempInfo"_ls].toMap();
|
||||||
|
|
||||||
|
QCOMPARE(mediaInfo["source"_ls], room->makeMediaUrl(replyEvent->id(), QUrl("mxc://kde.org/1234567"_ls)));
|
||||||
|
QCOMPARE(mediaInfo["mimeType"_ls], QStringLiteral("video/mp4"));
|
||||||
|
QCOMPARE(mediaInfo["mimeIcon"_ls], QStringLiteral("video-mp4"));
|
||||||
|
QCOMPARE(mediaInfo["size"_ls], 62650636);
|
||||||
|
QCOMPARE(mediaInfo["duration"_ls], 10);
|
||||||
|
QCOMPARE(mediaInfo["width"_ls], 1920);
|
||||||
|
QCOMPARE(mediaInfo["height"_ls], 1080);
|
||||||
|
QCOMPARE(thumbnailInfo["source"_ls], room->makeMediaUrl(replyEvent->id(), QUrl("mxc://kde.org/2234567"_ls)));
|
||||||
|
QCOMPARE(thumbnailInfo["mimeType"_ls], QStringLiteral("image/jpeg"));
|
||||||
|
QCOMPARE(thumbnailInfo["mimeIcon"_ls], QStringLiteral("image-jpeg"));
|
||||||
|
QCOMPARE(thumbnailInfo["size"_ls], 382249);
|
||||||
|
QCOMPARE(thumbnailInfo["width"_ls], 800);
|
||||||
|
QCOMPARE(thumbnailInfo["height"_ls], 450);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::location()
|
||||||
|
{
|
||||||
|
auto event = room->messageEvents().at(7).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.getLatitude(), QStringLiteral("51.7035").toFloat());
|
||||||
|
QCOMPARE(eventHandler.getLongitude(), QStringLiteral("-1.14394").toFloat());
|
||||||
|
QCOMPARE(eventHandler.getLocationAssetType(), QStringLiteral("m.pin"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandlerTest::readMarkers()
|
||||||
|
{
|
||||||
|
auto event = room->messageEvents().at(0).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.hasReadMarkers(), true);
|
||||||
|
|
||||||
|
auto readMarkers = eventHandler.getReadMarkers();
|
||||||
|
|
||||||
|
QCOMPARE(readMarkers.size(), 1);
|
||||||
|
QCOMPARE(readMarkers[0].toMap()["id"_ls], QStringLiteral("@alice:matrix.org"));
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.getNumberExcessReadMarkers(), QString());
|
||||||
|
QCOMPARE(eventHandler.getReadMarkersString(), QStringLiteral("1 user: @alice:matrix.org"));
|
||||||
|
|
||||||
|
event = room->messageEvents().at(2).get();
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.hasReadMarkers(), true);
|
||||||
|
|
||||||
|
readMarkers = eventHandler.getReadMarkers();
|
||||||
|
|
||||||
|
QCOMPARE(readMarkers.size(), 5);
|
||||||
|
|
||||||
|
QCOMPARE(eventHandler.getNumberExcessReadMarkers(), QStringLiteral("+ 1"));
|
||||||
|
// There are no guarantees on the order of the users it will be different every time so don't match the whole string.
|
||||||
|
QCOMPARE(eventHandler.getReadMarkersString().startsWith(QStringLiteral("6 users:")), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
QTEST_MAIN(EventHandlerTest)
|
||||||
|
#include "eventhandlertest.moc"
|
||||||
@@ -363,6 +363,7 @@ void TextHandlerTest::receiveRichInPlainOut_data()
|
|||||||
|
|
||||||
QTest::newRow("ampersand") << QStringLiteral("a & b") << QStringLiteral("a & b");
|
QTest::newRow("ampersand") << QStringLiteral("a & b") << QStringLiteral("a & b");
|
||||||
QTest::newRow("quote") << QStringLiteral(""a and b"") << QStringLiteral("\"a and b\"");
|
QTest::newRow("quote") << QStringLiteral(""a and b"") << QStringLiteral("\"a and b\"");
|
||||||
|
QTest::newRow("new line") << QStringLiteral("new<br>line") << QStringLiteral("new\nline");
|
||||||
}
|
}
|
||||||
|
|
||||||
void TextHandlerTest::receiveRichInPlainOut()
|
void TextHandlerTest::receiveRichInPlainOut()
|
||||||
|
|||||||
@@ -134,6 +134,8 @@ add_library(neochat STATIC
|
|||||||
jobs/neochatchangepasswordjob.h
|
jobs/neochatchangepasswordjob.h
|
||||||
mediasizehelper.cpp
|
mediasizehelper.cpp
|
||||||
mediasizehelper.h
|
mediasizehelper.h
|
||||||
|
eventhandler.cpp
|
||||||
|
enums/delegatetype.h
|
||||||
)
|
)
|
||||||
|
|
||||||
ecm_qt_declare_logging_category(neochat
|
ecm_qt_declare_logging_category(neochat
|
||||||
@@ -145,6 +147,13 @@ ecm_qt_declare_logging_category(neochat
|
|||||||
EXPORT NEOCHAT
|
EXPORT NEOCHAT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ecm_qt_declare_logging_category(neochat
|
||||||
|
HEADER "eventhandler_logging.h"
|
||||||
|
IDENTIFIER "EventHandling"
|
||||||
|
CATEGORY_NAME "org.kde.neochat.eventhandler"
|
||||||
|
DEFAULT_SEVERITY Info
|
||||||
|
)
|
||||||
|
|
||||||
add_executable(neochat-app
|
add_executable(neochat-app
|
||||||
main.cpp
|
main.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/res.generated.qrc
|
${CMAKE_CURRENT_SOURCE_DIR}/res.generated.qrc
|
||||||
|
|||||||
43
src/enums/delegatetype.h
Normal file
43
src/enums/delegatetype.h
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class DelegateType
|
||||||
|
*
|
||||||
|
* This class is designed to define the DelegateType enumeration.
|
||||||
|
*/
|
||||||
|
class DelegateType
|
||||||
|
{
|
||||||
|
Q_GADGET
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief The type of delegate that is needed for the event.
|
||||||
|
*
|
||||||
|
* @note While similar this is not the matrix event or message type. This is
|
||||||
|
* to tell a QML ListView what delegate to show for each event. So while
|
||||||
|
* similar to the spec it is not the same.
|
||||||
|
*/
|
||||||
|
enum Type {
|
||||||
|
Emote, /**< A message that begins with /me. */
|
||||||
|
Notice, /**< A notice event. */
|
||||||
|
Image, /**< A message that is an image. */
|
||||||
|
Audio, /**< A message that is an audio recording. */
|
||||||
|
Video, /**< A message that is a video. */
|
||||||
|
File, /**< A message that is a file. */
|
||||||
|
Message, /**< A text message. */
|
||||||
|
Sticker, /**< A message that is a sticker. */
|
||||||
|
State, /**< A state event in the room. */
|
||||||
|
Encrypted, /**< An encrypted message that cannot be decrypted. */
|
||||||
|
ReadMarker, /**< The local user read marker. */
|
||||||
|
Poll, /**< The initial event for a poll. */
|
||||||
|
Location, /**< A location event. */
|
||||||
|
LiveLocation, /**< The initial event of a shared live location (i.e., the place where this is supposed to be shown in the timeline). */
|
||||||
|
Other, /**< Anything that cannot be classified as another type. */
|
||||||
|
};
|
||||||
|
Q_ENUM(Type);
|
||||||
|
};
|
||||||
983
src/eventhandler.cpp
Normal file
983
src/eventhandler.cpp
Normal file
@@ -0,0 +1,983 @@
|
|||||||
|
// 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 "eventhandler.h"
|
||||||
|
|
||||||
|
#include <KLocalizedString>
|
||||||
|
|
||||||
|
#include <Quotient/eventitem.h>
|
||||||
|
#include <Quotient/events/encryptionevent.h>
|
||||||
|
#include <Quotient/events/reactionevent.h>
|
||||||
|
#include <Quotient/events/redactionevent.h>
|
||||||
|
#include <Quotient/events/roomavatarevent.h>
|
||||||
|
#include <Quotient/events/roomcanonicalaliasevent.h>
|
||||||
|
#include <Quotient/events/roommemberevent.h>
|
||||||
|
#include <Quotient/events/roompowerlevelsevent.h>
|
||||||
|
#include <Quotient/events/simplestateevents.h>
|
||||||
|
#include <Quotient/events/stickerevent.h>
|
||||||
|
#include <Quotient/quotient_common.h>
|
||||||
|
|
||||||
|
#include "eventhandler_logging.h"
|
||||||
|
#include "events/pollevent.h"
|
||||||
|
#include "linkpreviewer.h"
|
||||||
|
#include "models/reactionmodel.h"
|
||||||
|
#include "neochatconfig.h"
|
||||||
|
#include "neochatroom.h"
|
||||||
|
#include "texthandler.h"
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
|
using namespace Quotient;
|
||||||
|
|
||||||
|
const NeoChatRoom *EventHandler::getRoom() const
|
||||||
|
{
|
||||||
|
return m_room;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandler::setRoom(const NeoChatRoom *room)
|
||||||
|
{
|
||||||
|
if (room == m_room) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_room = room;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Quotient::Event *EventHandler::getEvent() const
|
||||||
|
{
|
||||||
|
return m_event;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandler::setEvent(const Quotient::RoomEvent *event)
|
||||||
|
{
|
||||||
|
if (event == m_event) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_event = event;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString EventHandler::getId() const
|
||||||
|
{
|
||||||
|
if (m_event == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "getId called with m_event set to nullptr.";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return !m_event->id().isEmpty() ? m_event->id() : m_event->transactionId();
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateType::Type EventHandler::getDelegateTypeForEvent(const Quotient::RoomEvent *event) const
|
||||||
|
{
|
||||||
|
if (auto e = eventCast<const RoomMessageEvent>(event)) {
|
||||||
|
switch (e->msgtype()) {
|
||||||
|
case MessageEventType::Emote:
|
||||||
|
return DelegateType::Emote;
|
||||||
|
case MessageEventType::Notice:
|
||||||
|
return DelegateType::Notice;
|
||||||
|
case MessageEventType::Image:
|
||||||
|
return DelegateType::Image;
|
||||||
|
case MessageEventType::Audio:
|
||||||
|
return DelegateType::Audio;
|
||||||
|
case MessageEventType::Video:
|
||||||
|
return DelegateType::Video;
|
||||||
|
case MessageEventType::Location:
|
||||||
|
return DelegateType::Location;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (e->hasFileContent()) {
|
||||||
|
return DelegateType::File;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DelegateType::Message;
|
||||||
|
}
|
||||||
|
if (is<const StickerEvent>(*event)) {
|
||||||
|
return DelegateType::Sticker;
|
||||||
|
}
|
||||||
|
if (event->isStateEvent()) {
|
||||||
|
if (event->matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
|
||||||
|
return DelegateType::LiveLocation;
|
||||||
|
}
|
||||||
|
return DelegateType::State;
|
||||||
|
}
|
||||||
|
if (is<const EncryptedEvent>(*event)) {
|
||||||
|
return DelegateType::Encrypted;
|
||||||
|
}
|
||||||
|
if (is<PollStartEvent>(*event)) {
|
||||||
|
const auto pollEvent = eventCast<const PollStartEvent>(event);
|
||||||
|
if (pollEvent->isRedacted()) {
|
||||||
|
return DelegateType::Message;
|
||||||
|
}
|
||||||
|
return DelegateType::Poll;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DelegateType::Other;
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateType::Type EventHandler::getDelegateType() const
|
||||||
|
{
|
||||||
|
if (m_event == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "getDelegateType called with m_event set to nullptr.";
|
||||||
|
return DelegateType::Other;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDelegateTypeForEvent(m_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap EventHandler::getAuthor(bool isPending) const
|
||||||
|
{
|
||||||
|
if (m_room == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "getAuthor called with m_room set to nullptr.";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
// If we have a room we can return an empty user by handing nullptr to m_room->getUser.
|
||||||
|
if (m_event == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "getAuthor called with m_event set to nullptr. Returning empty user.";
|
||||||
|
return m_room->getUser(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
|
||||||
|
return m_room->getUser(author);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString EventHandler::getAuthorDisplayName(bool isPending) const
|
||||||
|
{
|
||||||
|
if (m_room == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "getAuthorDisplayName called with m_room set to nullptr.";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (m_event == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "getAuthorDisplayName called with m_event set to nullptr.";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is<RoomMemberEvent>(*m_event) && !m_event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].isNull()
|
||||||
|
&& m_event->stateKey() == m_event->senderId()) {
|
||||||
|
auto previousDisplayName = m_event->unsignedJson()[QStringLiteral("prev_content")][QStringLiteral("displayname")].toString().toHtmlEscaped();
|
||||||
|
if (previousDisplayName.isEmpty()) {
|
||||||
|
previousDisplayName = m_event->senderId();
|
||||||
|
}
|
||||||
|
return previousDisplayName;
|
||||||
|
} else {
|
||||||
|
const auto author = isPending ? m_room->localUser() : m_room->user(m_event->senderId());
|
||||||
|
return m_room->htmlSafeMemberName(author->id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime EventHandler::getTime(bool isPending, QDateTime lastUpdated) const
|
||||||
|
{
|
||||||
|
if (m_event == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "getTime called with m_event set to nullptr.";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (isPending && lastUpdated == QDateTime()) {
|
||||||
|
qCWarning(EventHandling) << "a value must be provided for lastUpdated for a pending event.";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return isPending ? lastUpdated : m_event->originTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString EventHandler::getTimeString(bool relative, QLocale::FormatType format, bool isPending, QDateTime lastUpdated) const
|
||||||
|
{
|
||||||
|
if (m_event == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "getTime called with m_event set to nullptr.";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (isPending && lastUpdated == QDateTime()) {
|
||||||
|
qCWarning(EventHandling) << "a value must be provided for lastUpdated for a pending event.";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ts = getTime(isPending, lastUpdated);
|
||||||
|
if (ts.isValid()) {
|
||||||
|
if (relative) {
|
||||||
|
return m_format.formatRelativeDate(ts.toLocalTime().date(), format);
|
||||||
|
} else {
|
||||||
|
return QLocale().toString(ts.toLocalTime().time(), format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EventHandler::isHighlighted()
|
||||||
|
{
|
||||||
|
if (m_room == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "isHighlighted called with m_room set to nullptr.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (m_event == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "isHighlighted called with m_event set to nullptr.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !m_room->isDirectChat() && m_room->isEventHighlighted(m_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EventHandler::isHidden()
|
||||||
|
{
|
||||||
|
if (m_event->isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(m_event)) {
|
||||||
|
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
|
||||||
|
return true;
|
||||||
|
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::self()->showRename()) {
|
||||||
|
return true;
|
||||||
|
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave()
|
||||||
|
&& !NeoChatConfig::self()->showAvatarUpdate()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_event->isStateEvent() && eventCast<const StateEvent>(m_event)->repeatsState()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// isReplacement?
|
||||||
|
if (auto e = eventCast<const RoomMessageEvent>(m_event)) {
|
||||||
|
if (!e->replacedEvent().isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is<RedactionEvent>(*m_event) || is<ReactionEvent>(*m_event)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto e = eventCast<const RoomMessageEvent>(m_event)) {
|
||||||
|
if (!e->replacedEvent().isEmpty() && e->replacedEvent() != e->id()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_room->connection()->isIgnored(m_room->user(m_event->senderId()))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// hide ending live location beacons
|
||||||
|
if (m_event->isStateEvent() && m_event->matrixType() == "org.matrix.msc3672.beacon_info"_ls && !m_event->contentJson()["live"_ls].toBool()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString EventHandler::getRichBody(bool stripNewlines) const
|
||||||
|
{
|
||||||
|
if (m_event == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "getRichBody called with m_event set to nullptr.";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return getBody(m_event, Qt::RichText, stripNewlines);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString EventHandler::getPlainBody(bool stripNewlines) const
|
||||||
|
{
|
||||||
|
if (m_event == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "getPlainBody called with m_event set to nullptr.";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return getBody(m_event, Qt::PlainText, stripNewlines);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString EventHandler::getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const
|
||||||
|
{
|
||||||
|
if (event->isRedacted()) {
|
||||||
|
auto reason = event->redactedBecause()->reason();
|
||||||
|
return (reason.isEmpty()) ? i18n("<i>[This message was deleted]</i>") : i18n("<i>[This message was deleted: %1]</i>", reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool prettyPrint = (format == Qt::RichText);
|
||||||
|
|
||||||
|
return switchOnType(
|
||||||
|
*event,
|
||||||
|
[this, format, stripNewlines](const RoomMessageEvent &event) {
|
||||||
|
return getMessageBody(event, format, stripNewlines);
|
||||||
|
},
|
||||||
|
[](const StickerEvent &e) {
|
||||||
|
return e.body();
|
||||||
|
},
|
||||||
|
[this, prettyPrint](const RoomMemberEvent &e) {
|
||||||
|
// FIXME: Rewind to the name that was at the time of this event
|
||||||
|
auto subjectName = m_room->htmlSafeMemberName(e.userId());
|
||||||
|
if (e.membership() == Membership::Leave) {
|
||||||
|
if (e.prevContent() && e.prevContent()->displayName) {
|
||||||
|
subjectName = sanitized(*e.prevContent()->displayName).toHtmlEscaped();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prettyPrint) {
|
||||||
|
subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a>")
|
||||||
|
.arg(e.userId(), Utils::getUserColor(m_room->user(e.userId())->hueF()).name(), subjectName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The below code assumes senderName output in AuthorRole
|
||||||
|
switch (e.membership()) {
|
||||||
|
case Membership::Invite:
|
||||||
|
if (e.repeatsState()) {
|
||||||
|
auto text = i18n("reinvited %1 to the room", subjectName);
|
||||||
|
if (!e.reason().isEmpty()) {
|
||||||
|
text += i18nc("Optional reason for an invitation", ": %1") + e.reason().toHtmlEscaped();
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
Q_FALLTHROUGH();
|
||||||
|
case Membership::Join: {
|
||||||
|
QString text{};
|
||||||
|
// Part 1: invites and joins
|
||||||
|
if (e.repeatsState()) {
|
||||||
|
text = i18n("joined the room (repeated)");
|
||||||
|
} else if (e.changesMembership()) {
|
||||||
|
text = e.membership() == Membership::Invite ? i18n("invited %1 to the room", subjectName) : i18n("joined the room");
|
||||||
|
}
|
||||||
|
if (!text.isEmpty()) {
|
||||||
|
if (!e.reason().isEmpty()) {
|
||||||
|
text += i18n(": %1", e.reason().toHtmlEscaped());
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
// Part 2: profile changes of joined members
|
||||||
|
if (e.isRename()) {
|
||||||
|
if (!e.newDisplayName()) {
|
||||||
|
text = i18nc("their refers to a singular user", "cleared their display name");
|
||||||
|
} else {
|
||||||
|
text = i18nc("their refers to a singular user", "changed their display name to %1", e.newDisplayName()->toHtmlEscaped());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (e.isAvatarUpdate()) {
|
||||||
|
if (!text.isEmpty()) {
|
||||||
|
text += i18n(" and ");
|
||||||
|
}
|
||||||
|
if (!e.newAvatarUrl()) {
|
||||||
|
text += i18nc("their refers to a singular user", "cleared their avatar");
|
||||||
|
} else if (!e.prevContent()->avatarUrl) {
|
||||||
|
text += i18n("set an avatar");
|
||||||
|
} else {
|
||||||
|
text += i18nc("their refers to a singular user", "updated their avatar");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
text = i18nc("<user> changed nothing", "changed nothing");
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
case Membership::Leave:
|
||||||
|
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
|
||||||
|
return (e.senderId() != e.userId()) ? i18n("withdrew %1's invitation", subjectName) : i18n("rejected the invitation");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
|
||||||
|
return (e.senderId() != e.userId()) ? i18n("unbanned %1", subjectName) : i18n("self-unbanned");
|
||||||
|
}
|
||||||
|
return (e.senderId() != e.userId())
|
||||||
|
? i18n("has put %1 out of the room: %2", subjectName, e.contentJson()["reason"_ls].toString().toHtmlEscaped())
|
||||||
|
: i18n("left the room");
|
||||||
|
case Membership::Ban:
|
||||||
|
if (e.senderId() != e.userId()) {
|
||||||
|
if (e.reason().isEmpty()) {
|
||||||
|
return i18n("banned %1 from the room", subjectName);
|
||||||
|
} else {
|
||||||
|
return i18n("banned %1 from the room: %2", subjectName, e.reason().toHtmlEscaped());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return i18n("self-banned from the room");
|
||||||
|
}
|
||||||
|
case Membership::Knock: {
|
||||||
|
QString reason(e.contentJson()["reason"_ls].toString().toHtmlEscaped());
|
||||||
|
return reason.isEmpty() ? i18n("requested an invite") : i18n("requested an invite with reason: %1", reason);
|
||||||
|
}
|
||||||
|
default:;
|
||||||
|
}
|
||||||
|
return i18n("made something unknown");
|
||||||
|
},
|
||||||
|
[](const RoomCanonicalAliasEvent &e) {
|
||||||
|
return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias to: %1", e.alias());
|
||||||
|
},
|
||||||
|
[](const RoomNameEvent &e) {
|
||||||
|
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name to: %1", e.name().toHtmlEscaped());
|
||||||
|
},
|
||||||
|
[prettyPrint, stripNewlines](const RoomTopicEvent &e) {
|
||||||
|
return (e.topic().isEmpty()) ? i18n("cleared the topic")
|
||||||
|
: i18n("set the topic to: %1",
|
||||||
|
prettyPrint ? Quotient::prettyPrint(e.topic())
|
||||||
|
: stripNewlines ? e.topic().replace(u'\n', u' ')
|
||||||
|
: e.topic());
|
||||||
|
},
|
||||||
|
[](const RoomAvatarEvent &) {
|
||||||
|
return i18n("changed the room avatar");
|
||||||
|
},
|
||||||
|
[](const EncryptionEvent &) {
|
||||||
|
return i18n("activated End-to-End Encryption");
|
||||||
|
},
|
||||||
|
[](const RoomCreateEvent &e) {
|
||||||
|
return e.isUpgrade() ? i18n("upgraded the room to version %1", e.version().isEmpty() ? "1"_ls : e.version().toHtmlEscaped())
|
||||||
|
: i18n("created the room, version %1", e.version().isEmpty() ? "1"_ls : e.version().toHtmlEscaped());
|
||||||
|
},
|
||||||
|
[](const RoomPowerLevelsEvent &) {
|
||||||
|
return i18nc("'power level' means permission level", "changed the power levels for this room");
|
||||||
|
},
|
||||||
|
[](const StateEvent &e) {
|
||||||
|
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
|
||||||
|
return i18n("changed the server access control lists for this room");
|
||||||
|
}
|
||||||
|
if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
|
||||||
|
if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
|
||||||
|
return i18nc("[User] added <name> widget", "added %1 widget", e.contentJson()["name"_ls].toString());
|
||||||
|
}
|
||||||
|
if (e.contentJson().isEmpty()) {
|
||||||
|
return i18nc("[User] removed <name> widget", "removed %1 widget", e.fullJson()["unsigned"_ls]["prev_content"_ls]["name"_ls].toString());
|
||||||
|
}
|
||||||
|
return i18nc("[User] configured <name> widget", "configured %1 widget", e.contentJson()["name"_ls].toString());
|
||||||
|
}
|
||||||
|
if (e.matrixType() == "org.matrix.msc3672.beacon_info"_ls) {
|
||||||
|
return e.contentJson()["description"_ls].toString();
|
||||||
|
}
|
||||||
|
return e.stateKey().isEmpty() ? i18n("updated %1 state", e.matrixType())
|
||||||
|
: i18n("updated %1 state for %2", e.matrixType(), e.stateKey().toHtmlEscaped());
|
||||||
|
},
|
||||||
|
[](const PollStartEvent &e) {
|
||||||
|
return e.question();
|
||||||
|
},
|
||||||
|
i18n("Unknown event"));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString EventHandler::getMessageBody(const RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines) const
|
||||||
|
{
|
||||||
|
TextHandler textHandler;
|
||||||
|
|
||||||
|
if (event.hasFileContent()) {
|
||||||
|
auto fileCaption = event.content()->fileInfo()->originalName;
|
||||||
|
if (fileCaption.isEmpty()) {
|
||||||
|
fileCaption = event.plainBody();
|
||||||
|
} else if (event.content()->fileInfo()->originalName != event.plainBody()) {
|
||||||
|
fileCaption = event.plainBody() + " | "_ls + fileCaption;
|
||||||
|
}
|
||||||
|
textHandler.setData(fileCaption);
|
||||||
|
return !fileCaption.isEmpty() ? textHandler.handleRecievePlainText(Qt::PlainText, stripNewlines) : i18n("a file");
|
||||||
|
}
|
||||||
|
|
||||||
|
QString body;
|
||||||
|
if (event.hasTextContent() && event.content()) {
|
||||||
|
body = static_cast<const MessageEventContent::TextContent *>(event.content())->body;
|
||||||
|
} else {
|
||||||
|
body = event.plainBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
textHandler.setData(body);
|
||||||
|
|
||||||
|
Qt::TextFormat inputFormat;
|
||||||
|
if (event.mimeType().name() == "text/plain"_ls) {
|
||||||
|
inputFormat = Qt::PlainText;
|
||||||
|
} else {
|
||||||
|
inputFormat = Qt::RichText;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format == Qt::RichText) {
|
||||||
|
return textHandler.handleRecieveRichText(inputFormat, m_room, &event, stripNewlines);
|
||||||
|
} else {
|
||||||
|
return textHandler.handleRecievePlainText(inputFormat, stripNewlines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString EventHandler::getGenericBody() const
|
||||||
|
{
|
||||||
|
if (m_event == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "getGenericBody called with m_event set to nullptr.";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (m_event->isRedacted()) {
|
||||||
|
return i18n("<i>[This message was deleted]</i>");
|
||||||
|
}
|
||||||
|
|
||||||
|
return switchOnType(
|
||||||
|
*m_event,
|
||||||
|
[](const RoomMessageEvent &e) {
|
||||||
|
Q_UNUSED(e)
|
||||||
|
return i18n("sent a message");
|
||||||
|
},
|
||||||
|
[](const StickerEvent &e) {
|
||||||
|
Q_UNUSED(e)
|
||||||
|
return i18n("sent a sticker");
|
||||||
|
},
|
||||||
|
[](const RoomMemberEvent &e) {
|
||||||
|
switch (e.membership()) {
|
||||||
|
case Membership::Invite:
|
||||||
|
if (e.repeatsState()) {
|
||||||
|
return i18n("reinvited someone to the room");
|
||||||
|
}
|
||||||
|
Q_FALLTHROUGH();
|
||||||
|
case Membership::Join: {
|
||||||
|
QString text{};
|
||||||
|
// Part 1: invites and joins
|
||||||
|
if (e.repeatsState()) {
|
||||||
|
text = i18n("joined the room (repeated)");
|
||||||
|
} else if (e.changesMembership()) {
|
||||||
|
text = e.membership() == Membership::Invite ? i18n("invited someone to the room") : i18n("joined the room");
|
||||||
|
}
|
||||||
|
if (!text.isEmpty()) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
// Part 2: profile changes of joined members
|
||||||
|
if (e.isRename()) {
|
||||||
|
if (!e.newDisplayName()) {
|
||||||
|
text = i18nc("their refers to a singular user", "cleared their display name");
|
||||||
|
} else {
|
||||||
|
text = i18nc("their refers to a singular user", "changed their display name");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (e.isAvatarUpdate()) {
|
||||||
|
if (!text.isEmpty()) {
|
||||||
|
text += i18n(" and ");
|
||||||
|
}
|
||||||
|
if (!e.newAvatarUrl()) {
|
||||||
|
text += i18nc("their refers to a singular user", "cleared their avatar");
|
||||||
|
} else if (!e.prevContent()->avatarUrl) {
|
||||||
|
text += i18n("set an avatar");
|
||||||
|
} else {
|
||||||
|
text += i18nc("their refers to a singular user", "updated their avatar");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
text = i18nc("<user> changed nothing", "changed nothing");
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
case Membership::Leave:
|
||||||
|
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
|
||||||
|
return (e.senderId() != e.userId()) ? i18n("withdrew a user's invitation") : i18n("rejected the invitation");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
|
||||||
|
return (e.senderId() != e.userId()) ? i18n("unbanned a user") : i18n("self-unbanned");
|
||||||
|
}
|
||||||
|
return (e.senderId() != e.userId()) ? i18n("put a user out of the room") : i18n("left the room");
|
||||||
|
case Membership::Ban:
|
||||||
|
if (e.senderId() != e.userId()) {
|
||||||
|
return i18n("banned a user from the room");
|
||||||
|
} else {
|
||||||
|
return i18n("self-banned from the room");
|
||||||
|
}
|
||||||
|
case Membership::Knock: {
|
||||||
|
return i18n("requested an invite");
|
||||||
|
}
|
||||||
|
default:;
|
||||||
|
}
|
||||||
|
return i18n("made something unknown");
|
||||||
|
},
|
||||||
|
[](const RoomCanonicalAliasEvent &e) {
|
||||||
|
return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias");
|
||||||
|
},
|
||||||
|
[](const RoomNameEvent &e) {
|
||||||
|
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name");
|
||||||
|
},
|
||||||
|
[](const RoomTopicEvent &e) {
|
||||||
|
return (e.topic().isEmpty()) ? i18n("cleared the topic") : i18n("set the topic");
|
||||||
|
},
|
||||||
|
[](const RoomAvatarEvent &) {
|
||||||
|
return i18n("changed the room avatar");
|
||||||
|
},
|
||||||
|
[](const EncryptionEvent &) {
|
||||||
|
return i18n("activated End-to-End Encryption");
|
||||||
|
},
|
||||||
|
[](const RoomCreateEvent &e) {
|
||||||
|
return e.isUpgrade() ? i18n("upgraded the room version") : i18n("created the room");
|
||||||
|
},
|
||||||
|
[](const RoomPowerLevelsEvent &) {
|
||||||
|
return i18nc("'power level' means permission level", "changed the power levels for this room");
|
||||||
|
},
|
||||||
|
[](const StateEvent &e) {
|
||||||
|
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
|
||||||
|
return i18n("changed the server access control lists for this room");
|
||||||
|
}
|
||||||
|
if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
|
||||||
|
if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
|
||||||
|
return i18n("added a widget");
|
||||||
|
}
|
||||||
|
if (e.contentJson().isEmpty()) {
|
||||||
|
return i18n("removed a widget");
|
||||||
|
}
|
||||||
|
return i18n("configured a widget");
|
||||||
|
}
|
||||||
|
return i18n("updated the state");
|
||||||
|
},
|
||||||
|
[](const PollStartEvent &e) {
|
||||||
|
Q_UNUSED(e);
|
||||||
|
return i18n("started a poll");
|
||||||
|
},
|
||||||
|
i18n("Unknown event"));
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap EventHandler::getMediaInfo() const
|
||||||
|
{
|
||||||
|
return getMediaInfoForEvent(m_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap EventHandler::getMediaInfoForEvent(const Quotient::RoomEvent *event) const
|
||||||
|
{
|
||||||
|
QString eventId = event->id();
|
||||||
|
|
||||||
|
// Get the file info for the event.
|
||||||
|
const EventContent::FileInfo *fileInfo;
|
||||||
|
if (event->is<RoomMessageEvent>()) {
|
||||||
|
auto roomMessageEvent = eventCast<const RoomMessageEvent>(event);
|
||||||
|
if (!roomMessageEvent->hasFileContent()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
fileInfo = roomMessageEvent->content()->fileInfo();
|
||||||
|
} else if (event->is<StickerEvent>()) {
|
||||||
|
auto stickerEvent = eventCast<const StickerEvent>(event);
|
||||||
|
fileInfo = &stickerEvent->image();
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return getMediaInfoFromFileInfo(fileInfo, eventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap EventHandler::getMediaInfoFromFileInfo(const EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail) const
|
||||||
|
{
|
||||||
|
QVariantMap mediaInfo;
|
||||||
|
|
||||||
|
// Get the mxc URL for the media.
|
||||||
|
if (!fileInfo->url().isValid() || eventId.isEmpty()) {
|
||||||
|
mediaInfo["source"_ls] = QUrl();
|
||||||
|
} else {
|
||||||
|
QUrl source = m_room->makeMediaUrl(eventId, fileInfo->url());
|
||||||
|
|
||||||
|
if (source.isValid() && source.scheme() == QStringLiteral("mxc")) {
|
||||||
|
mediaInfo["source"_ls] = source;
|
||||||
|
} else {
|
||||||
|
mediaInfo["source"_ls] = QUrl();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto mimeType = fileInfo->mimeType;
|
||||||
|
// Add the MIME type for the media if available.
|
||||||
|
mediaInfo["mimeType"_ls] = mimeType.name();
|
||||||
|
|
||||||
|
// Add the MIME type icon if available.
|
||||||
|
mediaInfo["mimeIcon"_ls] = mimeType.iconName();
|
||||||
|
|
||||||
|
// Add media size if available.
|
||||||
|
mediaInfo["size"_ls] = fileInfo->payloadSize;
|
||||||
|
|
||||||
|
// Add parameter depending on media type.
|
||||||
|
if (mimeType.name().contains(QStringLiteral("image"))) {
|
||||||
|
if (auto castInfo = static_cast<const EventContent::ImageContent *>(fileInfo)) {
|
||||||
|
mediaInfo["width"_ls] = castInfo->imageSize.width();
|
||||||
|
mediaInfo["height"_ls] = castInfo->imageSize.height();
|
||||||
|
|
||||||
|
if (!isThumbnail) {
|
||||||
|
QVariantMap tempInfo;
|
||||||
|
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
|
||||||
|
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
|
||||||
|
tempInfo = thumbnailInfo;
|
||||||
|
} else {
|
||||||
|
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
|
||||||
|
if (blurhash.isEmpty()) {
|
||||||
|
tempInfo["source"_ls] = QUrl();
|
||||||
|
} else {
|
||||||
|
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mediaInfo["tempInfo"_ls] = tempInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mimeType.name().contains(QStringLiteral("video"))) {
|
||||||
|
if (auto castInfo = static_cast<const EventContent::VideoContent *>(fileInfo)) {
|
||||||
|
mediaInfo["width"_ls] = castInfo->imageSize.width();
|
||||||
|
mediaInfo["height"_ls] = castInfo->imageSize.height();
|
||||||
|
mediaInfo["duration"_ls] = castInfo->duration;
|
||||||
|
|
||||||
|
if (!isThumbnail) {
|
||||||
|
QVariantMap tempInfo;
|
||||||
|
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
|
||||||
|
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
|
||||||
|
tempInfo = thumbnailInfo;
|
||||||
|
} else {
|
||||||
|
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
|
||||||
|
if (blurhash.isEmpty()) {
|
||||||
|
tempInfo["source"_ls] = QUrl();
|
||||||
|
} else {
|
||||||
|
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mediaInfo["tempInfo"_ls] = tempInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mimeType.name().contains(QStringLiteral("audio"))) {
|
||||||
|
if (auto castInfo = static_cast<const EventContent::AudioContent *>(fileInfo)) {
|
||||||
|
mediaInfo["duration"_ls] = castInfo->duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mediaInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSharedPointer<LinkPreviewer> EventHandler::getLinkPreviewer() const
|
||||||
|
{
|
||||||
|
if (!m_event->is<RoomMessageEvent>()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString text;
|
||||||
|
auto event = eventCast<const RoomMessageEvent>(m_event);
|
||||||
|
if (event->hasTextContent()) {
|
||||||
|
auto textContent = static_cast<const EventContent::TextContent *>(event->content());
|
||||||
|
if (textContent) {
|
||||||
|
text = textContent->body;
|
||||||
|
} else {
|
||||||
|
text = event->plainBody();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text = event->plainBody();
|
||||||
|
}
|
||||||
|
TextHandler textHandler;
|
||||||
|
textHandler.setData(text);
|
||||||
|
|
||||||
|
QList<QUrl> links = textHandler.getLinkPreviews();
|
||||||
|
if (links.size() > 0) {
|
||||||
|
return QSharedPointer<LinkPreviewer>(new LinkPreviewer(nullptr, m_room, links.size() > 0 ? links[0] : QUrl()));
|
||||||
|
} else {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QSharedPointer<ReactionModel> EventHandler::getReactions() const
|
||||||
|
{
|
||||||
|
if (m_room == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "getReactions called with m_room set to nullptr.";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (m_event == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "getReactions called with m_event set to nullptr.";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (!m_event->is<RoomMessageEvent>()) {
|
||||||
|
qCWarning(EventHandling) << "getReactions called with on a non-message event.";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto eventId = m_event->id();
|
||||||
|
const auto &annotations = m_room->relatedEvents(eventId, EventRelation::AnnotationType);
|
||||||
|
if (annotations.isEmpty()) {
|
||||||
|
return nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
QMap<QString, QList<User *>> reactions = {};
|
||||||
|
for (const auto &a : annotations) {
|
||||||
|
if (a->isRedacted()) { // Just in case?
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (const auto &e = eventCast<const ReactionEvent>(a)) {
|
||||||
|
reactions[e->key()].append(m_room->user(e->senderId()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reactions.isEmpty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<ReactionModel::Reaction> res;
|
||||||
|
auto i = reactions.constBegin();
|
||||||
|
while (i != reactions.constEnd()) {
|
||||||
|
QVariantList authors;
|
||||||
|
for (const auto &author : i.value()) {
|
||||||
|
authors.append(m_room->getUser(author));
|
||||||
|
}
|
||||||
|
|
||||||
|
res.append(ReactionModel::Reaction{i.key(), authors});
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.size() > 0) {
|
||||||
|
return QSharedPointer<ReactionModel>(new ReactionModel(nullptr, res, m_room->localUser()));
|
||||||
|
} else {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EventHandler::hasReply() const
|
||||||
|
{
|
||||||
|
return !m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString EventHandler::getReplyId() const
|
||||||
|
{
|
||||||
|
return m_event->contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateType::Type EventHandler::getReplyDelegateType() const
|
||||||
|
{
|
||||||
|
auto replyEvent = m_room->getReplyForEvent(*m_event);
|
||||||
|
if (replyEvent == nullptr) {
|
||||||
|
return DelegateType::Other;
|
||||||
|
}
|
||||||
|
return getDelegateTypeForEvent(replyEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap EventHandler::getReplyAuthor() const
|
||||||
|
{
|
||||||
|
if (m_room == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "getReplyAuthor called with m_room set to nullptr.";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
// If we have a room we can return an empty user by handing nullptr to m_room->getUser.
|
||||||
|
if (m_event == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "getReplyAuthor called with m_event set to nullptr. Returning empty user.";
|
||||||
|
return m_room->getUser(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto replyPtr = m_room->getReplyForEvent(*m_event);
|
||||||
|
|
||||||
|
if (replyPtr) {
|
||||||
|
auto replyUser = m_room->user(replyPtr->senderId());
|
||||||
|
return m_room->getUser(replyUser);
|
||||||
|
} else {
|
||||||
|
return m_room->getUser(nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString EventHandler::getReplyRichBody(bool stripNewlines) const
|
||||||
|
{
|
||||||
|
if (m_room == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "getReplyRichBody called with m_room set to nullptr.";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (m_event == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "getReplyRichBody called with m_event set to nullptr.";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto replyEvent = m_room->getReplyForEvent(*m_event);
|
||||||
|
if (replyEvent == nullptr) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return getBody(replyEvent, Qt::RichText, stripNewlines);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString EventHandler::getReplyPlainBody(bool stripNewlines) const
|
||||||
|
{
|
||||||
|
if (m_room == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "getReplyPlainBody called with m_room set to nullptr.";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (m_event == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "getReplyPlainBody called with m_event set to nullptr.";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto replyEvent = m_room->getReplyForEvent(*m_event);
|
||||||
|
if (replyEvent == nullptr) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return getBody(replyEvent, Qt::PlainText, stripNewlines);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap EventHandler::getReplyMediaInfo() const
|
||||||
|
{
|
||||||
|
if (m_room == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "getReplyMediaInfo called with m_room set to nullptr.";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (m_event == nullptr) {
|
||||||
|
qCWarning(EventHandling) << "getReplyMediaInfo called with m_event set to nullptr.";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto replyPtr = m_room->getReplyForEvent(*m_event);
|
||||||
|
if (!replyPtr) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return getMediaInfoForEvent(replyPtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
float EventHandler::getLatitude() const
|
||||||
|
{
|
||||||
|
const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString();
|
||||||
|
if (geoUri.isEmpty()) {
|
||||||
|
return -100.0; // latitude runs from -90deg to +90deg so -100 is out of range.
|
||||||
|
}
|
||||||
|
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[0];
|
||||||
|
return latitude.toFloat();
|
||||||
|
}
|
||||||
|
|
||||||
|
float EventHandler::getLongitude() const
|
||||||
|
{
|
||||||
|
const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString();
|
||||||
|
if (geoUri.isEmpty()) {
|
||||||
|
return -200.0; // longitude runs from -180deg to +180deg so -200 is out of range.
|
||||||
|
}
|
||||||
|
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[1];
|
||||||
|
return latitude.toFloat();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString EventHandler::getLocationAssetType() const
|
||||||
|
{
|
||||||
|
const auto assetType = m_event->contentJson()["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
|
||||||
|
if (assetType.isEmpty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return assetType;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EventHandler::hasReadMarkers() const
|
||||||
|
{
|
||||||
|
auto userIds = m_room->userIdsAtEvent(m_event->id());
|
||||||
|
userIds.remove(m_room->localUser()->id());
|
||||||
|
return userIds.size() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList EventHandler::getReadMarkers(int maxMarkers) const
|
||||||
|
{
|
||||||
|
auto userIds_temp = m_room->userIdsAtEvent(m_event->id());
|
||||||
|
userIds_temp.remove(m_room->localUser()->id());
|
||||||
|
|
||||||
|
auto userIds = userIds_temp.values();
|
||||||
|
if (userIds.count() > maxMarkers) {
|
||||||
|
userIds = userIds.mid(0, maxMarkers);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList users;
|
||||||
|
users.reserve(userIds.size());
|
||||||
|
for (const auto &userId : userIds) {
|
||||||
|
auto user = m_room->user(userId);
|
||||||
|
users += m_room->getUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString EventHandler::getNumberExcessReadMarkers(int maxMarkers) const
|
||||||
|
{
|
||||||
|
auto userIds = m_room->userIdsAtEvent(m_event->id());
|
||||||
|
userIds.remove(m_room->localUser()->id());
|
||||||
|
|
||||||
|
if (userIds.count() > maxMarkers) {
|
||||||
|
return QStringLiteral("+ ") + QString::number(userIds.count() - maxMarkers);
|
||||||
|
} else {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString EventHandler::getReadMarkersString() const
|
||||||
|
{
|
||||||
|
auto userIds = m_room->userIdsAtEvent(m_event->id());
|
||||||
|
userIds.remove(m_room->localUser()->id());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The string ends up in the form
|
||||||
|
* "x users: user1DisplayName, user2DisplayName, etc."
|
||||||
|
*/
|
||||||
|
QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size());
|
||||||
|
for (const auto &userId : userIds) {
|
||||||
|
auto user = m_room->user(userId);
|
||||||
|
readMarkersString += user->displayname(m_room) + i18nc("list separator", ", ");
|
||||||
|
}
|
||||||
|
readMarkersString.chop(2);
|
||||||
|
return readMarkersString;
|
||||||
|
}
|
||||||
400
src/eventhandler.h
Normal file
400
src/eventhandler.h
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include <KFormat>
|
||||||
|
|
||||||
|
#include <Quotient/eventitem.h>
|
||||||
|
#include <Quotient/events/roomevent.h>
|
||||||
|
#include <Quotient/events/roommessageevent.h>
|
||||||
|
|
||||||
|
#include "enums/delegatetype.h"
|
||||||
|
|
||||||
|
class LinkPreviewer;
|
||||||
|
class NeoChatRoom;
|
||||||
|
class ReactionModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class EventHandler
|
||||||
|
*
|
||||||
|
* This class is designed to handle a Quotient::RoomEvent allowing data to be extracted
|
||||||
|
* in a form ready for the NeoChat UI.
|
||||||
|
*
|
||||||
|
* To use this properly both the room and the event should be set (and the event should
|
||||||
|
* be from the given room).
|
||||||
|
*
|
||||||
|
* @note EventHandler will always try to return something even when not properly
|
||||||
|
* initialised, this is usually the best empty value it can create with available
|
||||||
|
* information. This is to minimize warnings from QML especially during startup
|
||||||
|
* and room changes.
|
||||||
|
*/
|
||||||
|
class EventHandler : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Return the current room the EventHandler is using.
|
||||||
|
*/
|
||||||
|
const NeoChatRoom *getRoom() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the current room the EventHandler to using.
|
||||||
|
*/
|
||||||
|
void setRoom(const NeoChatRoom *room);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the current event the EventHandler is using.
|
||||||
|
*/
|
||||||
|
const Quotient::Event *getEvent() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set the current event the EventHandler to using.
|
||||||
|
*/
|
||||||
|
void setEvent(const Quotient::RoomEvent *event);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the Matrix ID of the event.
|
||||||
|
*/
|
||||||
|
QString getId() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the DelegateType of the event.
|
||||||
|
*
|
||||||
|
* @note While similar this is not the matrix event or message type. This is
|
||||||
|
* to tell a QML ListView what delegate to show for each event. So while
|
||||||
|
* similar to the spec it is not the same.
|
||||||
|
*/
|
||||||
|
DelegateType::Type getDelegateType() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the author of the event in context of the room.
|
||||||
|
*
|
||||||
|
* This is different to getting a Quotient::User object
|
||||||
|
* as neither of those can provide details like the displayName or avatarMediaId
|
||||||
|
* without the room context as these can vary from room to room. This function
|
||||||
|
* uses the room context and outputs the result as QVariantMap.
|
||||||
|
*
|
||||||
|
* An empty QVariantMap will be returned if the EventHandler hasn't had the room
|
||||||
|
* intialised. An empty user (i.e. a QVariantMap with all the correct keys
|
||||||
|
* but empty values) will be returned if the room has been set but not an event.
|
||||||
|
*
|
||||||
|
* @param isPending if the event is pending, i.e. has not been confirmed by
|
||||||
|
* the server.
|
||||||
|
*
|
||||||
|
* @return a QVariantMap for the user with the following properties:
|
||||||
|
* - isLocalUser - Whether the user is the local user.
|
||||||
|
* - id - The matrix ID of the user.
|
||||||
|
* - displayName - Display name in the context of this room.
|
||||||
|
* - avatarSource - The mxc URL for the user's avatar in the current room.
|
||||||
|
* - avatarMediaId - Avatar id in the context of this room.
|
||||||
|
* - color - Color for the user.
|
||||||
|
* - object - The Quotient::User object for the user.
|
||||||
|
*
|
||||||
|
* @sa Quotient::User
|
||||||
|
*/
|
||||||
|
QVariantMap getAuthor(bool isPending = false) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the display name of the event author.
|
||||||
|
*
|
||||||
|
* This method is separate from getAuthor() and special in that it will return
|
||||||
|
* the old display name of the author if the current event is one that caused it
|
||||||
|
* to change. This allows for scenarios where the UI wishes to notify that a
|
||||||
|
* user's display name has changed and what it changed from.
|
||||||
|
*
|
||||||
|
* @param isPending whether the event is pending as this cannot be derived from
|
||||||
|
* just the event object.
|
||||||
|
*/
|
||||||
|
QString getAuthorDisplayName(bool isPending = false) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return a QDateTime object for the event timestamp.
|
||||||
|
*/
|
||||||
|
QDateTime getTime(bool isPending = false, QDateTime lastUpdated = {}) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return a QString for the event timestamp.
|
||||||
|
*
|
||||||
|
* This is intended to return a string that is read for display in the UI without
|
||||||
|
* any further manipulation required.
|
||||||
|
*
|
||||||
|
* @param relative whether the string is realtive to the current date, i.e.
|
||||||
|
* Yesterday or Wednesday, etc.
|
||||||
|
* @param format the QLocale::FormatType to use.
|
||||||
|
* @param isPending whether the event is pending as this cannot be derived from
|
||||||
|
* just the event object.
|
||||||
|
* @param lastUpdated the time the event was last updated locally as this cannot be
|
||||||
|
* obtained from the event.
|
||||||
|
*/
|
||||||
|
QString getTimeString(bool relative, QLocale::FormatType format = QLocale::ShortFormat, bool isPending = false, QDateTime lastUpdated = {}) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the event should be highlighted in the timeline.
|
||||||
|
*
|
||||||
|
* @note Messages in direct chats are never highlighted.
|
||||||
|
*/
|
||||||
|
bool isHighlighted();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the event should be hidden in the timeline.
|
||||||
|
*
|
||||||
|
* This could be for numerous reasons, e.g. if it's a replacement event, if the
|
||||||
|
* user has hidden all state events or if the sender has been ignored by the local
|
||||||
|
* user.
|
||||||
|
*/
|
||||||
|
bool isHidden();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Output a string for the message content ready for display in a rich text field.
|
||||||
|
*
|
||||||
|
* The output string is dependant upon the event type and the desired output format.
|
||||||
|
*
|
||||||
|
* For most messages this is the body content of the message. For media messages
|
||||||
|
* this will be the caption and for state events it will be a string specific
|
||||||
|
* to that event with some dynamic details about the event added.
|
||||||
|
*
|
||||||
|
* E.g. For a room topic state event the text will be:
|
||||||
|
* "set the topic to: <new topic text>"
|
||||||
|
*
|
||||||
|
* @param stripNewlines whether the output should have new lines in it.
|
||||||
|
*/
|
||||||
|
QString getRichBody(bool stripNewlines = false) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Output a string for the message content ready for display in a plain text field.
|
||||||
|
*
|
||||||
|
* The output string is dependant upon the event type and the desired output format.
|
||||||
|
*
|
||||||
|
* For most messages this is the body content of the message. For media messages
|
||||||
|
* this will be the caption and for state events it will be a string specific
|
||||||
|
* to that event with some dynamic details about the event added.
|
||||||
|
*
|
||||||
|
* E.g. For a room topic state event the text will be:
|
||||||
|
* "set the topic to: <new topic text>"
|
||||||
|
*
|
||||||
|
* @param stripNewlines whether the output should have new lines in it.
|
||||||
|
*/
|
||||||
|
QString getPlainBody(bool stripNewlines = false) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Output a generic string for the message content ready for display.
|
||||||
|
*
|
||||||
|
* The output string is dependant upon the event type.
|
||||||
|
*
|
||||||
|
* Unlike EventHandler::getRichBody or EventHandler::getPlainBody the string
|
||||||
|
* is the same for all events of the same type.
|
||||||
|
*
|
||||||
|
* E.g. For a message the text will be:
|
||||||
|
* "sent a message"
|
||||||
|
*
|
||||||
|
* @sa getRichBody(), getPlainBody()
|
||||||
|
*/
|
||||||
|
QString getGenericBody() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the media info for the event.
|
||||||
|
*
|
||||||
|
* An empty QVariantMap will be returned for any event that doesn't have any
|
||||||
|
* media info.
|
||||||
|
*
|
||||||
|
* @return This should consist of the following:
|
||||||
|
* - source - The mxc URL for the media.
|
||||||
|
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
|
||||||
|
* - mimeIcon - The MIME icon name (should be image-xxx).
|
||||||
|
* - size - The file size in bytes.
|
||||||
|
* - width - The width in pixels of the audio media.
|
||||||
|
* - height - The height in pixels of the audio media.
|
||||||
|
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||||
|
*/
|
||||||
|
QVariantMap getMediaInfo() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return a LinkPreviewer object for the event.
|
||||||
|
*
|
||||||
|
* A nullptr will be returned for any event that doesn't have any links so the
|
||||||
|
* return should be null checked and an empty LinkPreviewer provided if null.
|
||||||
|
*
|
||||||
|
* @sa LinkPreviewer
|
||||||
|
*/
|
||||||
|
QSharedPointer<LinkPreviewer> getLinkPreviewer() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return a ReactionModel object for the event.
|
||||||
|
*
|
||||||
|
* A nullptr will be returned for any event that doesn't have any links so the
|
||||||
|
* return should be null checked and an empty QVariantList (or other suitable
|
||||||
|
* empty mode) provided if null.
|
||||||
|
*/
|
||||||
|
QSharedPointer<ReactionModel> getReactions() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the event is a reply to another in the timeline.
|
||||||
|
*/
|
||||||
|
bool hasReply() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the Matrix ID of the event replied to.
|
||||||
|
*/
|
||||||
|
QString getReplyId() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the DelegateType of the event replied to.
|
||||||
|
*
|
||||||
|
* @note While similar this is not the matrix event or message type. This is
|
||||||
|
* to tell a QML ListView what delegate to show for each event. So while
|
||||||
|
* similar to the spec it is not the same.
|
||||||
|
*/
|
||||||
|
DelegateType::Type getReplyDelegateType() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the author of the event replied to in context of the room.
|
||||||
|
*
|
||||||
|
* This is different to getting a Quotient::User object
|
||||||
|
* as neither of those can provide details like the displayName or avatarMediaId
|
||||||
|
* without the room context as these can vary from room to room. This function
|
||||||
|
* uses the room context and outputs the result as QVariantMap.
|
||||||
|
*
|
||||||
|
* An empty QVariantMap will be returned if the EventHandler hasn't had the room
|
||||||
|
* intialised. An empty user (i.e. a QVariantMap with all the correct keys
|
||||||
|
* but empty values) will be returned if the room has been set but not an event.
|
||||||
|
*
|
||||||
|
* @return a QVariantMap for the user with the following properties:
|
||||||
|
* - isLocalUser - Whether the user is the local user.
|
||||||
|
* - id - The matrix ID of the user.
|
||||||
|
* - displayName - Display name in the context of this room.
|
||||||
|
* - avatarSource - The mxc URL for the user's avatar in the current room.
|
||||||
|
* - avatarMediaId - Avatar id in the context of this room.
|
||||||
|
* - color - Color for the user.
|
||||||
|
* - object - The Quotient::User object for the user.
|
||||||
|
*
|
||||||
|
* @sa Quotient::User
|
||||||
|
*/
|
||||||
|
QVariantMap getReplyAuthor() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Output a string for the message content of the event replied to ready
|
||||||
|
* for display in a rich text field.
|
||||||
|
*
|
||||||
|
* The output string is dependant upon the event type and the desired output format.
|
||||||
|
*
|
||||||
|
* For most messages this is the body content of the message. For media messages
|
||||||
|
* this will be the caption and for state events it will be a string specific
|
||||||
|
* to that event with some dynamic details about the event added.
|
||||||
|
*
|
||||||
|
* E.g. For a room topic state event the text will be:
|
||||||
|
* "set the topic to: <new topic text>"
|
||||||
|
*
|
||||||
|
* @param stripNewlines whether the output should have new lines in it.
|
||||||
|
*/
|
||||||
|
QString getReplyRichBody(bool stripNewlines = false) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Output a string for the message content of the event replied to ready
|
||||||
|
* for display in a plain text field.
|
||||||
|
*
|
||||||
|
* The output string is dependant upon the event type and the desired output format.
|
||||||
|
*
|
||||||
|
* For most messages this is the body content of the message. For media messages
|
||||||
|
* this will be the caption and for state events it will be a string specific
|
||||||
|
* to that event with some dynamic details about the event added.
|
||||||
|
*
|
||||||
|
* E.g. For a room topic state event the text will be:
|
||||||
|
* "set the topic to: <new topic text>"
|
||||||
|
*
|
||||||
|
* @param stripNewlines whether the output should have new lines in it.
|
||||||
|
*/
|
||||||
|
QString getReplyPlainBody(bool stripNewlines = false) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the media info for the event replied to.
|
||||||
|
*
|
||||||
|
* An empty QVariantMap will be returned for any event that doesn't have any
|
||||||
|
* media info.
|
||||||
|
*
|
||||||
|
* @return This should consist of the following:
|
||||||
|
* - source - The mxc URL for the media.
|
||||||
|
* - mimeType - The MIME type of the media (should be image/xxx for this delegate).
|
||||||
|
* - mimeIcon - The MIME icon name (should be image-xxx).
|
||||||
|
* - size - The file size in bytes.
|
||||||
|
* - width - The width in pixels of the audio media.
|
||||||
|
* - height - The height in pixels of the audio media.
|
||||||
|
* - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads.
|
||||||
|
*/
|
||||||
|
QVariantMap getReplyMediaInfo() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the latitude for the event.
|
||||||
|
*
|
||||||
|
* Returns -100.0 if the event doesn't have a location (latitudes are in the
|
||||||
|
* range -90deg to +90deg so -100 is out of range).
|
||||||
|
*/
|
||||||
|
float getLatitude() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the longitude for the event.
|
||||||
|
*
|
||||||
|
* Returns -200.0 if the event doesn't have a location (latitudes are in the
|
||||||
|
* range -180deg to +180deg so -200 is out of range).
|
||||||
|
*/
|
||||||
|
float getLongitude() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the type of location marker for the event.
|
||||||
|
*/
|
||||||
|
QString getLocationAssetType() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the event has any read marker for other users.
|
||||||
|
*/
|
||||||
|
bool hasReadMarkers() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns a list of user read marker for the event.
|
||||||
|
*
|
||||||
|
* @param maxMarkers the maximum number of users to return. Usually the number
|
||||||
|
* of user read makers shown is limited to not clutter the UI.
|
||||||
|
* This needs to be the same as used in getNumberExcessReadMarkers
|
||||||
|
* so that the markers line up with the number displayed, i.e.
|
||||||
|
* the number of users shown plus the excess number will be
|
||||||
|
* the total number of other user read markers at an event.
|
||||||
|
*/
|
||||||
|
QVariantList getReadMarkers(int maxMarkers = 5) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns the number of excess user read markers for the event.
|
||||||
|
*
|
||||||
|
* This returns a string in the form "+ x" ready for use in the UI.
|
||||||
|
*
|
||||||
|
* @param maxMarkers the maximum number of markers shown in the UI. This needs to
|
||||||
|
* be the same as used in getReadMarkers so that the value lines
|
||||||
|
* up with the number displayed, i.e. the number of users shown
|
||||||
|
* plus the excess number will be the total number of other user
|
||||||
|
* read markers at an event.
|
||||||
|
*/
|
||||||
|
QString getNumberExcessReadMarkers(int maxMarkers = 5) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns a string with the names of the read markers at the event.
|
||||||
|
*
|
||||||
|
* This is in the form "x users: name 1, name 2, ...".
|
||||||
|
*/
|
||||||
|
QString getReadMarkersString() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const NeoChatRoom *m_room = nullptr;
|
||||||
|
const Quotient::RoomEvent *m_event = nullptr;
|
||||||
|
|
||||||
|
KFormat m_format;
|
||||||
|
|
||||||
|
DelegateType::Type getDelegateTypeForEvent(const Quotient::RoomEvent *event) const;
|
||||||
|
|
||||||
|
QString getBody(const Quotient::RoomEvent *event, Qt::TextFormat format, bool stripNewlines) const;
|
||||||
|
QString getMessageBody(const Quotient::RoomMessageEvent &event, Qt::TextFormat format, bool stripNewlines) const;
|
||||||
|
|
||||||
|
QVariantMap getMediaInfoForEvent(const Quotient::RoomEvent *event) const;
|
||||||
|
QVariantMap getMediaInfoFromFileInfo(const Quotient::EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail = false) const;
|
||||||
|
};
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
using namespace Quotient;
|
using namespace Quotient;
|
||||||
|
|
||||||
LinkPreviewer::LinkPreviewer(QObject *parent, NeoChatRoom *room, const QUrl &url)
|
LinkPreviewer::LinkPreviewer(QObject *parent, const NeoChatRoom *room, const QUrl &url)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_currentRoom(room)
|
, m_currentRoom(room)
|
||||||
, m_loaded(false)
|
, m_loaded(false)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class LinkPreviewer : public QObject
|
|||||||
Q_PROPERTY(QUrl imageSource READ imageSource NOTIFY imageSourceChanged)
|
Q_PROPERTY(QUrl imageSource READ imageSource NOTIFY imageSourceChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit LinkPreviewer(QObject *parent = nullptr, NeoChatRoom *room = nullptr, const QUrl &url = {});
|
explicit LinkPreviewer(QObject *parent = nullptr, const NeoChatRoom *room = nullptr, const QUrl &url = {});
|
||||||
|
|
||||||
[[nodiscard]] QUrl url() const;
|
[[nodiscard]] QUrl url() const;
|
||||||
void setUrl(QUrl);
|
void setUrl(QUrl);
|
||||||
@@ -55,7 +55,7 @@ public:
|
|||||||
[[nodiscard]] QUrl imageSource() const;
|
[[nodiscard]] QUrl imageSource() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
NeoChatRoom *m_currentRoom = nullptr;
|
const NeoChatRoom *m_currentRoom = nullptr;
|
||||||
|
|
||||||
bool m_loaded;
|
bool m_loaded;
|
||||||
QString m_title = QString();
|
QString m_title = QString();
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
#include "clipboard.h"
|
#include "clipboard.h"
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "delegatesizehelper.h"
|
#include "delegatesizehelper.h"
|
||||||
|
#include "enums/delegatetype.h"
|
||||||
#include "filetypesingleton.h"
|
#include "filetypesingleton.h"
|
||||||
#include "linkpreviewer.h"
|
#include "linkpreviewer.h"
|
||||||
#include "locationhelper.h"
|
#include "locationhelper.h"
|
||||||
@@ -280,6 +281,7 @@ int main(int argc, char *argv[])
|
|||||||
qmlRegisterType<EmoticonFilterModel>("org.kde.neochat", 1, 0, "EmoticonFilterModel");
|
qmlRegisterType<EmoticonFilterModel>("org.kde.neochat", 1, 0, "EmoticonFilterModel");
|
||||||
qmlRegisterType<DelegateSizeHelper>("org.kde.neochat", 1, 0, "DelegateSizeHelper");
|
qmlRegisterType<DelegateSizeHelper>("org.kde.neochat", 1, 0, "DelegateSizeHelper");
|
||||||
qmlRegisterType<MediaSizeHelper>("org.kde.neochat", 1, 0, "MediaSizeHelper");
|
qmlRegisterType<MediaSizeHelper>("org.kde.neochat", 1, 0, "MediaSizeHelper");
|
||||||
|
qmlRegisterUncreatableType<DelegateType>("org.kde.neochat", 1, 0, "DelegateType", "ENUM"_ls);
|
||||||
qmlRegisterUncreatableType<PushNotificationKind>("org.kde.neochat", 1, 0, "PushNotificationKind", "ENUM"_ls);
|
qmlRegisterUncreatableType<PushNotificationKind>("org.kde.neochat", 1, 0, "PushNotificationKind", "ENUM"_ls);
|
||||||
qmlRegisterUncreatableType<PushNotificationSection>("org.kde.neochat", 1, 0, "PushNotificationSection", "ENUM"_ls);
|
qmlRegisterUncreatableType<PushNotificationSection>("org.kde.neochat", 1, 0, "PushNotificationSection", "ENUM"_ls);
|
||||||
qmlRegisterUncreatableType<PushNotificationState>("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM"_ls);
|
qmlRegisterUncreatableType<PushNotificationState>("org.kde.neochat", 1, 0, "PushNotificationState", "ENUM"_ls);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include <Quotient/room.h>
|
#include <Quotient/room.h>
|
||||||
|
|
||||||
|
#include "enums/delegatetype.h"
|
||||||
#include "messageeventmodel.h"
|
#include "messageeventmodel.h"
|
||||||
#include "messagefiltermodel.h"
|
#include "messagefiltermodel.h"
|
||||||
|
|
||||||
@@ -19,8 +20,8 @@ bool MediaMessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex
|
|||||||
{
|
{
|
||||||
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
|
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
|
||||||
|
|
||||||
if (index.data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Image
|
if (index.data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Image
|
||||||
|| index.data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Video) {
|
|| index.data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Video) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -29,9 +30,9 @@ bool MediaMessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex
|
|||||||
QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
|
QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
|
||||||
{
|
{
|
||||||
if (role == SourceRole) {
|
if (role == SourceRole) {
|
||||||
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Image) {
|
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Image) {
|
||||||
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("source")].toUrl();
|
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("source")].toUrl();
|
||||||
} else if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Video) {
|
} else if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Video) {
|
||||||
auto progressInfo = mapToSource(index).data(MessageEventModel::ProgressInfoRole).value<Quotient::FileTransferInfo>();
|
auto progressInfo = mapToSource(index).data(MessageEventModel::ProgressInfoRole).value<Quotient::FileTransferInfo>();
|
||||||
|
|
||||||
if (progressInfo.completed()) {
|
if (progressInfo.completed()) {
|
||||||
@@ -47,7 +48,7 @@ QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const
|
|||||||
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("tempInfo")].toMap()[QStringLiteral("source")].toUrl();
|
return mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap()[QStringLiteral("tempInfo")].toMap()[QStringLiteral("source")].toUrl();
|
||||||
}
|
}
|
||||||
if (role == TypeRole) {
|
if (role == TypeRole) {
|
||||||
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == MessageEventModel::DelegateType::Image) {
|
if (mapToSource(index).data(MessageEventModel::DelegateTypeRole).toInt() == DelegateType::Image) {
|
||||||
return MediaType::Image;
|
return MediaType::Image;
|
||||||
} else {
|
} else {
|
||||||
return MediaType::Video;
|
return MediaType::Video;
|
||||||
|
|||||||
@@ -8,15 +8,9 @@
|
|||||||
|
|
||||||
#include <Quotient/connection.h>
|
#include <Quotient/connection.h>
|
||||||
#include <Quotient/csapi/rooms.h>
|
#include <Quotient/csapi/rooms.h>
|
||||||
#include <Quotient/events/reactionevent.h>
|
|
||||||
#include <Quotient/events/redactionevent.h>
|
#include <Quotient/events/redactionevent.h>
|
||||||
#include <Quotient/events/roomavatarevent.h>
|
|
||||||
#include <Quotient/events/roommemberevent.h>
|
|
||||||
#include <Quotient/events/simplestateevents.h>
|
|
||||||
#include <Quotient/user.h>
|
|
||||||
|
|
||||||
#include "events/pollevent.h"
|
|
||||||
#include <Quotient/events/stickerevent.h>
|
#include <Quotient/events/stickerevent.h>
|
||||||
|
#include <Quotient/user.h>
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
@@ -25,8 +19,9 @@
|
|||||||
|
|
||||||
#include <KLocalizedString>
|
#include <KLocalizedString>
|
||||||
|
|
||||||
|
#include "enums/delegatetype.h"
|
||||||
|
#include "eventhandler.h"
|
||||||
#include "models/reactionmodel.h"
|
#include "models/reactionmodel.h"
|
||||||
#include "texthandler.h"
|
|
||||||
|
|
||||||
using namespace Quotient;
|
using namespace Quotient;
|
||||||
|
|
||||||
@@ -37,6 +32,7 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
|||||||
roles[PlainText] = "plainText";
|
roles[PlainText] = "plainText";
|
||||||
roles[EventIdRole] = "eventId";
|
roles[EventIdRole] = "eventId";
|
||||||
roles[TimeRole] = "time";
|
roles[TimeRole] = "time";
|
||||||
|
roles[TimeStringRole] = "timeString";
|
||||||
roles[SectionRole] = "section";
|
roles[SectionRole] = "section";
|
||||||
roles[AuthorRole] = "author";
|
roles[AuthorRole] = "author";
|
||||||
roles[ContentRole] = "content";
|
roles[ContentRole] = "content";
|
||||||
@@ -48,8 +44,9 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
|||||||
roles[MediaInfoRole] = "mediaInfo";
|
roles[MediaInfoRole] = "mediaInfo";
|
||||||
roles[IsReplyRole] = "isReply";
|
roles[IsReplyRole] = "isReply";
|
||||||
roles[ReplyAuthor] = "replyAuthor";
|
roles[ReplyAuthor] = "replyAuthor";
|
||||||
roles[ReplyRole] = "reply";
|
|
||||||
roles[ReplyIdRole] = "replyId";
|
roles[ReplyIdRole] = "replyId";
|
||||||
|
roles[ReplyDelegateTypeRole] = "replyDelegateType";
|
||||||
|
roles[ReplyDisplayRole] = "replyDisplay";
|
||||||
roles[ReplyMediaInfoRole] = "replyMediaInfo";
|
roles[ReplyMediaInfoRole] = "replyMediaInfo";
|
||||||
roles[ShowAuthorRole] = "showAuthor";
|
roles[ShowAuthorRole] = "showAuthor";
|
||||||
roles[ShowSectionRole] = "showSection";
|
roles[ShowSectionRole] = "showSection";
|
||||||
@@ -60,10 +57,8 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const
|
|||||||
roles[ReactionRole] = "reaction";
|
roles[ReactionRole] = "reaction";
|
||||||
roles[ShowReactionsRole] = "showReactions";
|
roles[ShowReactionsRole] = "showReactions";
|
||||||
roles[SourceRole] = "jsonSource";
|
roles[SourceRole] = "jsonSource";
|
||||||
roles[MimeTypeRole] = "mimeType";
|
|
||||||
roles[AuthorIdRole] = "authorId";
|
roles[AuthorIdRole] = "authorId";
|
||||||
roles[VerifiedRole] = "verified";
|
roles[VerifiedRole] = "verified";
|
||||||
roles[DisplayNameForInitialsRole] = "displayNameForInitials";
|
|
||||||
roles[AuthorDisplayNameRole] = "authorDisplayName";
|
roles[AuthorDisplayNameRole] = "authorDisplayName";
|
||||||
roles[IsRedactedRole] = "isRedacted";
|
roles[IsRedactedRole] = "isRedacted";
|
||||||
roles[GenericDisplayRole] = "genericDisplay";
|
roles[GenericDisplayRole] = "genericDisplay";
|
||||||
@@ -97,7 +92,6 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
if (m_currentRoom) {
|
if (m_currentRoom) {
|
||||||
m_currentRoom->disconnect(this);
|
m_currentRoom->disconnect(this);
|
||||||
m_linkPreviewers.clear();
|
m_linkPreviewers.clear();
|
||||||
qDeleteAll(m_reactionModels);
|
|
||||||
m_reactionModels.clear();
|
m_reactionModels.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,10 +101,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
room->setDisplayed();
|
room->setDisplayed();
|
||||||
|
|
||||||
for (auto event = m_currentRoom->messageEvents().begin(); event != m_currentRoom->messageEvents().end(); ++event) {
|
for (auto event = m_currentRoom->messageEvents().begin(); event != m_currentRoom->messageEvents().end(); ++event) {
|
||||||
if (auto e = &*event->viewAs<RoomMessageEvent>()) {
|
createEventObjects(&*event->viewAs<RoomMessageEvent>());
|
||||||
createLinkPreviewerForEvent(e);
|
|
||||||
createReactionModelForEvent(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_currentRoom->timelineSize() < 10 && !room->allHistoryLoaded()) {
|
if (m_currentRoom->timelineSize() < 10 && !room->allHistoryLoaded()) {
|
||||||
@@ -123,16 +114,16 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
if (row == -1) {
|
if (row == -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyRole, ReplyMediaInfoRole, ReplyAuthor});
|
Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyDelegateTypeRole, ReplyDisplayRole, ReplyMediaInfoRole, ReplyAuthor});
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) {
|
connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) {
|
||||||
for (auto &&event : events) {
|
for (auto &&event : events) {
|
||||||
const RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
|
const RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
|
||||||
if (message != nullptr) {
|
|
||||||
createLinkPreviewerForEvent(message);
|
|
||||||
createReactionModelForEvent(message);
|
|
||||||
|
|
||||||
|
createEventObjects(message);
|
||||||
|
|
||||||
|
if (message != nullptr) {
|
||||||
if (NeoChatConfig::self()->showFancyEffects()) {
|
if (NeoChatConfig::self()->showFancyEffects()) {
|
||||||
QString planBody = message->plainBody();
|
QString planBody = message->plainBody();
|
||||||
// snowflake
|
// snowflake
|
||||||
@@ -169,10 +160,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) {
|
connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) {
|
||||||
for (auto &event : events) {
|
for (auto &event : events) {
|
||||||
RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
|
RoomMessageEvent *message = dynamic_cast<RoomMessageEvent *>(event.get());
|
||||||
if (message) {
|
createEventObjects(message);
|
||||||
createLinkPreviewerForEvent(message);
|
|
||||||
createReactionModelForEvent(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (rowCount() > 0) {
|
if (rowCount() > 0) {
|
||||||
rowBelowInserted = rowCount() - 1; // See #312
|
rowBelowInserted = rowCount() - 1; // See #312
|
||||||
@@ -241,7 +229,7 @@ void MessageEventModel::setRoom(NeoChatRoom *room)
|
|||||||
}
|
}
|
||||||
const auto eventIt = m_currentRoom->findInTimeline(eventId);
|
const auto eventIt = m_currentRoom->findInTimeline(eventId);
|
||||||
if (eventIt != m_currentRoom->historyEdge()) {
|
if (eventIt != m_currentRoom->historyEdge()) {
|
||||||
createReactionModelForEvent(static_cast<const RoomMessageEvent *>(&**eventIt));
|
createEventObjects(static_cast<const RoomMessageEvent *>(&**eventIt));
|
||||||
}
|
}
|
||||||
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole, Qt::DisplayRole});
|
refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole, Qt::DisplayRole});
|
||||||
});
|
});
|
||||||
@@ -340,7 +328,7 @@ int MessageEventModel::refreshEventRoles(const QString &id, const QVector<int> &
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
row = int(timelineIt - m_currentRoom->messageEvents().rbegin()) + timelineBaseIndex();
|
row = int(timelineIt - m_currentRoom->messageEvents().rbegin()) + timelineBaseIndex();
|
||||||
if (data(index(row, 0), DelegateTypeRole).toInt() == ReadMarker || data(index(row, 0), DelegateTypeRole).toInt() == Other) {
|
if (data(index(row, 0), DelegateTypeRole).toInt() == DelegateType::ReadMarker || data(index(row, 0), DelegateTypeRole).toInt() == DelegateType::Other) {
|
||||||
row++;
|
row++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -458,6 +446,10 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
const auto pendingIt = m_currentRoom->pendingEvents().crbegin() + std::min(row, timelineBaseIndex());
|
const auto pendingIt = m_currentRoom->pendingEvents().crbegin() + std::min(row, timelineBaseIndex());
|
||||||
const auto &evt = isPending ? **pendingIt : **timelineIt;
|
const auto &evt = isPending ? **pendingIt : **timelineIt;
|
||||||
|
|
||||||
|
EventHandler eventHandler;
|
||||||
|
eventHandler.setRoom(m_currentRoom);
|
||||||
|
eventHandler.setEvent(&evt);
|
||||||
|
|
||||||
if (role == Qt::DisplayRole) {
|
if (role == Qt::DisplayRole) {
|
||||||
if (evt.isRedacted()) {
|
if (evt.isRedacted()) {
|
||||||
auto reason = evt.redactedBecause()->reason();
|
auto reason = evt.redactedBecause()->reason();
|
||||||
@@ -465,19 +457,15 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
: i18n("<i>[This message was deleted: %1]</i>", evt.redactedBecause()->reason());
|
: i18n("<i>[This message was deleted: %1]</i>", evt.redactedBecause()->reason());
|
||||||
}
|
}
|
||||||
|
|
||||||
return m_currentRoom->eventToString(evt, Qt::RichText);
|
return eventHandler.getRichBody();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == GenericDisplayRole) {
|
if (role == GenericDisplayRole) {
|
||||||
if (evt.isRedacted()) {
|
return eventHandler.getGenericBody();
|
||||||
return i18n("<i>[This message was deleted]</i>");
|
|
||||||
}
|
|
||||||
|
|
||||||
return m_currentRoom->eventToGenericString(evt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == PlainText) {
|
if (role == PlainText) {
|
||||||
return m_currentRoom->eventToString(evt);
|
return eventHandler.getPlainBody();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == SourceRole) {
|
if (role == SourceRole) {
|
||||||
@@ -485,54 +473,11 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (role == DelegateTypeRole) {
|
if (role == DelegateTypeRole) {
|
||||||
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
return eventHandler.getDelegateType();
|
||||||
switch (e->msgtype()) {
|
|
||||||
case MessageEventType::Emote:
|
|
||||||
return DelegateType::Emote;
|
|
||||||
case MessageEventType::Notice:
|
|
||||||
return DelegateType::Notice;
|
|
||||||
case MessageEventType::Image:
|
|
||||||
return DelegateType::Image;
|
|
||||||
case MessageEventType::Audio:
|
|
||||||
return DelegateType::Audio;
|
|
||||||
case MessageEventType::Video:
|
|
||||||
return DelegateType::Video;
|
|
||||||
case MessageEventType::Location:
|
|
||||||
return DelegateType::Location;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (e->hasFileContent()) {
|
|
||||||
return DelegateType::File;
|
|
||||||
}
|
|
||||||
|
|
||||||
return DelegateType::Message;
|
|
||||||
}
|
|
||||||
if (is<const StickerEvent>(evt)) {
|
|
||||||
return DelegateType::Sticker;
|
|
||||||
}
|
|
||||||
if (evt.isStateEvent()) {
|
|
||||||
if (evt.matrixType() == "org.matrix.msc3672.beacon_info"_ls) {
|
|
||||||
return DelegateType::LiveLocation;
|
|
||||||
}
|
|
||||||
return DelegateType::State;
|
|
||||||
}
|
|
||||||
if (is<const EncryptedEvent>(evt)) {
|
|
||||||
return DelegateType::Encrypted;
|
|
||||||
}
|
|
||||||
if (is<PollStartEvent>(evt)) {
|
|
||||||
if (evt.isRedacted()) {
|
|
||||||
return DelegateType::Message;
|
|
||||||
}
|
|
||||||
return DelegateType::Poll;
|
|
||||||
}
|
|
||||||
|
|
||||||
return DelegateType::Other;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == AuthorRole) {
|
if (role == AuthorRole) {
|
||||||
auto author = isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId());
|
return eventHandler.getAuthor(isPending);
|
||||||
return m_currentRoom->getUser(author);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == ContentRole) {
|
if (role == ContentRole) {
|
||||||
@@ -558,21 +503,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (role == HighlightRole) {
|
if (role == HighlightRole) {
|
||||||
return !m_currentRoom->isDirectChat() && m_currentRoom->isEventHighlighted(&evt);
|
return eventHandler.isHighlighted();
|
||||||
}
|
|
||||||
|
|
||||||
if (role == MimeTypeRole) {
|
|
||||||
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
|
||||||
if (!e || !e->hasFileContent()) {
|
|
||||||
return QVariant();
|
|
||||||
}
|
|
||||||
|
|
||||||
return e->content()->fileInfo()->mimeType.name();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auto e = eventCast<const StickerEvent>(&evt)) {
|
|
||||||
return e->image().mimeType.name();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == SpecialMarksRole) {
|
if (role == SpecialMarksRole) {
|
||||||
@@ -585,46 +516,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
return pendingIt->deliveryStatus();
|
return pendingIt->deliveryStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (evt.isStateEvent() && !NeoChatConfig::self()->showStateEvent()) {
|
if (eventHandler.isHidden()) {
|
||||||
return EventStatus::Hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auto roomMemberEvent = eventCast<const RoomMemberEvent>(&evt)) {
|
|
||||||
if ((roomMemberEvent->isJoin() || roomMemberEvent->isLeave()) && !NeoChatConfig::self()->showLeaveJoinEvent()) {
|
|
||||||
return EventStatus::Hidden;
|
|
||||||
} else if (roomMemberEvent->isRename() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave() && !NeoChatConfig::self()->showRename()) {
|
|
||||||
return EventStatus::Hidden;
|
|
||||||
} else if (roomMemberEvent->isAvatarUpdate() && !roomMemberEvent->isJoin() && !roomMemberEvent->isLeave()
|
|
||||||
&& !NeoChatConfig::self()->showAvatarUpdate()) {
|
|
||||||
return EventStatus::Hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// isReplacement?
|
|
||||||
if (auto e = eventCast<const RoomMessageEvent>(&evt))
|
|
||||||
if (!e->replacedEvent().isEmpty())
|
|
||||||
return EventStatus::Hidden;
|
|
||||||
|
|
||||||
if (is<RedactionEvent>(evt) || is<ReactionEvent>(evt)) {
|
|
||||||
return EventStatus::Hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (evt.isStateEvent() && static_cast<const StateEvent &>(evt).repeatsState()) {
|
|
||||||
return EventStatus::Hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
|
|
||||||
if (!e->replacedEvent().isEmpty() && e->replacedEvent() != e->id()) {
|
|
||||||
return EventStatus::Hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_currentRoom->connection()->isIgnored(m_currentRoom->user(evt.senderId()))) {
|
|
||||||
return EventStatus::Hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
// hide ending live location beacons
|
|
||||||
if (evt.isStateEvent() && evt.matrixType() == "org.matrix.msc3672.beacon_info"_ls && !evt.contentJson()["live"_ls].toBool()) {
|
|
||||||
return EventStatus::Hidden;
|
return EventStatus::Hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -632,7 +524,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (role == EventIdRole) {
|
if (role == EventIdRole) {
|
||||||
return !evt.id().isEmpty() ? evt.id() : evt.transactionId();
|
return eventHandler.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == ProgressInfoRole) {
|
if (role == ProgressInfoRole) {
|
||||||
@@ -646,9 +538,19 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == TimeRole || role == SectionRole) {
|
if (role == TimeRole) {
|
||||||
auto ts = isPending ? pendingIt->lastUpdated() : makeMessageTimestamp(timelineIt);
|
auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime();
|
||||||
return role == TimeRole ? QVariant(ts) : m_format.formatRelativeDate(ts.toLocalTime().date(), QLocale::ShortFormat);
|
return eventHandler.getTime(isPending, lastUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role == TimeStringRole) {
|
||||||
|
auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime();
|
||||||
|
return eventHandler.getTimeString(false, QLocale::ShortFormat, isPending, lastUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role == SectionRole) {
|
||||||
|
auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime();
|
||||||
|
return eventHandler.getTimeString(true, QLocale::ShortFormat, isPending, lastUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == ShowLinkPreviewRole) {
|
if (role == ShowLinkPreviewRole) {
|
||||||
@@ -657,85 +559,38 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
|
|
||||||
if (role == LinkPreviewRole) {
|
if (role == LinkPreviewRole) {
|
||||||
if (m_linkPreviewers.contains(evt.id())) {
|
if (m_linkPreviewers.contains(evt.id())) {
|
||||||
return QVariant::fromValue<LinkPreviewer *>(m_linkPreviewers[evt.id()]);
|
return QVariant::fromValue<LinkPreviewer *>(m_linkPreviewers[evt.id()].data());
|
||||||
} else {
|
} else {
|
||||||
return QVariant::fromValue<LinkPreviewer *>(emptyLinkPreview);
|
return QVariant::fromValue<LinkPreviewer *>(emptyLinkPreview);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == MediaInfoRole) {
|
if (role == MediaInfoRole) {
|
||||||
return getMediaInfoForEvent(evt);
|
return eventHandler.getMediaInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == IsReplyRole) {
|
if (role == IsReplyRole) {
|
||||||
return !evt.contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString().isEmpty();
|
return eventHandler.hasReply();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == ReplyIdRole) {
|
if (role == ReplyIdRole) {
|
||||||
return evt.contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
|
return eventHandler.getReplyId();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role == ReplyDelegateTypeRole) {
|
||||||
|
return eventHandler.getReplyDelegateType();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == ReplyAuthor) {
|
if (role == ReplyAuthor) {
|
||||||
auto replyPtr = m_currentRoom->getReplyForEvent(evt);
|
return eventHandler.getReplyAuthor();
|
||||||
|
}
|
||||||
|
|
||||||
if (replyPtr) {
|
if (role == ReplyDisplayRole) {
|
||||||
auto replyUser = m_currentRoom->user(replyPtr->senderId());
|
return eventHandler.getReplyRichBody();
|
||||||
return m_currentRoom->getUser(replyUser);
|
|
||||||
} else {
|
|
||||||
return m_currentRoom->getUser(nullptr);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == ReplyMediaInfoRole) {
|
if (role == ReplyMediaInfoRole) {
|
||||||
auto replyPtr = m_currentRoom->getReplyForEvent(evt);
|
return eventHandler.getReplyMediaInfo();
|
||||||
if (!replyPtr) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return getMediaInfoForEvent(*replyPtr);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role == ReplyRole) {
|
|
||||||
auto replyPtr = m_currentRoom->getReplyForEvent(evt);
|
|
||||||
if (!replyPtr) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
DelegateType type;
|
|
||||||
if (auto e = eventCast<const RoomMessageEvent>(replyPtr)) {
|
|
||||||
switch (e->msgtype()) {
|
|
||||||
case MessageEventType::Emote:
|
|
||||||
type = DelegateType::Emote;
|
|
||||||
break;
|
|
||||||
case MessageEventType::Notice:
|
|
||||||
type = DelegateType::Notice;
|
|
||||||
break;
|
|
||||||
case MessageEventType::Image:
|
|
||||||
type = DelegateType::Image;
|
|
||||||
break;
|
|
||||||
case MessageEventType::Audio:
|
|
||||||
type = DelegateType::Audio;
|
|
||||||
break;
|
|
||||||
case MessageEventType::Video:
|
|
||||||
type = DelegateType::Video;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (e->hasFileContent()) {
|
|
||||||
type = DelegateType::File;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
type = DelegateType::Message;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (is<const StickerEvent>(*replyPtr)) {
|
|
||||||
type = DelegateType::Sticker;
|
|
||||||
} else {
|
|
||||||
type = DelegateType::Other;
|
|
||||||
}
|
|
||||||
|
|
||||||
return QVariantMap{
|
|
||||||
{"display"_ls, m_currentRoom->eventToString(*replyPtr, Qt::RichText)},
|
|
||||||
{"type"_ls, type},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == ShowAuthorRole) {
|
if (role == ShowAuthorRole) {
|
||||||
@@ -745,7 +600,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
// While the row is removed the subsequent row indexes are not changed so we need to skip over the removed index.
|
// While the row is removed the subsequent row indexes are not changed so we need to skip over the removed index.
|
||||||
// See - https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows
|
// See - https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows
|
||||||
if (data(i, SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) {
|
if (data(i, SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) {
|
||||||
return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, DelegateTypeRole) == MessageEventModel::State
|
return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, DelegateTypeRole) == DelegateType::State
|
||||||
|| data(i, TimeRole).toDateTime().msecsTo(data(idx, TimeRole).toDateTime()) > 600000
|
|| data(i, TimeRole).toDateTime().msecsTo(data(idx, TimeRole).toDateTime()) > 600000
|
||||||
|| data(i, TimeRole).toDateTime().toLocalTime().date().day() != data(idx, TimeRole).toDateTime().toLocalTime().date().day();
|
|| data(i, TimeRole).toDateTime().toLocalTime().date().day() != data(idx, TimeRole).toDateTime().toLocalTime().date().day();
|
||||||
}
|
}
|
||||||
@@ -771,87 +626,36 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (role == LatitudeRole) {
|
if (role == LatitudeRole) {
|
||||||
const auto geoUri = evt.contentJson()["geo_uri"_ls].toString();
|
return eventHandler.getLatitude();
|
||||||
if (geoUri.isEmpty()) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[0];
|
|
||||||
return latitude.toFloat();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == LongitudeRole) {
|
if (role == LongitudeRole) {
|
||||||
const auto geoUri = evt.contentJson()["geo_uri"_ls].toString();
|
return eventHandler.getLongitude();
|
||||||
if (geoUri.isEmpty()) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[1];
|
|
||||||
return latitude.toFloat();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == AssetRole) {
|
if (role == AssetRole) {
|
||||||
const auto assetType = evt.contentJson()["org.matrix.msc3488.asset"_ls].toObject()["type"_ls].toString();
|
return eventHandler.getLocationAssetType();
|
||||||
if (assetType.isEmpty()) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return assetType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == ReadMarkersRole) {
|
if (role == ReadMarkersRole) {
|
||||||
auto userIds_temp = room()->userIdsAtEvent(evt.id());
|
return eventHandler.getReadMarkers();
|
||||||
userIds_temp.remove(m_currentRoom->localUser()->id());
|
|
||||||
|
|
||||||
auto userIds = userIds_temp.values();
|
|
||||||
if (userIds.count() > 5) {
|
|
||||||
userIds = userIds.mid(0, 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariantList users;
|
|
||||||
users.reserve(userIds.size());
|
|
||||||
for (const auto &userId : userIds) {
|
|
||||||
auto user = m_currentRoom->user(userId);
|
|
||||||
users += m_currentRoom->getUser(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
return users;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == ExcessReadMarkersRole) {
|
if (role == ExcessReadMarkersRole) {
|
||||||
auto userIds = room()->userIdsAtEvent(evt.id());
|
return eventHandler.getNumberExcessReadMarkers();
|
||||||
userIds.remove(m_currentRoom->localUser()->id());
|
|
||||||
|
|
||||||
if (userIds.count() > 5) {
|
|
||||||
return QStringLiteral("+ %1").arg(userIds.count() - 5);
|
|
||||||
} else {
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == ReadMarkersStringRole) {
|
if (role == ReadMarkersStringRole) {
|
||||||
auto userIds = room()->userIdsAtEvent(evt.id());
|
return eventHandler.getReadMarkersString();
|
||||||
userIds.remove(m_currentRoom->localUser()->id());
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The string ends up in the form
|
|
||||||
* "x users: user1DisplayName, user2DisplayName, etc."
|
|
||||||
*/
|
|
||||||
QString readMarkersString = i18np("1 user: ", "%1 users: ", userIds.size());
|
|
||||||
for (const auto &userId : userIds) {
|
|
||||||
auto user = m_currentRoom->user(userId);
|
|
||||||
readMarkersString += user->displayname(m_currentRoom) + i18nc("list separator", ", ");
|
|
||||||
}
|
|
||||||
readMarkersString.chop(2);
|
|
||||||
return readMarkersString;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == ShowReadMarkersRole) {
|
if (role == ShowReadMarkersRole) {
|
||||||
auto userIds = room()->userIdsAtEvent(evt.id());
|
return eventHandler.hasReadMarkers();
|
||||||
userIds.remove(m_currentRoom->localUser()->id());
|
|
||||||
return userIds.size() > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == ReactionRole) {
|
if (role == ReactionRole) {
|
||||||
if (m_reactionModels.contains(evt.id())) {
|
if (m_reactionModels.contains(evt.id())) {
|
||||||
return QVariant::fromValue<ReactionModel *>(m_reactionModels[evt.id()]);
|
return QVariant::fromValue<ReactionModel *>(m_reactionModels[evt.id()].data());
|
||||||
} else {
|
} else {
|
||||||
return QVariantList();
|
return QVariantList();
|
||||||
}
|
}
|
||||||
@@ -874,22 +678,8 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == DisplayNameForInitialsRole) {
|
|
||||||
auto user = isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId());
|
|
||||||
return user->displayname(m_currentRoom).remove(QStringLiteral(" (%1)").arg(user->id()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role == AuthorDisplayNameRole) {
|
if (role == AuthorDisplayNameRole) {
|
||||||
if (is<RoomMemberEvent>(evt) && !evt.unsignedJson()["prev_content"_ls]["displayname"_ls].isNull() && evt.stateKey() == evt.senderId()) {
|
return eventHandler.getAuthorDisplayName(isPending);
|
||||||
auto previousDisplayName = evt.unsignedJson()["prev_content"_ls]["displayname"_ls].toString().toHtmlEscaped();
|
|
||||||
if (previousDisplayName.isEmpty()) {
|
|
||||||
previousDisplayName = evt.senderId();
|
|
||||||
}
|
|
||||||
return previousDisplayName;
|
|
||||||
} else {
|
|
||||||
auto author = isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId());
|
|
||||||
return m_currentRoom->htmlSafeMemberName(author->id());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role == IsRedactedRole) {
|
if (role == IsRedactedRole) {
|
||||||
@@ -913,195 +703,27 @@ int MessageEventModel::eventIdToRow(const QString &eventID) const
|
|||||||
return it - m_currentRoom->messageEvents().rbegin() + timelineBaseIndex();
|
return it - m_currentRoom->messageEvents().rbegin() + timelineBaseIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantMap MessageEventModel::getMediaInfoForEvent(const RoomEvent &event) const
|
void MessageEventModel::createEventObjects(const Quotient::RoomMessageEvent *event)
|
||||||
{
|
|
||||||
QVariantMap mediaInfo;
|
|
||||||
|
|
||||||
QString eventId = event.id();
|
|
||||||
|
|
||||||
// Get the file info for the event.
|
|
||||||
const EventContent::FileInfo *fileInfo;
|
|
||||||
if (event.is<RoomMessageEvent>()) {
|
|
||||||
auto roomMessageEvent = eventCast<const RoomMessageEvent>(&event);
|
|
||||||
if (!roomMessageEvent->hasFileContent()) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
fileInfo = roomMessageEvent->content()->fileInfo();
|
|
||||||
} else if (event.is<StickerEvent>()) {
|
|
||||||
auto stickerEvent = eventCast<const StickerEvent>(&event);
|
|
||||||
fileInfo = &stickerEvent->image();
|
|
||||||
} else {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return getMediaInfoFromFileInfo(fileInfo, eventId);
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariantMap MessageEventModel::getMediaInfoFromFileInfo(const EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail) const
|
|
||||||
{
|
|
||||||
QVariantMap mediaInfo;
|
|
||||||
|
|
||||||
// Get the mxc URL for the media.
|
|
||||||
if (!fileInfo->url().isValid() || eventId.isEmpty()) {
|
|
||||||
mediaInfo["source"_ls] = QUrl();
|
|
||||||
} else {
|
|
||||||
QUrl source = m_currentRoom->makeMediaUrl(eventId, fileInfo->url());
|
|
||||||
|
|
||||||
if (source.isValid() && source.scheme() == QStringLiteral("mxc")) {
|
|
||||||
mediaInfo["source"_ls] = source;
|
|
||||||
} else {
|
|
||||||
mediaInfo["source"_ls] = QUrl();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto mimeType = fileInfo->mimeType;
|
|
||||||
// Add the MIME type for the media if available.
|
|
||||||
mediaInfo["mimeType"_ls] = mimeType.name();
|
|
||||||
|
|
||||||
// Add the MIME type icon if available.
|
|
||||||
mediaInfo["mimeIcon"_ls] = mimeType.iconName();
|
|
||||||
|
|
||||||
// Add media size if available.
|
|
||||||
mediaInfo["size"_ls] = fileInfo->payloadSize;
|
|
||||||
|
|
||||||
// Add parameter depending on media type.
|
|
||||||
if (mimeType.name().contains(QStringLiteral("image"))) {
|
|
||||||
if (auto castInfo = static_cast<const EventContent::ImageContent *>(fileInfo)) {
|
|
||||||
mediaInfo["width"_ls] = castInfo->imageSize.width();
|
|
||||||
mediaInfo["height"_ls] = castInfo->imageSize.height();
|
|
||||||
|
|
||||||
// TODO: Images in certain formats (e.g. WebP) will be erroneously marked as animated, even if they are static.
|
|
||||||
mediaInfo["animated"_ls] = QMovie::supportedFormats().contains(mimeType.preferredSuffix().toUtf8());
|
|
||||||
|
|
||||||
if (!isThumbnail) {
|
|
||||||
QVariantMap tempInfo;
|
|
||||||
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
|
|
||||||
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
|
|
||||||
tempInfo = thumbnailInfo;
|
|
||||||
} else {
|
|
||||||
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
|
|
||||||
if (blurhash.isEmpty()) {
|
|
||||||
tempInfo["source"_ls] = QUrl();
|
|
||||||
} else {
|
|
||||||
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mediaInfo["tempInfo"_ls] = tempInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (mimeType.name().contains(QStringLiteral("video"))) {
|
|
||||||
if (auto castInfo = static_cast<const EventContent::VideoContent *>(fileInfo)) {
|
|
||||||
mediaInfo["width"_ls] = castInfo->imageSize.width();
|
|
||||||
mediaInfo["height"_ls] = castInfo->imageSize.height();
|
|
||||||
mediaInfo["duration"_ls] = castInfo->duration;
|
|
||||||
|
|
||||||
if (!isThumbnail) {
|
|
||||||
QVariantMap tempInfo;
|
|
||||||
auto thumbnailInfo = getMediaInfoFromFileInfo(castInfo->thumbnailInfo(), eventId, true);
|
|
||||||
if (thumbnailInfo["source"_ls].toUrl().scheme() == "mxc"_ls) {
|
|
||||||
tempInfo = thumbnailInfo;
|
|
||||||
} else {
|
|
||||||
QString blurhash = castInfo->originalInfoJson["xyz.amorgan.blurhash"_ls].toString();
|
|
||||||
if (blurhash.isEmpty()) {
|
|
||||||
tempInfo["source"_ls] = QUrl();
|
|
||||||
} else {
|
|
||||||
tempInfo["source"_ls] = QUrl("image://blurhash/"_ls + blurhash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mediaInfo["tempInfo"_ls] = tempInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (mimeType.name().contains(QStringLiteral("audio"))) {
|
|
||||||
if (auto castInfo = static_cast<const EventContent::AudioContent *>(fileInfo)) {
|
|
||||||
mediaInfo["duration"_ls] = castInfo->duration;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mediaInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessageEventModel::createLinkPreviewerForEvent(const Quotient::RoomMessageEvent *event)
|
|
||||||
{
|
|
||||||
if (m_linkPreviewers.contains(event->id())) {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
QString text;
|
|
||||||
if (event->hasTextContent()) {
|
|
||||||
auto textContent = static_cast<const EventContent::TextContent *>(event->content());
|
|
||||||
if (textContent) {
|
|
||||||
text = textContent->body;
|
|
||||||
} else {
|
|
||||||
text = event->plainBody();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
text = event->plainBody();
|
|
||||||
}
|
|
||||||
TextHandler textHandler;
|
|
||||||
textHandler.setData(text);
|
|
||||||
|
|
||||||
QList<QUrl> links = textHandler.getLinkPreviews();
|
|
||||||
if (links.size() > 0) {
|
|
||||||
m_linkPreviewers[event->id()] = new LinkPreviewer(nullptr, m_currentRoom, links.size() > 0 ? links[0] : QUrl());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessageEventModel::createReactionModelForEvent(const Quotient::RoomMessageEvent *event)
|
|
||||||
{
|
{
|
||||||
if (event == nullptr) {
|
if (event == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto eventId = event->id();
|
auto eventId = event->id();
|
||||||
const auto &annotations = m_currentRoom->relatedEvents(eventId, EventRelation::AnnotationType);
|
|
||||||
if (annotations.isEmpty()) {
|
|
||||||
if (m_reactionModels.contains(eventId)) {
|
|
||||||
delete m_reactionModels[eventId];
|
|
||||||
m_reactionModels.remove(eventId);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
QMap<QString, QList<Quotient::User *>> reactions = {};
|
EventHandler eventHandler;
|
||||||
for (const auto &a : annotations) {
|
eventHandler.setRoom(m_currentRoom);
|
||||||
if (a->isRedacted()) { // Just in case?
|
eventHandler.setEvent(event);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (const auto &e = eventCast<const ReactionEvent>(a)) {
|
|
||||||
reactions[e->key()].append(m_currentRoom->user(e->senderId()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reactions.isEmpty()) {
|
if (auto linkPreviewer = eventHandler.getLinkPreviewer()) {
|
||||||
if (m_reactionModels.contains(eventId)) {
|
m_linkPreviewers[eventId] = linkPreviewer;
|
||||||
delete m_reactionModels[eventId];
|
|
||||||
m_reactionModels.remove(eventId);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<ReactionModel::Reaction> res;
|
|
||||||
auto i = reactions.constBegin();
|
|
||||||
while (i != reactions.constEnd()) {
|
|
||||||
QVariantList authors;
|
|
||||||
for (const auto &author : i.value()) {
|
|
||||||
authors.append(m_currentRoom->getUser(author));
|
|
||||||
}
|
|
||||||
|
|
||||||
res.append(ReactionModel::Reaction{i.key(), authors});
|
|
||||||
++i;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_reactionModels.contains(eventId)) {
|
|
||||||
m_reactionModels[eventId]->setReactions(res);
|
|
||||||
} else if (res.size() > 0) {
|
|
||||||
m_reactionModels[eventId] = new ReactionModel(this, res, m_currentRoom->localUser());
|
|
||||||
} else {
|
} else {
|
||||||
if (m_reactionModels.contains(eventId)) {
|
m_linkPreviewers.remove(eventId);
|
||||||
delete m_reactionModels[eventId];
|
}
|
||||||
m_reactionModels.remove(eventId);
|
if (auto reactionModel = eventHandler.getReactions()) {
|
||||||
}
|
m_reactionModels[eventId] = reactionModel;
|
||||||
|
} else {
|
||||||
|
m_reactionModels.remove(eventId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,32 +32,6 @@ class MessageEventModel : public QAbstractListModel
|
|||||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/**
|
|
||||||
* @brief The type of delegate that is needed for the event.
|
|
||||||
*
|
|
||||||
* @note While similar this is not the matrix event or message type. This is
|
|
||||||
* to tell a QML ListView what delegate to show for each event. So while
|
|
||||||
* similar to the spec it is not the same.
|
|
||||||
*/
|
|
||||||
enum DelegateType {
|
|
||||||
Emote, /**< A message that begins with /me. */
|
|
||||||
Notice, /**< A notice event. */
|
|
||||||
Image, /**< A message that is an image. */
|
|
||||||
Audio, /**< A message that is an audio recording. */
|
|
||||||
Video, /**< A message that is a video. */
|
|
||||||
File, /**< A message that is a file. */
|
|
||||||
Message, /**< A text message. */
|
|
||||||
Sticker, /**< A message that is a sticker. */
|
|
||||||
State, /**< A state event in the room. */
|
|
||||||
Encrypted, /**< An encrypted message that cannot be decrypted. */
|
|
||||||
ReadMarker, /**< The local user read marker. */
|
|
||||||
Poll, /**< The initial event for a poll. */
|
|
||||||
Location, /**< A location event. */
|
|
||||||
LiveLocation, /**< The initial event of a shared live location (i.e., the place where this is supposed to be shown in the timeline). */
|
|
||||||
Other, /**< Anything that cannot be classified as another type. */
|
|
||||||
};
|
|
||||||
Q_ENUM(DelegateType)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Defines the model roles.
|
* @brief Defines the model roles.
|
||||||
*/
|
*/
|
||||||
@@ -65,7 +39,8 @@ public:
|
|||||||
DelegateTypeRole = Qt::UserRole + 1, /**< The delegate type of the message. */
|
DelegateTypeRole = Qt::UserRole + 1, /**< The delegate type of the message. */
|
||||||
PlainText, /**< Plain text representation of the message. */
|
PlainText, /**< Plain text representation of the message. */
|
||||||
EventIdRole, /**< The matrix event ID of the event. */
|
EventIdRole, /**< The matrix event ID of the event. */
|
||||||
TimeRole, /**< The timestamp for when the event was sent. */
|
TimeRole, /**< The timestamp for when the event was sent (as a QDateTime). */
|
||||||
|
TimeStringRole, /**< The timestamp for when the event was sent as a string (in QLocale::ShortFormat). */
|
||||||
SectionRole, /**< The date of the event as a string. */
|
SectionRole, /**< The date of the event as a string. */
|
||||||
AuthorRole, /**< The author of the event. */
|
AuthorRole, /**< The author of the event. */
|
||||||
ContentRole, /**< The full message content. */
|
ContentRole, /**< The full message content. */
|
||||||
@@ -78,13 +53,13 @@ public:
|
|||||||
LinkPreviewRole, /**< The link preview details. */
|
LinkPreviewRole, /**< The link preview details. */
|
||||||
|
|
||||||
MediaInfoRole, /**< The media info for the event. */
|
MediaInfoRole, /**< The media info for the event. */
|
||||||
MimeTypeRole, /**< The mime type of the message's file or media. */
|
|
||||||
|
|
||||||
IsReplyRole, /**< Is the message a reply to another event. */
|
IsReplyRole, /**< Is the message a reply to another event. */
|
||||||
ReplyAuthor, /**< The author of the event that was replied to. */
|
ReplyAuthor, /**< The author of the event that was replied to. */
|
||||||
ReplyIdRole, /**< The matrix ID of the message that was replied to. */
|
ReplyIdRole, /**< The matrix ID of the message that was replied to. */
|
||||||
|
ReplyDelegateTypeRole, /**< The delegate type of the message that was replied to. */
|
||||||
|
ReplyDisplayRole, /**< The body of the message that was replied to. */
|
||||||
ReplyMediaInfoRole, /**< The media info of the message that was replied to. */
|
ReplyMediaInfoRole, /**< The media info of the message that was replied to. */
|
||||||
ReplyRole, /**< The content data of the message that was replied to. */
|
|
||||||
|
|
||||||
ShowAuthorRole, /**< Whether the author's name should be shown. */
|
ShowAuthorRole, /**< Whether the author's name should be shown. */
|
||||||
ShowSectionRole, /**< Whether the section header should be shown. */
|
ShowSectionRole, /**< Whether the section header should be shown. */
|
||||||
@@ -100,7 +75,6 @@ public:
|
|||||||
AuthorIdRole, /**< Matrix ID of the message author. */
|
AuthorIdRole, /**< Matrix ID of the message author. */
|
||||||
|
|
||||||
VerifiedRole, /**< Whether an encrypted message is sent in a verified session. */
|
VerifiedRole, /**< Whether an encrypted message is sent in a verified session. */
|
||||||
DisplayNameForInitialsRole, /**< Sender's displayname, always without the matrix id. */
|
|
||||||
AuthorDisplayNameRole, /**< The displayname for the event's sender; for name change events, the old displayname. */
|
AuthorDisplayNameRole, /**< The displayname for the event's sender; for name change events, the old displayname. */
|
||||||
IsRedactedRole, /**< Whether an event has been deleted. */
|
IsRedactedRole, /**< Whether an event has been deleted. */
|
||||||
IsPendingRole, /**< Whether an event is waiting to be accepted by the server. */
|
IsPendingRole, /**< Whether an event is waiting to be accepted by the server. */
|
||||||
@@ -154,8 +128,8 @@ private:
|
|||||||
bool movingEvent = false;
|
bool movingEvent = false;
|
||||||
KFormat m_format;
|
KFormat m_format;
|
||||||
|
|
||||||
QMap<QString, LinkPreviewer *> m_linkPreviewers;
|
QMap<QString, QSharedPointer<LinkPreviewer>> m_linkPreviewers;
|
||||||
QMap<QString, ReactionModel *> m_reactionModels;
|
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
|
||||||
|
|
||||||
[[nodiscard]] int timelineBaseIndex() const;
|
[[nodiscard]] int timelineBaseIndex() const;
|
||||||
[[nodiscard]] QDateTime makeMessageTimestamp(const Quotient::Room::rev_iter_t &baseIt) const;
|
[[nodiscard]] QDateTime makeMessageTimestamp(const Quotient::Room::rev_iter_t &baseIt) const;
|
||||||
@@ -168,10 +142,7 @@ private:
|
|||||||
int refreshEventRoles(const QString &eventId, const QVector<int> &roles = {});
|
int refreshEventRoles(const QString &eventId, const QVector<int> &roles = {});
|
||||||
void moveReadMarker(const QString &toEventId);
|
void moveReadMarker(const QString &toEventId);
|
||||||
|
|
||||||
QVariantMap getMediaInfoForEvent(const Quotient::RoomEvent &event) const;
|
void createEventObjects(const Quotient::RoomMessageEvent *event);
|
||||||
QVariantMap getMediaInfoFromFileInfo(const Quotient::EventContent::FileInfo *fileInfo, const QString &eventId, bool isThumbnail = false) const;
|
|
||||||
void createLinkPreviewerForEvent(const Quotient::RoomMessageEvent *event);
|
|
||||||
void createReactionModelForEvent(const Quotient::RoomMessageEvent *event);
|
|
||||||
// Hack to ensure that we don't call endInsertRows when we haven't called beginInsertRows
|
// Hack to ensure that we don't call endInsertRows when we haven't called beginInsertRows
|
||||||
bool m_initialized = false;
|
bool m_initialized = false;
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include <KLocalizedString>
|
#include <KLocalizedString>
|
||||||
|
|
||||||
|
#include "enums/delegatetype.h"
|
||||||
#include "messageeventmodel.h"
|
#include "messageeventmodel.h"
|
||||||
#include "neochatconfig.h"
|
#include "neochatconfig.h"
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ bool MessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sour
|
|||||||
|
|
||||||
// Don't show events with an unknown type.
|
// Don't show events with an unknown type.
|
||||||
const auto eventType = index.data(MessageEventModel::DelegateTypeRole).toInt();
|
const auto eventType = index.data(MessageEventModel::DelegateTypeRole).toInt();
|
||||||
if (eventType == MessageEventModel::Other) {
|
if (eventType == DelegateType::Other) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,10 +59,10 @@ bool MessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sour
|
|||||||
// same day as they will be grouped as a single delegate.
|
// same day as they will be grouped as a single delegate.
|
||||||
const bool notLastRow = sourceRow < sourceModel()->rowCount() - 1;
|
const bool notLastRow = sourceRow < sourceModel()->rowCount() - 1;
|
||||||
const bool previousEventIsState = notLastRow
|
const bool previousEventIsState = notLastRow
|
||||||
? sourceModel()->data(sourceModel()->index(sourceRow + 1, 0), MessageEventModel::DelegateTypeRole) == MessageEventModel::DelegateType::State
|
? sourceModel()->data(sourceModel()->index(sourceRow + 1, 0), MessageEventModel::DelegateTypeRole) == DelegateType::State
|
||||||
: false;
|
: false;
|
||||||
const bool newDay = sourceModel()->data(sourceModel()->index(sourceRow, 0), MessageEventModel::ShowSectionRole).toBool();
|
const bool newDay = sourceModel()->data(sourceModel()->index(sourceRow, 0), MessageEventModel::ShowSectionRole).toBool();
|
||||||
if (eventType == MessageEventModel::State && notLastRow && previousEventIsState && !newDay) {
|
if (eventType == DelegateType::State && notLastRow && previousEventIsState && !newDay) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,8 +104,7 @@ QString MessageFilterModel::aggregateEventToString(int sourceRow) const
|
|||||||
uniqueAuthors.append(nextAuthor);
|
uniqueAuthors.append(nextAuthor);
|
||||||
}
|
}
|
||||||
if (i > 0
|
if (i > 0
|
||||||
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event
|
||||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
|
||||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||||
)) {
|
)) {
|
||||||
break;
|
break;
|
||||||
@@ -162,8 +162,7 @@ QVariantList MessageFilterModel::stateEventsList(int sourceRow) const
|
|||||||
};
|
};
|
||||||
stateEvents.append(nextState);
|
stateEvents.append(nextState);
|
||||||
if (i > 0
|
if (i > 0
|
||||||
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event
|
||||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
|
||||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||||
)) {
|
)) {
|
||||||
break;
|
break;
|
||||||
@@ -181,8 +180,7 @@ QVariantList MessageFilterModel::authorList(int sourceRow) const
|
|||||||
uniqueAuthors.append(nextAvatar);
|
uniqueAuthors.append(nextAvatar);
|
||||||
}
|
}
|
||||||
if (i > 0
|
if (i > 0
|
||||||
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event
|
||||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
|
||||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||||
)) {
|
)) {
|
||||||
break;
|
break;
|
||||||
@@ -204,8 +202,7 @@ QString MessageFilterModel::excessAuthors(int row) const
|
|||||||
uniqueAuthors.append(nextAvatar);
|
uniqueAuthors.append(nextAvatar);
|
||||||
}
|
}
|
||||||
if (i > 0
|
if (i > 0
|
||||||
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole)
|
&& (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event
|
||||||
!= MessageEventModel::DelegateType::State // If it's not a state event
|
|
||||||
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
|| sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible
|
||||||
)) {
|
)) {
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include "searchmodel.h"
|
#include "searchmodel.h"
|
||||||
|
|
||||||
|
#include "eventhandler.h"
|
||||||
#include "messageeventmodel.h"
|
#include "messageeventmodel.h"
|
||||||
#include "neochatroom.h"
|
#include "neochatroom.h"
|
||||||
|
|
||||||
@@ -91,85 +92,55 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
|
|||||||
{
|
{
|
||||||
auto row = index.row();
|
auto row = index.row();
|
||||||
const auto &event = *m_result->results[row].result;
|
const auto &event = *m_result->results[row].result;
|
||||||
|
|
||||||
|
EventHandler eventHandler;
|
||||||
|
eventHandler.setRoom(m_room);
|
||||||
|
eventHandler.setEvent(&event);
|
||||||
|
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case DisplayRole:
|
case DisplayRole:
|
||||||
return m_room->eventToString(*m_result->results[row].result);
|
return eventHandler.getRichBody();
|
||||||
case ShowAuthorRole:
|
case ShowAuthorRole:
|
||||||
return true;
|
return true;
|
||||||
case AuthorRole:
|
case AuthorRole:
|
||||||
return m_room->getUser(event.senderId());
|
return eventHandler.getAuthor();
|
||||||
case ShowSectionRole:
|
case ShowSectionRole:
|
||||||
if (row == 0) {
|
if (row == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return event.originTimestamp().date() != m_result->results[row - 1].result->originTimestamp().date();
|
return event.originTimestamp().date() != m_result->results[row - 1].result->originTimestamp().date();
|
||||||
case SectionRole:
|
case SectionRole:
|
||||||
return renderDate(event.originTimestamp());
|
return eventHandler.getTimeString(true);
|
||||||
case TimeRole:
|
case TimeRole:
|
||||||
return event.originTimestamp();
|
return eventHandler.getTime();
|
||||||
|
case TimeStringRole:
|
||||||
|
return eventHandler.getTimeString(false);
|
||||||
case ShowReactionsRole:
|
case ShowReactionsRole:
|
||||||
return false;
|
return false;
|
||||||
case ShowReadMarkersRole:
|
case ShowReadMarkersRole:
|
||||||
return false;
|
return false;
|
||||||
|
case IsReplyRole:
|
||||||
|
return eventHandler.hasReply();
|
||||||
|
case ReplyIdRole:
|
||||||
|
return eventHandler.hasReply();
|
||||||
case ReplyAuthorRole:
|
case ReplyAuthorRole:
|
||||||
if (const auto &replyPtr = m_room->getReplyForEvent(event)) {
|
return eventHandler.getReplyAuthor();
|
||||||
return m_room->getUser(m_room->user(replyPtr->senderId()));
|
case ReplyDelegateTypeRole:
|
||||||
} else {
|
return eventHandler.getReplyDelegateType();
|
||||||
return m_room->getUser(nullptr);
|
case ReplyDisplayRole:
|
||||||
}
|
return eventHandler.getReplyRichBody();
|
||||||
case ReplyRole:
|
case ReplyMediaInfoRole:
|
||||||
if (auto replyPtr = m_room->getReplyForEvent(event)) {
|
return eventHandler.getReplyMediaInfo();
|
||||||
MessageEventModel::DelegateType type;
|
|
||||||
if (auto e = eventCast<const RoomMessageEvent>(replyPtr)) {
|
|
||||||
switch (e->msgtype()) {
|
|
||||||
case MessageEventType::Emote:
|
|
||||||
type = MessageEventModel::DelegateType::Emote;
|
|
||||||
break;
|
|
||||||
case MessageEventType::Notice:
|
|
||||||
type = MessageEventModel::DelegateType::Notice;
|
|
||||||
break;
|
|
||||||
case MessageEventType::Image:
|
|
||||||
type = MessageEventModel::DelegateType::Image;
|
|
||||||
break;
|
|
||||||
case MessageEventType::Audio:
|
|
||||||
type = MessageEventModel::DelegateType::Audio;
|
|
||||||
break;
|
|
||||||
case MessageEventType::Video:
|
|
||||||
type = MessageEventModel::DelegateType::Video;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (e->hasFileContent()) {
|
|
||||||
type = MessageEventModel::DelegateType::File;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
type = MessageEventModel::DelegateType::Message;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (is<const StickerEvent>(*replyPtr)) {
|
|
||||||
type = MessageEventModel::DelegateType::Sticker;
|
|
||||||
} else {
|
|
||||||
type = MessageEventModel::DelegateType::Other;
|
|
||||||
}
|
|
||||||
return QVariantMap{
|
|
||||||
{"display"_ls, m_room->eventToString(*replyPtr, Qt::RichText)},
|
|
||||||
{"type"_ls, type},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case IsPendingRole:
|
case IsPendingRole:
|
||||||
return false;
|
return false;
|
||||||
case ShowLinkPreviewRole:
|
case ShowLinkPreviewRole:
|
||||||
return false;
|
return false;
|
||||||
case IsReplyRole:
|
|
||||||
return !event.contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString().isEmpty();
|
|
||||||
case HighlightRole:
|
case HighlightRole:
|
||||||
return !m_room->isDirectChat() && m_room->isEventHighlighted(&event);
|
return eventHandler.isHighlighted();
|
||||||
case EventIdRole:
|
case EventIdRole:
|
||||||
return event.id();
|
return eventHandler.getId();
|
||||||
case ReplyIdRole:
|
|
||||||
return event.contentJson()["m.relates_to"_ls].toObject()["m.in_reply_to"_ls].toObject()["event_id"_ls].toString();
|
|
||||||
}
|
}
|
||||||
return MessageEventModel::DelegateType::Message;
|
return DelegateType::Message;
|
||||||
}
|
}
|
||||||
|
|
||||||
int SearchModel::rowCount(const QModelIndex &parent) const
|
int SearchModel::rowCount(const QModelIndex &parent) const
|
||||||
@@ -190,6 +161,7 @@ QHash<int, QByteArray> SearchModel::roleNames() const
|
|||||||
{ShowSectionRole, "showSection"},
|
{ShowSectionRole, "showSection"},
|
||||||
{SectionRole, "section"},
|
{SectionRole, "section"},
|
||||||
{TimeRole, "time"},
|
{TimeRole, "time"},
|
||||||
|
{TimeStringRole, "timeString"},
|
||||||
{ShowAuthorRole, "showAuthor"},
|
{ShowAuthorRole, "showAuthor"},
|
||||||
{EventIdRole, "eventId"},
|
{EventIdRole, "eventId"},
|
||||||
{ExcessReadMarkersRole, "excessReadMarkers"},
|
{ExcessReadMarkersRole, "excessReadMarkers"},
|
||||||
@@ -197,21 +169,22 @@ QHash<int, QByteArray> SearchModel::roleNames() const
|
|||||||
{ReadMarkersString, "readMarkersString"},
|
{ReadMarkersString, "readMarkersString"},
|
||||||
{PlainTextRole, "plainText"},
|
{PlainTextRole, "plainText"},
|
||||||
{VerifiedRole, "verified"},
|
{VerifiedRole, "verified"},
|
||||||
{ReplyAuthorRole, "replyAuthor"},
|
|
||||||
{ProgressInfoRole, "progressInfo"},
|
{ProgressInfoRole, "progressInfo"},
|
||||||
{IsReplyRole, "isReply"},
|
|
||||||
{ShowReactionsRole, "showReactions"},
|
{ShowReactionsRole, "showReactions"},
|
||||||
{ReplyRole, "reply"},
|
{IsReplyRole, "isReply"},
|
||||||
{ReactionRole, "reaction"},
|
{ReplyAuthorRole, "replyAuthor"},
|
||||||
|
{ReplyIdRole, "replyId"},
|
||||||
|
{ReplyDelegateTypeRole, "replyDelegateType"},
|
||||||
|
{ReplyDisplayRole, "replyDisplay"},
|
||||||
{ReplyMediaInfoRole, "replyMediaInfo"},
|
{ReplyMediaInfoRole, "replyMediaInfo"},
|
||||||
|
{ReactionRole, "reaction"},
|
||||||
{ReadMarkersRole, "readMarkers"},
|
{ReadMarkersRole, "readMarkers"},
|
||||||
{IsPendingRole, "isPending"},
|
{IsPendingRole, "isPending"},
|
||||||
{ShowReadMarkersRole, "showReadMarkers"},
|
{ShowReadMarkersRole, "showReadMarkers"},
|
||||||
{ReplyIdRole, "replyId"},
|
|
||||||
{MimeTypeRole, "mimeType"},
|
{MimeTypeRole, "mimeType"},
|
||||||
{ShowLinkPreviewRole, "showLinkPreview"},
|
{ShowLinkPreviewRole, "showLinkPreview"},
|
||||||
{LinkPreviewRole, "linkPreview"},
|
{LinkPreviewRole, "linkPreview"},
|
||||||
{SourceRole, "source"},
|
{SourceRole, "jsonSource"},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,30 +211,10 @@ void SearchModel::setRoom(NeoChatRoom *room)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto row = it - results.begin();
|
auto row = it - results.begin();
|
||||||
Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyRole, ReplyMediaInfoRole, ReplyAuthorRole});
|
Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyDelegateTypeRole, ReplyDisplayRole, ReplyMediaInfoRole, ReplyAuthorRole});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO deduplicate with messageeventmodel
|
|
||||||
QString renderDate(const QDateTime ×tamp)
|
|
||||||
{
|
|
||||||
auto date = timestamp.toLocalTime().date();
|
|
||||||
if (date == QDate::currentDate()) {
|
|
||||||
return i18n("Today");
|
|
||||||
}
|
|
||||||
if (date == QDate::currentDate().addDays(-1)) {
|
|
||||||
return i18n("Yesterday");
|
|
||||||
}
|
|
||||||
if (date == QDate::currentDate().addDays(-2)) {
|
|
||||||
return i18n("The day before yesterday");
|
|
||||||
}
|
|
||||||
if (date > QDate::currentDate().addDays(-7)) {
|
|
||||||
return date.toString("dddd"_ls);
|
|
||||||
}
|
|
||||||
|
|
||||||
return QLocale::system().toString(date, QLocale::ShortFormat);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SearchModel::searching() const
|
bool SearchModel::searching() const
|
||||||
{
|
{
|
||||||
return m_searching;
|
return m_searching;
|
||||||
|
|||||||
@@ -61,23 +61,25 @@ public:
|
|||||||
ShowSectionRole,
|
ShowSectionRole,
|
||||||
SectionRole,
|
SectionRole,
|
||||||
TimeRole,
|
TimeRole,
|
||||||
|
TimeStringRole,
|
||||||
EventIdRole,
|
EventIdRole,
|
||||||
ExcessReadMarkersRole,
|
ExcessReadMarkersRole,
|
||||||
HighlightRole,
|
HighlightRole,
|
||||||
ReadMarkersString,
|
ReadMarkersString,
|
||||||
PlainTextRole,
|
PlainTextRole,
|
||||||
VerifiedRole,
|
VerifiedRole,
|
||||||
ReplyAuthorRole,
|
|
||||||
ProgressInfoRole,
|
ProgressInfoRole,
|
||||||
IsReplyRole,
|
|
||||||
ShowReactionsRole,
|
ShowReactionsRole,
|
||||||
ReplyRole,
|
IsReplyRole,
|
||||||
ReactionRole,
|
ReplyAuthorRole,
|
||||||
|
ReplyIdRole,
|
||||||
|
ReplyDelegateTypeRole,
|
||||||
|
ReplyDisplayRole,
|
||||||
ReplyMediaInfoRole,
|
ReplyMediaInfoRole,
|
||||||
|
ReactionRole,
|
||||||
ReadMarkersRole,
|
ReadMarkersRole,
|
||||||
IsPendingRole,
|
IsPendingRole,
|
||||||
ShowReadMarkersRole,
|
ShowReadMarkersRole,
|
||||||
ReplyIdRole,
|
|
||||||
MimeTypeRole,
|
MimeTypeRole,
|
||||||
ShowLinkPreviewRole,
|
ShowLinkPreviewRole,
|
||||||
LinkPreviewRole,
|
LinkPreviewRole,
|
||||||
@@ -139,5 +141,3 @@ private:
|
|||||||
Quotient::SearchJob *m_job = nullptr;
|
Quotient::SearchJob *m_job = nullptr;
|
||||||
bool m_searching = false;
|
bool m_searching = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
QString renderDate(const QDateTime &dateTime);
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
#include <Quotient/qt_connection_util.h>
|
#include <Quotient/qt_connection_util.h>
|
||||||
|
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
|
#include "eventhandler.h"
|
||||||
#include "events/joinrulesevent.h"
|
#include "events/joinrulesevent.h"
|
||||||
#include "events/pollevent.h"
|
#include "events/pollevent.h"
|
||||||
#include "filetransferpseudojob.h"
|
#include "filetransferpseudojob.h"
|
||||||
@@ -344,8 +345,18 @@ bool NeoChatRoom::lastEventIsSpoiler() const
|
|||||||
QString NeoChatRoom::lastEventToString(Qt::TextFormat format, bool stripNewlines) const
|
QString NeoChatRoom::lastEventToString(Qt::TextFormat format, bool stripNewlines) const
|
||||||
{
|
{
|
||||||
if (auto event = lastEvent()) {
|
if (auto event = lastEvent()) {
|
||||||
return safeMemberName(event->senderId()) + (event->isStateEvent() ? QLatin1String(" ") : QLatin1String(": "))
|
EventHandler eventHandler;
|
||||||
+ eventToString(*event, format, stripNewlines);
|
eventHandler.setRoom(this);
|
||||||
|
eventHandler.setEvent(event);
|
||||||
|
|
||||||
|
QString body;
|
||||||
|
if (format == Qt::TextFormat::RichText) {
|
||||||
|
body = eventHandler.getRichBody(stripNewlines);
|
||||||
|
} else {
|
||||||
|
body = eventHandler.getPlainBody(stripNewlines);
|
||||||
|
}
|
||||||
|
|
||||||
|
return safeMemberName(event->senderId()) + (event->isStateEvent() ? QLatin1String(" ") : QLatin1String(": ")) + body;
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -483,318 +494,6 @@ QString NeoChatRoom::avatarMediaId() const
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
QString NeoChatRoom::eventToString(const RoomEvent &evt, Qt::TextFormat format, bool stripNewlines) const
|
|
||||||
{
|
|
||||||
const bool prettyPrint = (format == Qt::RichText);
|
|
||||||
|
|
||||||
using namespace Quotient;
|
|
||||||
return switchOnType(
|
|
||||||
evt,
|
|
||||||
[this, format, stripNewlines](const RoomMessageEvent &e) {
|
|
||||||
using namespace MessageEventContent;
|
|
||||||
|
|
||||||
TextHandler textHandler;
|
|
||||||
|
|
||||||
if (e.hasFileContent()) {
|
|
||||||
auto fileCaption = e.content()->fileInfo()->originalName;
|
|
||||||
if (fileCaption.isEmpty()) {
|
|
||||||
fileCaption = e.plainBody();
|
|
||||||
} else if (e.content()->fileInfo()->originalName != e.plainBody()) {
|
|
||||||
fileCaption = e.plainBody() + " | "_ls + fileCaption;
|
|
||||||
}
|
|
||||||
textHandler.setData(fileCaption);
|
|
||||||
return !fileCaption.isEmpty() ? textHandler.handleRecievePlainText(Qt::PlainText, stripNewlines) : i18n("a file");
|
|
||||||
}
|
|
||||||
|
|
||||||
QString body;
|
|
||||||
if (e.hasTextContent() && e.content()) {
|
|
||||||
body = static_cast<const TextContent *>(e.content())->body;
|
|
||||||
} else {
|
|
||||||
body = e.plainBody();
|
|
||||||
}
|
|
||||||
|
|
||||||
textHandler.setData(body);
|
|
||||||
|
|
||||||
Qt::TextFormat inputFormat;
|
|
||||||
if (e.mimeType().name() == "text/plain"_ls) {
|
|
||||||
inputFormat = Qt::PlainText;
|
|
||||||
} else {
|
|
||||||
inputFormat = Qt::RichText;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (format == Qt::RichText) {
|
|
||||||
return textHandler.handleRecieveRichText(inputFormat, this, &e, stripNewlines);
|
|
||||||
} else {
|
|
||||||
return textHandler.handleRecievePlainText(inputFormat, stripNewlines);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[](const StickerEvent &e) {
|
|
||||||
return e.body();
|
|
||||||
},
|
|
||||||
[this, prettyPrint](const RoomMemberEvent &e) {
|
|
||||||
// FIXME: Rewind to the name that was at the time of this event
|
|
||||||
auto subjectName = this->htmlSafeMemberName(e.userId());
|
|
||||||
if (e.membership() == Membership::Leave) {
|
|
||||||
if (e.prevContent() && e.prevContent()->displayName) {
|
|
||||||
subjectName = sanitized(*e.prevContent()->displayName).toHtmlEscaped();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prettyPrint) {
|
|
||||||
subjectName = QStringLiteral("<a href=\"https://matrix.to/#/%1\" style=\"color: %2\">%3</a>")
|
|
||||||
.arg(e.userId(), Utils::getUserColor(user(e.userId())->hueF()).name(), subjectName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The below code assumes senderName output in AuthorRole
|
|
||||||
switch (e.membership()) {
|
|
||||||
case Membership::Invite:
|
|
||||||
if (e.repeatsState()) {
|
|
||||||
auto text = i18n("reinvited %1 to the room", subjectName);
|
|
||||||
if (!e.reason().isEmpty()) {
|
|
||||||
text += i18nc("Optional reason for an invitation", ": %1") + e.reason().toHtmlEscaped();
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
Q_FALLTHROUGH();
|
|
||||||
case Membership::Join: {
|
|
||||||
QString text{};
|
|
||||||
// Part 1: invites and joins
|
|
||||||
if (e.repeatsState()) {
|
|
||||||
text = i18n("joined the room (repeated)");
|
|
||||||
} else if (e.changesMembership()) {
|
|
||||||
text = e.membership() == Membership::Invite ? i18n("invited %1 to the room", subjectName) : i18n("joined the room");
|
|
||||||
}
|
|
||||||
if (!text.isEmpty()) {
|
|
||||||
if (!e.reason().isEmpty()) {
|
|
||||||
text += i18n(": %1", e.reason().toHtmlEscaped());
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
// Part 2: profile changes of joined members
|
|
||||||
if (e.isRename()) {
|
|
||||||
if (!e.newDisplayName()) {
|
|
||||||
text = i18nc("their refers to a singular user", "cleared their display name");
|
|
||||||
} else {
|
|
||||||
text = i18nc("their refers to a singular user", "changed their display name to %1", e.newDisplayName()->toHtmlEscaped());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (e.isAvatarUpdate()) {
|
|
||||||
if (!text.isEmpty()) {
|
|
||||||
text += i18n(" and ");
|
|
||||||
}
|
|
||||||
if (!e.newAvatarUrl()) {
|
|
||||||
text += i18nc("their refers to a singular user", "cleared their avatar");
|
|
||||||
} else if (!e.prevContent()->avatarUrl) {
|
|
||||||
text += i18n("set an avatar");
|
|
||||||
} else {
|
|
||||||
text += i18nc("their refers to a singular user", "updated their avatar");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (text.isEmpty()) {
|
|
||||||
text = i18nc("<user> changed nothing", "changed nothing");
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
case Membership::Leave:
|
|
||||||
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
|
|
||||||
return (e.senderId() != e.userId()) ? i18n("withdrew %1's invitation", subjectName) : i18n("rejected the invitation");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
|
|
||||||
return (e.senderId() != e.userId()) ? i18n("unbanned %1", subjectName) : i18n("self-unbanned");
|
|
||||||
}
|
|
||||||
return (e.senderId() != e.userId())
|
|
||||||
? i18n("has put %1 out of the room: %2", subjectName, e.contentJson()["reason"_ls].toString().toHtmlEscaped())
|
|
||||||
: i18n("left the room");
|
|
||||||
case Membership::Ban:
|
|
||||||
if (e.senderId() != e.userId()) {
|
|
||||||
if (e.reason().isEmpty()) {
|
|
||||||
return i18n("banned %1 from the room", subjectName);
|
|
||||||
} else {
|
|
||||||
return i18n("banned %1 from the room: %2", subjectName, e.reason().toHtmlEscaped());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return i18n("self-banned from the room");
|
|
||||||
}
|
|
||||||
case Membership::Knock: {
|
|
||||||
QString reason(e.contentJson()["reason"_ls].toString().toHtmlEscaped());
|
|
||||||
return reason.isEmpty() ? i18n("requested an invite") : i18n("requested an invite with reason: %1", reason);
|
|
||||||
}
|
|
||||||
default:;
|
|
||||||
}
|
|
||||||
return i18n("made something unknown");
|
|
||||||
},
|
|
||||||
[](const RoomCanonicalAliasEvent &e) {
|
|
||||||
return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias to: %1", e.alias());
|
|
||||||
},
|
|
||||||
[](const RoomNameEvent &e) {
|
|
||||||
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name to: %1", e.name().toHtmlEscaped());
|
|
||||||
},
|
|
||||||
[prettyPrint, stripNewlines](const RoomTopicEvent &e) {
|
|
||||||
return (e.topic().isEmpty()) ? i18n("cleared the topic")
|
|
||||||
: i18n("set the topic to: %1",
|
|
||||||
prettyPrint ? Quotient::prettyPrint(e.topic())
|
|
||||||
: stripNewlines ? e.topic().replace(u'\n', u' ')
|
|
||||||
: e.topic());
|
|
||||||
},
|
|
||||||
[](const RoomAvatarEvent &) {
|
|
||||||
return i18n("changed the room avatar");
|
|
||||||
},
|
|
||||||
[](const EncryptionEvent &) {
|
|
||||||
return i18n("activated End-to-End Encryption");
|
|
||||||
},
|
|
||||||
[](const RoomCreateEvent &e) {
|
|
||||||
return e.isUpgrade() ? i18n("upgraded the room to version %1", e.version().isEmpty() ? "1"_ls : e.version().toHtmlEscaped())
|
|
||||||
: i18n("created the room, version %1", e.version().isEmpty() ? "1"_ls : e.version().toHtmlEscaped());
|
|
||||||
},
|
|
||||||
[](const RoomPowerLevelsEvent &) {
|
|
||||||
return i18nc("'power level' means permission level", "changed the power levels for this room");
|
|
||||||
},
|
|
||||||
[](const StateEvent &e) {
|
|
||||||
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
|
|
||||||
return i18n("changed the server access control lists for this room");
|
|
||||||
}
|
|
||||||
if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
|
|
||||||
if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
|
|
||||||
return i18nc("[User] added <name> widget", "added %1 widget", e.contentJson()["name"_ls].toString());
|
|
||||||
}
|
|
||||||
if (e.contentJson().isEmpty()) {
|
|
||||||
return i18nc("[User] removed <name> widget", "removed %1 widget", e.fullJson()["unsigned"_ls]["prev_content"_ls]["name"_ls].toString());
|
|
||||||
}
|
|
||||||
return i18nc("[User] configured <name> widget", "configured %1 widget", e.contentJson()["name"_ls].toString());
|
|
||||||
}
|
|
||||||
if (e.matrixType() == "org.matrix.msc3672.beacon_info"_ls) {
|
|
||||||
return e.contentJson()["description"_ls].toString();
|
|
||||||
}
|
|
||||||
return e.stateKey().isEmpty() ? i18n("updated %1 state", e.matrixType())
|
|
||||||
: i18n("updated %1 state for %2", e.matrixType(), e.stateKey().toHtmlEscaped());
|
|
||||||
},
|
|
||||||
[](const PollStartEvent &e) {
|
|
||||||
return e.question();
|
|
||||||
},
|
|
||||||
i18n("Unknown event"));
|
|
||||||
}
|
|
||||||
|
|
||||||
QString NeoChatRoom::eventToGenericString(const RoomEvent &evt) const
|
|
||||||
{
|
|
||||||
return switchOnType(
|
|
||||||
evt,
|
|
||||||
[](const RoomMessageEvent &e) {
|
|
||||||
Q_UNUSED(e)
|
|
||||||
return i18n("sent a message");
|
|
||||||
},
|
|
||||||
[](const StickerEvent &e) {
|
|
||||||
Q_UNUSED(e)
|
|
||||||
return i18n("sent a sticker");
|
|
||||||
},
|
|
||||||
[](const RoomMemberEvent &e) {
|
|
||||||
switch (e.membership()) {
|
|
||||||
case Membership::Invite:
|
|
||||||
if (e.repeatsState()) {
|
|
||||||
return i18n("reinvited someone to the room");
|
|
||||||
}
|
|
||||||
Q_FALLTHROUGH();
|
|
||||||
case Membership::Join: {
|
|
||||||
QString text{};
|
|
||||||
// Part 1: invites and joins
|
|
||||||
if (e.repeatsState()) {
|
|
||||||
text = i18n("joined the room (repeated)");
|
|
||||||
} else if (e.changesMembership()) {
|
|
||||||
text = e.membership() == Membership::Invite ? i18n("invited someone to the room") : i18n("joined the room");
|
|
||||||
}
|
|
||||||
if (!text.isEmpty()) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
// Part 2: profile changes of joined members
|
|
||||||
if (e.isRename()) {
|
|
||||||
if (!e.newDisplayName()) {
|
|
||||||
text = i18nc("their refers to a singular user", "cleared their display name");
|
|
||||||
} else {
|
|
||||||
text = i18nc("their refers to a singular user", "changed their display name");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (e.isAvatarUpdate()) {
|
|
||||||
if (!text.isEmpty()) {
|
|
||||||
text += i18n(" and ");
|
|
||||||
}
|
|
||||||
if (!e.newAvatarUrl()) {
|
|
||||||
text += i18nc("their refers to a singular user", "cleared their avatar");
|
|
||||||
} else if (!e.prevContent()->avatarUrl) {
|
|
||||||
text += i18n("set an avatar");
|
|
||||||
} else {
|
|
||||||
text += i18nc("their refers to a singular user", "updated their avatar");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (text.isEmpty()) {
|
|
||||||
text = i18nc("<user> changed nothing", "changed nothing");
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
case Membership::Leave:
|
|
||||||
if (e.prevContent() && e.prevContent()->membership == Membership::Invite) {
|
|
||||||
return (e.senderId() != e.userId()) ? i18n("withdrew a user's invitation") : i18n("rejected the invitation");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.prevContent() && e.prevContent()->membership == Membership::Ban) {
|
|
||||||
return (e.senderId() != e.userId()) ? i18n("unbanned a user") : i18n("self-unbanned");
|
|
||||||
}
|
|
||||||
return (e.senderId() != e.userId()) ? i18n("put a user out of the room") : i18n("left the room");
|
|
||||||
case Membership::Ban:
|
|
||||||
if (e.senderId() != e.userId()) {
|
|
||||||
return i18n("banned a user from the room");
|
|
||||||
} else {
|
|
||||||
return i18n("self-banned from the room");
|
|
||||||
}
|
|
||||||
case Membership::Knock: {
|
|
||||||
return i18n("requested an invite");
|
|
||||||
}
|
|
||||||
default:;
|
|
||||||
}
|
|
||||||
return i18n("made something unknown");
|
|
||||||
},
|
|
||||||
[](const RoomCanonicalAliasEvent &e) {
|
|
||||||
return (e.alias().isEmpty()) ? i18n("cleared the room main alias") : i18n("set the room main alias");
|
|
||||||
},
|
|
||||||
[](const RoomNameEvent &e) {
|
|
||||||
return (e.name().isEmpty()) ? i18n("cleared the room name") : i18n("set the room name");
|
|
||||||
},
|
|
||||||
[](const RoomTopicEvent &e) {
|
|
||||||
return (e.topic().isEmpty()) ? i18n("cleared the topic") : i18n("set the topic");
|
|
||||||
},
|
|
||||||
[](const RoomAvatarEvent &) {
|
|
||||||
return i18n("changed the room avatar");
|
|
||||||
},
|
|
||||||
[](const EncryptionEvent &) {
|
|
||||||
return i18n("activated End-to-End Encryption");
|
|
||||||
},
|
|
||||||
[](const RoomCreateEvent &e) {
|
|
||||||
return e.isUpgrade() ? i18n("upgraded the room version") : i18n("created the room");
|
|
||||||
},
|
|
||||||
[](const RoomPowerLevelsEvent &) {
|
|
||||||
return i18nc("'power level' means permission level", "changed the power levels for this room");
|
|
||||||
},
|
|
||||||
[](const StateEvent &e) {
|
|
||||||
if (e.matrixType() == QLatin1String("m.room.server_acl")) {
|
|
||||||
return i18n("changed the server access control lists for this room");
|
|
||||||
}
|
|
||||||
if (e.matrixType() == QLatin1String("im.vector.modular.widgets")) {
|
|
||||||
if (e.fullJson()["unsigned"_ls]["prev_content"_ls].toObject().isEmpty()) {
|
|
||||||
return i18n("added a widget");
|
|
||||||
}
|
|
||||||
if (e.contentJson().isEmpty()) {
|
|
||||||
return i18n("removed a widget");
|
|
||||||
}
|
|
||||||
return i18n("configured a widget");
|
|
||||||
}
|
|
||||||
return i18n("updated the state");
|
|
||||||
},
|
|
||||||
[](const PollStartEvent &e) {
|
|
||||||
Q_UNUSED(e);
|
|
||||||
return i18n("started a poll");
|
|
||||||
},
|
|
||||||
i18n("Unknown event"));
|
|
||||||
}
|
|
||||||
|
|
||||||
void NeoChatRoom::changeAvatar(const QUrl &localFile)
|
void NeoChatRoom::changeAvatar(const QUrl &localFile)
|
||||||
{
|
{
|
||||||
const auto job = connection()->uploadFile(localFile.toLocalFile());
|
const auto job = connection()->uploadFile(localFile.toLocalFile());
|
||||||
@@ -861,10 +560,14 @@ void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, Mess
|
|||||||
if (isReply) {
|
if (isReply) {
|
||||||
const auto &replyEvt = **replyIt;
|
const auto &replyEvt = **replyIt;
|
||||||
|
|
||||||
|
EventHandler eventHandler;
|
||||||
|
eventHandler.setRoom(this);
|
||||||
|
eventHandler.setEvent(&**replyIt);
|
||||||
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
QJsonObject json{
|
QJsonObject json{
|
||||||
{"msgtype"_ls, msgTypeToString(type)},
|
{"msgtype"_ls, msgTypeToString(type)},
|
||||||
{"body"_ls, "> <%1> %2\n\n%3"_ls.arg(replyEvt.senderId(), eventToString(replyEvt), text)},
|
{"body"_ls, "> <%1> %2\n\n%3"_ls.arg(replyEvt.senderId(), eventHandler.getPlainBody(), text)},
|
||||||
{"format"_ls, "org.matrix.custom.html"_ls},
|
{"format"_ls, "org.matrix.custom.html"_ls},
|
||||||
{"m.relates_to"_ls,
|
{"m.relates_to"_ls,
|
||||||
QJsonObject {
|
QJsonObject {
|
||||||
@@ -876,7 +579,7 @@ void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, Mess
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{"formatted_body"_ls,
|
{"formatted_body"_ls,
|
||||||
"<mx-reply><blockquote><a href=\"https://matrix.to/#/%1/%2\">In reply to</a> <a href=\"https://matrix.to/#/%3\">%4</a><br>%5</blockquote></mx-reply>%6"_ls.arg(id(), replyEventId, replyEvt.senderId(), replyEvt.senderId(), eventToString(replyEvt, Qt::RichText), html)
|
"<mx-reply><blockquote><a href=\"https://matrix.to/#/%1/%2\">In reply to</a> <a href=\"https://matrix.to/#/%3\">%4</a><br>%5</blockquote></mx-reply>%6"_ls.arg(id(), replyEventId, replyEvt.senderId(), replyEvt.senderId(), eventHandler.getRichBody(), html)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// clang-format on
|
// clang-format on
|
||||||
@@ -1701,7 +1404,11 @@ QString NeoChatRoom::chatBoxReplyMessage() const
|
|||||||
if (m_chatBoxReplyId.isEmpty()) {
|
if (m_chatBoxReplyId.isEmpty()) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return eventToString(*static_cast<const RoomMessageEvent *>(&**findInTimeline(m_chatBoxReplyId)));
|
|
||||||
|
EventHandler eventhandler;
|
||||||
|
eventhandler.setRoom(this);
|
||||||
|
eventhandler.setEvent(&**findInTimeline(m_chatBoxReplyId));
|
||||||
|
return eventhandler.getPlainBody();
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantMap NeoChatRoom::chatBoxEditUser() const
|
QVariantMap NeoChatRoom::chatBoxEditUser() const
|
||||||
@@ -1717,7 +1424,11 @@ QString NeoChatRoom::chatBoxEditMessage() const
|
|||||||
if (m_chatBoxEditId.isEmpty()) {
|
if (m_chatBoxEditId.isEmpty()) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return eventToString(*static_cast<const RoomMessageEvent *>(&**findInTimeline(m_chatBoxEditId)));
|
|
||||||
|
EventHandler eventhandler;
|
||||||
|
eventhandler.setRoom(this);
|
||||||
|
eventhandler.setEvent(&**findInTimeline(m_chatBoxEditId));
|
||||||
|
return eventhandler.getPlainBody();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString NeoChatRoom::chatBoxAttachmentPath() const
|
QString NeoChatRoom::chatBoxAttachmentPath() const
|
||||||
|
|||||||
@@ -475,39 +475,6 @@ public:
|
|||||||
*/
|
*/
|
||||||
[[nodiscard]] const Quotient::RoomEvent *lastEvent() const;
|
[[nodiscard]] const Quotient::RoomEvent *lastEvent() const;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Output a string for the message content ready for display.
|
|
||||||
*
|
|
||||||
* The output string is dependant upon the event type and the desired output format.
|
|
||||||
*
|
|
||||||
* For most messages this is the body content of the message. For media messages
|
|
||||||
* This will be the caption and for state events it will be a string specific
|
|
||||||
* to that event with some dynamic details about the event added.
|
|
||||||
*
|
|
||||||
* E.g. For a room topic state event the text will be:
|
|
||||||
* "set the topic to: <new topic text>"
|
|
||||||
*
|
|
||||||
* @param evt the event for which a string is desired.
|
|
||||||
* @param format the output format, usually Qt::PlainText or Qt::RichText.
|
|
||||||
* @param stripNewlines whether the output should have new lines in it.
|
|
||||||
*/
|
|
||||||
[[nodiscard]] QString eventToString(const Quotient::RoomEvent &evt, Qt::TextFormat format = Qt::PlainText, bool stripNewlines = false) const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Output a generic string for the message content ready for display.
|
|
||||||
*
|
|
||||||
* The output string is dependant upon the event type.
|
|
||||||
*
|
|
||||||
* Unlike NeoChatRoom::eventToString the string is the same for all events of
|
|
||||||
* the same type
|
|
||||||
*
|
|
||||||
* E.g. For a message the text will be:
|
|
||||||
* "sent a message"
|
|
||||||
*
|
|
||||||
* @sa eventToString()
|
|
||||||
*/
|
|
||||||
[[nodiscard]] QString eventToGenericString(const Quotient::RoomEvent &evt) const;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Convenient way to call eventToString on the last event.
|
* @brief Convenient way to call eventToString on the last event.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ Components.AlbumMaximizeComponent {
|
|||||||
eventId: root.currentEventId,
|
eventId: root.currentEventId,
|
||||||
source: root.currentJsonSource,
|
source: root.currentJsonSource,
|
||||||
file: parent,
|
file: parent,
|
||||||
mimeType: root.currentMimeType,
|
|
||||||
progressInfo: root.currentProgressInfo,
|
progressInfo: root.currentProgressInfo,
|
||||||
plainText: root.currentPlainText
|
plainText: root.currentPlainText
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,78 +14,78 @@ DelegateChooser {
|
|||||||
property var room
|
property var room
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MessageEventModel.State
|
roleValue: DelegateType.State
|
||||||
delegate: StateDelegate {}
|
delegate: StateDelegate {}
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MessageEventModel.Emote
|
roleValue: DelegateType.Emote
|
||||||
delegate: MessageDelegate {}
|
delegate: MessageDelegate {}
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MessageEventModel.Message
|
roleValue: DelegateType.Message
|
||||||
delegate: MessageDelegate {}
|
delegate: MessageDelegate {}
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MessageEventModel.Notice
|
roleValue: DelegateType.Notice
|
||||||
delegate: MessageDelegate {}
|
delegate: MessageDelegate {}
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MessageEventModel.Image
|
roleValue: DelegateType.Image
|
||||||
delegate: ImageDelegate {}
|
delegate: ImageDelegate {}
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MessageEventModel.Sticker
|
roleValue: DelegateType.Sticker
|
||||||
delegate: ImageDelegate {}
|
delegate: ImageDelegate {}
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MessageEventModel.Audio
|
roleValue: DelegateType.Audio
|
||||||
delegate: AudioDelegate {}
|
delegate: AudioDelegate {}
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MessageEventModel.Video
|
roleValue: DelegateType.Video
|
||||||
delegate: VideoDelegate {}
|
delegate: VideoDelegate {}
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MessageEventModel.File
|
roleValue: DelegateType.File
|
||||||
delegate: FileDelegate {}
|
delegate: FileDelegate {}
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MessageEventModel.Encrypted
|
roleValue: DelegateType.Encrypted
|
||||||
delegate: EncryptedDelegate {}
|
delegate: EncryptedDelegate {}
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MessageEventModel.ReadMarker
|
roleValue: DelegateType.ReadMarker
|
||||||
delegate: ReadMarkerDelegate {}
|
delegate: ReadMarkerDelegate {}
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MessageEventModel.Poll
|
roleValue: DelegateType.Poll
|
||||||
delegate: PollDelegate {}
|
delegate: PollDelegate {}
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MessageEventModel.Location
|
roleValue: DelegateType.Location
|
||||||
delegate: LocationDelegate {}
|
delegate: LocationDelegate {}
|
||||||
}
|
}
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MessageEventModel.LiveLocation
|
roleValue: DelegateType.LiveLocation
|
||||||
delegate: LiveLocationDelegate {
|
delegate: LiveLocationDelegate {
|
||||||
room: root.room
|
room: root.room
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MessageEventModel.Other
|
roleValue: DelegateType.Other
|
||||||
delegate: Item {}
|
delegate: Item {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,22 +117,22 @@ Item {
|
|||||||
id: loader
|
id: loader
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.maximumHeight: loader.item && (root.type == MessageEventModel.Image || root.type == MessageEventModel.Sticker) ? loader.item.height : -1
|
Layout.maximumHeight: loader.item && (root.type == DelegateType.Image || root.type == DelegateType.Sticker) ? loader.item.height : -1
|
||||||
Layout.columnSpan: 2
|
Layout.columnSpan: 2
|
||||||
|
|
||||||
sourceComponent: {
|
sourceComponent: {
|
||||||
switch (root.type) {
|
switch (root.type) {
|
||||||
case MessageEventModel.Image:
|
case DelegateType.Image:
|
||||||
case MessageEventModel.Sticker:
|
case DelegateType.Sticker:
|
||||||
return imageComponent;
|
return imageComponent;
|
||||||
case MessageEventModel.Message:
|
case DelegateType.Message:
|
||||||
case MessageEventModel.Notice:
|
case DelegateType.Notice:
|
||||||
return textComponent;
|
return textComponent;
|
||||||
case MessageEventModel.File:
|
case DelegateType.File:
|
||||||
case MessageEventModel.Video:
|
case DelegateType.Video:
|
||||||
case MessageEventModel.Audio:
|
case DelegateType.Audio:
|
||||||
return mimeComponent;
|
return mimeComponent;
|
||||||
case MessageEventModel.Encrypted:
|
case DelegateType.Encrypted:
|
||||||
return encryptedComponent;
|
return encryptedComponent;
|
||||||
default:
|
default:
|
||||||
return textComponent;
|
return textComponent;
|
||||||
@@ -187,7 +187,7 @@ Item {
|
|||||||
MimeComponent {
|
MimeComponent {
|
||||||
mimeIconSource: root.mediaInfo.mimeIcon
|
mimeIconSource: root.mediaInfo.mimeIcon
|
||||||
label: root.display
|
label: root.display
|
||||||
subLabel: root.type === MessageEventModel.File ? Controller.formatByteSize(root.mediaInfo.size) : Controller.formatDuration(root.mediaInfo.duration)
|
subLabel: root.type === DelegateType.File ? Controller.formatByteSize(root.mediaInfo.size) : Controller.formatDuration(root.mediaInfo.duration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Component {
|
Component {
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ ColumnLayout {
|
|||||||
*/
|
*/
|
||||||
required property var time
|
required property var time
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The timestamp of the message as a string.
|
||||||
|
*/
|
||||||
|
required property string timeString
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The message author.
|
* @brief The message author.
|
||||||
*
|
*
|
||||||
@@ -154,13 +159,14 @@ ColumnLayout {
|
|||||||
required property var replyAuthor
|
required property var replyAuthor
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The reply content.
|
* @brief The delegate type of the message replied to.
|
||||||
*
|
|
||||||
* This should consist of the following:
|
|
||||||
* - display - The display text of the reply.
|
|
||||||
* - type - The delegate type of the reply.
|
|
||||||
*/
|
*/
|
||||||
required property var reply
|
required property int replyDelegateType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The display text of the message replied to.
|
||||||
|
*/
|
||||||
|
required property string replyDisplay
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The media info for the reply event.
|
* @brief The media info for the reply event.
|
||||||
@@ -206,11 +212,6 @@ ColumnLayout {
|
|||||||
*/
|
*/
|
||||||
required property bool verified
|
required property bool verified
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The mime type of the message's file or media.
|
|
||||||
*/
|
|
||||||
required property var mimeType
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The full message source JSON.
|
* @brief The full message source JSON.
|
||||||
*/
|
*/
|
||||||
@@ -357,7 +358,7 @@ ColumnLayout {
|
|||||||
implicitHeight: Math.max(root.showAuthor || root.alwaysShowAuthor ? avatar.implicitHeight : 0, bubble.height)
|
implicitHeight: Math.max(root.showAuthor || root.alwaysShowAuthor ? avatar.implicitHeight : 0, bubble.height)
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (root.isReply && root.reply === undefined) {
|
if (root.isReply && root.replyDelegateType === DelegateType.Other) {
|
||||||
currentRoom.loadReply(root.eventId, root.replyId)
|
currentRoom.loadReply(root.eventId, root.replyId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -478,7 +479,7 @@ ColumnLayout {
|
|||||||
QQC2.Label {
|
QQC2.Label {
|
||||||
id: timeLabel
|
id: timeLabel
|
||||||
|
|
||||||
text: visible ? root.time.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) : ""
|
text: root.timeString
|
||||||
color: Kirigami.Theme.disabledTextColor
|
color: Kirigami.Theme.disabledTextColor
|
||||||
QQC2.ToolTip.visible: hoverHandler.hovered
|
QQC2.ToolTip.visible: hoverHandler.hovered
|
||||||
QQC2.ToolTip.text: root.time.toLocaleString(Qt.locale(), Locale.LongFormat)
|
QQC2.ToolTip.text: root.time.toLocaleString(Qt.locale(), Locale.LongFormat)
|
||||||
@@ -494,13 +495,13 @@ ColumnLayout {
|
|||||||
|
|
||||||
Layout.maximumWidth: contentMaxWidth
|
Layout.maximumWidth: contentMaxWidth
|
||||||
|
|
||||||
active: root.isReply && root.reply
|
active: root.isReply && root.replyDelegateType !== DelegateType.Other
|
||||||
visible: active
|
visible: active
|
||||||
|
|
||||||
sourceComponent: ReplyComponent {
|
sourceComponent: ReplyComponent {
|
||||||
author: root.replyAuthor
|
author: root.replyAuthor
|
||||||
type: root.reply.type
|
type: root.replyDelegateType
|
||||||
display: root.reply.display
|
display: root.replyDisplay
|
||||||
mediaInfo: root.replyMediaInfo
|
mediaInfo: root.replyMediaInfo
|
||||||
contentMaxWidth: bubbleSizeHelper.currentWidth
|
contentMaxWidth: bubbleSizeHelper.currentWidth
|
||||||
}
|
}
|
||||||
@@ -609,7 +610,6 @@ ColumnLayout {
|
|||||||
eventId: root.eventId,
|
eventId: root.eventId,
|
||||||
source: root.jsonSource,
|
source: root.jsonSource,
|
||||||
file: file,
|
file: file,
|
||||||
mimeType: root.mimeType,
|
|
||||||
progressInfo: root.progressInfo,
|
progressInfo: root.progressInfo,
|
||||||
plainText: root.plainText,
|
plainText: root.plainText,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ QQC2.ScrollView {
|
|||||||
currentRoom: root.currentRoom
|
currentRoom: root.currentRoom
|
||||||
showActions: delegate && delegate.hovered
|
showActions: delegate && delegate.hovered
|
||||||
verified: delegate && delegate.verified
|
verified: delegate && delegate.verified
|
||||||
editable: delegate && delegate.author.isLocalUser && (delegate.delegateType === MessageEventModel.Emote || delegate.delegateType === MessageEventModel.Message)
|
editable: delegate && delegate.author.isLocalUser && (delegate.delegateType === DelegateType.Emote || delegate.delegateType === DelegateType.Message)
|
||||||
|
|
||||||
onReactClicked: (emoji) => {
|
onReactClicked: (emoji) => {
|
||||||
root.currentRoom.toggleReaction(delegate.eventId, emoji);
|
root.currentRoom.toggleReaction(delegate.eventId, emoji);
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ MessageDelegateContextMenu {
|
|||||||
|
|
||||||
required property var file
|
required property var file
|
||||||
required property var progressInfo
|
required property var progressInfo
|
||||||
required property string mimeType
|
|
||||||
|
|
||||||
property list<Kirigami.Action> actions: [
|
property list<Kirigami.Action> actions: [
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
@@ -108,7 +107,7 @@ MessageDelegateContextMenu {
|
|||||||
id: shareAction
|
id: shareAction
|
||||||
inputData: {
|
inputData: {
|
||||||
'urls': [],
|
'urls': [],
|
||||||
'mimeType': [mimeType]
|
'mimeType': [root.file.mediaInfo.mimeType]
|
||||||
}
|
}
|
||||||
property string filename: StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId);
|
property string filename: StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId);
|
||||||
|
|
||||||
@@ -118,7 +117,7 @@ MessageDelegateContextMenu {
|
|||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
shareAction.inputData = {
|
shareAction.inputData = {
|
||||||
urls: [filename],
|
urls: [filename],
|
||||||
mimeType: [mimeType]
|
mimeType: [root.file.mediaInfo.mimeType]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ Loader {
|
|||||||
currentRoom.chatBoxEditId = eventId;
|
currentRoom.chatBoxEditId = eventId;
|
||||||
currentRoom.chatBoxReplyId = "";
|
currentRoom.chatBoxReplyId = "";
|
||||||
}
|
}
|
||||||
visible: author.id === Controller.activeConnection.localUserId && (loadRoot.eventType === MessageEventModel.Emote || loadRoot.eventType === MessageEventModel.Message)
|
visible: author.id === Controller.activeConnection.localUserId && (loadRoot.eventType === DelegateType.Emote || loadRoot.eventType === DelegateType.Message)
|
||||||
},
|
},
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
text: i18n("Reply")
|
text: i18n("Reply")
|
||||||
|
|||||||
@@ -202,6 +202,9 @@ QString TextHandler::handleRecievePlainText(Qt::TextFormat inputFormat, const bo
|
|||||||
* arrive (e.g. in a caption body) it can then be stripped by the same code.
|
* arrive (e.g. in a caption body) it can then be stripped by the same code.
|
||||||
*/
|
*/
|
||||||
m_dataBuffer = markdownToHTML(m_dataBuffer);
|
m_dataBuffer = markdownToHTML(m_dataBuffer);
|
||||||
|
// This is how \n is converted and for plain text we need it to just be <br>
|
||||||
|
// to prevent extra newlines being inserted.
|
||||||
|
m_dataBuffer.replace(QStringLiteral("<br />\n"), QStringLiteral("<br>"));
|
||||||
|
|
||||||
if (stripNewlines) {
|
if (stripNewlines) {
|
||||||
m_dataBuffer.replace(QStringLiteral("<br>\n"), QStringLiteral(" "));
|
m_dataBuffer.replace(QStringLiteral("<br>\n"), QStringLiteral(" "));
|
||||||
@@ -222,7 +225,11 @@ QString TextHandler::handleRecievePlainText(Qt::TextFormat inputFormat, const bo
|
|||||||
if (m_nextTokenType == Type::TextCode) {
|
if (m_nextTokenType == Type::TextCode) {
|
||||||
nextTokenBuffer = unescapeHtml(nextTokenBuffer);
|
nextTokenBuffer = unescapeHtml(nextTokenBuffer);
|
||||||
} else if (m_nextTokenType == Type::Tag) {
|
} else if (m_nextTokenType == Type::Tag) {
|
||||||
nextTokenBuffer = QString();
|
if (getTagType() == QStringLiteral("br") && !stripNewlines) {
|
||||||
|
nextTokenBuffer = u'\n';
|
||||||
|
} else {
|
||||||
|
nextTokenBuffer = QString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
outputString.append(nextTokenBuffer);
|
outputString.append(nextTokenBuffer);
|
||||||
|
|||||||
Reference in New Issue
Block a user