Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85810308
D368.1792307061.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
7 KB
Referenced Files
None
Subscribers
None
D368.1792307061.diff
View Options
diff --git a/src/client/room/room-model.hpp b/src/client/room/room-model.hpp
--- a/src/client/room/room-model.hpp
+++ b/src/client/room/room-model.hpp
@@ -404,6 +404,17 @@
*/
bool isReadBy(const std::string &eventId, const std::string &userId) const;
+ /**
+ * Check if the room has events unread by the current user.
+ *
+ * An event is considered read if its order in timeline is before
+ * the local read marker or the read receipt of the current user.
+ *
+ * @param selfUserId The user id of the current user.
+ * @return true iff the room has events unread by the current user.
+ */
+ bool hasSelfUnreadEvents(const std::string &selfUserId) const;
+
using Action = std::variant<
AddStateEventsAction,
MaybeAddStateEventsAction,
diff --git a/src/client/room/room-model.cpp b/src/client/room/room-model.cpp
--- a/src/client/room/room-model.cpp
+++ b/src/client/room/room-model.cpp
@@ -815,4 +815,33 @@
auto event = messages[eventId];
return sortKeyForTimelineEvent(readReceiptAt) >= sortKeyForTimelineEvent(event);
}
+
+ bool RoomModel::hasSelfUnreadEvents(const std::string &selfUserId) const
+ {
+ auto k = [this](const std::string &eventId) {
+ return sortKeyForTimelineEvent(messages[eventId]);
+ };
+
+ auto readReceiptForCurrentUser = getMaxInTimeline(
+ readReceipts[selfUserId].eventId,
+ localReadMarker,
+ k
+ );
+
+ // The timeline is sorted from oldest to latest, and self-sent events
+ // are sparse. Scan from the end to find the latest non-self-sent event;
+ // the room has unread events iff that event is not yet read.
+ for (auto it = timeline.rbegin(); it != timeline.rend(); ++it) {
+ const auto &eventId = *it;
+ if (messages[eventId].sender() == selfUserId) {
+ continue;
+ }
+ if (!readReceiptForCurrentUser.empty()
+ && k(readReceiptForCurrentUser) >= k(eventId)) {
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
}
diff --git a/src/client/room/room.hpp b/src/client/room/room.hpp
--- a/src/client/room/room.hpp
+++ b/src/client/room/room.hpp
@@ -876,6 +876,19 @@
*/
bool isReadBy(const std::string &eventId, const std::string &userId) const;
+ /**
+ * Get whether the room has events unread by the current user.
+ *
+ * An event is considered read if its order in timeline is before
+ * the local read marker or the read receipt of the current user.
+ *
+ * The returned cursor takes O(1) time to be updated.
+ *
+ * @return A lager::reader of a boolean, which is true iff the room
+ * has events unread by the current user.
+ */
+ auto hasSelfUnreadEvents() const -> lager::reader<bool>;
+
private:
const lager::reader<SdkModel> &sdkCursor() const;
const lager::reader<RoomModel> &roomCursor() const;
diff --git a/src/client/room/room.cpp b/src/client/room/room.cpp
--- a/src/client/room/room.cpp
+++ b/src/client/room/room.cpp
@@ -983,4 +983,14 @@
{
return m_roomCursor.get().isReadBy(eventId, userId);
}
+
+ auto Room::hasSelfUnreadEvents() const -> lager::reader<bool>
+ {
+ return lager::with(
+ sdkCursor().map(&SdkModel::c)[&ClientModel::userId],
+ roomCursor()
+ ).map([](const std::string &selfUserId, const RoomModel &r) {
+ return r.hasSelfUnreadEvents(selfUserId);
+ });
+ }
}
diff --git a/src/tests/client/room/read-receipt-test.cpp b/src/tests/client/room/read-receipt-test.cpp
--- a/src/tests/client/room/read-receipt-test.cpp
+++ b/src/tests/client/room/read-receipt-test.cpp
@@ -436,3 +436,82 @@
REQUIRE(room.isReadBy("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Z2", "@rikj:jki.re") == true);
REQUIRE(room.isReadBy("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Z1", "@rikj:jki.re") == false);
}
+
+TEST_CASE("Room::hasSelfUnreadEvents()", "[client][room][receipt]")
+{
+ auto selfUserId = "@self:example.com"s;
+
+ auto e0 = makeEvent(withEventTs(100) | withEventId("$e0"));
+ auto e1 = makeEvent(withEventTs(200) | withEventId("$e1"));
+ auto e2 = makeEvent(withEventTs(300) | withEventId("$e2"));
+ auto selfE = makeEvent(withEventTs(400) | withEventId("$e4") | withEventSenderId(selfUserId));
+
+ auto checkHasSelfUnread = [selfUserId](RoomModel room, bool expected) {
+ boost::asio::io_context io;
+ AsioPromiseHandler ph{io.get_executor()};
+
+ auto model = makeClient(withRoom(room));
+ model.userId = selfUserId;
+ auto store = createTestClientStoreFrom(model, ph);
+ auto client = Client(store.reader().map([](auto c) { return SdkModel{c}; }),
+ store,
+ std::nullopt);
+ auto r = client.room(room.roomId);
+ REQUIRE(r.hasSelfUnreadEvents().get() == expected);
+ };
+
+ SECTION("no events") {
+ checkHasSelfUnread(makeRoom(), false);
+ }
+
+ SECTION("no read receipt or local read marker") {
+ checkHasSelfUnread(makeRoom(withRoomTimeline(EventList{e0, e1, e2})), true);
+ }
+
+ SECTION("read receipt at last event") {
+ auto room = makeRoom(withRoomTimeline(EventList{e0, e1, e2}));
+ room.readReceipts = {{selfUserId, ReadReceipt{e2.id(), 0}}};
+ checkHasSelfUnread(room, false);
+ }
+
+ SECTION("read receipt at middle event") {
+ auto room = makeRoom(withRoomTimeline(EventList{e0, e1, e2}));
+ room.readReceipts = {{selfUserId, ReadReceipt{e1.id(), 0}}};
+ checkHasSelfUnread(room, true);
+ }
+
+ SECTION("local read marker at last event") {
+ auto room = makeRoom(withRoomTimeline(EventList{e0, e1, e2}));
+ room.localReadMarker = e2.id();
+ checkHasSelfUnread(room, false);
+ }
+
+ SECTION("local read marker at middle event") {
+ auto room = makeRoom(withRoomTimeline(EventList{e0, e1, e2}));
+ room.localReadMarker = e1.id();
+ checkHasSelfUnread(room, true);
+ }
+
+ SECTION("read receipt and local read marker, the later one wins") {
+ auto room = makeRoom(withRoomTimeline(EventList{e0, e1, e2}));
+ room.readReceipts = {{selfUserId, ReadReceipt{e0.id(), 0}}};
+ room.localReadMarker = e2.id();
+ checkHasSelfUnread(room, false);
+ }
+
+ SECTION("only self-sent events") {
+ checkHasSelfUnread(makeRoom(withRoomTimeline(EventList{selfE})), false);
+ }
+
+ SECTION("self-sent event after read position") {
+ auto room = makeRoom(withRoomTimeline(EventList{e0, selfE}));
+ room.localReadMarker = e0.id();
+ checkHasSelfUnread(room, false);
+ }
+
+ SECTION("non-self event after read position") {
+ auto room = makeRoom(withRoomTimeline(EventList{e0, e2, selfE}));
+ room.readReceipts = {{selfUserId, ReadReceipt{e0.id(), 0}}};
+ checkHasSelfUnread(room, true);
+ }
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sun, Oct 18, 12:04 AM (22 h, 26 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1789005
Default Alt Text
D368.1792307061.diff (7 KB)
Attached To
Mode
D368: Expose whether a room is read
Attached
Detach File
Event Timeline
Log In to Comment