diff --git a/autotests/data/test-eventhandler-sync.json b/autotests/data/test-eventhandler-sync.json new file mode 100644 index 000000000..827089cf4 --- /dev/null +++ b/autotests/data/test-eventhandler-sync.json @@ -0,0 +1,381 @@ +{ + "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": "This is an example
text message
", + "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": "
In reply to @example:example.org
This is an example
text message
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 + } + }, + { + "content": { + "body": "Thread root", + "format": "org.matrix.custom.html", + "msgtype": "m.text" + }, + "event_id": "$threadroot:example.org", + "origin_server_ts": 1690821582879, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "sender": "@example:example.org", + "type": "m.room.message", + "unsigned": { + "age": 1232 + } + }, + { + "content": { + "body": "Thread message 1", + "msgtype": "m.text", + "m.relates_to": { + "rel_type": "m.thread", + "event_id": "$threadroot:example.org", + "m.in_reply_to": { + "event_id": "$threadroot:example.org" + }, + "is_falling_back": true + } + }, + "event_id": "$threadmessage1:example.org", + "origin_server_ts": 1690821582890, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "sender": "@example:example.org", + "type": "m.room.message", + "unsigned": { + "age": 1238 + } + }, + { + "content": { + "body": "Thread message 2", + "msgtype": "m.text", + "m.relates_to": { + "rel_type": "m.thread", + "event_id": "$threadroot:example.org", + "m.in_reply_to": { + "event_id": "$threadmessage1:example.org" + }, + "is_falling_back": true + } + }, + "event_id": "$threadmessage2:example.org", + "origin_server_ts": 1690821582890, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "sender": "@example:example.org", + "type": "m.room.message", + "unsigned": { + "age": 1238 + } + } + ], + "limited": true, + "prev_batch": "t34-23535_0_0" + } +} diff --git a/autotests/eventhandlertest.cpp b/autotests/eventhandlertest.cpp index eda47b4c3..d68619df9 100644 --- a/autotests/eventhandlertest.cpp +++ b/autotests/eventhandlertest.cpp @@ -64,6 +64,7 @@ private Q_SLOTS: void replyAuthor(); void replyBody(); void replyMediaInfo(); + void thread(); void location(); void readMarkers(); }; @@ -73,329 +74,11 @@ 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": "This is an example
text message
", - "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": "
In reply to @example:example.org
This is an example
text message
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()); + QFile testEventHandlerSyncFile; + testEventHandlerSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-eventhandler-sync.json")); + testEventHandlerSyncFile.open(QIODevice::ReadOnly); + const auto testEventHandlerSyncJson = QJsonDocument::fromJson(testEventHandlerSyncFile.readAll()); + SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testEventHandlerSyncJson.object()); room->update(std::move(roomData)); eventHandler.setRoom(room); @@ -686,6 +369,29 @@ void EventHandlerTest::replyMediaInfo() QCOMPARE(thumbnailInfo["height"_ls], 450); } +void EventHandlerTest::thread() +{ + auto event = room->messageEvents().at(0).get(); + eventHandler.setEvent(event); + + QCOMPARE(eventHandler.isThreaded(), false); + QCOMPARE(eventHandler.threadRoot(), QString()); + + event = room->messageEvents().at(9).get(); + eventHandler.setEvent(event); + + QCOMPARE(eventHandler.isThreaded(), true); + QCOMPARE(eventHandler.threadRoot(), QStringLiteral("$threadroot:example.org")); + QCOMPARE(eventHandler.getReplyId(), QStringLiteral("$threadroot:example.org")); + + event = room->messageEvents().at(10).get(); + eventHandler.setEvent(event); + + QCOMPARE(eventHandler.isThreaded(), true); + QCOMPARE(eventHandler.threadRoot(), QStringLiteral("$threadroot:example.org")); + QCOMPARE(eventHandler.getReplyId(), QStringLiteral("$threadmessage1:example.org")); +} + void EventHandlerTest::location() { auto event = room->messageEvents().at(7).get(); diff --git a/src/actionshandler.cpp b/src/actionshandler.cpp index 42dd75385..20507753b 100644 --- a/src/actionshandler.cpp +++ b/src/actionshandler.cpp @@ -149,7 +149,7 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, Cha return; } - m_room->postMessage(text, handledText, messageType, chatBarCache->replyId(), chatBarCache->editId()); + m_room->postMessage(text, handledText, messageType, chatBarCache->replyId(), chatBarCache->editId(), chatBarCache->threadId()); } void ActionsHandler::checkEffects(const QString &text) diff --git a/src/chatbarcache.cpp b/src/chatbarcache.cpp index 54508ff2d..e15c0cd26 100644 --- a/src/chatbarcache.cpp +++ b/src/chatbarcache.cpp @@ -51,6 +51,7 @@ void ChatBarCache::setReplyId(const QString &replyId) } m_attachmentPath = QString(); Q_EMIT relationIdChanged(); + Q_EMIT attachmentPathChanged(); } bool ChatBarCache::isEditing() const @@ -79,6 +80,7 @@ void ChatBarCache::setEditId(const QString &editId) } m_attachmentPath = QString(); Q_EMIT relationIdChanged(); + Q_EMIT attachmentPathChanged(); } QVariantMap ChatBarCache::relationUser() const @@ -121,6 +123,25 @@ QString ChatBarCache::relationMessage() const return {}; } +bool ChatBarCache::isThreaded() const +{ + return !m_threadId.isEmpty(); +} + +QString ChatBarCache::threadId() const +{ + return m_threadId; +} + +void ChatBarCache::setThreadId(const QString &threadId) +{ + if (m_threadId == threadId) { + return; + } + m_threadId = threadId; + Q_EMIT threadIdChanged(); +} + QString ChatBarCache::attachmentPath() const { return m_attachmentPath; @@ -135,6 +156,7 @@ void ChatBarCache::setAttachmentPath(const QString &attachmentPath) m_relationType = None; m_relationId = QString(); Q_EMIT attachmentPathChanged(); + Q_EMIT relationIdChanged(); } QList *ChatBarCache::mentions() diff --git a/src/chatbarcache.h b/src/chatbarcache.h index 358c059a6..433295e16 100644 --- a/src/chatbarcache.h +++ b/src/chatbarcache.h @@ -113,6 +113,16 @@ class ChatBarCache : public QObject */ Q_PROPERTY(QString relationMessage READ relationMessage NOTIFY relationIdChanged) + /** + * @brief Whether the chat bar is replying in a thread. + */ + Q_PROPERTY(bool isThreaded READ isThreaded NOTIFY threadIdChanged) + + /** + * @brief The Matrix message ID of thread root event, if any. + */ + Q_PROPERTY(QString threadId READ threadId WRITE setThreadId NOTIFY threadIdChanged) + /** * @brief The local path for a file to send, if any. * @@ -152,6 +162,10 @@ public: QString relationMessage() const; + bool isThreaded() const; + QString threadId() const; + void setThreadId(const QString &threadId); + QString attachmentPath() const; void setAttachmentPath(const QString &attachmentPath); @@ -173,12 +187,14 @@ public: Q_SIGNALS: void textChanged(); void relationIdChanged(); + void threadIdChanged(); void attachmentPathChanged(); private: QString m_text = QString(); QString m_relationId = QString(); RelationType m_relationType = RelationType::None; + QString m_threadId = QString(); QString m_attachmentPath = QString(); QList m_mentions; QString m_savedText; diff --git a/src/eventhandler.cpp b/src/eventhandler.cpp index e312391ea..3b94f5809 100644 --- a/src/eventhandler.cpp +++ b/src/eventhandler.cpp @@ -901,6 +901,28 @@ QVariantMap EventHandler::getReplyMediaInfo() const return getMediaInfoForEvent(replyPtr); } +bool EventHandler::isThreaded() const +{ + return (m_event->contentPart("m.relates_to"_ls).contains("rel_type"_ls) + && m_event->contentPart("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls) + || (!m_event->unsignedPart("m.relations"_ls).isEmpty() && m_event->unsignedPart("m.relations"_ls).contains("m.thread"_ls)); +} + +QString EventHandler::threadRoot() const +{ + // Get the thread root ID from m.relates_to if it exists. + if (m_event->contentPart("m.relates_to"_ls).contains("rel_type"_ls) + && m_event->contentPart("m.relates_to"_ls)["rel_type"_ls].toString() == "m.thread"_ls) { + return m_event->contentPart("m.relates_to"_ls)["event_id"_ls].toString(); + } + // For thread root events they have an m.relations in the unsigned part with a m.thread object. + // If so return the event ID as it is the root. + if (!m_event->unsignedPart("m.relations"_ls).isEmpty() && m_event->unsignedPart("m.relations"_ls).contains("m.thread"_ls)) { + return getId(); + } + return {}; +} + float EventHandler::getLatitude() const { const auto geoUri = m_event->contentJson()["geo_uri"_ls].toString(); diff --git a/src/eventhandler.h b/src/eventhandler.h index ee492685e..fb4003bf2 100644 --- a/src/eventhandler.h +++ b/src/eventhandler.h @@ -326,6 +326,20 @@ public: */ QVariantMap getReplyMediaInfo() const; + /** + * @brief Whether the message is part of a thread. + * + * i.e. There is a rel_type of m.thread. + */ + bool isThreaded() const; + + /** + * @brief Return the Matrix ID of the thread's root message. + * + * Empty if this not part of a thread. + */ + QString threadRoot() const; + /** * @brief Return the latitude for the event. * diff --git a/src/models/messageeventmodel.cpp b/src/models/messageeventmodel.cpp index c53f298dc..140306daf 100644 --- a/src/models/messageeventmodel.cpp +++ b/src/models/messageeventmodel.cpp @@ -47,6 +47,8 @@ QHash MessageEventModel::roleNames() const roles[ReplyDelegateTypeRole] = "replyDelegateType"; roles[ReplyDisplayRole] = "replyDisplay"; roles[ReplyMediaInfoRole] = "replyMediaInfo"; + roles[IsThreadedRole] = "isThreaded"; + roles[ThreadRootRole] = "threadRoot"; roles[ShowAuthorRole] = "showAuthor"; roles[ShowSectionRole] = "showSection"; roles[ReadMarkersRole] = "readMarkers"; @@ -586,6 +588,14 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const return eventHandler.getReplyMediaInfo(); } + if (role == IsThreadedRole) { + return eventHandler.isThreaded(); + } + + if (role == ThreadRootRole) { + return eventHandler.threadRoot(); + } + if (role == ShowAuthorRole) { for (auto r = row + 1; r < rowCount(); ++r) { auto i = index(r); diff --git a/src/models/messageeventmodel.h b/src/models/messageeventmodel.h index d1d328a4c..4fa5cd45d 100644 --- a/src/models/messageeventmodel.h +++ b/src/models/messageeventmodel.h @@ -63,6 +63,9 @@ public: ReplyDisplayRole, /**< The body of the message that was replied to. */ ReplyMediaInfoRole, /**< The media info of the message that was replied to. */ + IsThreadedRole, + ThreadRootRole, + ShowAuthorRole, /**< Whether the author's name should be shown. */ ShowSectionRole, /**< Whether the section header should be shown. */ diff --git a/src/neochatroom.cpp b/src/neochatroom.cpp index 517fffc5c..eaa6c3dab 100644 --- a/src/neochatroom.cpp +++ b/src/neochatroom.cpp @@ -526,20 +526,63 @@ QString msgTypeToString(MessageEventType msgType) } } -void NeoChatRoom::postMessage(const QString &rawText, const QString &text, MessageEventType type, const QString &replyEventId, const QString &relateToEventId) +void NeoChatRoom::postMessage(const QString &rawText, + const QString &text, + MessageEventType type, + const QString &replyEventId, + const QString &relateToEventId, + const QString &threadRootId) { - postHtmlMessage(rawText, text, type, replyEventId, relateToEventId); + postHtmlMessage(rawText, text, type, replyEventId, relateToEventId, threadRootId); } -void NeoChatRoom::postHtmlMessage(const QString &text, const QString &html, MessageEventType type, const QString &replyEventId, const QString &relateToEventId) +void NeoChatRoom::postHtmlMessage(const QString &text, + const QString &html, + MessageEventType type, + const QString &replyEventId, + const QString &relateToEventId, + const QString &threadRootId) { bool isReply = !replyEventId.isEmpty(); bool isEdit = !relateToEventId.isEmpty(); + bool isThread = !threadRootId.isEmpty(); const auto replyIt = findInTimeline(replyEventId); if (replyIt == historyEdge()) { isReply = false; } + if (isThread) { + EventHandler eventHandler; + eventHandler.setRoom(this); + eventHandler.setEvent(&**replyIt); + + const bool isFallingBack = !eventHandler.isThreaded(); + + // clang-format off + QJsonObject json{ + {"msgtype"_ls, msgTypeToString(type)}, + {"body"_ls, text}, + {"format"_ls, "org.matrix.custom.html"_ls}, + {"m.relates_to"_ls, + QJsonObject { + {"rel_type"_ls, "m.thread"_ls}, + {"event_id"_ls, threadRootId}, + {"is_falling_back"_ls, isFallingBack}, + {"m.in_reply_to"_ls, + QJsonObject { + {"event_id"_ls, replyEventId} + } + } + } + }, + {"formatted_body"_ls, html} + }; + // clang-format on + + postJson("m.room.message"_ls, json); + return; + } + if (isEdit) { QJsonObject json{ {"type"_ls, "m.room.message"_ls}, diff --git a/src/neochatroom.h b/src/neochatroom.h index 895b91cc2..4abb43e2b 100644 --- a/src/neochatroom.h +++ b/src/neochatroom.h @@ -884,7 +884,8 @@ public Q_SLOTS: const QString &cleanedText, Quotient::MessageEventType type = Quotient::MessageEventType::Text, const QString &replyEventId = QString(), - const QString &relateToEventId = QString()); + const QString &relateToEventId = QString(), + const QString &threadRootId = QString()); /** * @brief Send an html message to the room. @@ -899,7 +900,8 @@ public Q_SLOTS: const QString &html, Quotient::MessageEventType type = Quotient::MessageEventType::Text, const QString &replyEventId = QString(), - const QString &relateToEventId = QString()); + const QString &relateToEventId = QString(), + const QString &threadRootId = QString()); /** * @brief Set the room avatar. diff --git a/src/qml/HoverActions.qml b/src/qml/HoverActions.qml index 92dba6c67..d8f990821 100644 --- a/src/qml/HoverActions.qml +++ b/src/qml/HoverActions.qml @@ -105,6 +105,16 @@ QQC2.Control { root.currentRoom.editCache.editId = ""; root.focusChatBar(); } + }, + Kirigami.Action { + text: i18n("Reply in Thread") + icon.name: "dialog-messages" + onTriggered: { + root.currentRoom.mainCache.replyId = root.delegate.eventId; + root.currentRoom.mainCache.threadId = root.delegate.isThreaded ? root.delegate.threadRoot : root.delegate.eventId; + root.currentRoom.editCache.editId = ""; + root.focusChatBar(); + } } ] diff --git a/src/qml/MessageDelegate.qml b/src/qml/MessageDelegate.qml index 610d0a8c0..742cd8013 100644 --- a/src/qml/MessageDelegate.qml +++ b/src/qml/MessageDelegate.qml @@ -187,6 +187,10 @@ TimelineDelegate { */ required property var replyMediaInfo + required property bool isThreaded + + required property string threadRoot + /** * @brief Whether this message is replying to another. */