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.
*/