1use matrix_sdk_base::{
95 read_receipts::{LatestReadReceipt, RoomReadReceipts},
96 serde_helpers::extract_relation,
97 store::DynStateStore,
98};
99use matrix_sdk_common::{
100 deserialized_responses::TimelineEvent, ring_buffer::RingBuffer,
101 serde_helpers::extract_thread_root,
102};
103use ruma::{
104 EventId, OwnedEventId, OwnedUserId, RoomId, UserId,
105 events::{
106 AnySyncTimelineEvent, MessageLikeEventType,
107 receipt::{ReceiptEventContent, ReceiptThread, ReceiptType},
108 relation::RelationType,
109 },
110 serde::Raw,
111};
112use tracing::{debug, instrument, trace, warn};
113
114use crate::event_cache::{
115 automatic_pagination::AutomaticPagination, caches::event_linked_chunk::EventLinkedChunk,
116};
117
118trait RoomReadReceiptsExt {
119 fn process_event(
124 &mut self,
125 event: &TimelineEvent,
126 user_id: &UserId,
127 with_threading_support: bool,
128 );
129
130 fn reset(&mut self);
131
132 fn find_and_process_events<'a>(
135 &mut self,
136 receipt_event_id: &EventId,
137 user_id: &UserId,
138 events: impl IntoIterator<Item = &'a TimelineEvent>,
139 with_threading_support: bool,
140 ) -> bool;
141}
142
143impl RoomReadReceiptsExt for RoomReadReceipts {
144 #[inline(always)]
149 fn process_event(
150 &mut self,
151 event: &TimelineEvent,
152 user_id: &UserId,
153 with_threading_support: bool,
154 ) {
155 if with_threading_support && extract_thread_root(event.raw()).is_some() {
156 return;
157 }
158
159 if marks_as_unread(event.raw(), user_id) {
160 self.num_unread += 1;
161 }
162
163 let mut has_notify = false;
164 let mut has_mention = false;
165
166 let Some(actions) = event.push_actions() else {
167 return;
168 };
169
170 for action in actions.iter() {
171 if !has_notify && action.should_notify() {
172 self.num_notifications += 1;
173 has_notify = true;
174 }
175 if !has_mention && action.is_highlight() {
176 self.num_mentions += 1;
177 has_mention = true;
178 }
179 }
180 }
181
182 #[inline(always)]
183 fn reset(&mut self) {
184 self.num_unread = 0;
185 self.num_notifications = 0;
186 self.num_mentions = 0;
187 }
188
189 #[instrument(skip_all)]
192 fn find_and_process_events<'a>(
193 &mut self,
194 receipt_event_id: &EventId,
195 user_id: &UserId,
196 events: impl IntoIterator<Item = &'a TimelineEvent>,
197 with_threading_support: bool,
198 ) -> bool {
199 let mut counting_receipts = false;
200
201 for event in events {
202 if let Some(event_id) = event.event_id()
206 && event_id == receipt_event_id
207 {
208 trace!("Found the event the receipt was referring to! Starting to count.");
211 self.reset();
212 counting_receipts = true;
213 continue;
214 }
215
216 if counting_receipts {
217 self.process_event(event, user_id, with_threading_support);
218 }
219 }
220
221 counting_receipts
222 }
223}
224
225const ALL_RECEIPT_TYPES: [ReceiptType; 2] = [ReceiptType::ReadPrivate, ReceiptType::Read];
228
229fn select_best_receipt(
241 user_id: &UserId,
242 linked_chunk: &EventLinkedChunk,
243 pending_receipts: &mut RingBuffer<OwnedEventId>,
244 new_receipt_event: Option<&ReceiptEventContent>,
245 latest_active: Option<&EventId>,
246 with_threading_support: bool,
247) -> Option<OwnedEventId> {
248 if let Some(receipt_event) = new_receipt_event {
251 for (event_id, receipts) in &receipt_event.0 {
252 for ty in ALL_RECEIPT_TYPES {
253 if let Some(receipts) = receipts.get(&ty)
254 && let Some(receipt) = receipts.get(user_id)
255 && matches!(receipt.thread, ReceiptThread::Main | ReceiptThread::Unthreaded)
256 {
257 trace!(%event_id, "found new receipt (added to pending)");
259 pending_receipts.push(event_id.clone());
260 }
261 }
262 }
263 }
264
265 let mut receipt = None;
276
277 for (event, event_id) in
278 linked_chunk.revents().filter_map(|(_pos, ev)| Some((ev, ev.event_id()?)))
279 {
280 if receipt.is_none() {
281 if latest_active == Some(&event_id) {
283 trace!(active = %event_id, "the latest active receipt is still the most recent; stopping search");
285 receipt = Some(event_id.clone());
286 }
287 else if event.sender().as_deref() == Some(user_id)
292 && (!with_threading_support || extract_thread_root(event.raw()).is_none())
293 {
294 trace!(implicit = %event_id, "found an implicit receipt; stopping search");
295 receipt = Some(event_id.clone());
296 }
297 }
298
299 if receipt.is_some() && pending_receipts.is_empty() {
303 trace!("exiting loop; found a better receipt, and no more pending receipt to match");
304 break;
305 }
306
307 pending_receipts.retain(|pending| {
311 if *pending == event_id {
312 if receipt.is_none() {
313 trace!(pending = %event_id, "found a pending receipt; stopping search");
314 receipt = Some(event_id.clone());
315 } else {
316 trace!(%event_id, "discarding a pending receipt that wasn't selected");
317 }
318
319 false
322 } else {
323 true
325 }
326 });
327 }
328
329 receipt
330}
331
332async fn try_find_store_receipts(
339 store: &DynStateStore,
340 user_id: &UserId,
341 room_id: &RoomId,
342 read_receipts: &mut RoomReadReceipts,
343) {
344 for receipt_type in ALL_RECEIPT_TYPES {
345 for receipt_thread in [ReceiptThread::Unthreaded, ReceiptThread::Main] {
348 if let Ok(Some((event_id, _receipt))) = store
349 .get_user_room_receipt_event(
350 room_id,
351 receipt_type.clone(),
352 receipt_thread.clone(),
353 user_id,
354 )
355 .await
356 {
357 trace!(%event_id, ?receipt_type, ?receipt_thread, "Found a dormant receipt in the store");
358
359 if read_receipts.latest_active.is_none() {
360 read_receipts.latest_active =
361 Some(LatestReadReceipt { event_id: event_id.clone() });
362 } else {
363 read_receipts.pending.push(event_id.clone());
367 }
368 }
369 }
370 }
371}
372
373#[instrument(skip_all, fields(room_id = %room_id))]
379#[allow(clippy::too_many_arguments)]
380pub(crate) async fn compute_unread_counts(
381 user_id: &UserId,
382 room_id: &RoomId,
383 receipt_event: Option<&ReceiptEventContent>,
384 linked_chunk: &EventLinkedChunk,
385 read_receipts: &mut RoomReadReceipts,
386 with_threading_support: bool,
387 automatic_pagination: Option<&AutomaticPagination>,
388 state_store: &DynStateStore,
389) {
390 debug!(?read_receipts, "Starting");
391
392 if read_receipts.latest_active.is_none() {
395 try_find_store_receipts(state_store, user_id, room_id, read_receipts).await;
396 }
397
398 let better_receipt = select_best_receipt(
399 user_id,
400 linked_chunk,
401 &mut read_receipts.pending,
402 receipt_event,
403 read_receipts.latest_active.as_ref().map(|latest_active| latest_active.event_id.as_ref()),
404 with_threading_support,
405 );
406
407 if let Some(event_id) = better_receipt {
408 trace!(%event_id, "Saving a new active read receipt");
415 read_receipts.latest_active = Some(LatestReadReceipt { event_id: event_id.clone() });
416
417 read_receipts.find_and_process_events(
420 &event_id,
421 user_id,
422 linked_chunk.events().map(|(_pos, event)| event),
423 with_threading_support,
424 );
425
426 debug!(?read_receipts, "after finding a better receipt");
427 return;
428 }
429
430 if let Some(automatic_pagination) = automatic_pagination {
433 if automatic_pagination.run_once(room_id) {
434 trace!("Requested pagination to find a better receipt");
435 } else {
436 warn!("Failed to request pagination to find a better receipt");
437 }
438 }
439
440 read_receipts.reset();
447
448 for (_pos, event) in linked_chunk.events() {
449 read_receipts.process_event(event, user_id, with_threading_support);
450 }
451
452 debug!(?read_receipts, "no better receipt");
453}
454
455fn marks_as_unread(event: &Raw<AnySyncTimelineEvent>, user_id: &UserId) -> bool {
457 if event.get_field::<OwnedUserId>("sender").ok().flatten().as_deref() == Some(user_id) {
459 tracing::trace!("not interesting because sent by the current user");
460 return false;
461 }
462
463 let Some(event_type) = event.get_field::<MessageLikeEventType>("type").ok().flatten() else {
464 tracing::trace!(
465 "failed to parse event type for event with id {:?}, skipping it",
466 event.get_field::<OwnedEventId>("event_id").ok().flatten()
467 );
468 return false;
469 };
470
471 match event_type {
472 MessageLikeEventType::Message
473 | MessageLikeEventType::PollStart
474 | MessageLikeEventType::UnstablePollStart
475 | MessageLikeEventType::PollEnd
476 | MessageLikeEventType::UnstablePollEnd
477 | MessageLikeEventType::RoomEncrypted
478 | MessageLikeEventType::RoomMessage
479 | MessageLikeEventType::Sticker => {}
480
481 _ => {
482 tracing::trace!("not interesting because not an interesting message-like");
483 return false;
484 }
485 }
486
487 if let Some((RelationType::Replacement, _)) = extract_relation(event) {
489 tracing::trace!("not interesting because edited");
490 return false;
491 }
492
493 #[derive(serde::Deserialize)]
495 struct UnsignedContent {
496 redacted_because: Option<Raw<AnySyncTimelineEvent>>,
497 }
498
499 if let Ok(Some(UnsignedContent { redacted_because: Some(_redaction) })) =
501 event.get_field::<UnsignedContent>("unsigned")
502 {
503 tracing::trace!("not interesting because redacted");
504 return false;
505 }
506
507 true
508}
509
510#[cfg(test)]
511mod tests {
512 use std::{num::NonZeroUsize, ops::Not as _};
513
514 use matrix_sdk_base::read_receipts::RoomReadReceipts;
515 use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
516 use matrix_sdk_test::{ALICE, event_factory::EventFactory};
517 use ruma::{
518 EventId, UserId, event_id,
519 events::{
520 receipt::{ReceiptThread, ReceiptType},
521 room::{member::MembershipState, message::MessageType},
522 },
523 owned_event_id,
524 push::{Action, HighlightTweakValue, Tweak},
525 room_id, user_id,
526 };
527
528 use super::marks_as_unread;
529 use crate::event_cache::caches::{
530 event_linked_chunk::EventLinkedChunk,
531 read_receipts::{RoomReadReceiptsExt as _, select_best_receipt},
532 };
533
534 #[test]
535 fn test_room_message_marks_as_unread() {
536 let user_id = user_id!("@alice:example.org");
537 let other_user_id = user_id!("@bob:example.org");
538
539 let f = EventFactory::new();
540
541 let ev = f.text_msg("A").event_id(event_id!("$ida")).sender(other_user_id).into_raw_sync();
543 assert!(marks_as_unread(&ev, user_id));
544
545 let ev = f.text_msg("A").event_id(event_id!("$ida")).sender(user_id).into_raw_sync();
547 assert!(marks_as_unread(&ev, user_id).not());
548 }
549
550 #[test]
551 fn test_room_edit_doesnt_mark_as_unread() {
552 let user_id = user_id!("@alice:example.org");
553 let other_user_id = user_id!("@bob:example.org");
554
555 let ev = EventFactory::new()
557 .text_msg("* edited message")
558 .edit(
559 event_id!("$someeventid:localhost"),
560 MessageType::text_plain("edited message").into(),
561 )
562 .event_id(event_id!("$ida"))
563 .sender(other_user_id)
564 .into_raw_sync();
565
566 assert!(marks_as_unread(&ev, user_id).not());
567 }
568
569 #[test]
570 fn test_redaction_doesnt_mark_room_as_unread() {
571 let user_id = user_id!("@alice:example.org");
572 let other_user_id = user_id!("@bob:example.org");
573
574 let ev = EventFactory::new()
576 .redaction(event_id!("$151957878228ssqrj:localhost"))
577 .sender(other_user_id)
578 .event_id(event_id!("$151957878228ssqrJ:localhost"))
579 .into_raw_sync();
580
581 assert!(marks_as_unread(&ev, user_id).not());
582 }
583
584 #[test]
585 fn test_reaction_doesnt_mark_room_as_unread() {
586 let user_id = user_id!("@alice:example.org");
587 let other_user_id = user_id!("@bob:example.org");
588
589 let ev = EventFactory::new()
591 .reaction(event_id!("$15275047031IXQRj:localhost"), "👍")
592 .sender(other_user_id)
593 .event_id(event_id!("$15275047031IXQRi:localhost"))
594 .into_raw_sync();
595
596 assert!(marks_as_unread(&ev, user_id).not());
597 }
598
599 #[test]
600 fn test_state_event_doesnt_mark_as_unread() {
601 let user_id = user_id!("@alice:example.org");
602 let event_id = event_id!("$1");
603
604 let ev = EventFactory::new()
605 .member(user_id)
606 .membership(MembershipState::Join)
607 .display_name("Alice")
608 .event_id(event_id)
609 .into_raw_sync();
610 assert!(marks_as_unread(&ev, user_id).not());
611
612 let other_user_id = user_id!("@bob:example.org");
613 assert!(marks_as_unread(&ev, other_user_id).not());
614 }
615
616 #[test]
617 fn test_count_unread_and_mentions() {
618 fn make_event(user_id: &UserId, push_actions: Vec<Action>) -> TimelineEvent {
619 let mut ev = EventFactory::new()
620 .text_msg("A")
621 .sender(user_id)
622 .event_id(event_id!("$ida"))
623 .into_event();
624 ev.set_push_actions(push_actions);
625 ev
626 }
627
628 let user_id = user_id!("@alice:example.org");
629 let threading_support = false;
630
631 let event = make_event(user_id, Vec::new());
633 let mut receipts = RoomReadReceipts::default();
634 receipts.process_event(&event, user_id, threading_support);
635 assert_eq!(receipts.num_unread, 0);
636 assert_eq!(receipts.num_mentions, 0);
637 assert_eq!(receipts.num_notifications, 0);
638
639 let event = make_event(user_id!("@bob:example.org"), Vec::new());
641 let mut receipts = RoomReadReceipts::default();
642 receipts.process_event(&event, user_id, threading_support);
643 assert_eq!(receipts.num_unread, 1);
644 assert_eq!(receipts.num_mentions, 0);
645 assert_eq!(receipts.num_notifications, 0);
646
647 let event = make_event(user_id!("@bob:example.org"), vec![Action::Notify]);
649 let mut receipts = RoomReadReceipts::default();
650 receipts.process_event(&event, user_id, threading_support);
651 assert_eq!(receipts.num_unread, 1);
652 assert_eq!(receipts.num_mentions, 0);
653 assert_eq!(receipts.num_notifications, 1);
654
655 let event = make_event(
656 user_id!("@bob:example.org"),
657 vec![Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes))],
658 );
659 let mut receipts = RoomReadReceipts::default();
660 receipts.process_event(&event, user_id, threading_support);
661 assert_eq!(receipts.num_unread, 1);
662 assert_eq!(receipts.num_mentions, 1);
663 assert_eq!(receipts.num_notifications, 0);
664
665 let event = make_event(
666 user_id!("@bob:example.org"),
667 vec![Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)), Action::Notify],
668 );
669 let mut receipts = RoomReadReceipts::default();
670 receipts.process_event(&event, user_id, threading_support);
671 assert_eq!(receipts.num_unread, 1);
672 assert_eq!(receipts.num_mentions, 1);
673 assert_eq!(receipts.num_notifications, 1);
674
675 let event = make_event(user_id!("@bob:example.org"), vec![Action::Notify, Action::Notify]);
678 let mut receipts = RoomReadReceipts::default();
679 receipts.process_event(&event, user_id, threading_support);
680 assert_eq!(receipts.num_unread, 1);
681 assert_eq!(receipts.num_mentions, 0);
682 assert_eq!(receipts.num_notifications, 1);
683 }
684
685 #[test]
686 fn test_find_and_process_events() {
687 let ev0 = event_id!("$0");
688 let user_id = user_id!("@alice:example.org");
689 let thread_support = false;
690
691 let mut receipts = RoomReadReceipts::default();
694 assert!(receipts.find_and_process_events(ev0, user_id, &[], thread_support).not());
695 assert_eq!(receipts.num_unread, 0);
696 assert_eq!(receipts.num_notifications, 0);
697 assert_eq!(receipts.num_mentions, 0);
698
699 fn make_event(event_id: &EventId) -> TimelineEvent {
702 EventFactory::new()
703 .text_msg("A")
704 .sender(user_id!("@bob:example.org"))
705 .event_id(event_id)
706 .into()
707 }
708
709 let mut receipts = RoomReadReceipts {
710 num_unread: 42,
711 num_notifications: 13,
712 num_mentions: 37,
713 ..Default::default()
714 };
715 assert!(
716 receipts
717 .find_and_process_events(
718 ev0,
719 user_id,
720 &[make_event(event_id!("$1"))],
721 thread_support
722 )
723 .not()
724 );
725 assert_eq!(receipts.num_unread, 42);
726 assert_eq!(receipts.num_notifications, 13);
727 assert_eq!(receipts.num_mentions, 37);
728
729 let mut receipts = RoomReadReceipts {
733 num_unread: 42,
734 num_notifications: 13,
735 num_mentions: 37,
736 ..Default::default()
737 };
738 assert!(receipts.find_and_process_events(ev0, user_id, &[make_event(ev0)], thread_support),);
739 assert_eq!(receipts.num_unread, 0);
740 assert_eq!(receipts.num_notifications, 0);
741 assert_eq!(receipts.num_mentions, 0);
742
743 let mut receipts = RoomReadReceipts {
746 num_unread: 42,
747 num_notifications: 13,
748 num_mentions: 37,
749 ..Default::default()
750 };
751 assert!(
752 receipts
753 .find_and_process_events(
754 ev0,
755 user_id,
756 &[
757 make_event(event_id!("$1")),
758 make_event(event_id!("$2")),
759 make_event(event_id!("$3"))
760 ],
761 thread_support
762 )
763 .not()
764 );
765 assert_eq!(receipts.num_unread, 42);
766 assert_eq!(receipts.num_notifications, 13);
767 assert_eq!(receipts.num_mentions, 37);
768
769 let mut receipts = RoomReadReceipts {
772 num_unread: 42,
773 num_notifications: 13,
774 num_mentions: 37,
775 ..Default::default()
776 };
777 assert!(receipts.find_and_process_events(
778 ev0,
779 user_id,
780 &[
781 make_event(event_id!("$1")),
782 make_event(ev0),
783 make_event(event_id!("$2")),
784 make_event(event_id!("$3"))
785 ],
786 thread_support
787 ));
788 assert_eq!(receipts.num_unread, 2);
789 assert_eq!(receipts.num_notifications, 0);
790 assert_eq!(receipts.num_mentions, 0);
791
792 let mut receipts = RoomReadReceipts {
794 num_unread: 42,
795 num_notifications: 13,
796 num_mentions: 37,
797 ..Default::default()
798 };
799 assert!(receipts.find_and_process_events(
800 ev0,
801 user_id,
802 &[
803 make_event(ev0),
804 make_event(event_id!("$1")),
805 make_event(ev0),
806 make_event(event_id!("$2")),
807 make_event(event_id!("$3"))
808 ],
809 thread_support
810 ));
811 assert_eq!(receipts.num_unread, 2);
812 assert_eq!(receipts.num_notifications, 0);
813 assert_eq!(receipts.num_mentions, 0);
814 }
815
816 #[test]
817 fn test_compute_unread_counts_with_threading_enabled() {
818 fn make_event(user_id: &UserId, thread_root: &EventId) -> TimelineEvent {
819 EventFactory::new()
820 .text_msg("A")
821 .sender(user_id)
822 .event_id(event_id!("$ida"))
823 .in_thread(thread_root, event_id!("$latest_event"))
824 .into_event()
825 }
826
827 let mut receipts = RoomReadReceipts::default();
828
829 let own_alice = user_id!("@alice:example.org");
830 let bob = user_id!("@bob:example.org");
831
832 let threading_support = true;
833
834 receipts.process_event(
837 &make_event(own_alice, event_id!("$some_thread_root")),
838 own_alice,
839 threading_support,
840 );
841 receipts.process_event(
842 &make_event(own_alice, event_id!("$some_other_thread_root")),
843 own_alice,
844 threading_support,
845 );
846
847 receipts.process_event(
848 &make_event(bob, event_id!("$some_thread_root")),
849 own_alice,
850 threading_support,
851 );
852 receipts.process_event(
853 &make_event(bob, event_id!("$some_other_thread_root")),
854 own_alice,
855 threading_support,
856 );
857
858 assert_eq!(receipts.num_unread, 0);
859 assert_eq!(receipts.num_mentions, 0);
860 assert_eq!(receipts.num_notifications, 0);
861
862 receipts.process_event(
864 &EventFactory::new().text_msg("A").sender(bob).event_id(event_id!("$ida")).into_event(),
865 own_alice,
866 threading_support,
867 );
868
869 assert_eq!(receipts.num_unread, 1);
870 assert_eq!(receipts.num_mentions, 0);
871 assert_eq!(receipts.num_notifications, 0);
872 }
873
874 #[test]
875 fn test_select_best_receipt_noop() {
876 let room_id = room_id!("!roomid:example.org");
877 let f = EventFactory::new().room(room_id).sender(*ALICE);
878
879 let mut lc = EventLinkedChunk::new();
881 lc.push_events(vec![
882 f.text_msg("Event 1").event_id(event_id!("$1")).into_event(),
883 f.text_msg("Event 2").event_id(event_id!("$2")).into_event(),
884 f.text_msg("Event 3").event_id(event_id!("$3")).into_event(),
885 ]);
886
887 let own_user_id = user_id!("@not_alice:example.org");
888
889 let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
891 let new_receipt_event = None;
893 let active_receipt = None;
895 let with_threading_support = false;
896
897 let receipt = select_best_receipt(
899 own_user_id,
900 &lc,
901 &mut pending_receipts,
902 new_receipt_event,
903 active_receipt,
904 with_threading_support,
905 );
906 assert!(receipt.is_none());
907 assert!(pending_receipts.is_empty());
909 }
910
911 #[test]
912 fn test_select_best_receipt_implicit() {
913 let room_id = room_id!("!roomid:example.org");
914 let f = EventFactory::new().room(room_id).sender(*ALICE);
915 let own_user_id = user_id!("@not_alice:example.org");
916
917 let mut lc = EventLinkedChunk::new();
920 lc.push_events(vec![
921 f.text_msg("Event 1").event_id(event_id!("$1")).into_event(),
922 f.text_msg("Event 2").event_id(event_id!("$2")).sender(own_user_id).into_event(),
923 f.text_msg("Event 3").event_id(event_id!("$3")).into_event(),
924 ]);
925
926 let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
928 let new_receipt_event = None;
930 let active_receipt = None;
932 let with_threading_support = false;
933
934 let receipt = select_best_receipt(
936 own_user_id,
937 &lc,
938 &mut pending_receipts,
939 new_receipt_event,
940 active_receipt,
941 with_threading_support,
942 );
943 assert_eq!(receipt.unwrap(), event_id!("$2"));
944 assert!(pending_receipts.is_empty());
946 }
947
948 #[test]
949 fn test_select_best_receipt_active_receipt() {
950 let room_id = room_id!("!roomid:example.org");
951 let f = EventFactory::new().room(room_id).sender(*ALICE);
952
953 let mut lc = EventLinkedChunk::new();
955 lc.push_events(vec![
956 f.text_msg("Event 1").event_id(event_id!("$1")).into_event(),
957 f.text_msg("Event 2").event_id(event_id!("$2")).into_event(),
958 f.text_msg("Event 3").event_id(event_id!("$3")).into_event(),
959 ]);
960
961 let own_user_id = user_id!("@not_alice:example.org");
962
963 let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
965 let new_receipt_event = None;
967 let active_receipt = Some(event_id!("$2"));
969 let with_threading_support = false;
970
971 let receipt = select_best_receipt(
973 own_user_id,
974 &lc,
975 &mut pending_receipts,
976 new_receipt_event,
977 active_receipt,
978 with_threading_support,
979 );
980 assert_eq!(receipt.unwrap(), event_id!("$2"));
981 assert!(pending_receipts.is_empty());
983 }
984
985 #[test]
986 fn test_select_best_receipt_new_receipt_event() {
987 let room_id = room_id!("!roomid:example.org");
988 let f = EventFactory::new().room(room_id).sender(*ALICE);
989 let own_user_id = user_id!("@not_alice:example.org");
990
991 let mut lc = EventLinkedChunk::new();
993 lc.push_events(vec![
994 f.text_msg("Event 1").event_id(event_id!("$1")).into_event(),
995 f.text_msg("Event 2").event_id(event_id!("$2")).into_event(),
996 f.text_msg("Event 3").event_id(event_id!("$3")).into_event(),
997 ]);
998
999 let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
1001
1002 let new_receipt_event = Some(
1004 f.read_receipts()
1005 .add(event_id!("$2"), own_user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
1006 .into_content(),
1007 );
1008
1009 let active_receipt = None;
1011 let with_threading_support = false;
1012
1013 let receipt = select_best_receipt(
1015 own_user_id,
1016 &lc,
1017 &mut pending_receipts,
1018 new_receipt_event.as_ref(),
1019 active_receipt,
1020 with_threading_support,
1021 );
1022 assert_eq!(receipt.unwrap(), event_id!("$2"));
1023 assert!(pending_receipts.is_empty());
1025 }
1026
1027 #[test]
1028 fn test_select_best_receipt_stashes_pending_receipts() {
1029 let room_id = room_id!("!roomid:example.org");
1030 let f = EventFactory::new().room(room_id).sender(*ALICE);
1031 let own_user_id = user_id!("@not_alice:example.org");
1032
1033 let mut lc = EventLinkedChunk::new();
1035 lc.push_events(vec![
1036 f.text_msg("Event 1").event_id(event_id!("$1")).into_event(),
1037 f.text_msg("Event 2").event_id(event_id!("$2")).into_event(),
1038 f.text_msg("Event 3").event_id(event_id!("$3")).into_event(),
1039 ]);
1040
1041 let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
1043
1044 let new_receipt_event = Some(
1046 f.read_receipts()
1047 .add(event_id!("$4"), own_user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
1048 .into_content(),
1049 );
1050
1051 let active_receipt = None;
1053 let with_threading_support = false;
1054
1055 let receipt = select_best_receipt(
1057 own_user_id,
1058 &lc,
1059 &mut pending_receipts,
1060 new_receipt_event.as_ref(),
1061 active_receipt,
1062 with_threading_support,
1063 );
1064
1065 assert!(receipt.is_none());
1066 assert_eq!(pending_receipts.len(), 1);
1068 assert_eq!(pending_receipts.get(0).unwrap(), event_id!("$4"));
1069 }
1070
1071 #[test]
1072 fn test_select_best_receipt_matched_pending_receipt() {
1073 let room_id = room_id!("!roomid:example.org");
1074 let f = EventFactory::new().room(room_id).sender(*ALICE);
1075 let own_user_id = user_id!("@not_alice:example.org");
1076
1077 let mut lc = EventLinkedChunk::new();
1079 lc.push_events(vec![
1080 f.text_msg("Event 1").event_id(event_id!("$1")).into_event(),
1081 f.text_msg("Event 2").event_id(event_id!("$2")).into_event(),
1082 f.text_msg("Event 3").event_id(event_id!("$3")).into_event(),
1083 ]);
1084
1085 let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
1087 pending_receipts.push(owned_event_id!("$2"));
1088
1089 let new_receipt_event = None;
1091
1092 let active_receipt = None;
1094 let with_threading_support = false;
1095
1096 let receipt = select_best_receipt(
1098 own_user_id,
1099 &lc,
1100 &mut pending_receipts,
1101 new_receipt_event.as_ref(),
1102 active_receipt,
1103 with_threading_support,
1104 );
1105 assert_eq!(receipt.unwrap(), event_id!("$2"));
1106 assert!(pending_receipts.is_empty());
1108 }
1109
1110 #[test]
1111 fn test_select_best_receipt_mixed() {
1112 let room_id = room_id!("!roomid:example.org");
1113 let f = EventFactory::new().room(room_id).sender(*ALICE);
1114 let own_user_id = user_id!("@not_alice:example.org");
1115
1116 let mut lc = EventLinkedChunk::new();
1119 lc.push_events(vec![
1120 f.text_msg("Event 1").event_id(event_id!("$1")).into_event(),
1121 f.text_msg("Event 2").event_id(event_id!("$2")).into_event(),
1122 f.text_msg("Event 3").event_id(event_id!("$3")).sender(own_user_id).into_event(),
1123 f.text_msg("Event 4").event_id(event_id!("$4")).into_event(),
1124 f.text_msg("Event 5").event_id(event_id!("$5")).into_event(),
1125 ]);
1126
1127 let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
1129 pending_receipts.push(owned_event_id!("$2"));
1130 pending_receipts.push(owned_event_id!("$6"));
1131
1132 let new_receipt_event = Some(
1134 f.read_receipts()
1135 .add(event_id!("$4"), own_user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
1136 .add(event_id!("$7"), own_user_id, ReceiptType::ReadPrivate, ReceiptThread::Main)
1137 .into_content(),
1138 );
1139
1140 let active_receipt = Some(event_id!("$1"));
1142
1143 let with_threading_support = false;
1144
1145 let receipt = select_best_receipt(
1148 own_user_id,
1149 &lc,
1150 &mut pending_receipts,
1151 new_receipt_event.as_ref(),
1152 active_receipt,
1153 with_threading_support,
1154 );
1155 assert_eq!(receipt.unwrap(), event_id!("$4"));
1156
1157 assert_eq!(pending_receipts.len(), 2);
1160 assert!(pending_receipts.iter().any(|ev| ev == event_id!("$6")));
1161 assert!(pending_receipts.iter().any(|ev| ev == event_id!("$7")));
1162 }
1163}