[network/ruqola] src/core: Update read receipts live when the room is read

Laurent Montel <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit b8152574fc721d37b3ca46907aac7f428224f3b4 by Laurent Montel, on behalf of Till Adam.
Committed on 27/07/2026 at 11:39.
Pushed by mlaurent into branch 'master'.

Update read receipts live when the room is read

The read-receipt checkmark is driven by Message::unread(), which was only
ever set while parsing a message on load or from the local database. When
the other participant read a message there was no live update, so the
single→double-check transition only appeared after a reload.

Rocket.Chat broadcasts a "messagesRead" event on stream-notify-room once
everyone in the room has read up to a given timestamp (the oldest
last-seen across participants), but Ruqola subscribed to it and then
dropped it in the "unknown event" branch. Handle it: parse the "until"
timestamp and clear the unread flag on every message up to that point,
repainting the affected rows so the indicator updates in real time.

Known limitation: the cleared flag is not written back to the local
message-cache database, so a room reopened from cache before the next
server sync can briefly show the old state; it self-heals on sync. Also,
per-thread reads (payload with tmid) are not handled yet.

M  +28   -0    src/core/model/messagesmodel.cpp
M  +9    -0    src/core/model/messagesmodel.h
M  +19   -0    src/core/rocketchatbackend.cpp

https://invent.kde.org/network/ruqola/-/commit/b8152574fc721d37b3ca46907aac7f428224f3b4

diff --git a/src/core/model/messagesmodel.cpp b/src/core/model/messagesmodel.cpp
index 9242c1d4dc..b88ac40a38 100644
--- a/src/core/model/messagesmodel.cpp
+++ b/src/core/model/messagesmodel.cpp
@@ -789,6 +789,34 @@ void MessagesModel::updateTextToSpeech(const QByteArray &messageId, bool inProgr
     setData(index, inProgress, MessagesModel::TextToSpeechInProgress);
 }
 
+void MessagesModel::markMessagesReadUntil(qint64 until)
+{
+    if (until <= 0) {
+        return;
+    }
+    // mAllMessages is sorted by ascending timestamp, so the messages read (ts <= until) form a
+    // contiguous prefix. Clear their unread flag and repaint that range in one go.
+    int firstRow = -1;
+    int lastRow = -1;
+    for (int row = 0, total = mAllMessages.count(); row < total; ++row) {
+        Message &message = mAllMessages[row];
+        if (message.timeStamp() > until) {
+            break;
+        }
+        if (message.unread()) {
+            message.setUnread(false);
+            if (firstRow == -1) {
+                firstRow = row;
+            }
+            lastRow = row;
+        }
+    }
+    if (firstRow != -1) {
+        qCDebug(RUQOLA_MESSAGEMODELS_LOG) << "markMessagesReadUntil until=" << until << "cleared unread on rows" << firstRow << "-" << lastRow;
+        Q_EMIT dataChanged(createIndex(firstRow, 0), createIndex(lastRow, 0));
+    }
+}
+
 RuqolaQuickSearchMessageSettings *MessagesModel::quickSearchMessageSettings() const
 {
     return mQuickSearchMessageSettings;
diff --git a/src/core/model/messagesmodel.h b/src/core/model/messagesmodel.h
index ee7ee2d98f..ca39ddf853 100644
--- a/src/core/model/messagesmodel.h
+++ b/src/core/model/messagesmodel.h
@@ -170,6 +170,15 @@ public:
 
     void updateTextToSpeech(const QByteArray &messageId, bool inProgress);
 
+    /**
+     * @brief Clears the read-receipt "unread" flag on every message sent no later than @p until.
+     *
+     * Called in response to the server's @c messagesRead notification, whose @c until is the
+     * oldest last-seen timestamp across all room participants — i.e. the point up to which
+     * everyone has read. Repaints the affected rows so the read-receipt indicator updates live.
+     */
+    void markMessagesReadUntil(qint64 until);
+
     void generateText(const Message &message, const QString &searchText, int hightLightStringIndex);
 
 private:
diff --git a/src/core/rocketchatbackend.cpp b/src/core/rocketchatbackend.cpp
index 0b6d048695..d8a6223477 100644
--- a/src/core/rocketchatbackend.cpp
+++ b/src/core/rocketchatbackend.cpp
@@ -611,6 +611,25 @@ void RocketChatBackend::slotChanged(const QJsonObject &object)
             roomId.remove(u"/deleteMessageBulk"_s);
             qCDebug(RUQOLA_BACKEND_LOG) << "UNIMPLEMENT!!!!!! deleteMessageBulk " << collection << " object " << object;
             // QJsonObject({"collection":"stream-notify-room","fields":{"args":[{"excludePinned":false,"ignoreDiscussion":true,"rid":"QgCf8GcnXYW5QXiHN","ts":{"$gt":{"$date":946681200000},"$lt":{"$date":1599602400000}},"users":[]}],"eventName":"QgCf8GcnXYW5QXiHN/deleteMessageBulk"},"id":"id","msg":"changed"})
+        } else if (eventname.endsWith("/messagesRead"_L1)) {
+            if (mRocketChatAccount->ruqolaLogger()) {
+                QJsonDocument d;
+                d.setObject(object);
+                mRocketChatAccount->ruqolaLogger()->dataReceived("stream-notify-room: messagesRead:"_ba + d.toJson());
+            }
+            // Rocket.Chat emits this (only when Message_Read_Receipt_Enabled) once everyone in the
+            // room has read up to "until" — the oldest last-seen timestamp across participants. Clear
+            // the read-receipt flag on messages up to that point so the indicator updates live.
+            // Args: [{ until: Date, tmid?: string }]; a per-thread read (tmid set) is not handled here.
+            QString roomId = eventname;
+            roomId.remove(u"/messagesRead"_s);
+            const QJsonObject readInfo = contents.at(0).toObject();
+            const qint64 until = Utils::parseDate(u"until"_s, readInfo);
+            if (until > 0 && readInfo.value("tmid"_L1).toString().isEmpty()) {
+                if (MessagesModel *messageModel = mRocketChatAccount->messageModelForRoom(roomId.toLatin1())) {
+                    messageModel->markMessagesReadUntil(until);
+                }
+            }
         } else {
             if (mRocketChatAccount->ruqolaLogger()) {
                 QJsonDocument d;
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.