Page MenuHomePhorge

D106.1732309438.diff
No OneTemporary

Size
13 KB
Referenced Files
None
Subscribers
None

D106.1732309438.diff

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
@@ -22,7 +22,7 @@
#include <event.hpp>
#include <crypto.hpp>
-
+#include "push-rules-desc.hpp"
#include "local-echo.hpp"
#include "clientutil.hpp"
@@ -150,6 +150,22 @@
std::size_t invitedMemberCount;
};
+ /// Update local notifications to include the new events
+ ///
+ /// Precondition: newEvents are already in room.messages
+ struct AddLocalNotificationsAction
+ {
+ EventList newEvents;
+ PushRulesDesc pushRulesDesc;
+ std::string myUserId;
+ };
+
+ /// Remove local notifications that are already read
+ struct RemoveReadLocalNotificationsAction
+ {
+ std::string myUserId;
+ };
+
inline bool operator==(const PendingRoomKeyEvent &a, const PendingRoomKeyEvent &b)
{
return a.txnId == b.txnId && a.messages == b.messages;
@@ -178,6 +194,18 @@
}
}
+ /**
+ * Get the sort key for a timeline event.
+ *
+ * If the key is larger, the event should be placed
+ * at the more recent end of the timeline.
+ *
+ * @param e The event to get the sort key for.
+ * @return The sort key. You MUST use `auto` to store the
+ * result.
+ */
+ auto sortKeyForTimelineEvent(Event e) -> std::tuple<Timestamp, std::string>;
+
struct RoomModel
{
using Membership = RoomMembership;
@@ -229,6 +257,7 @@
/// The local unread count for this room.
std::size_t localUnreadCount{0};
/// The local unread notification count for this room.
+ /// XXX this is never used.
std::size_t localNotificationCount{0};
/// Read receipts for all users
immer::map<std::string /* userId */, ReadReceipt> readReceipts;
@@ -244,6 +273,8 @@
std::string /* sessionId */,
immer::flex_vector<std::string /* eventId */>> undecryptedEvents;
+ immer::flex_vector<std::string> unreadNotificationEventIds;
+
immer::flex_vector<std::string> joinedMemberIds() const;
immer::flex_vector<std::string> invitedMemberIds() const;
immer::flex_vector<std::string> knockedMemberIds() const;
@@ -307,7 +338,9 @@
AddPendingRoomKeyAction,
RemovePendingRoomKeyAction,
UpdateJoinedMemberCountAction,
- UpdateInvitedMemberCountAction
+ UpdateInvitedMemberCountAction,
+ AddLocalNotificationsAction,
+ RemoveReadLocalNotificationsAction
>;
static RoomModel update(RoomModel r, Action a);
@@ -343,6 +376,7 @@
&& a.readReceipts == b.readReceipts
&& a.eventReadUsers == b.eventReadUsers
&& a.undecryptedEvents == b.undecryptedEvents
+ && a.unreadNotificationEventIds == b.unreadNotificationEventIds
;
}
@@ -435,6 +469,9 @@
r.recalculateUndecryptedEvents();
}
}
+ if (version >= 8) {
+ ar & r.unreadNotificationEventIds;
+ }
}
template<class Archive>
@@ -446,5 +483,5 @@
BOOST_CLASS_VERSION(Kazv::PendingRoomKeyEvent, 1)
BOOST_CLASS_VERSION(Kazv::ReadReceipt, 0)
-BOOST_CLASS_VERSION(Kazv::RoomModel, 7)
+BOOST_CLASS_VERSION(Kazv::RoomModel, 8)
BOOST_CLASS_VERSION(Kazv::RoomListModel, 0)
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
@@ -58,6 +58,11 @@
return !(a == b);
}
+ auto sortKeyForTimelineEvent(Event e) -> std::tuple<Timestamp, std::string>
+ {
+ return std::make_tuple(e.originServerTs(), e.id());
+ }
+
RoomModel RoomModel::update(RoomModel r, Action a)
{
return lager::match(std::move(a))(
@@ -97,7 +102,7 @@
auto key =
[=](auto eventId) {
// sort first by timestamp, then by id
- return std::make_tuple(r.messages[eventId].originServerTs(), eventId);
+ return sortKeyForTimelineEvent(r.messages[eventId]);
};
auto handleRedaction =
@@ -313,6 +318,65 @@
[&](UpdateInvitedMemberCountAction a) {
r.invitedMemberCount = a.invitedMemberCount;
return r;
+ },
+ [&](AddLocalNotificationsAction a) {
+ auto readReceiptForCurrentUser = r.readReceipts.count(a.myUserId) ? r.readReceipts[a.myUserId].eventId : ""s;
+
+ auto newEventIds = intoImmer(
+ immer::flex_vector<std::string>{},
+ zug::map(&Event::id),
+ a.newEvents
+ );
+
+ auto k = [r](const auto &id) {
+ return sortKeyForTimelineEvent(r.messages[id]);
+ };
+
+ auto needToAddPredicate = [a, r, k, readReceiptForCurrentUser](const auto &eid) {
+ if (!readReceiptForCurrentUser.empty()
+ && k(readReceiptForCurrentUser) >= k(eid)) {
+ // this means this event is already read
+ return false;
+ }
+ auto e = r.messages[eid];
+ return e.sender() != a.myUserId
+ && a.pushRulesDesc.handle(e, r).shouldNotify;
+ };
+
+ r.unreadNotificationEventIds = sortedUniqueMerge(std::move(r.unreadNotificationEventIds), newEventIds, [needToAddPredicate](const auto &e) { return !needToAddPredicate(e); }, k);
+ return r;
+ },
+ [&](RemoveReadLocalNotificationsAction a) {
+ if (!r.readReceipts.count(a.myUserId)) {
+ return r;
+ }
+
+ auto rr = r.readReceipts[a.myUserId].eventId;
+
+ auto k = [r](const auto &id) {
+ return sortKeyForTimelineEvent(r.messages[id]);
+ };
+ auto cmp = [k](const auto &a, const auto &b) {
+ return k(a) < k(b);
+ };
+
+ auto it = std::upper_bound(
+ r.unreadNotificationEventIds.begin(),
+ r.unreadNotificationEventIds.end(),
+ rr,
+ cmp
+ );
+ // *it > rr, *(it - 1) <= rr (if it - 1 is valid)
+ if (it == r.unreadNotificationEventIds.end()) {
+ // If it == end(), it means everything is read
+ r.unreadNotificationEventIds = {};
+ } else if (it == r.unreadNotificationEventIds.begin()) {
+ // If it == begin(), it means everything is unread, so nothing to do
+ } else {
+ // it is somewhere in the middle, pointing to the first element that is unread
+ r.unreadNotificationEventIds = std::move(r.unreadNotificationEventIds).erase(0, it.index());
+ }
+ return r;
}
);
}
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
@@ -770,6 +770,14 @@
*/
PromiseT postReceipt(std::string eventId) const;
+ /**
+ * Get a list of event ids of unread notifications.
+ *
+ * @return A lager::reader of a RangeT of event ids that
+ * has an unread notification.
+ */
+ auto unreadNotificationEventIds() const -> lager::reader<immer::flex_vector<std::string>>;
+
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
@@ -879,4 +879,9 @@
{
return m_ctx.dispatch(PostReceiptAction{roomId().make().get(), eventId});
}
+
+ auto Room::unreadNotificationEventIds() const -> lager::reader<immer::flex_vector<std::string>>
+ {
+ return m_roomCursor[&RoomModel::unreadNotificationEventIds];
+ }
}
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
@@ -25,6 +25,7 @@
#include "client-test-util.hpp"
#include "action-mock-utils.hpp"
+#include "push-rules-test-util.hpp"
using namespace Kazv;
using namespace Kazv::Factory;
@@ -215,3 +216,132 @@
REQUIRE(json::parse(std::get<Bytes>(job.requestBody())) == json::object());
});
}
+
+TEST_CASE("AddLocalNotificationsAction", "[client][room][receipt]")
+{
+ auto myUserId = "@mew:example.com"s;
+
+ auto newEvents = EventList{
+ makeEvent(withEventType("moe.kazv.mxc.test-event")),
+ makeEvent(),
+ makeEvent(withEventType("moe.kazv.mxc.test-event")),
+ makeEvent(withEventType("moe.kazv.mxc.test-event") | withEventSenderId(myUserId)),
+ makeEvent(withEventType("moe.kazv.mxc.test-event")),
+ };
+ auto r = makeRoom(withRoomTimeline(newEvents));
+ r.readReceipts = {{myUserId, ReadReceipt{newEvents[0].id(), 0}}};
+
+ WHEN("push rules does not notify") {
+ auto next = RoomModel::update(r, AddLocalNotificationsAction{
+ newEvents,
+ PushRulesDesc(Event()),
+ myUserId,
+ });
+ REQUIRE(next.unreadNotificationEventIds == immer::flex_vector<std::string>{});
+ }
+
+ WHEN("push rules notifies some") {
+ auto next = RoomModel::update(r, AddLocalNotificationsAction{
+ newEvents,
+ PushRulesDesc(Event(R"({
+ "type": "m.push_rules",
+ "content": {
+ "global": {
+ "override": [{
+ "rule_id": "moe.kazv.mxc.some-rule",
+ "default": true,
+ "enabled": true,
+ "conditions": [{
+ "kind": "event_match",
+ "key": "type",
+ "pattern": "moe.kazv.mxc.test-event"
+ }],
+ "actions": ["notify"]
+ }]
+ }
+ }
+})"_json)),
+ myUserId,
+ });
+ REQUIRE(
+ next.unreadNotificationEventIds
+ == immer::flex_vector<std::string>{
+ newEvents[2].id(),
+ newEvents[4].id(),
+ }
+ );
+ }
+}
+
+TEST_CASE("RemoveReadLocalNotificationsAction", "[client][room][receipt]")
+{
+ auto myUserId = "@mew:example.com"s;
+
+ auto oldEvents = EventList{
+ makeEvent(),
+ makeEvent(),
+ };
+ auto newEvents = EventList{
+ makeEvent(),
+ makeEvent(),
+ makeEvent(),
+ makeEvent(),
+ makeEvent(),
+ makeEvent(),
+ };
+ auto timeline = oldEvents + newEvents;
+ auto r = makeRoom(withRoomTimeline(timeline));
+ r.unreadNotificationEventIds = intoImmer(
+ immer::flex_vector<std::string>(),
+ zug::map(&Event::id),
+ newEvents.erase(5)
+ );
+
+ WHEN("a read receipt is in the middle") {
+ r.readReceipts = {{myUserId, ReadReceipt{newEvents[2].id(), 0}}};
+ auto next = RoomModel::update(r, RemoveReadLocalNotificationsAction{myUserId});
+ REQUIRE(next.unreadNotificationEventIds == immer::flex_vector<std::string>{newEvents[3].id(), newEvents[4].id()});
+ }
+
+ WHEN("a read receipt is before the beginning") {
+ r.readReceipts = {{myUserId, ReadReceipt{oldEvents[0].id(), 0}}};
+ auto next = RoomModel::update(r, RemoveReadLocalNotificationsAction{myUserId});
+ REQUIRE(next.unreadNotificationEventIds == r.unreadNotificationEventIds);
+ }
+
+ WHEN("a read receipt does not exist") {
+ r.readReceipts = {};
+ auto next = RoomModel::update(r, RemoveReadLocalNotificationsAction{myUserId});
+ REQUIRE(next.unreadNotificationEventIds == r.unreadNotificationEventIds);
+ }
+
+ WHEN("a read receipt is at the end") {
+ r.readReceipts = {{myUserId, ReadReceipt{newEvents[5].id(), 0}}};
+ auto next = RoomModel::update(r, RemoveReadLocalNotificationsAction{myUserId});
+ REQUIRE(next.unreadNotificationEventIds == immer::flex_vector<std::string>{});
+ }
+
+ WHEN("a read receipt is after the end") {
+ r.readReceipts = {{myUserId, ReadReceipt{newEvents[5].id(), 0}}};
+ auto next = RoomModel::update(r, RemoveReadLocalNotificationsAction{myUserId});
+ REQUIRE(next.unreadNotificationEventIds == immer::flex_vector<std::string>{});
+ }
+}
+
+TEST_CASE("Room::unreadNotificationEventIds()", "[client][room][receipt]")
+{
+ boost::asio::io_context io;
+ AsioPromiseHandler ph{io.get_executor()};
+
+ auto room = makeRoom();
+ room.unreadNotificationEventIds = {"$foo", "$bar"};
+ auto model = makeClient(withRoom(room));
+ 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.unreadNotificationEventIds().get()
+ == room.unreadNotificationEventIds);
+}

File Metadata

Mime Type
text/plain
Expires
Fri, Nov 22, 1:03 PM (4 h, 38 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
38885
Default Alt Text
D106.1732309438.diff (13 KB)

Event Timeline