Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F112301
D106.1732290914.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
13 KB
Referenced Files
None
Subscribers
None
D106.1732290914.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
@@ -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
Details
Attached
Mime Type
text/plain
Expires
Fri, Nov 22, 7:55 AM (6 h, 1 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
38885
Default Alt Text
D106.1732290914.diff (13 KB)
Attached To
Mode
D106: Implement calculating local notifications
Attached
Detach File
Event Timeline
Log In to Comment