1use 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
135const READ_RECEIPT_MAX_BATCHES: usize = 20;
138
139fn 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 Ok(handle) => handle.detach(),
161 Err(err) => warn!(%room_id, "couldn't enqueue a read-receipt backfill request: {err}"),
162 }
163}
164
165fn 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 fn process_event(&mut self, event: &TimelineEvent, user_id: &UserId);
186
187 fn reset(&mut self);
188
189 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 #[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 #[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 if event.event_id() == Some(receipt_event_id) {
252 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
269pub trait EventFilter {
272 fn room_id(&self) -> &RoomId;
274
275 fn filter(&self, event: &TimelineEvent) -> bool;
277
278 fn receipt_thread_matches(&self, receipt_thread: &ReceiptThread) -> bool;
281
282 async fn stored_receipt_event_for_user(
284 &self,
285 user_id: &UserId,
286 receipt_type: ReceiptType,
287 ) -> Option<(OwnedEventId, Receipt)>;
288}
289
290pub struct RoomReadReceiptEventFilter<'cache> {
292 room_id: &'cache RoomId,
294
295 with_threading_support: bool,
297
298 state_store: &'cache DynStateStore,
300}
301
302impl<'cache> RoomReadReceiptEventFilter<'cache> {
303 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 (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 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
360pub 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 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 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
418const ALL_RECEIPT_TYPES: [ReceiptType; 2] = [ReceiptType::ReadPrivate, ReceiptType::Read];
421
422fn 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 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 trace!(%event_id, "found new receipt (added to pending)");
455 pending_receipts.push(event_id.clone());
456 }
457 }
458 }
459 }
460
461 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 if latest_active == Some(event_id) {
479 trace!(active = %event_id, "the latest active receipt is still the most recent; stopping search");
481 receipt = Some(event_id.to_owned());
482 }
483 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 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 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 false
514 } else {
515 true
517 }
518 });
519 }
520
521 receipt
522}
523
524async 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 read_receipts.pending.push(event_id);
550 }
551 }
552 }
553}
554
555#[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 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 trace!(%event_id, "Saving a new active read receipt");
596 read_receipts.latest_active = Some(LatestReadReceipt { event_id: event_id.clone() });
597
598 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 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 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
643fn marks_as_unread(event: &Raw<AnySyncTimelineEvent>, user_id: &UserId) -> bool {
645 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 if let Some((RelationType::Replacement, _)) = extract_relation(event) {
677 tracing::trace!("not interesting because edited");
678 return false;
679 }
680
681 #[derive(serde::Deserialize)]
683 struct UnsignedContent {
684 redacted_because: Option<Raw<AnySyncTimelineEvent>>,
685 }
686
687 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
698pub 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 #[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 assert!(stop_on_event_ids(HashSet::from([owned_event_id!("$2")]))(&outcome).is_break());
786 assert!(stop_on_event_ids(HashSet::from([owned_event_id!("$3")]))(&outcome).is_continue());
788 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
1160 let new_receipt_event = None;
1162 let active_receipt = None;
1164
1165 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 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 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 let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
1196 let new_receipt_event = None;
1198 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 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 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 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 let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
1239 let new_receipt_event = None;
1241 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 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 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 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 let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
1281
1282 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 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 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 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 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 let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
1329
1330 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 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 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 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 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 let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
1379 pending_receipts.push(owned_event_id!("$2"));
1380
1381 let new_receipt_event = None;
1383
1384 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 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 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 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 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 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 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 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 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}