use matrix_sdk_base::{
read_receipts::{LatestReadReceipt, RoomReadReceipts},
serde_helpers::extract_relation,
store::DynStateStore,
};
use matrix_sdk_common::{
deserialized_responses::TimelineEvent, ring_buffer::RingBuffer,
serde_helpers::extract_thread_root,
};
use ruma::{
EventId, OwnedEventId, OwnedUserId, RoomId, UserId,
events::{
AnySyncTimelineEvent, MessageLikeEventType,
receipt::{ReceiptEventContent, ReceiptThread, ReceiptType},
relation::RelationType,
},
serde::Raw,
};
use tracing::{debug, instrument, trace, warn};
use crate::event_cache::{
automatic_pagination::AutomaticPagination, caches::event_linked_chunk::EventLinkedChunk,
};
trait RoomReadReceiptsExt {
fn process_event(
&mut self,
event: &TimelineEvent,
user_id: &UserId,
with_threading_support: bool,
);
fn reset(&mut self);
fn find_and_process_events<'a>(
&mut self,
receipt_event_id: &EventId,
user_id: &UserId,
events: impl IntoIterator<Item = &'a TimelineEvent>,
with_threading_support: bool,
) -> bool;
}
impl RoomReadReceiptsExt for RoomReadReceipts {
#[inline(always)]
fn process_event(
&mut self,
event: &TimelineEvent,
user_id: &UserId,
with_threading_support: bool,
) {
if with_threading_support && extract_thread_root(event.raw()).is_some() {
return;
}
if marks_as_unread(event.raw(), user_id) {
self.num_unread += 1;
}
let mut has_notify = false;
let mut has_mention = false;
let Some(actions) = event.push_actions() else {
return;
};
for action in actions.iter() {
if !has_notify && action.should_notify() {
self.num_notifications += 1;
has_notify = true;
}
if !has_mention && action.is_highlight() {
self.num_mentions += 1;
has_mention = true;
}
}
}
#[inline(always)]
fn reset(&mut self) {
self.num_unread = 0;
self.num_notifications = 0;
self.num_mentions = 0;
}
#[instrument(skip_all)]
fn find_and_process_events<'a>(
&mut self,
receipt_event_id: &EventId,
user_id: &UserId,
events: impl IntoIterator<Item = &'a TimelineEvent>,
with_threading_support: bool,
) -> bool {
let mut counting_receipts = false;
for event in events {
if let Some(event_id) = event.event_id()
&& event_id == receipt_event_id
{
trace!("Found the event the receipt was referring to! Starting to count.");
self.reset();
counting_receipts = true;
continue;
}
if counting_receipts {
self.process_event(event, user_id, with_threading_support);
}
}
counting_receipts
}
}
const ALL_RECEIPT_TYPES: [ReceiptType; 2] = [ReceiptType::ReadPrivate, ReceiptType::Read];
fn select_best_receipt(
user_id: &UserId,
linked_chunk: &EventLinkedChunk,
pending_receipts: &mut RingBuffer<OwnedEventId>,
new_receipt_event: Option<&ReceiptEventContent>,
latest_active: Option<&EventId>,
with_threading_support: bool,
) -> Option<OwnedEventId> {
if let Some(receipt_event) = new_receipt_event {
for (event_id, receipts) in &receipt_event.0 {
for ty in ALL_RECEIPT_TYPES {
if let Some(receipts) = receipts.get(&ty)
&& let Some(receipt) = receipts.get(user_id)
&& matches!(receipt.thread, ReceiptThread::Main | ReceiptThread::Unthreaded)
{
trace!(%event_id, "found new receipt (added to pending)");
pending_receipts.push(event_id.clone());
}
}
}
}
let mut receipt = None;
for (event, event_id) in
linked_chunk.revents().filter_map(|(_pos, ev)| Some((ev, ev.event_id()?)))
{
if receipt.is_none() {
if latest_active == Some(&event_id) {
trace!(active = %event_id, "the latest active receipt is still the most recent; stopping search");
receipt = Some(event_id.clone());
}
else if event.sender().as_deref() == Some(user_id)
&& (!with_threading_support || extract_thread_root(event.raw()).is_none())
{
trace!(implicit = %event_id, "found an implicit receipt; stopping search");
receipt = Some(event_id.clone());
}
}
if receipt.is_some() && pending_receipts.is_empty() {
trace!("exiting loop; found a better receipt, and no more pending receipt to match");
break;
}
pending_receipts.retain(|pending| {
if *pending == event_id {
if receipt.is_none() {
trace!(pending = %event_id, "found a pending receipt; stopping search");
receipt = Some(event_id.clone());
} else {
trace!(%event_id, "discarding a pending receipt that wasn't selected");
}
false
} else {
true
}
});
}
receipt
}
async fn try_find_store_receipts(
store: &DynStateStore,
user_id: &UserId,
room_id: &RoomId,
read_receipts: &mut RoomReadReceipts,
) {
for receipt_type in ALL_RECEIPT_TYPES {
for receipt_thread in [ReceiptThread::Unthreaded, ReceiptThread::Main] {
if let Ok(Some((event_id, _receipt))) = store
.get_user_room_receipt_event(
room_id,
receipt_type.clone(),
receipt_thread.clone(),
user_id,
)
.await
{
trace!(%event_id, ?receipt_type, ?receipt_thread, "Found a dormant receipt in the store");
if read_receipts.latest_active.is_none() {
read_receipts.latest_active =
Some(LatestReadReceipt { event_id: event_id.clone() });
} else {
read_receipts.pending.push(event_id.clone());
}
}
}
}
}
#[instrument(skip_all, fields(room_id = %room_id))]
#[allow(clippy::too_many_arguments)]
pub(crate) async fn compute_unread_counts(
user_id: &UserId,
room_id: &RoomId,
receipt_event: Option<&ReceiptEventContent>,
linked_chunk: &EventLinkedChunk,
read_receipts: &mut RoomReadReceipts,
with_threading_support: bool,
automatic_pagination: Option<&AutomaticPagination>,
state_store: &DynStateStore,
) {
debug!(?read_receipts, "Starting");
if read_receipts.latest_active.is_none() {
try_find_store_receipts(state_store, user_id, room_id, read_receipts).await;
}
let better_receipt = select_best_receipt(
user_id,
linked_chunk,
&mut read_receipts.pending,
receipt_event,
read_receipts.latest_active.as_ref().map(|latest_active| latest_active.event_id.as_ref()),
with_threading_support,
);
if let Some(event_id) = better_receipt {
trace!(%event_id, "Saving a new active read receipt");
read_receipts.latest_active = Some(LatestReadReceipt { event_id: event_id.clone() });
read_receipts.find_and_process_events(
&event_id,
user_id,
linked_chunk.events().map(|(_pos, event)| event),
with_threading_support,
);
debug!(?read_receipts, "after finding a better receipt");
return;
}
if let Some(automatic_pagination) = automatic_pagination {
if automatic_pagination.run_once(room_id) {
trace!("Requested pagination to find a better receipt");
} else {
warn!("Failed to request pagination to find a better receipt");
}
}
read_receipts.reset();
for (_pos, event) in linked_chunk.events() {
read_receipts.process_event(event, user_id, with_threading_support);
}
debug!(?read_receipts, "no better receipt");
}
fn marks_as_unread(event: &Raw<AnySyncTimelineEvent>, user_id: &UserId) -> bool {
if event.get_field::<OwnedUserId>("sender").ok().flatten().as_deref() == Some(user_id) {
tracing::trace!("not interesting because sent by the current user");
return false;
}
let Some(event_type) = event.get_field::<MessageLikeEventType>("type").ok().flatten() else {
tracing::trace!(
"failed to parse event type for event with id {:?}, skipping it",
event.get_field::<OwnedEventId>("event_id").ok().flatten()
);
return false;
};
match event_type {
MessageLikeEventType::Message
| MessageLikeEventType::PollStart
| MessageLikeEventType::UnstablePollStart
| MessageLikeEventType::PollEnd
| MessageLikeEventType::UnstablePollEnd
| MessageLikeEventType::RoomEncrypted
| MessageLikeEventType::RoomMessage
| MessageLikeEventType::Sticker => {}
_ => {
tracing::trace!("not interesting because not an interesting message-like");
return false;
}
}
if let Some((RelationType::Replacement, _)) = extract_relation(event) {
tracing::trace!("not interesting because edited");
return false;
}
#[derive(serde::Deserialize)]
struct UnsignedContent {
redacted_because: Option<Raw<AnySyncTimelineEvent>>,
}
if let Ok(Some(UnsignedContent { redacted_because: Some(_redaction) })) =
event.get_field::<UnsignedContent>("unsigned")
{
tracing::trace!("not interesting because redacted");
return false;
}
true
}
#[cfg(test)]
mod tests {
use std::{num::NonZeroUsize, ops::Not as _};
use matrix_sdk_base::read_receipts::RoomReadReceipts;
use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
use matrix_sdk_test::{ALICE, event_factory::EventFactory};
use ruma::{
EventId, UserId, event_id,
events::{
receipt::{ReceiptThread, ReceiptType},
room::{member::MembershipState, message::MessageType},
},
owned_event_id,
push::{Action, HighlightTweakValue, Tweak},
room_id, user_id,
};
use super::marks_as_unread;
use crate::event_cache::caches::{
event_linked_chunk::EventLinkedChunk,
read_receipts::{RoomReadReceiptsExt as _, select_best_receipt},
};
#[test]
fn test_room_message_marks_as_unread() {
let user_id = user_id!("@alice:example.org");
let other_user_id = user_id!("@bob:example.org");
let f = EventFactory::new();
let ev = f.text_msg("A").event_id(event_id!("$ida")).sender(other_user_id).into_raw_sync();
assert!(marks_as_unread(&ev, user_id));
let ev = f.text_msg("A").event_id(event_id!("$ida")).sender(user_id).into_raw_sync();
assert!(marks_as_unread(&ev, user_id).not());
}
#[test]
fn test_room_edit_doesnt_mark_as_unread() {
let user_id = user_id!("@alice:example.org");
let other_user_id = user_id!("@bob:example.org");
let ev = EventFactory::new()
.text_msg("* edited message")
.edit(
event_id!("$someeventid:localhost"),
MessageType::text_plain("edited message").into(),
)
.event_id(event_id!("$ida"))
.sender(other_user_id)
.into_raw_sync();
assert!(marks_as_unread(&ev, user_id).not());
}
#[test]
fn test_redaction_doesnt_mark_room_as_unread() {
let user_id = user_id!("@alice:example.org");
let other_user_id = user_id!("@bob:example.org");
let ev = EventFactory::new()
.redaction(event_id!("$151957878228ssqrj:localhost"))
.sender(other_user_id)
.event_id(event_id!("$151957878228ssqrJ:localhost"))
.into_raw_sync();
assert!(marks_as_unread(&ev, user_id).not());
}
#[test]
fn test_reaction_doesnt_mark_room_as_unread() {
let user_id = user_id!("@alice:example.org");
let other_user_id = user_id!("@bob:example.org");
let ev = EventFactory::new()
.reaction(event_id!("$15275047031IXQRj:localhost"), "👍")
.sender(other_user_id)
.event_id(event_id!("$15275047031IXQRi:localhost"))
.into_raw_sync();
assert!(marks_as_unread(&ev, user_id).not());
}
#[test]
fn test_state_event_doesnt_mark_as_unread() {
let user_id = user_id!("@alice:example.org");
let event_id = event_id!("$1");
let ev = EventFactory::new()
.member(user_id)
.membership(MembershipState::Join)
.display_name("Alice")
.event_id(event_id)
.into_raw_sync();
assert!(marks_as_unread(&ev, user_id).not());
let other_user_id = user_id!("@bob:example.org");
assert!(marks_as_unread(&ev, other_user_id).not());
}
#[test]
fn test_count_unread_and_mentions() {
fn make_event(user_id: &UserId, push_actions: Vec<Action>) -> TimelineEvent {
let mut ev = EventFactory::new()
.text_msg("A")
.sender(user_id)
.event_id(event_id!("$ida"))
.into_event();
ev.set_push_actions(push_actions);
ev
}
let user_id = user_id!("@alice:example.org");
let threading_support = false;
let event = make_event(user_id, Vec::new());
let mut receipts = RoomReadReceipts::default();
receipts.process_event(&event, user_id, threading_support);
assert_eq!(receipts.num_unread, 0);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 0);
let event = make_event(user_id!("@bob:example.org"), Vec::new());
let mut receipts = RoomReadReceipts::default();
receipts.process_event(&event, user_id, threading_support);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 0);
let event = make_event(user_id!("@bob:example.org"), vec![Action::Notify]);
let mut receipts = RoomReadReceipts::default();
receipts.process_event(&event, user_id, threading_support);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 1);
let event = make_event(
user_id!("@bob:example.org"),
vec![Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes))],
);
let mut receipts = RoomReadReceipts::default();
receipts.process_event(&event, user_id, threading_support);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 1);
assert_eq!(receipts.num_notifications, 0);
let event = make_event(
user_id!("@bob:example.org"),
vec![Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)), Action::Notify],
);
let mut receipts = RoomReadReceipts::default();
receipts.process_event(&event, user_id, threading_support);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 1);
assert_eq!(receipts.num_notifications, 1);
let event = make_event(user_id!("@bob:example.org"), vec![Action::Notify, Action::Notify]);
let mut receipts = RoomReadReceipts::default();
receipts.process_event(&event, user_id, threading_support);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 1);
}
#[test]
fn test_find_and_process_events() {
let ev0 = event_id!("$0");
let user_id = user_id!("@alice:example.org");
let thread_support = false;
let mut receipts = RoomReadReceipts::default();
assert!(receipts.find_and_process_events(ev0, user_id, &[], thread_support).not());
assert_eq!(receipts.num_unread, 0);
assert_eq!(receipts.num_notifications, 0);
assert_eq!(receipts.num_mentions, 0);
fn make_event(event_id: &EventId) -> TimelineEvent {
EventFactory::new()
.text_msg("A")
.sender(user_id!("@bob:example.org"))
.event_id(event_id)
.into()
}
let mut receipts = RoomReadReceipts {
num_unread: 42,
num_notifications: 13,
num_mentions: 37,
..Default::default()
};
assert!(
receipts
.find_and_process_events(
ev0,
user_id,
&[make_event(event_id!("$1"))],
thread_support
)
.not()
);
assert_eq!(receipts.num_unread, 42);
assert_eq!(receipts.num_notifications, 13);
assert_eq!(receipts.num_mentions, 37);
let mut receipts = RoomReadReceipts {
num_unread: 42,
num_notifications: 13,
num_mentions: 37,
..Default::default()
};
assert!(receipts.find_and_process_events(ev0, user_id, &[make_event(ev0)], thread_support),);
assert_eq!(receipts.num_unread, 0);
assert_eq!(receipts.num_notifications, 0);
assert_eq!(receipts.num_mentions, 0);
let mut receipts = RoomReadReceipts {
num_unread: 42,
num_notifications: 13,
num_mentions: 37,
..Default::default()
};
assert!(
receipts
.find_and_process_events(
ev0,
user_id,
&[
make_event(event_id!("$1")),
make_event(event_id!("$2")),
make_event(event_id!("$3"))
],
thread_support
)
.not()
);
assert_eq!(receipts.num_unread, 42);
assert_eq!(receipts.num_notifications, 13);
assert_eq!(receipts.num_mentions, 37);
let mut receipts = RoomReadReceipts {
num_unread: 42,
num_notifications: 13,
num_mentions: 37,
..Default::default()
};
assert!(receipts.find_and_process_events(
ev0,
user_id,
&[
make_event(event_id!("$1")),
make_event(ev0),
make_event(event_id!("$2")),
make_event(event_id!("$3"))
],
thread_support
));
assert_eq!(receipts.num_unread, 2);
assert_eq!(receipts.num_notifications, 0);
assert_eq!(receipts.num_mentions, 0);
let mut receipts = RoomReadReceipts {
num_unread: 42,
num_notifications: 13,
num_mentions: 37,
..Default::default()
};
assert!(receipts.find_and_process_events(
ev0,
user_id,
&[
make_event(ev0),
make_event(event_id!("$1")),
make_event(ev0),
make_event(event_id!("$2")),
make_event(event_id!("$3"))
],
thread_support
));
assert_eq!(receipts.num_unread, 2);
assert_eq!(receipts.num_notifications, 0);
assert_eq!(receipts.num_mentions, 0);
}
#[test]
fn test_compute_unread_counts_with_threading_enabled() {
fn make_event(user_id: &UserId, thread_root: &EventId) -> TimelineEvent {
EventFactory::new()
.text_msg("A")
.sender(user_id)
.event_id(event_id!("$ida"))
.in_thread(thread_root, event_id!("$latest_event"))
.into_event()
}
let mut receipts = RoomReadReceipts::default();
let own_alice = user_id!("@alice:example.org");
let bob = user_id!("@bob:example.org");
let threading_support = true;
receipts.process_event(
&make_event(own_alice, event_id!("$some_thread_root")),
own_alice,
threading_support,
);
receipts.process_event(
&make_event(own_alice, event_id!("$some_other_thread_root")),
own_alice,
threading_support,
);
receipts.process_event(
&make_event(bob, event_id!("$some_thread_root")),
own_alice,
threading_support,
);
receipts.process_event(
&make_event(bob, event_id!("$some_other_thread_root")),
own_alice,
threading_support,
);
assert_eq!(receipts.num_unread, 0);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 0);
receipts.process_event(
&EventFactory::new().text_msg("A").sender(bob).event_id(event_id!("$ida")).into_event(),
own_alice,
threading_support,
);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 0);
}
#[test]
fn test_select_best_receipt_noop() {
let room_id = room_id!("!roomid:example.org");
let f = EventFactory::new().room(room_id).sender(*ALICE);
let mut lc = EventLinkedChunk::new();
lc.push_events(vec![
f.text_msg("Event 1").event_id(event_id!("$1")).into_event(),
f.text_msg("Event 2").event_id(event_id!("$2")).into_event(),
f.text_msg("Event 3").event_id(event_id!("$3")).into_event(),
]);
let own_user_id = user_id!("@not_alice:example.org");
let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
let new_receipt_event = None;
let active_receipt = None;
let with_threading_support = false;
let receipt = select_best_receipt(
own_user_id,
&lc,
&mut pending_receipts,
new_receipt_event,
active_receipt,
with_threading_support,
);
assert!(receipt.is_none());
assert!(pending_receipts.is_empty());
}
#[test]
fn test_select_best_receipt_implicit() {
let room_id = room_id!("!roomid:example.org");
let f = EventFactory::new().room(room_id).sender(*ALICE);
let own_user_id = user_id!("@not_alice:example.org");
let mut lc = EventLinkedChunk::new();
lc.push_events(vec![
f.text_msg("Event 1").event_id(event_id!("$1")).into_event(),
f.text_msg("Event 2").event_id(event_id!("$2")).sender(own_user_id).into_event(),
f.text_msg("Event 3").event_id(event_id!("$3")).into_event(),
]);
let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
let new_receipt_event = None;
let active_receipt = None;
let with_threading_support = false;
let receipt = select_best_receipt(
own_user_id,
&lc,
&mut pending_receipts,
new_receipt_event,
active_receipt,
with_threading_support,
);
assert_eq!(receipt.unwrap(), event_id!("$2"));
assert!(pending_receipts.is_empty());
}
#[test]
fn test_select_best_receipt_active_receipt() {
let room_id = room_id!("!roomid:example.org");
let f = EventFactory::new().room(room_id).sender(*ALICE);
let mut lc = EventLinkedChunk::new();
lc.push_events(vec![
f.text_msg("Event 1").event_id(event_id!("$1")).into_event(),
f.text_msg("Event 2").event_id(event_id!("$2")).into_event(),
f.text_msg("Event 3").event_id(event_id!("$3")).into_event(),
]);
let own_user_id = user_id!("@not_alice:example.org");
let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
let new_receipt_event = None;
let active_receipt = Some(event_id!("$2"));
let with_threading_support = false;
let receipt = select_best_receipt(
own_user_id,
&lc,
&mut pending_receipts,
new_receipt_event,
active_receipt,
with_threading_support,
);
assert_eq!(receipt.unwrap(), event_id!("$2"));
assert!(pending_receipts.is_empty());
}
#[test]
fn test_select_best_receipt_new_receipt_event() {
let room_id = room_id!("!roomid:example.org");
let f = EventFactory::new().room(room_id).sender(*ALICE);
let own_user_id = user_id!("@not_alice:example.org");
let mut lc = EventLinkedChunk::new();
lc.push_events(vec![
f.text_msg("Event 1").event_id(event_id!("$1")).into_event(),
f.text_msg("Event 2").event_id(event_id!("$2")).into_event(),
f.text_msg("Event 3").event_id(event_id!("$3")).into_event(),
]);
let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
let new_receipt_event = Some(
f.read_receipts()
.add(event_id!("$2"), own_user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
.into_content(),
);
let active_receipt = None;
let with_threading_support = false;
let receipt = select_best_receipt(
own_user_id,
&lc,
&mut pending_receipts,
new_receipt_event.as_ref(),
active_receipt,
with_threading_support,
);
assert_eq!(receipt.unwrap(), event_id!("$2"));
assert!(pending_receipts.is_empty());
}
#[test]
fn test_select_best_receipt_stashes_pending_receipts() {
let room_id = room_id!("!roomid:example.org");
let f = EventFactory::new().room(room_id).sender(*ALICE);
let own_user_id = user_id!("@not_alice:example.org");
let mut lc = EventLinkedChunk::new();
lc.push_events(vec![
f.text_msg("Event 1").event_id(event_id!("$1")).into_event(),
f.text_msg("Event 2").event_id(event_id!("$2")).into_event(),
f.text_msg("Event 3").event_id(event_id!("$3")).into_event(),
]);
let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
let new_receipt_event = Some(
f.read_receipts()
.add(event_id!("$4"), own_user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
.into_content(),
);
let active_receipt = None;
let with_threading_support = false;
let receipt = select_best_receipt(
own_user_id,
&lc,
&mut pending_receipts,
new_receipt_event.as_ref(),
active_receipt,
with_threading_support,
);
assert!(receipt.is_none());
assert_eq!(pending_receipts.len(), 1);
assert_eq!(pending_receipts.get(0).unwrap(), event_id!("$4"));
}
#[test]
fn test_select_best_receipt_matched_pending_receipt() {
let room_id = room_id!("!roomid:example.org");
let f = EventFactory::new().room(room_id).sender(*ALICE);
let own_user_id = user_id!("@not_alice:example.org");
let mut lc = EventLinkedChunk::new();
lc.push_events(vec![
f.text_msg("Event 1").event_id(event_id!("$1")).into_event(),
f.text_msg("Event 2").event_id(event_id!("$2")).into_event(),
f.text_msg("Event 3").event_id(event_id!("$3")).into_event(),
]);
let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
pending_receipts.push(owned_event_id!("$2"));
let new_receipt_event = None;
let active_receipt = None;
let with_threading_support = false;
let receipt = select_best_receipt(
own_user_id,
&lc,
&mut pending_receipts,
new_receipt_event.as_ref(),
active_receipt,
with_threading_support,
);
assert_eq!(receipt.unwrap(), event_id!("$2"));
assert!(pending_receipts.is_empty());
}
#[test]
fn test_select_best_receipt_mixed() {
let room_id = room_id!("!roomid:example.org");
let f = EventFactory::new().room(room_id).sender(*ALICE);
let own_user_id = user_id!("@not_alice:example.org");
let mut lc = EventLinkedChunk::new();
lc.push_events(vec![
f.text_msg("Event 1").event_id(event_id!("$1")).into_event(),
f.text_msg("Event 2").event_id(event_id!("$2")).into_event(),
f.text_msg("Event 3").event_id(event_id!("$3")).sender(own_user_id).into_event(),
f.text_msg("Event 4").event_id(event_id!("$4")).into_event(),
f.text_msg("Event 5").event_id(event_id!("$5")).into_event(),
]);
let mut pending_receipts = RingBuffer::new(NonZeroUsize::new(16).unwrap());
pending_receipts.push(owned_event_id!("$2"));
pending_receipts.push(owned_event_id!("$6"));
let new_receipt_event = Some(
f.read_receipts()
.add(event_id!("$4"), own_user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
.add(event_id!("$7"), own_user_id, ReceiptType::ReadPrivate, ReceiptThread::Main)
.into_content(),
);
let active_receipt = Some(event_id!("$1"));
let with_threading_support = false;
let receipt = select_best_receipt(
own_user_id,
&lc,
&mut pending_receipts,
new_receipt_event.as_ref(),
active_receipt,
with_threading_support,
);
assert_eq!(receipt.unwrap(), event_id!("$4"));
assert_eq!(pending_receipts.len(), 2);
assert!(pending_receipts.iter().any(|ev| ev == event_id!("$6")));
assert!(pending_receipts.iter().any(|ev| ev == event_id!("$7")));
}
}