Expand description

§Client-side read receipts computation

While Matrix servers have the ability to provide basic information about the unread status of rooms, via crate::sync::UnreadNotificationsCount, it’s not reliable for encrypted rooms. Indeed, the server doesn’t have access to the content of encrypted events, so it can only makes guesses when estimating unread and highlight counts.

Instead, this module provides facilities to compute the number of unread messages, unread notifications and unread highlights in a room.

Counting unread messages is performed by looking at the latest receipt of the current user, and inferring which events are following it, according to the sync ordering.

For notifications and highlights to be precisely accounted for, we also need to pay attention to the user’s notification settings. Fortunately, this is also something we need to for notifications, so we can reuse this code.

Of course, not all events are created equal, and some are less interesting than others, and shouldn’t cause a room to be marked unread. This module’s marks_as_unread function shows the opiniated set of rules that will filter out uninterested events.

The only pub(crate) method in that module is compute_unread_counts, which updates the RoomInfo in place according to the new counts.

§Implementation details: How to get the latest receipt?

§Preliminary context

We do have an unbounded, in-memory cache for sync events, as part of sliding sync. It’s reset as soon as we get a “limited” (gappy) sync for a room. Not as ideal as an on-disk timeline, but it’s sufficient to do some interesting computations already.

§How-to

When we call compute_unread_counts, that’s for one of two reasons (and maybe both at once, or maybe none at all):

  • we received a new receipt
  • new events came in.

A read receipt is considered active if it’s been received from sync and it matches a known event in the in-memory sync events cache.

The latest active receipt is the one that’s active, with the latest order (according to sync ordering, aka position in the sync cache).

The problem of keeping a precise read count is thus equivalent to finding the latest active receipt, and counting interesting events after it (in the sync ordering).

When we get new events, we’ll incorporate them into an inverse mapping of event id -> sync order (event_id_to_pos). This gives us a simple way to select a “better” active receipt, using the ReceiptSelector. An event that has a read receipt can be passed to ReceiptSelector::try_select_later, which compares the order of the current best active, to that of the new event, and records the better one, if applicable.

When we receive a new receipt event in ReceiptSelector::handle_new_receipt, if we find a {public|private} {main-threaded|unthreaded} receipt attached to an event, there are two possibilities:

  • we knew the event, so we can immediately try to select it as a better event with try_select_later,
  • or we don’t, which may mean the receipt refers to a past event we lost track of (because of a restart of the app — remember the cache is mostly memory-only, and a few items on disk), or the receipt refers to a future event. To cover for the latter possibility, we stash the receipt and mark it as pending (we only keep a limited number of pending read receipts using a RingBuffer).

That means that when we receive new events, we’ll check if their id matches one of the pending receipts in handle_pending_receipts; if so, we can remove it from the pending set, and try to consider it a better receipt with try_select_later. If not, it’s still pending, until it’ll be forgotten or matched.

Once we have a new better active receipt, we’ll save it in the RoomReadReceipt data (stored in RoomInfo), and we’ll compute the counts, starting from the event the better active receipt was referring to.

If we don’t have a better active receipt, that means that all the events received in that sync batch aren’t referred to by a known read receipt, and we didn’t get a new better receipt that matched known events. In that case, we can just consider that all the events are new, and count them as such.

§Edge cases

  • compute_unread_counts is called after receiving a sliding sync response, at a time where we haven’t tried to “reconcile” the cached timeline items with the new ones. The only kind of reconciliation we’d do anyways is clearing the timeline if it was limited, which equates to having common events ids in both sets. As a matter of fact, we have to manually handle this edge case here. I hope that having an event database will help avoid this kind of workaround here later.
  • In addition to that, and as noted in the timeline code, it seems that the sliding-sync proxy could return the same event multiple times in a sync timeline, leading to incorrect results. We have to take that into account by resetting the read counts every time we see an event that was the target of the latest active read receipt.

Structs§

  • Public data about read receipts collected during processing of that room.

Traits§