Skip to main content

matrix_sdk/event_cache/caches/
read_receipts.rs

1// Copyright 2026 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! # Client-side read receipts computation
16//!
17//! While Matrix servers have the ability to provide basic information about the
18//! unread status of rooms, via [`crate::sync::UnreadNotificationsCount`], it's
19//! not reliable for encrypted rooms. Indeed, the server doesn't have access to
20//! the content of encrypted events, so it can only makes guesses when
21//! estimating unread and highlight counts.
22//!
23//! Instead, this module provides facilities to compute the number of unread
24//! messages, unread notifications (based on the push rules) and unread
25//! highlights in a room. More precisely, instead of speaking about room, we
26//! will speak about _timeline_ as in _collection of events handled by a cache_
27//! like [`RoomEventCache`] or [`ThreadEventCache`].
28//!
29//! Counting unread messages is performed by looking at the latest receipt of
30//! the current user, and inferring which events are following it, according to
31//! the sync ordering.
32//!
33//! For notifications and highlights to be precisely accounted for, we also need
34//! to pay attention to the user's notification settings. Fortunately, this is
35//! also something we need for notifications, so we can reuse this code.
36//!
37//! Of course, not all events are created equal, and some are less interesting
38//! than others, and shouldn't cause a room to be marked unread. This module's
39//! [`marks_as_unread`] function shows the opinionated set of rules that will
40//! filter out uninterested events.
41//!
42//! The only `pub(crate)` method in that module is [`compute_unread_counts`],
43//! which updates the [`RoomInfo`] in place according to the new counts.
44//!
45//! ## Implementation details: How to get the latest receipt?
46//!
47//! ### Preliminary context
48//!
49//! We reuse a room event cache's linked chunk, and iterate over the events that
50//! are stored in memory.
51//!
52//! ### How-to
53//!
54//! When we call [`compute_unread_counts`], that's for one of two reasons (and
55//! maybe both at once, or maybe none at all):
56//!
57//! - we received a new receipt,
58//! - new events came in.
59//!
60//! A read receipt is considered _active_ if it's been received from sync
61//! *and* it matches a known event in the in-memory linked chunk.
62//!
63//! The *latest active* receipt is the one that's active, with the latest order
64//! (according to the event cache ordering, aka its position in the linked
65//! chunk).
66//!
67//! The problem of keeping a precise read count is thus equivalent to finding
68//! the latest active receipt, and counting interesting events after it.
69//!
70//! When we need to recompute the unread counts, we go through all the linked
71//! chunk's events to select a "better" active receipt, using the following
72//! rules:
73//!
74//! - an event we sent counts as a read receipt (it's called the implicit read
75//!   receipt in the spec),
76//! - an event which is referenced in the read receipt event content (either a
77//!   private or a public read receipt, of type unthreaded or main, to keep
78//!   maximal compatibility with thread-unaware clients),
79//! - a previously stashed read receipt we've received from a read receipt event
80//!   content, but for which we couldn't find the corresponding event. It's
81//!   possible that a read receipt is received before the corresponding event
82//!   (think about limited sync response).
83//!
84//! The read receipt that wins is always the one that points to the most recent
85//! event in the linked chunk ordering. In other words, the receipt type (as
86//! described above) doesn't matter; it's the relative position in the linked
87//! chunk ordering which does.
88//!
89//! Once we have a new *better active receipt*, we'll save it in the
90//! [`ReadReceipts`] data (stored in [`RoomInfo`]), and we'll compute the
91//! counts, starting from the event the better active receipt was referring to.
92//!
93//! If we *don't* have a better active receipt, that means that all the events
94//! received in that batch aren't referred to by a known read receipt, _and_ we
95//! didn't get a new better receipt that matched known events. In that case, we
96//! can just consider that all the events are new, and count them as such.
97//!
98//! [`RoomInfo`]: crate::RoomInfo
99//! [`RoomEventCache`]: super::room::RoomEventCache
100//! [`ThreadEventCache`]: super::thread::ThreadEventCache
101
102use std::{
103    collections::HashSet,
104    ops::{ControlFlow, Deref, DerefMut, Not},
105};
106
107use matrix_sdk_base::{
108    read_receipts::{LatestReadReceipt, ReadReceipts},
109    serde_helpers::extract_relation,
110    store::DynStateStore,
111};
112use matrix_sdk_common::{
113    deserialized_responses::TimelineEvent, ring_buffer::RingBuffer,
114    serde_helpers::extract_thread_root,
115};
116use ruma::{
117    EventId, OwnedEventId, OwnedUserId, RoomId, UserId,
118    events::{
119        AnySyncTimelineEvent, MessageLikeEventType,
120        receipt::{Receipt, ReceiptEventContent, ReceiptThread, ReceiptType, Receipts},
121        relation::RelationType,
122    },
123    serde::Raw,
124};
125use tracing::{debug, instrument, trace, warn};
126
127use super::{
128    super::back_pagination_queue::{
129        BATCH_SIZE, BackPaginationQueue, BackPaginationRequest, Priority,
130    },
131    event_linked_chunk::EventLinkedChunk,
132};
133use crate::event_cache::caches::pagination::BackPaginationOutcome;
134
135/// Number of paginations allowed per read-receipt request: the safety net for a
136/// receipt target that never surfaces.
137const READ_RECEIPT_MAX_BATCHES: usize = 20;
138
139/// Enqueue a fire-and-forget, batch-capped back-pagination for a room whose
140/// read receipt points at an event that isn't loaded yet. It stops as soon as a
141/// batch contains one of `targets`.
142fn paginate_for_read_receipt(
143    queue: &BackPaginationQueue,
144    room_id: &RoomId,
145    targets: HashSet<OwnedEventId>,
146) {
147    debug!(%room_id, "started backfill request for read receipts");
148
149    let request = BackPaginationRequest {
150        room_id: room_id.to_owned(),
151        priority: Priority::Normal,
152        stop: Box::new(stop_on_event_ids(targets)),
153        batch_size: BATCH_SIZE,
154        max_batches: Some(READ_RECEIPT_MAX_BATCHES),
155    };
156
157    match queue.enqueue(request) {
158        // Fire-and-forget: nobody awaits the result, so detach the handle to let the
159        // request run to completion instead of cancelling it on drop.
160        Ok(handle) => handle.detach(),
161        Err(err) => warn!(%room_id, "couldn't enqueue a read-receipt backfill request: {err}"),
162    }
163}
164
165/// A stop predicate that fires as soon as a batch loads any of `targets`. With
166/// no targets it never fires, so the request runs to its batch cap.
167fn stop_on_event_ids(
168    targets: HashSet<OwnedEventId>,
169) -> impl FnMut(&BackPaginationOutcome) -> ControlFlow<()> + Send + 'static {
170    move |outcome| {
171        let found = outcome
172            .events
173            .iter()
174            .any(|event| event.event_id().is_some_and(|id| targets.contains(id)));
175
176        if found { ControlFlow::Break(()) } else { ControlFlow::Continue(()) }
177    }
178}
179
180trait ReadReceiptsExt {
181    /// Update the [`ReadReceipts`] unread counts according to the new
182    /// event.
183    ///
184    /// Returns whether a new event triggered a new unread/notification/mention.
185    fn process_event(&mut self, event: &TimelineEvent, user_id: &UserId);
186
187    fn reset(&mut self);
188
189    /// Try to find the event to which the receipt attaches to, and if found,
190    /// will update the notification count in the room.
191    fn find_and_process_events<'a>(
192        &mut self,
193        receipt_event_id: &EventId,
194        user_id: &UserId,
195        events: impl Iterator<Item = &'a TimelineEvent>,
196    ) -> bool;
197}
198
199impl ReadReceiptsExt for ReadReceipts {
200    /// Update the [`ReadReceipts`] unread counts according to the new
201    /// event.
202    ///
203    /// Returns whether a new event triggered a new unread/notification/mention.
204    #[inline(always)]
205    fn process_event(&mut self, event: &TimelineEvent, user_id: &UserId) {
206        if marks_as_unread(event.raw(), user_id) {
207            self.num_unread += 1;
208        }
209
210        let mut has_notify = false;
211        let mut has_mention = false;
212
213        let Some(actions) = event.push_actions() else {
214            return;
215        };
216
217        for action in actions.iter() {
218            if !has_notify && action.should_notify() {
219                self.num_notifications += 1;
220                has_notify = true;
221            }
222            if !has_mention && action.is_highlight() {
223                self.num_mentions += 1;
224                has_mention = true;
225            }
226        }
227    }
228
229    #[inline(always)]
230    fn reset(&mut self) {
231        self.num_unread = 0;
232        self.num_notifications = 0;
233        self.num_mentions = 0;
234    }
235
236    /// Try to find the event to which the receipt attaches to, and if found,
237    /// will update the notification count in the room.
238    #[instrument(skip_all)]
239    fn find_and_process_events<'a>(
240        &mut self,
241        receipt_event_id: &EventId,
242        user_id: &UserId,
243        events: impl Iterator<Item = &'a TimelineEvent>,
244    ) -> bool {
245        let mut counting_receipts = false;
246
247        for event in events {
248            // Sliding sync sometimes sends the same event multiple times, so it can be at
249            // the beginning and end of a batch, for instance. In that case, just reset
250            // every time we see the event matching the receipt.
251            if event.event_id() == Some(receipt_event_id) {
252                // Bingo! Switch over to the counting state, after resetting the
253                // previous counts.
254                trace!("Found the event the receipt was referring to! Starting to count.");
255                self.reset();
256                counting_receipts = true;
257                continue;
258            }
259
260            if counting_receipts {
261                self.process_event(event, user_id);
262            }
263        }
264
265        counting_receipts
266    }
267}
268
269/// A trait to filter events from a [`LinkedChunk`] that will be consumed by
270/// this module.
271pub trait EventFilter {
272    /// Room ID of the room containing all the events.
273    fn room_id(&self) -> &RoomId;
274
275    /// Decide whether an event is a candidate for a read receipt.
276    fn filter(&self, event: &TimelineEvent) -> bool;
277
278    /// Check whether a given [`ReceiptThread`] is valid, i.e. matches our
279    /// expectation of possible `ReceiptThread` for `Self`.
280    fn receipt_thread_matches(&self, receipt_thread: &ReceiptThread) -> bool;
281
282    /// Find the receipt event for a specific user in the store.
283    async fn stored_receipt_event_for_user(
284        &self,
285        user_id: &UserId,
286        receipt_type: ReceiptType,
287    ) -> Option<(OwnedEventId, Receipt)>;
288}
289
290/// Type to filter room events that are candidates for read receipts.
291pub struct RoomReadReceiptEventFilter<'cache> {
292    /// The room ID.
293    room_id: &'cache RoomId,
294
295    /// Whether thread support is enabled.
296    with_threading_support: bool,
297
298    /// The state store to access stored receipt events.
299    state_store: &'cache DynStateStore,
300}
301
302impl<'cache> RoomReadReceiptEventFilter<'cache> {
303    /// Construct a new [`ReadReceiptsForRoom`].
304    pub fn new(
305        room_event_cache_state: &'cache super::room::RoomEventCacheState,
306        state_store: &'cache DynStateStore,
307    ) -> Self {
308        Self {
309            room_id: &room_event_cache_state.room_id,
310            with_threading_support: room_event_cache_state.enabled_thread_support,
311            state_store,
312        }
313    }
314}
315
316impl<'cache> EventFilter for RoomReadReceiptEventFilter<'cache> {
317    fn room_id(&self) -> &RoomId {
318        self.room_id
319    }
320
321    fn filter(&self, event: &TimelineEvent) -> bool {
322        // This type is built from a `RoomEventCacheState`. The room event cache
323        // contains all events, including in-thread events. We need to filter them!
324        (self.with_threading_support && extract_thread_root(event.raw()).is_some()).not()
325    }
326
327    fn receipt_thread_matches(&self, receipt_thread: &ReceiptThread) -> bool {
328        matches!(receipt_thread, ReceiptThread::Unthreaded | ReceiptThread::Main)
329    }
330
331    async fn stored_receipt_event_for_user(
332        &self,
333        user_id: &UserId,
334        receipt_type: ReceiptType,
335    ) -> Option<(OwnedEventId, Receipt)> {
336        // We want to prioritize an `Unthreaded` receipt over a `Main`-threaded one, for
337        // better compatibility with thread-unaware clients.
338        for receipt_thread in [ReceiptThread::Unthreaded, ReceiptThread::Main] {
339            let receipt_event = self
340                .state_store
341                .get_user_room_receipt_event(
342                    self.room_id,
343                    receipt_type.clone(),
344                    &receipt_thread,
345                    user_id,
346                )
347                .await
348                .ok()
349                .flatten();
350
351            if receipt_event.is_some() {
352                return receipt_event;
353            }
354        }
355
356        None
357    }
358}
359
360/// Type to filter in-thread events that are candidates for read receipts.
361pub struct ThreadReadReceiptEventFilter<'cache> {
362    room_id: &'cache RoomId,
363    thread_id: &'cache EventId,
364    state_store: &'cache DynStateStore,
365}
366
367impl<'cache> ThreadReadReceiptEventFilter<'cache> {
368    /// Construct a new [`ReadReceiptsForThread`].
369    pub fn new(
370        thread_event_cache_state: &'cache super::thread::ThreadEventCacheState,
371        state_store: &'cache DynStateStore,
372    ) -> Self {
373        Self {
374            room_id: &thread_event_cache_state.room_id,
375            thread_id: &thread_event_cache_state.thread_id,
376            state_store,
377        }
378    }
379}
380
381impl<'cache> EventFilter for ThreadReadReceiptEventFilter<'cache> {
382    fn room_id(&self) -> &RoomId {
383        self.room_id
384    }
385
386    fn filter(&self, _event: &TimelineEvent) -> bool {
387        // This type is built from a `ThreadEventCacheState`. The thread event cache
388        // contains all in-thread events for this particular thread. No need to filter
389        // them.
390        true
391    }
392
393    fn receipt_thread_matches(&self, receipt_thread: &ReceiptThread) -> bool {
394        matches!(
395            receipt_thread,
396            ReceiptThread::Thread(thread_id) if self.thread_id == thread_id
397        )
398    }
399
400    async fn stored_receipt_event_for_user(
401        &self,
402        user_id: &UserId,
403        receipt_type: ReceiptType,
404    ) -> Option<(OwnedEventId, Receipt)> {
405        self.state_store
406            .get_user_room_receipt_event(
407                self.room_id,
408                receipt_type,
409                &ReceiptThread::Thread(self.thread_id.to_owned()),
410                user_id,
411            )
412            .await
413            .ok()
414            .flatten()
415    }
416}
417
418/// The receipt types we look for, in order of priority (the first ones are more
419/// likely to be ahead in the timeline, so we look for them first).
420const ALL_RECEIPT_TYPES: [ReceiptType; 2] = [ReceiptType::ReadPrivate, ReceiptType::Read];
421
422/// Return a new better (i.e. more recent) receipt based on a search of the
423/// linked chunk.
424///
425/// A receipt is selected if:
426///
427/// - it's an implicit read receipt (i.e. an event we've sent),
428/// - it's holding onto a new read receipt we've just received,
429/// - it was a pending receipt for which we found the event now.
430///
431/// A receipt returned in this function **must** point to an event that is in
432/// the linked chunk.
433fn select_best_receipt<T>(
434    user_id: &UserId,
435    linked_chunk: &EventLinkedChunk,
436    event_filter: &T,
437    pending_receipts: &mut RingBuffer<OwnedEventId>,
438    new_receipt_event: Option<&ReceiptEventContent>,
439    latest_active: Option<&EventId>,
440) -> Option<OwnedEventId>
441where
442    T: EventFilter,
443{
444    // If we had a new receipt event, add the main/unthreaded receipts it contains
445    // to the pending receipts list. We'll try to chase them later.
446    if let Some(receipt_event) = new_receipt_event {
447        for (event_id, receipts) in &receipt_event.0 {
448            for ty in ALL_RECEIPT_TYPES {
449                if let Some(receipts) = receipts.get(&ty)
450                    && let Some(receipt) = receipts.get(user_id)
451                    && event_filter.receipt_thread_matches(&receipt.thread)
452                {
453                    // Add it to the pending receipts list.
454                    trace!(%event_id, "found new receipt (added to pending)");
455                    pending_receipts.push(event_id.clone());
456                }
457            }
458        }
459    }
460
461    // This loop folds two actions at once:
462    // - try to find the most recent receipt, by looking at the events in reverse
463    //   order (i.e. from the most recent to the least recent),
464    // - try to match stashed receipts against known events in the linked chunk, so
465    //   as to shrink the stash of pending receipts.
466    //
467    // We can early exit out of this loop, as soon as there's no more work to do,
468    // i.e., we've found a better receipt, *and* there's no more pending receipt
469    // to try to match against events in the linked chunk.
470
471    let mut receipt = None;
472
473    for (event, event_id) in linked_chunk.revents().filter_map(|(_pos, event)| {
474        event_filter.filter(event).then_some((event, event.event_id()?))
475    }) {
476        if receipt.is_none() {
477            // Try to see if the latest active receipt is still the most recent receipt.
478            if latest_active == Some(event_id) {
479                // The latest active receipt is still the most recent receipt, so keep it.
480                trace!(active = %event_id, "the latest active receipt is still the most recent; stopping search");
481                receipt = Some(event_id.to_owned());
482            }
483            // Try to find an implicit read receipt (i.e. an event sent by the current
484            // user).
485            else if event.sender().as_deref() == Some(user_id) {
486                trace!(implicit = %event_id, "found an implicit receipt; stopping search");
487                receipt = Some(event_id.to_owned());
488            }
489        }
490
491        // Early exit condition (see the comment above): we've already found a most
492        // recent receipt, and there's no other pending receipts to match against known
493        // events.
494        if receipt.is_some() && pending_receipts.is_empty() {
495            trace!("exiting loop; found a better receipt, and no more pending receipt to match");
496            break;
497        }
498
499        // Try to match pending receipts to events known in the linked chunk. If we
500        // haven't found any receipt yet, the first matched pending receipt is a better
501        // one!
502        pending_receipts.retain(|pending| {
503            if *pending == event_id {
504                if receipt.is_none() {
505                    trace!(pending = %event_id, "found a pending receipt; stopping search");
506                    receipt = Some(event_id.to_owned());
507                } else {
508                    trace!(%event_id, "discarding a pending receipt that wasn't selected");
509                }
510
511                // Don't keep the pending receipt in the pending list: we've already identified
512                // a better, more recent receipt at this point (found == Some).
513                false
514            } else {
515                // Keep the receipt, in case the associated event shows up later.
516                true
517            }
518        });
519    }
520
521    receipt
522}
523
524/// Try to find extra read receipts that were in the store but never saved in
525/// the [`ReadReceipts`] data structure.
526///
527/// Doesn't return a `Result`, because this is entirely optional; if the store
528/// fails to load these receipts, the worst that can happen is incorrect unread
529/// counts until the next receipt event is received from sync.
530async fn try_find_stored_receipts<T>(
531    user_id: &UserId,
532    event_filter: &T,
533    read_receipts: &mut ReadReceipts,
534) where
535    T: EventFilter,
536{
537    for receipt_type in ALL_RECEIPT_TYPES {
538        if let Some((event_id, _receipt)) =
539            event_filter.stored_receipt_event_for_user(user_id, receipt_type).await
540        {
541            trace!(%event_id, "Found a dormant receipt in the store");
542
543            if read_receipts.latest_active.is_none() {
544                read_receipts.latest_active = Some(LatestReadReceipt { event_id });
545            } else {
546                // This loop has already flagged a read receipt as the new `latest_active`.
547                // Extra read receipts can go to the pending receipts list, as they're lower
548                // priority, by the implementation notes above.
549                read_receipts.pending.push(event_id);
550            }
551        }
552    }
553}
554
555/// Given a set of events coming from sync, for a _timeline_, update the
556/// [`ReadReceipts`]'s counts of unread messages, notifications and
557/// highlights' in place.
558///
559/// See this module's documentation for more information.
560#[instrument(skip_all, fields(room_id = %event_filter.room_id()))]
561pub(crate) async fn compute_unread_counts<T>(
562    user_id: &UserId,
563    receipt_event: Option<&ReceiptEventContent>,
564    linked_chunk: &EventLinkedChunk,
565    event_filter: &T,
566    read_receipts: &mut ReadReceipts,
567    back_pagination_queue: Option<&BackPaginationQueue>,
568) where
569    T: EventFilter,
570{
571    debug!(?read_receipts, "Starting");
572
573    // If we don't have a latest active receipt for this timeline, try to reload one
574    // from the state store into the `ReadReceipts`.
575    if read_receipts.latest_active.is_none() {
576        try_find_stored_receipts(user_id, event_filter, read_receipts).await;
577    }
578
579    let better_receipt = select_best_receipt(
580        user_id,
581        linked_chunk,
582        event_filter,
583        &mut read_receipts.pending,
584        receipt_event,
585        read_receipts.latest_active.as_ref().map(|latest_active| latest_active.event_id.as_ref()),
586    );
587
588    if let Some(event_id) = better_receipt {
589        // We've found the id of an event to which the receipt attaches. The associated
590        // event may either come from the new batch of events associated to
591        // this sync, or it may live in the past timeline events we know
592        // about.
593
594        // First, save the event id as the latest one that has a read receipt.
595        trace!(%event_id, "Saving a new active read receipt");
596        read_receipts.latest_active = Some(LatestReadReceipt { event_id: event_id.clone() });
597
598        // The event for the receipt is in the linked chunk, so we'll find it and can
599        // count safely from here.
600        read_receipts.find_and_process_events(
601            &event_id,
602            user_id,
603            linked_chunk
604                .events()
605                .filter_map(|(_pos, event)| event_filter.filter(event).then_some(event)),
606        );
607
608        debug!(?read_receipts, "after finding a better receipt");
609        return;
610    }
611
612    // Request a pagination: we haven't found a better receipt, but we haven't even
613    // found the latest active receipt! Hand it the receipt event ids we're chasing
614    // so the backfill can stop as soon as one of them is loaded.
615    if let Some(back_pagination_queue) = back_pagination_queue {
616        let targets: HashSet<OwnedEventId> = read_receipts
617            .pending
618            .iter()
619            .cloned()
620            .chain(read_receipts.latest_active.as_ref().map(|receipt| receipt.event_id.clone()))
621            .collect();
622        paginate_for_read_receipt(back_pagination_queue, event_filter.room_id(), targets);
623    }
624
625    // If we haven't returned at this point, it means we don't have any new "active"
626    // read receipt. So either there was a previous one further in the past, or
627    // none.
628    //
629    // In that case, the number of unreads is *at most* the number of processed
630    // events. Reset the number of unreads, and recount them all.
631    read_receipts.reset();
632
633    for event in linked_chunk
634        .events()
635        .filter_map(|(_pos, event)| event_filter.filter(event).then_some(event))
636    {
637        read_receipts.process_event(event, user_id);
638    }
639
640    debug!(?read_receipts, "no better receipt");
641}
642
643/// Is the event worth marking a timeline as unread?
644fn marks_as_unread(event: &Raw<AnySyncTimelineEvent>, user_id: &UserId) -> bool {
645    // Parse the sender from the raw event.
646    if event.get_field::<OwnedUserId>("sender").ok().flatten().as_deref() == Some(user_id) {
647        tracing::trace!("not interesting because sent by the current user");
648        return false;
649    }
650
651    let Some(event_type) = event.get_field::<MessageLikeEventType>("type").ok().flatten() else {
652        tracing::trace!(
653            "failed to parse event type for event with id {:?}, skipping it",
654            event.get_field::<OwnedEventId>("event_id").ok().flatten()
655        );
656        return false;
657    };
658
659    match event_type {
660        MessageLikeEventType::Message
661        | MessageLikeEventType::PollStart
662        | MessageLikeEventType::UnstablePollStart
663        | MessageLikeEventType::PollEnd
664        | MessageLikeEventType::UnstablePollEnd
665        | MessageLikeEventType::RoomEncrypted
666        | MessageLikeEventType::RoomMessage
667        | MessageLikeEventType::Sticker => {}
668
669        _ => {
670            tracing::trace!("not interesting because not an interesting message-like");
671            return false;
672        }
673    }
674
675    // Filter out edits.
676    if let Some((RelationType::Replacement, _)) = extract_relation(event) {
677        tracing::trace!("not interesting because edited");
678        return false;
679    }
680
681    // Filter out redacted events.
682    #[derive(serde::Deserialize)]
683    struct UnsignedContent {
684        redacted_because: Option<Raw<AnySyncTimelineEvent>>,
685    }
686
687    // Filter out redactions.
688    if let Ok(Some(UnsignedContent { redacted_because: Some(_redaction) })) =
689        event.get_field::<UnsignedContent>("unsigned")
690    {
691        tracing::trace!("not interesting because redacted");
692        return false;
693    }
694
695    true
696}
697
698/// A type representing `Option<ReceiptEventContent>`.
699///
700/// It is useful because it implements [`FromIterator`], similarly to
701/// [`ReceiptEventContent`], except it produces `None` if source iterator is
702/// empty instead of an empty `BTreeMap`.
703pub struct MaybeReceiptEventContent(Option<ReceiptEventContent>);
704
705impl MaybeReceiptEventContent {
706    pub fn none() -> Self {
707        Self(None)
708    }
709
710    pub fn into_inner(self) -> Option<ReceiptEventContent> {
711        self.0
712    }
713}
714
715impl Deref for MaybeReceiptEventContent {
716    type Target = Option<ReceiptEventContent>;
717
718    fn deref(&self) -> &Self::Target {
719        &self.0
720    }
721}
722
723impl DerefMut for MaybeReceiptEventContent {
724    fn deref_mut(&mut self) -> &mut Self::Target {
725        &mut self.0
726    }
727}
728
729impl FromIterator<(OwnedEventId, Receipts)> for MaybeReceiptEventContent {
730    fn from_iter<T>(iterator: T) -> Self
731    where
732        T: IntoIterator<Item = (OwnedEventId, Receipts)>,
733    {
734        let mut iterator = iterator.into_iter().peekable();
735
736        Self(if iterator.peek().is_some() { Some(iterator.collect()) } else { None })
737    }
738}
739
740#[cfg(test)]
741mod tests {
742    use std::{num::NonZeroUsize, ops::Not as _};
743
744    use matrix_sdk_base::{read_receipts::ReadReceipts, store::MemoryStore};
745    use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
746    use matrix_sdk_test::{ALICE, event_factory::EventFactory};
747    use ruma::{
748        EventId, MilliSecondsSinceUnixEpoch, RoomId, UserId, event_id,
749        events::{
750            receipt::{Receipt, ReceiptThread, ReceiptType, UserReceipts},
751            room::{member::MembershipState, message::MessageType},
752        },
753        owned_event_id,
754        push::{Action, HighlightTweakValue, Tweak},
755        room_id, user_id,
756    };
757
758    use super::{
759        EventFilter, MaybeReceiptEventContent, ReadReceiptsExt as _, Receipts,
760        RoomReadReceiptEventFilter, marks_as_unread, select_best_receipt, stop_on_event_ids,
761    };
762    use crate::event_cache::caches::{
763        event_linked_chunk::EventLinkedChunk, pagination::BackPaginationOutcome,
764    };
765
766    /// `stop_on_event_ids` breaks as soon as one of its target ids is loaded;
767    /// with no targets it never breaks (falls back to the batch cap).
768    #[test]
769    fn test_stop_on_event_ids() {
770        use std::collections::HashSet;
771
772        use matrix_sdk_test::BOB;
773
774        let room = room_id!("!omelette:fromage.fr");
775        let f = EventFactory::new().room(room).sender(*BOB);
776        let outcome = BackPaginationOutcome {
777            reached_start: false,
778            events: vec![
779                f.text_msg("a").event_id(event_id!("$1")).into_event(),
780                f.text_msg("b").event_id(event_id!("$2")).into_event(),
781            ],
782        };
783
784        // A target present in the batch → stop.
785        assert!(stop_on_event_ids(HashSet::from([owned_event_id!("$2")]))(&outcome).is_break());
786        // No target in the batch → keep going.
787        assert!(stop_on_event_ids(HashSet::from([owned_event_id!("$3")]))(&outcome).is_continue());
788        // No targets at all → never stops on content.
789        assert!(stop_on_event_ids(HashSet::new())(&outcome).is_continue());
790    }
791
792    #[test]
793    fn test_room_message_marks_as_unread() {
794        let user_id = user_id!("@alice:example.org");
795        let other_user_id = user_id!("@bob:example.org");
796
797        let f = EventFactory::new();
798
799        // A message from somebody else marks the room as unread...
800        let ev = f.text_msg("A").event_id(event_id!("$ida")).sender(other_user_id).into_raw_sync();
801        assert!(marks_as_unread(&ev, user_id));
802
803        // ... but a message from ourselves doesn't.
804        let ev = f.text_msg("A").event_id(event_id!("$ida")).sender(user_id).into_raw_sync();
805        assert!(marks_as_unread(&ev, user_id).not());
806    }
807
808    #[test]
809    fn test_room_edit_does_not_mark_as_unread() {
810        let user_id = user_id!("@alice:example.org");
811        let other_user_id = user_id!("@bob:example.org");
812
813        // An edit to a message from somebody else doesn't mark the room as unread.
814        let ev = EventFactory::new()
815            .text_msg("* edited message")
816            .edit(
817                event_id!("$someeventid:localhost"),
818                MessageType::text_plain("edited message").into(),
819            )
820            .event_id(event_id!("$ida"))
821            .sender(other_user_id)
822            .into_raw_sync();
823
824        assert!(marks_as_unread(&ev, user_id).not());
825    }
826
827    #[test]
828    fn test_redaction_does_not_mark_room_as_unread() {
829        let user_id = user_id!("@alice:example.org");
830        let other_user_id = user_id!("@bob:example.org");
831
832        // A redact of a message from somebody else doesn't mark the room as unread.
833        let ev = EventFactory::new()
834            .redaction(event_id!("$151957878228ssqrj:localhost"))
835            .sender(other_user_id)
836            .event_id(event_id!("$151957878228ssqrJ:localhost"))
837            .into_raw_sync();
838
839        assert!(marks_as_unread(&ev, user_id).not());
840    }
841
842    #[test]
843    fn test_reaction_does_not_mark_room_as_unread() {
844        let user_id = user_id!("@alice:example.org");
845        let other_user_id = user_id!("@bob:example.org");
846
847        // A reaction from somebody else to a message doesn't mark the room as unread.
848        let ev = EventFactory::new()
849            .reaction(event_id!("$15275047031IXQRj:localhost"), "👍")
850            .sender(other_user_id)
851            .event_id(event_id!("$15275047031IXQRi:localhost"))
852            .into_raw_sync();
853
854        assert!(marks_as_unread(&ev, user_id).not());
855    }
856
857    #[test]
858    fn test_state_event_does_not_mark_as_unread() {
859        let user_id = user_id!("@alice:example.org");
860        let event_id = event_id!("$1");
861
862        let ev = EventFactory::new()
863            .member(user_id)
864            .membership(MembershipState::Join)
865            .display_name("Alice")
866            .event_id(event_id)
867            .into_raw_sync();
868        assert!(marks_as_unread(&ev, user_id).not());
869
870        let other_user_id = user_id!("@bob:example.org");
871        assert!(marks_as_unread(&ev, other_user_id).not());
872    }
873
874    #[test]
875    fn test_count_unread_and_mentions() {
876        fn make_event(user_id: &UserId, push_actions: Vec<Action>) -> TimelineEvent {
877            let mut ev = EventFactory::new()
878                .text_msg("A")
879                .sender(user_id)
880                .event_id(event_id!("$ida"))
881                .into_event();
882            ev.set_push_actions(push_actions);
883            ev
884        }
885
886        let user_id = user_id!("@alice:example.org");
887
888        // An interesting event from oneself doesn't count as a new unread message.
889        let event = make_event(user_id, Vec::new());
890        let mut receipts = ReadReceipts::default();
891        receipts.process_event(&event, user_id);
892        assert_eq!(receipts.num_unread, 0);
893        assert_eq!(receipts.num_mentions, 0);
894        assert_eq!(receipts.num_notifications, 0);
895
896        // An interesting event from someone else does count as a new unread message.
897        let event = make_event(user_id!("@bob:example.org"), Vec::new());
898        let mut receipts = ReadReceipts::default();
899        receipts.process_event(&event, user_id);
900        assert_eq!(receipts.num_unread, 1);
901        assert_eq!(receipts.num_mentions, 0);
902        assert_eq!(receipts.num_notifications, 0);
903
904        // Push actions computed beforehand are respected.
905        let event = make_event(user_id!("@bob:example.org"), vec![Action::Notify]);
906        let mut receipts = ReadReceipts::default();
907        receipts.process_event(&event, user_id);
908        assert_eq!(receipts.num_unread, 1);
909        assert_eq!(receipts.num_mentions, 0);
910        assert_eq!(receipts.num_notifications, 1);
911
912        let event = make_event(
913            user_id!("@bob:example.org"),
914            vec![Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes))],
915        );
916        let mut receipts = ReadReceipts::default();
917        receipts.process_event(&event, user_id);
918        assert_eq!(receipts.num_unread, 1);
919        assert_eq!(receipts.num_mentions, 1);
920        assert_eq!(receipts.num_notifications, 0);
921
922        let event = make_event(
923            user_id!("@bob:example.org"),
924            vec![Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)), Action::Notify],
925        );
926        let mut receipts = ReadReceipts::default();
927        receipts.process_event(&event, user_id);
928        assert_eq!(receipts.num_unread, 1);
929        assert_eq!(receipts.num_mentions, 1);
930        assert_eq!(receipts.num_notifications, 1);
931
932        // Technically this `push_actions` set would be a bug somewhere else, but let's
933        // make sure to resist against it.
934        let event = make_event(user_id!("@bob:example.org"), vec![Action::Notify, Action::Notify]);
935        let mut receipts = ReadReceipts::default();
936        receipts.process_event(&event, user_id);
937        assert_eq!(receipts.num_unread, 1);
938        assert_eq!(receipts.num_mentions, 0);
939        assert_eq!(receipts.num_notifications, 1);
940    }
941
942    #[test]
943    fn test_find_and_process_events() {
944        let ev0 = event_id!("$0");
945        let user_id = user_id!("@alice:example.org");
946
947        // When provided with no events, we report not finding the event to which the
948        // receipt relates.
949        let mut receipts = ReadReceipts::default();
950        assert!(receipts.find_and_process_events(ev0, user_id, [].iter()).not());
951        assert_eq!(receipts.num_unread, 0);
952        assert_eq!(receipts.num_notifications, 0);
953        assert_eq!(receipts.num_mentions, 0);
954
955        // When provided with one event, that's not the receipt event, we don't count
956        // it.
957        fn make_event(event_id: &EventId) -> TimelineEvent {
958            EventFactory::new()
959                .text_msg("A")
960                .sender(user_id!("@bob:example.org"))
961                .event_id(event_id)
962                .into()
963        }
964
965        let mut receipts = ReadReceipts {
966            num_unread: 42,
967            num_notifications: 13,
968            num_mentions: 37,
969            ..Default::default()
970        };
971        assert!(
972            receipts
973                .find_and_process_events(ev0, user_id, [make_event(event_id!("$1"))].iter())
974                .not()
975        );
976        assert_eq!(receipts.num_unread, 42);
977        assert_eq!(receipts.num_notifications, 13);
978        assert_eq!(receipts.num_mentions, 37);
979
980        // When provided with one event that's the receipt target, we find it, reset the
981        // count, and since there's nothing else, we stop there and end up with
982        // zero counts.
983        let mut receipts = ReadReceipts {
984            num_unread: 42,
985            num_notifications: 13,
986            num_mentions: 37,
987            ..Default::default()
988        };
989        assert!(receipts.find_and_process_events(ev0, user_id, [make_event(ev0)].iter()));
990        assert_eq!(receipts.num_unread, 0);
991        assert_eq!(receipts.num_notifications, 0);
992        assert_eq!(receipts.num_mentions, 0);
993
994        // When provided with multiple events and not the receipt event, we do not count
995        // anything..
996        let mut receipts = ReadReceipts {
997            num_unread: 42,
998            num_notifications: 13,
999            num_mentions: 37,
1000            ..Default::default()
1001        };
1002        assert!(
1003            receipts
1004                .find_and_process_events(
1005                    ev0,
1006                    user_id,
1007                    [
1008                        make_event(event_id!("$1")),
1009                        make_event(event_id!("$2")),
1010                        make_event(event_id!("$3"))
1011                    ]
1012                    .iter(),
1013                )
1014                .not()
1015        );
1016        assert_eq!(receipts.num_unread, 42);
1017        assert_eq!(receipts.num_notifications, 13);
1018        assert_eq!(receipts.num_mentions, 37);
1019
1020        // When provided with multiple events including one that's the receipt event, we
1021        // find it and count from it.
1022        let mut receipts = ReadReceipts {
1023            num_unread: 42,
1024            num_notifications: 13,
1025            num_mentions: 37,
1026            ..Default::default()
1027        };
1028        assert!(
1029            receipts.find_and_process_events(
1030                ev0,
1031                user_id,
1032                [
1033                    make_event(event_id!("$1")),
1034                    make_event(ev0),
1035                    make_event(event_id!("$2")),
1036                    make_event(event_id!("$3"))
1037                ]
1038                .iter(),
1039            )
1040        );
1041        assert_eq!(receipts.num_unread, 2);
1042        assert_eq!(receipts.num_notifications, 0);
1043        assert_eq!(receipts.num_mentions, 0);
1044
1045        // Even if duplicates are present in the new events list, the count is correct.
1046        let mut receipts = ReadReceipts {
1047            num_unread: 42,
1048            num_notifications: 13,
1049            num_mentions: 37,
1050            ..Default::default()
1051        };
1052        assert!(
1053            receipts.find_and_process_events(
1054                ev0,
1055                user_id,
1056                [
1057                    make_event(ev0),
1058                    make_event(event_id!("$1")),
1059                    make_event(ev0),
1060                    make_event(event_id!("$2")),
1061                    make_event(event_id!("$3"))
1062                ]
1063                .iter(),
1064            )
1065        );
1066        assert_eq!(receipts.num_unread, 2);
1067        assert_eq!(receipts.num_notifications, 0);
1068        assert_eq!(receipts.num_mentions, 0);
1069    }
1070
1071    #[test]
1072    fn test_compute_unread_counts_with_threading_enabled() {
1073        fn make_in_thread_event(
1074            user_id: &UserId,
1075            room_id: &RoomId,
1076            thread_root: &EventId,
1077        ) -> TimelineEvent {
1078            EventFactory::new()
1079                .room(room_id)
1080                .text_msg("A")
1081                .sender(user_id)
1082                .event_id(event_id!("$ida"))
1083                .in_thread(thread_root, event_id!("$latest_event"))
1084                .into_event()
1085        }
1086
1087        let mut receipts = ReadReceipts::default();
1088
1089        let state_store = MemoryStore::new();
1090        let room_id = room_id!("!r");
1091        let own_alice = user_id!("@alice:example.org");
1092        let bob = user_id!("@bob:example.org");
1093
1094        let event_filter = RoomReadReceiptEventFilter {
1095            room_id,
1096            with_threading_support: true,
1097            state_store: &state_store,
1098        };
1099
1100        // Threaded messages from myself or other users shouldn't change the
1101        // unread counts.
1102        for event in [
1103            make_in_thread_event(own_alice, room_id, event_id!("$some_thread_root")),
1104            make_in_thread_event(own_alice, room_id, event_id!("$some_other_thread_root")),
1105            make_in_thread_event(bob, room_id, event_id!("$some_thread_root")),
1106            make_in_thread_event(bob, room_id, event_id!("$some_other_thread_root")),
1107        ]
1108        .into_iter()
1109        .filter(|event| event_filter.filter(event))
1110        {
1111            receipts.process_event(&event, own_alice);
1112        }
1113
1114        assert_eq!(receipts.num_unread, 0);
1115        assert_eq!(receipts.num_mentions, 0);
1116        assert_eq!(receipts.num_notifications, 0);
1117
1118        // Processing an unthreaded message should still count as unread.
1119        for event in [EventFactory::new()
1120            .room(room_id)
1121            .text_msg("A")
1122            .sender(bob)
1123            .event_id(event_id!("$ida"))
1124            .into_event()]
1125        .into_iter()
1126        .filter(|event| event_filter.filter(event))
1127        {
1128            receipts.process_event(&event, own_alice);
1129        }
1130
1131        assert_eq!(receipts.num_unread, 1);
1132        assert_eq!(receipts.num_mentions, 0);
1133        assert_eq!(receipts.num_notifications, 0);
1134    }
1135
1136    #[test]
1137    fn test_select_best_receipt_noop() {
1138        let room_id = room_id!("!roomid:example.org");
1139        let f = EventFactory::new().room(room_id).sender(*ALICE);
1140
1141        // Create a non-empty linked chunk, with no messages sent by the current user.
1142        let mut linked_chunk = EventLinkedChunk::new();
1143        linked_chunk.push_events(vec![
1144            f.text_msg("Event 1").event_id(event_id!("$1")).into_event(),
1145            f.text_msg("Event 2").event_id(event_id!("$2")).into_event(),
1146            f.text_msg("Event 3").event_id(event_id!("$3")).into_event(),
1147        ]);
1148
1149        let state_store = MemoryStore::new();
1150        let own_user_id = user_id!("@not_alice:example.org");
1151
1152        let event_filter = RoomReadReceiptEventFilter {
1153            room_id,
1154            with_threading_support: false,
1155            state_store: &state_store,
1156        };
1157
1158        // When there are no pending receipts,
1159        let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
1160        // And no new receipt,
1161        let new_receipt_event = None;
1162        // And no active receipt,
1163        let active_receipt = None;
1164
1165        // Then there's no best receipt.
1166        let receipt = select_best_receipt(
1167            own_user_id,
1168            &linked_chunk,
1169            &event_filter,
1170            &mut pending_receipts,
1171            new_receipt_event,
1172            active_receipt,
1173        );
1174        assert!(receipt.is_none());
1175        // And there are no pending receipts.
1176        assert!(pending_receipts.is_empty());
1177    }
1178
1179    #[test]
1180    fn test_select_best_receipt_implicit() {
1181        let room_id = room_id!("!roomid:example.org");
1182        let f = EventFactory::new().room(room_id).sender(*ALICE);
1183        let own_user_id = user_id!("@not_alice:example.org");
1184
1185        // Create a non-empty linked chunk, with one message sent by the current user,
1186        // which will act as an implicit read receipt.
1187        let mut linked_chunk = EventLinkedChunk::new();
1188        linked_chunk.push_events(vec![
1189            f.text_msg("Event 1").event_id(event_id!("$1")).into_event(),
1190            f.text_msg("Event 2").event_id(event_id!("$2")).sender(own_user_id).into_event(),
1191            f.text_msg("Event 3").event_id(event_id!("$3")).into_event(),
1192        ]);
1193
1194        // When there are no pending receipts,
1195        let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
1196        // And no new receipt,
1197        let new_receipt_event = None;
1198        // And no active receipt,
1199        let active_receipt = None;
1200
1201        let state_store = MemoryStore::new();
1202        let event_filter = RoomReadReceiptEventFilter {
1203            room_id,
1204            with_threading_support: false,
1205            state_store: &state_store,
1206        };
1207
1208        // Then there's a new best receipt, which is the implicit one.
1209        let receipt = select_best_receipt(
1210            own_user_id,
1211            &linked_chunk,
1212            &event_filter,
1213            &mut pending_receipts,
1214            new_receipt_event,
1215            active_receipt,
1216        );
1217        assert_eq!(receipt.unwrap(), "$2");
1218        // And there are no pending receipts.
1219        assert!(pending_receipts.is_empty());
1220    }
1221
1222    #[test]
1223    fn test_select_best_receipt_active_receipt() {
1224        let room_id = room_id!("!roomid:example.org");
1225        let f = EventFactory::new().room(room_id).sender(*ALICE);
1226
1227        // Create a non-empty linked chunk, with no messages sent by the current user.
1228        let mut linked_chunk = EventLinkedChunk::new();
1229        linked_chunk.push_events(vec![
1230            f.text_msg("Event 1").event_id(event_id!("$1")).into_event(),
1231            f.text_msg("Event 2").event_id(event_id!("$2")).into_event(),
1232            f.text_msg("Event 3").event_id(event_id!("$3")).into_event(),
1233        ]);
1234
1235        let own_user_id = user_id!("@not_alice:example.org");
1236
1237        // When there are no pending receipts,
1238        let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
1239        // And no new receipt,
1240        let new_receipt_event = None;
1241        // And an active receipt pointing at $2,
1242        let active_receipt = Some(event_id!("$2"));
1243
1244        let state_store = MemoryStore::new();
1245        let event_filter = RoomReadReceiptEventFilter {
1246            room_id,
1247            with_threading_support: false,
1248            state_store: &state_store,
1249        };
1250
1251        // Then the best receipt is still $2.
1252        let receipt = select_best_receipt(
1253            own_user_id,
1254            &linked_chunk,
1255            &event_filter,
1256            &mut pending_receipts,
1257            new_receipt_event,
1258            active_receipt,
1259        );
1260        assert_eq!(receipt.unwrap(), "$2");
1261        // And there are no pending receipts.
1262        assert!(pending_receipts.is_empty());
1263    }
1264
1265    #[test]
1266    fn test_select_best_receipt_new_receipt_event() {
1267        let room_id = room_id!("!roomid:example.org");
1268        let f = EventFactory::new().room(room_id).sender(*ALICE);
1269        let own_user_id = user_id!("@not_alice:example.org");
1270
1271        // Create a non-empty linked chunk, with no messages sent by the current user.
1272        let mut linked_chunk = EventLinkedChunk::new();
1273        linked_chunk.push_events(vec![
1274            f.text_msg("Event 1").event_id(event_id!("$1")).into_event(),
1275            f.text_msg("Event 2").event_id(event_id!("$2")).into_event(),
1276            f.text_msg("Event 3").event_id(event_id!("$3")).into_event(),
1277        ]);
1278
1279        // When there are no pending receipts,
1280        let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
1281
1282        // And a new receipt event which points to $2,
1283        let new_receipt_event = Some(
1284            f.read_receipts()
1285                .add(event_id!("$2"), own_user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
1286                .into_content(),
1287        );
1288
1289        // And no active receipt,
1290        let active_receipt = None;
1291
1292        let state_store = MemoryStore::new();
1293        let event_filter = RoomReadReceiptEventFilter {
1294            room_id,
1295            with_threading_support: false,
1296            state_store: &state_store,
1297        };
1298
1299        // Then there's a new best receipt, which is the explicit one from the event
1300        let receipt = select_best_receipt(
1301            own_user_id,
1302            &linked_chunk,
1303            &event_filter,
1304            &mut pending_receipts,
1305            new_receipt_event.as_ref(),
1306            active_receipt,
1307        );
1308        assert_eq!(receipt.unwrap(), "$2");
1309        // And there are no pending receipts.
1310        assert!(pending_receipts.is_empty());
1311    }
1312
1313    #[test]
1314    fn test_select_best_receipt_stashes_pending_receipts() {
1315        let room_id = room_id!("!roomid:example.org");
1316        let f = EventFactory::new().room(room_id).sender(*ALICE);
1317        let own_user_id = user_id!("@not_alice:example.org");
1318
1319        // Create a non-empty linked chunk, with no messages sent by the current user.
1320        let mut linked_chunk = EventLinkedChunk::new();
1321        linked_chunk.push_events(vec![
1322            f.text_msg("Event 1").event_id(event_id!("$1")).into_event(),
1323            f.text_msg("Event 2").event_id(event_id!("$2")).into_event(),
1324            f.text_msg("Event 3").event_id(event_id!("$3")).into_event(),
1325        ]);
1326
1327        // When there are no pending receipts,
1328        let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
1329
1330        // And a new receipt event, for an event we don't know about,
1331        let new_receipt_event = Some(
1332            f.read_receipts()
1333                .add(event_id!("$4"), own_user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
1334                .into_content(),
1335        );
1336
1337        // And no active receipt,
1338        let active_receipt = None;
1339
1340        let state_store = MemoryStore::new();
1341        let event_filter = RoomReadReceiptEventFilter {
1342            room_id,
1343            with_threading_support: false,
1344            state_store: &state_store,
1345        };
1346
1347        // Then there's no new best receipts.
1348        let receipt = select_best_receipt(
1349            own_user_id,
1350            &linked_chunk,
1351            &event_filter,
1352            &mut pending_receipts,
1353            new_receipt_event.as_ref(),
1354            active_receipt,
1355        );
1356
1357        assert!(receipt.is_none());
1358        // And there's a new pending receipt for $4.
1359        assert_eq!(pending_receipts.len(), 1);
1360        assert_eq!(pending_receipts.get(0).unwrap(), "$4");
1361    }
1362
1363    #[test]
1364    fn test_select_best_receipt_matched_pending_receipt() {
1365        let room_id = room_id!("!roomid:example.org");
1366        let f = EventFactory::new().room(room_id).sender(*ALICE);
1367        let own_user_id = user_id!("@not_alice:example.org");
1368
1369        // Create a non-empty linked chunk, with no messages sent by the current user.
1370        let mut linked_chunk = EventLinkedChunk::new();
1371        linked_chunk.push_events(vec![
1372            f.text_msg("Event 1").event_id(event_id!("$1")).into_event(),
1373            f.text_msg("Event 2").event_id(event_id!("$2")).into_event(),
1374            f.text_msg("Event 3").event_id(event_id!("$3")).into_event(),
1375        ]);
1376
1377        // When there is a pending receipt for $2,
1378        let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
1379        pending_receipts.push(owned_event_id!("$2"));
1380
1381        // And no new receipt event,
1382        let new_receipt_event = None;
1383
1384        // And no active receipt,
1385        let active_receipt = None;
1386
1387        let state_store = MemoryStore::new();
1388        let event_filter = RoomReadReceiptEventFilter {
1389            room_id,
1390            with_threading_support: false,
1391            state_store: &state_store,
1392        };
1393
1394        // Then there's a new best receipt, which is the matched pending receipt.
1395        let receipt = select_best_receipt(
1396            own_user_id,
1397            &linked_chunk,
1398            &event_filter,
1399            &mut pending_receipts,
1400            new_receipt_event.as_ref(),
1401            active_receipt,
1402        );
1403        assert_eq!(receipt.unwrap(), "$2");
1404        // And there are no more pending receipts.
1405        assert!(pending_receipts.is_empty());
1406    }
1407
1408    #[test]
1409    fn test_select_best_receipt_mixed() {
1410        let room_id = room_id!("!roomid:example.org");
1411        let f = EventFactory::new().room(room_id).sender(*ALICE);
1412        let own_user_id = user_id!("@not_alice:example.org");
1413
1414        // Create a non-empty linked chunk, with one message sent by the current user,
1415        // which will act as an implicit read receipt.
1416        let mut linked_chunk = EventLinkedChunk::new();
1417        linked_chunk.push_events(vec![
1418            f.text_msg("Event 1").event_id(event_id!("$1")).into_event(),
1419            f.text_msg("Event 2").event_id(event_id!("$2")).into_event(),
1420            f.text_msg("Event 3").event_id(event_id!("$3")).sender(own_user_id).into_event(),
1421            f.text_msg("Event 4").event_id(event_id!("$4")).into_event(),
1422            f.text_msg("Event 5").event_id(event_id!("$5")).into_event(),
1423        ]);
1424
1425        // When there is a pending receipt for $2, and $6,
1426        let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
1427        pending_receipts.push(owned_event_id!("$2"));
1428        pending_receipts.push(owned_event_id!("$6"));
1429
1430        // And a new receipt event pointing at $4 and $6,
1431        let new_receipt_event = Some(
1432            f.read_receipts()
1433                .add(event_id!("$4"), own_user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
1434                .add(event_id!("$7"), own_user_id, ReceiptType::ReadPrivate, ReceiptThread::Main)
1435                .into_content(),
1436        );
1437
1438        // And an active receipt point at $1,
1439        let active_receipt = Some(event_id!("$1"));
1440
1441        let state_store = MemoryStore::new();
1442        let event_filter = RoomReadReceiptEventFilter {
1443            room_id,
1444            with_threading_support: false,
1445            state_store: &state_store,
1446        };
1447
1448        // Then there's a new best receipt, which is the most advanced in the linked
1449        // chunk: $4.
1450        let receipt = select_best_receipt(
1451            own_user_id,
1452            &linked_chunk,
1453            &event_filter,
1454            &mut pending_receipts,
1455            new_receipt_event.as_ref(),
1456            active_receipt,
1457        );
1458        assert_eq!(receipt.unwrap(), "$4");
1459
1460        // Receipt 6 is still pending, and there's a new pending receipt for 7 too. ($2
1461        // has been cleaned because it has been seen).
1462        assert_eq!(pending_receipts.len(), 2);
1463        assert!(pending_receipts.iter().any(|ev| ev == event_id!("$6")));
1464        assert!(pending_receipts.iter().any(|ev| ev == event_id!("$7")));
1465    }
1466
1467    #[test]
1468    fn test_maybe_receipt_event_content_from_empty_iterator() {
1469        let maybe: MaybeReceiptEventContent = std::iter::empty().collect();
1470
1471        assert!(maybe.is_none());
1472    }
1473
1474    #[test]
1475    fn test_maybe_receipt_event_content_from_iterator() {
1476        let maybe: MaybeReceiptEventContent = vec![(
1477            event_id!("$ev").to_owned(),
1478            Receipts::from([(
1479                ReceiptType::Read,
1480                UserReceipts::from([(
1481                    user_id!("@ali:ce").to_owned(),
1482                    Receipt::new(MilliSecondsSinceUnixEpoch::now()),
1483                )]),
1484            )]),
1485        )]
1486        .into_iter()
1487        .collect();
1488
1489        assert!(maybe.is_some());
1490
1491        let receipt_event_content = maybe.into_inner().unwrap();
1492        assert_eq!(receipt_event_content.len(), 1);
1493        assert!(receipt_event_content.contains_key(event_id!("$ev")));
1494    }
1495}