#![allow(dead_code)]
use std::{
collections::{BTreeMap, BTreeSet},
num::NonZeroUsize,
};
use matrix_sdk_common::{
deserialized_responses::TimelineEvent, ring_buffer::RingBuffer,
serde_helpers::extract_thread_root,
};
use ruma::{
EventId, OwnedEventId, OwnedUserId, RoomId, UserId,
events::{
AnySyncMessageLikeEvent, AnySyncTimelineEvent, OriginalSyncMessageLikeEvent,
SyncMessageLikeEvent,
poll::{start::PollStartEventContent, unstable_start::UnstablePollStartEventContent},
receipt::{ReceiptEventContent, ReceiptThread, ReceiptType},
room::message::Relation,
},
serde::Raw,
};
use serde::{Deserialize, Serialize};
use tracing::{debug, instrument, trace, warn};
use crate::ThreadingSupport;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
struct LatestReadReceipt {
event_id: OwnedEventId,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct RoomReadReceipts {
pub num_unread: u64,
pub num_notifications: u64,
pub num_mentions: u64,
#[serde(default)]
latest_active: Option<LatestReadReceipt>,
#[serde(default = "new_nonempty_ring_buffer")]
pending: RingBuffer<OwnedEventId>,
}
impl Default for RoomReadReceipts {
fn default() -> Self {
Self {
num_unread: Default::default(),
num_notifications: Default::default(),
num_mentions: Default::default(),
latest_active: Default::default(),
pending: new_nonempty_ring_buffer(),
}
}
}
fn new_nonempty_ring_buffer() -> RingBuffer<OwnedEventId> {
RingBuffer::new(NonZeroUsize::new(10).unwrap())
}
impl RoomReadReceipts {
#[inline(always)]
fn process_event(
&mut self,
event: &TimelineEvent,
user_id: &UserId,
threading_support: ThreadingSupport,
) {
if matches!(threading_support, ThreadingSupport::Enabled { .. })
&& 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>,
threading_support: ThreadingSupport,
) -> 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, threading_support);
}
}
counting_receipts
}
}
struct ReceiptSelector {
event_id_to_pos: BTreeMap<OwnedEventId, usize>,
latest_event_with_receipt: Option<OwnedEventId>,
latest_event_pos: Option<usize>,
}
impl ReceiptSelector {
fn new(all_events: &[TimelineEvent], latest_active_receipt_event: Option<&EventId>) -> Self {
let event_id_to_pos = Self::create_sync_index(all_events.iter());
let best_pos =
latest_active_receipt_event.and_then(|event_id| event_id_to_pos.get(event_id)).copied();
Self { latest_event_pos: best_pos, latest_event_with_receipt: None, event_id_to_pos }
}
fn create_sync_index<'a>(
events: impl Iterator<Item = &'a TimelineEvent> + 'a,
) -> BTreeMap<OwnedEventId, usize> {
BTreeMap::from_iter(
events
.enumerate()
.filter_map(|(pos, event)| event.event_id().map(|event_id| (event_id, pos))),
)
}
#[instrument(skip(self), fields(prev_pos = ?self.latest_event_pos, prev_receipt = ?self.latest_event_with_receipt))]
fn try_select_later(&mut self, event_id: &EventId, event_pos: usize) {
if let Some(best_pos) = self.latest_event_pos.as_mut() {
if event_pos >= *best_pos {
*best_pos = event_pos;
self.latest_event_with_receipt = Some(event_id.to_owned());
debug!("saving better");
} else {
trace!("not better, keeping previous");
}
} else {
self.latest_event_pos = Some(event_pos);
self.latest_event_with_receipt = Some(event_id.to_owned());
debug!("saving for the first time");
}
}
#[instrument(skip_all)]
fn handle_pending_receipts(&mut self, pending: &mut RingBuffer<OwnedEventId>) {
pending.retain(|event_id| {
if let Some(event_pos) = self.event_id_to_pos.get(event_id) {
trace!(%event_id, "matching event against its stashed receipt");
self.try_select_later(event_id, *event_pos);
false
} else {
true
}
});
}
#[instrument(skip_all)]
fn handle_new_receipt(
&mut self,
user_id: &UserId,
receipt_event: &ReceiptEventContent,
) -> Vec<OwnedEventId> {
let mut pending = Vec::new();
for (event_id, receipts) in &receipt_event.0 {
for ty in [ReceiptType::Read, ReceiptType::ReadPrivate] {
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 candidate");
if let Some(event_pos) = self.event_id_to_pos.get(event_id) {
self.try_select_later(event_id, *event_pos);
} else {
trace!(%event_id, "stashed as pending");
pending.push(event_id.clone());
}
}
}
}
pending
}
#[instrument(skip_all)]
fn try_match_implicit(&mut self, user_id: &UserId, new_events: &[TimelineEvent]) {
for ev in new_events {
let Ok(Some(sender)) = ev.raw().get_field::<OwnedUserId>("sender") else { continue };
if sender == user_id {
let Some(event_id) = ev.event_id() else { continue };
if let Some(event_pos) = self.event_id_to_pos.get(&event_id) {
trace!(%event_id, "found an implicit receipt candidate");
self.try_select_later(&event_id, *event_pos);
}
}
}
}
fn select(self) -> Option<LatestReadReceipt> {
self.latest_event_with_receipt.map(|event_id| LatestReadReceipt { event_id })
}
}
fn events_intersects<'a>(
previous_events: impl Iterator<Item = &'a TimelineEvent>,
new_events: &[TimelineEvent],
) -> bool {
let previous_events_ids = BTreeSet::from_iter(previous_events.filter_map(|ev| ev.event_id()));
new_events
.iter()
.any(|ev| ev.event_id().is_some_and(|event_id| previous_events_ids.contains(&event_id)))
}
#[instrument(skip_all, fields(room_id = %room_id))]
pub(crate) fn compute_unread_counts(
user_id: &UserId,
room_id: &RoomId,
receipt_event: Option<&ReceiptEventContent>,
mut previous_events: Vec<TimelineEvent>,
new_events: &[TimelineEvent],
read_receipts: &mut RoomReadReceipts,
threading_support: ThreadingSupport,
) {
debug!(?read_receipts, "Starting");
let all_events = if events_intersects(previous_events.iter(), new_events) {
new_events.to_owned()
} else {
previous_events.extend(new_events.iter().cloned());
previous_events
};
let new_receipt = {
let mut selector = ReceiptSelector::new(
&all_events,
read_receipts.latest_active.as_ref().map(|receipt| &*receipt.event_id),
);
selector.try_match_implicit(user_id, new_events);
selector.handle_pending_receipts(&mut read_receipts.pending);
if let Some(receipt_event) = receipt_event {
let new_pending = selector.handle_new_receipt(user_id, receipt_event);
if !new_pending.is_empty() {
read_receipts.pending.extend(new_pending);
}
}
selector.select()
};
if let Some(new_receipt) = new_receipt {
let event_id = new_receipt.event_id.clone();
trace!(%event_id, "Saving a new active read receipt");
read_receipts.latest_active = Some(new_receipt);
read_receipts.find_and_process_events(
&event_id,
user_id,
all_events.iter(),
threading_support,
);
debug!(?read_receipts, "after finding a better receipt");
return;
}
for event in new_events {
read_receipts.process_event(event, user_id, threading_support);
}
debug!(?read_receipts, "no better receipt, {} new events", new_events.len());
}
fn marks_as_unread(event: &Raw<AnySyncTimelineEvent>, user_id: &UserId) -> bool {
let event = match event.deserialize() {
Ok(event) => event,
Err(err) => {
warn!(
"couldn't deserialize event {:?}: {err}",
event.get_field::<String>("event_id").ok().flatten()
);
return false;
}
};
if event.sender() == user_id {
return false;
}
match event {
AnySyncTimelineEvent::MessageLike(event) => {
let Some(content) = event.original_content() else {
tracing::trace!("not interesting because redacted");
return false;
};
if matches!(
content.relation(),
Some(ruma::events::room::encrypted::Relation::Replacement(..))
) {
tracing::trace!("not interesting because edited");
return false;
}
match event {
AnySyncMessageLikeEvent::CallAnswer(_)
| AnySyncMessageLikeEvent::CallInvite(_)
| AnySyncMessageLikeEvent::RtcNotification(_)
| AnySyncMessageLikeEvent::CallHangup(_)
| AnySyncMessageLikeEvent::CallCandidates(_)
| AnySyncMessageLikeEvent::CallNegotiate(_)
| AnySyncMessageLikeEvent::CallReject(_)
| AnySyncMessageLikeEvent::CallSelectAnswer(_)
| AnySyncMessageLikeEvent::PollResponse(_)
| AnySyncMessageLikeEvent::UnstablePollResponse(_)
| AnySyncMessageLikeEvent::Reaction(_)
| AnySyncMessageLikeEvent::RoomRedaction(_)
| AnySyncMessageLikeEvent::KeyVerificationStart(_)
| AnySyncMessageLikeEvent::KeyVerificationReady(_)
| AnySyncMessageLikeEvent::KeyVerificationCancel(_)
| AnySyncMessageLikeEvent::KeyVerificationAccept(_)
| AnySyncMessageLikeEvent::KeyVerificationDone(_)
| AnySyncMessageLikeEvent::KeyVerificationMac(_)
| AnySyncMessageLikeEvent::KeyVerificationKey(_) => false,
AnySyncMessageLikeEvent::PollStart(SyncMessageLikeEvent::Original(
OriginalSyncMessageLikeEvent {
content:
PollStartEventContent { relates_to: Some(Relation::Replacement(_)), .. },
..
},
))
| AnySyncMessageLikeEvent::UnstablePollStart(SyncMessageLikeEvent::Original(
OriginalSyncMessageLikeEvent {
content: UnstablePollStartEventContent::Replacement(_),
..
},
)) => false,
AnySyncMessageLikeEvent::Message(_)
| AnySyncMessageLikeEvent::PollStart(_)
| AnySyncMessageLikeEvent::UnstablePollStart(_)
| AnySyncMessageLikeEvent::PollEnd(_)
| AnySyncMessageLikeEvent::UnstablePollEnd(_)
| AnySyncMessageLikeEvent::RoomEncrypted(_)
| AnySyncMessageLikeEvent::RoomMessage(_)
| AnySyncMessageLikeEvent::Sticker(_) => true,
_ => {
warn!("unhandled timeline event type: {}", event.event_type());
false
}
}
}
AnySyncTimelineEvent::State(_) => false,
}
}
#[cfg(test)]
mod tests {
use std::{num::NonZeroUsize, ops::Not as _};
use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer};
use matrix_sdk_test::event_factory::EventFactory;
use ruma::{
EventId, UserId, event_id,
events::{
receipt::{ReceiptThread, ReceiptType},
room::{member::MembershipState, message::MessageType},
},
owned_event_id, owned_user_id,
push::Action,
room_id, user_id,
};
use super::compute_unread_counts;
use crate::{
ThreadingSupport,
read_receipts::{ReceiptSelector, RoomReadReceipts, marks_as_unread},
};
#[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 event = make_event(user_id, Vec::new());
let mut receipts = RoomReadReceipts::default();
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
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, ThreadingSupport::Disabled);
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, ThreadingSupport::Disabled);
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(ruma::push::Tweak::Highlight(true))],
);
let mut receipts = RoomReadReceipts::default();
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
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(ruma::push::Tweak::Highlight(true)), Action::Notify],
);
let mut receipts = RoomReadReceipts::default();
receipts.process_event(&event, user_id, ThreadingSupport::Disabled);
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, ThreadingSupport::Disabled);
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 mut receipts = RoomReadReceipts::default();
assert!(
receipts.find_and_process_events(ev0, user_id, &[], ThreadingSupport::Disabled).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"))],
ThreadingSupport::Disabled
)
.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)],
ThreadingSupport::Disabled
),);
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"))
],
ThreadingSupport::Disabled
)
.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"))
],
ThreadingSupport::Disabled
));
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"))
],
ThreadingSupport::Disabled
));
assert_eq!(receipts.num_unread, 2);
assert_eq!(receipts.num_notifications, 0);
assert_eq!(receipts.num_mentions, 0);
}
#[test]
fn test_basic_compute_unread_counts() {
let user_id = user_id!("@alice:example.org");
let other_user_id = user_id!("@bob:example.org");
let room_id = room_id!("!room:example.org");
let receipt_event_id = event_id!("$1");
let mut previous_events = Vec::new();
let f = EventFactory::new();
let ev1 = f.text_msg("A").sender(other_user_id).event_id(receipt_event_id).into_event();
let ev2 = f.text_msg("A").sender(other_user_id).event_id(event_id!("$2")).into_event();
let receipt_event = f
.read_receipts()
.add(receipt_event_id, user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
.into_content();
let mut read_receipts = RoomReadReceipts::default();
compute_unread_counts(
user_id,
room_id,
Some(&receipt_event),
previous_events.clone(),
&[ev1.clone(), ev2.clone()],
&mut read_receipts,
ThreadingSupport::Disabled,
);
assert_eq!(read_receipts.num_unread, 1);
previous_events.push(ev1);
previous_events.push(ev2);
let new_event =
f.text_msg("A").sender(other_user_id).event_id(event_id!("$3")).into_event();
compute_unread_counts(
user_id,
room_id,
Some(&receipt_event),
previous_events,
&[new_event],
&mut read_receipts,
ThreadingSupport::Disabled,
);
assert_eq!(read_receipts.num_unread, 2);
}
fn make_test_events(user_id: &UserId) -> Vec<TimelineEvent> {
let f = EventFactory::new().sender(user_id);
let ev1 = f.text_msg("With the lights out, it's less dangerous").event_id(event_id!("$1"));
let ev2 = f.text_msg("Here we are now, entertain us").event_id(event_id!("$2"));
let ev3 = f.text_msg("I feel stupid and contagious").event_id(event_id!("$3"));
let ev4 = f.text_msg("Here we are now, entertain us").event_id(event_id!("$4"));
let ev5 = f.text_msg("Hello, hello, hello, how low?").event_id(event_id!("$5"));
[ev1, ev2, ev3, ev4, ev5].into_iter().map(Into::into).collect()
}
#[test]
fn test_compute_unread_counts_multiple_receipts_in_one_event() {
let user_id = user_id!("@alice:example.org");
let room_id = room_id!("!room:example.org");
let all_events = make_test_events(user_id!("@bob:example.org"));
let head_events: Vec<_> = all_events.iter().take(2).cloned().collect();
let tail_events: Vec<_> = all_events.iter().skip(2).cloned().collect();
let f = EventFactory::new();
for receipt_type_1 in &[ReceiptType::Read, ReceiptType::ReadPrivate] {
for receipt_thread_1 in &[ReceiptThread::Unthreaded, ReceiptThread::Main] {
for receipt_type_2 in &[ReceiptType::Read, ReceiptType::ReadPrivate] {
for receipt_thread_2 in &[ReceiptThread::Unthreaded, ReceiptThread::Main] {
let receipt_event = f
.read_receipts()
.add(
event_id!("$2"),
user_id,
receipt_type_1.clone(),
receipt_thread_1.clone(),
)
.add(
event_id!("$3"),
user_id,
receipt_type_2.clone(),
receipt_thread_2.clone(),
)
.add(
event_id!("$1"),
user_id,
receipt_type_1.clone(),
receipt_thread_2.clone(),
)
.into_content();
let mut read_receipts = RoomReadReceipts::default();
compute_unread_counts(
user_id,
room_id,
Some(&receipt_event),
all_events.clone(),
&[],
&mut read_receipts,
ThreadingSupport::Disabled,
);
assert!(
read_receipts != Default::default(),
"read receipts have been updated"
);
assert_eq!(read_receipts.num_unread, 2);
assert_eq!(read_receipts.num_mentions, 0);
assert_eq!(read_receipts.num_notifications, 0);
let mut read_receipts = RoomReadReceipts::default();
compute_unread_counts(
user_id,
room_id,
Some(&receipt_event),
head_events.clone(),
&tail_events,
&mut read_receipts,
ThreadingSupport::Disabled,
);
assert!(
read_receipts != Default::default(),
"read receipts have been updated"
);
assert_eq!(read_receipts.num_unread, 2);
assert_eq!(read_receipts.num_mentions, 0);
assert_eq!(read_receipts.num_notifications, 0);
}
}
}
}
}
#[test]
fn test_compute_unread_counts_updated_after_field_tracking() {
let user_id = owned_user_id!("@alice:example.org");
let room_id = room_id!("!room:example.org");
let events = make_test_events(user_id!("@bob:example.org"));
let receipt_event = EventFactory::new()
.read_receipts()
.add(event_id!("$6"), &user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
.into_content();
let mut read_receipts = RoomReadReceipts::default();
assert!(read_receipts.pending.is_empty());
compute_unread_counts(
&user_id,
room_id,
Some(&receipt_event),
events,
&[], &mut read_receipts,
ThreadingSupport::Disabled,
);
assert_eq!(read_receipts.num_unread, 0);
assert_eq!(read_receipts.pending.len(), 1);
assert!(read_receipts.pending.iter().any(|ev| ev == event_id!("$6")));
}
#[test]
fn test_compute_unread_counts_limited_sync() {
let user_id = owned_user_id!("@alice:example.org");
let room_id = room_id!("!room:example.org");
let events = make_test_events(user_id!("@bob:example.org"));
let receipt_event = EventFactory::new()
.read_receipts()
.add(event_id!("$1"), &user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
.into_content();
let mut read_receipts = RoomReadReceipts::default();
assert!(read_receipts.pending.is_empty());
let ev0 = events[0].clone();
compute_unread_counts(
&user_id,
room_id,
Some(&receipt_event),
events,
&[ev0], &mut read_receipts,
ThreadingSupport::Disabled,
);
assert_eq!(read_receipts.num_unread, 0);
assert!(read_receipts.pending.is_empty());
}
#[test]
fn test_receipt_selector_create_sync_index() {
let uid = user_id!("@bob:example.org");
let events = make_test_events(uid);
let ev6 = EventFactory::new().text_msg("yolo").sender(uid).no_event_id().into_event();
let index = ReceiptSelector::create_sync_index(events.iter().chain(&[ev6]));
assert_eq!(*index.get(event_id!("$1")).unwrap(), 0);
assert_eq!(*index.get(event_id!("$2")).unwrap(), 1);
assert_eq!(*index.get(event_id!("$3")).unwrap(), 2);
assert_eq!(*index.get(event_id!("$4")).unwrap(), 3);
assert_eq!(*index.get(event_id!("$5")).unwrap(), 4);
assert_eq!(index.get(event_id!("$6")), None);
assert_eq!(index.len(), 5);
let index = ReceiptSelector::create_sync_index(
[events[1].clone(), events[2].clone(), events[4].clone()].iter(),
);
assert_eq!(*index.get(event_id!("$2")).unwrap(), 0);
assert_eq!(*index.get(event_id!("$3")).unwrap(), 1);
assert_eq!(*index.get(event_id!("$5")).unwrap(), 2);
assert_eq!(index.len(), 3);
}
#[test]
fn test_receipt_selector_try_select_later() {
let events = make_test_events(user_id!("@bob:example.org"));
{
let mut selector = ReceiptSelector::new(&[], None);
selector.try_select_later(event_id!("$1"), 0);
let best_receipt = selector.select();
assert_eq!(best_receipt.unwrap().event_id, event_id!("$1"));
}
{
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$3")));
selector.try_select_later(event_id!("$1"), 0);
let best_receipt = selector.select();
assert!(best_receipt.is_none());
}
{
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$1")));
selector.try_select_later(event_id!("$1"), 0);
let best_receipt = selector.select();
assert_eq!(best_receipt.unwrap().event_id, event_id!("$1"));
}
{
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$3")));
selector.try_select_later(event_id!("$4"), 3);
let best_receipt = selector.select();
assert_eq!(best_receipt.unwrap().event_id, event_id!("$4"));
}
}
#[test]
fn test_receipt_selector_handle_pending_receipts_noop() {
let sender = user_id!("@bob:example.org");
let f = EventFactory::new().sender(sender);
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
let events = &[ev1, ev2][..];
{
let mut selector = ReceiptSelector::new(events, None);
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
selector.handle_pending_receipts(&mut pending);
assert!(pending.is_empty());
let best_receipt = selector.select();
assert!(best_receipt.is_none());
}
{
let mut selector = ReceiptSelector::new(events, Some(event_id!("$1")));
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
selector.handle_pending_receipts(&mut pending);
assert!(pending.is_empty());
let best_receipt = selector.select();
assert!(best_receipt.is_none());
}
}
#[test]
fn test_receipt_selector_handle_pending_receipts_doesnt_match_known_events() {
let sender = user_id!("@bob:example.org");
let f = EventFactory::new().sender(sender);
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
let events = &[ev1, ev2][..];
{
let mut selector = ReceiptSelector::new(events, None);
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
pending.push(owned_event_id!("$3"));
selector.handle_pending_receipts(&mut pending);
assert_eq!(pending.len(), 1);
let best_receipt = selector.select();
assert!(best_receipt.is_none());
}
{
let mut selector = ReceiptSelector::new(events, Some(event_id!("$1")));
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
pending.push(owned_event_id!("$3"));
selector.handle_pending_receipts(&mut pending);
assert_eq!(pending.len(), 1);
let best_receipt = selector.select();
assert!(best_receipt.is_none());
}
}
#[test]
fn test_receipt_selector_handle_pending_receipts_matches_known_events_no_initial() {
let sender = user_id!("@bob:example.org");
let f = EventFactory::new().sender(sender);
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
let events = &[ev1, ev2][..];
{
let mut selector = ReceiptSelector::new(events, None);
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
pending.push(owned_event_id!("$2"));
selector.handle_pending_receipts(&mut pending);
assert!(pending.is_empty());
let best_receipt = selector.select();
assert_eq!(best_receipt.unwrap().event_id, event_id!("$2"));
}
{
let mut selector = ReceiptSelector::new(events, None);
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
pending.push(owned_event_id!("$1"));
pending.push(owned_event_id!("$3"));
selector.handle_pending_receipts(&mut pending);
assert_eq!(pending.len(), 1);
assert!(pending.iter().any(|ev| ev == event_id!("$3")));
let best_receipt = selector.select();
assert_eq!(best_receipt.unwrap().event_id, event_id!("$1"));
}
}
#[test]
fn test_receipt_selector_handle_pending_receipts_matches_known_events_with_initial() {
let sender = user_id!("@bob:example.org");
let f = EventFactory::new().sender(sender);
let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event();
let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event();
let events = &[ev1, ev2][..];
{
let mut selector = ReceiptSelector::new(events, Some(event_id!("$1")));
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
pending.push(owned_event_id!("$2"));
selector.handle_pending_receipts(&mut pending);
assert!(pending.is_empty());
let best_receipt = selector.select();
assert_eq!(best_receipt.unwrap().event_id, event_id!("$2"));
}
{
let mut selector = ReceiptSelector::new(events, Some(event_id!("$2")));
let mut pending = RingBuffer::new(NonZeroUsize::new(16).unwrap());
pending.push(owned_event_id!("$1"));
selector.handle_pending_receipts(&mut pending);
assert!(pending.is_empty());
let best_receipt = selector.select();
assert!(best_receipt.is_none());
}
}
#[test]
fn test_receipt_selector_handle_new_receipt() {
let myself = user_id!("@alice:example.org");
let events = make_test_events(user_id!("@bob:example.org"));
let f = EventFactory::new();
{
let mut selector = ReceiptSelector::new(&events, None);
let receipt_event = f
.read_receipts()
.add(
event_id!("$5"),
myself,
ReceiptType::Read,
ReceiptThread::Thread(owned_event_id!("$2")),
)
.into_content();
let pending = selector.handle_new_receipt(myself, &receipt_event);
assert!(pending.is_empty());
let best_receipt = selector.select();
assert!(best_receipt.is_none());
}
for receipt_type in [ReceiptType::Read, ReceiptType::ReadPrivate] {
for receipt_thread in [ReceiptThread::Main, ReceiptThread::Unthreaded] {
{
let mut selector = ReceiptSelector::new(&events, None);
let receipt_event = f
.read_receipts()
.add(event_id!("$6"), myself, receipt_type.clone(), receipt_thread.clone())
.into_content();
let pending = selector.handle_new_receipt(myself, &receipt_event);
assert_eq!(pending[0], event_id!("$6"));
assert_eq!(pending.len(), 1);
let best_receipt = selector.select();
assert!(best_receipt.is_none());
}
{
let mut selector = ReceiptSelector::new(&events, None);
let receipt_event = f
.read_receipts()
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
.into_content();
let pending = selector.handle_new_receipt(myself, &receipt_event);
assert!(pending.is_empty());
let best_receipt = selector.select();
assert_eq!(best_receipt.unwrap().event_id, event_id!("$3"));
}
{
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$4")));
let receipt_event = f
.read_receipts()
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
.into_content();
let pending = selector.handle_new_receipt(myself, &receipt_event);
assert!(pending.is_empty());
let best_receipt = selector.select();
assert!(best_receipt.is_none());
}
{
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$2")));
let receipt_event = f
.read_receipts()
.add(event_id!("$3"), myself, receipt_type.clone(), receipt_thread.clone())
.into_content();
let pending = selector.handle_new_receipt(myself, &receipt_event);
assert!(pending.is_empty());
let best_receipt = selector.select();
assert_eq!(best_receipt.unwrap().event_id, event_id!("$3"));
}
}
}
{
let mut selector = ReceiptSelector::new(&events, Some(event_id!("$2")));
let receipt_event = f
.read_receipts()
.add(event_id!("$4"), myself, ReceiptType::ReadPrivate, ReceiptThread::Unthreaded)
.add(event_id!("$6"), myself, ReceiptType::ReadPrivate, ReceiptThread::Main)
.add(event_id!("$3"), myself, ReceiptType::Read, ReceiptThread::Main)
.into_content();
let pending = selector.handle_new_receipt(myself, &receipt_event);
assert_eq!(pending.len(), 1);
assert_eq!(pending[0], event_id!("$6"));
let best_receipt = selector.select();
assert_eq!(best_receipt.unwrap().event_id, event_id!("$4"));
}
}
#[test]
fn test_try_match_implicit() {
let myself = owned_user_id!("@alice:example.org");
let bob = user_id!("@bob:example.org");
let mut events = make_test_events(bob);
let mut selector = ReceiptSelector::new(&events, None);
selector.try_match_implicit(&myself, &events);
let best_receipt = selector.select();
assert!(best_receipt.is_none());
let f = EventFactory::new();
events.push(
f.text_msg("A mulatto, an albino")
.sender(&myself)
.event_id(event_id!("$6"))
.into_event(),
);
events.push(
f.text_msg("A mosquito, my libido").sender(bob).event_id(event_id!("$7")).into_event(),
);
let mut selector = ReceiptSelector::new(&events, None);
selector.try_match_implicit(&myself, &events);
let best_receipt = selector.select();
assert_eq!(best_receipt.unwrap().event_id, event_id!("$6"));
}
#[test]
fn test_compute_unread_counts_with_implicit_receipt() {
let user_id = user_id!("@alice:example.org");
let bob = user_id!("@bob:example.org");
let room_id = room_id!("!room:example.org");
let mut events = make_test_events(bob);
let f = EventFactory::new();
events.push(
f.text_msg("A mulatto, an albino")
.sender(user_id)
.event_id(event_id!("$6"))
.into_event(),
);
events.push(
f.text_msg("A mosquito, my libido").sender(bob).event_id(event_id!("$7")).into_event(),
);
events.push(
f.text_msg("A denial, a denial").sender(bob).event_id(event_id!("$8")).into_event(),
);
let events: Vec<_> = events.into_iter().collect();
let receipt_event = f
.read_receipts()
.add(event_id!("$3"), user_id, ReceiptType::Read, ReceiptThread::Unthreaded)
.into_content();
let mut read_receipts = RoomReadReceipts::default();
compute_unread_counts(
user_id,
room_id,
Some(&receipt_event),
Vec::new(),
&events,
&mut read_receipts,
ThreadingSupport::Disabled,
);
assert_eq!(read_receipts.num_unread, 2);
assert!(read_receipts.pending.is_empty());
assert_eq!(read_receipts.latest_active.unwrap().event_id, event_id!("$6"));
}
#[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");
receipts.process_event(
&make_event(own_alice, event_id!("$some_thread_root")),
own_alice,
ThreadingSupport::Enabled { with_subscriptions: false },
);
receipts.process_event(
&make_event(own_alice, event_id!("$some_other_thread_root")),
own_alice,
ThreadingSupport::Enabled { with_subscriptions: false },
);
receipts.process_event(
&make_event(bob, event_id!("$some_thread_root")),
own_alice,
ThreadingSupport::Enabled { with_subscriptions: false },
);
receipts.process_event(
&make_event(bob, event_id!("$some_other_thread_root")),
own_alice,
ThreadingSupport::Enabled { with_subscriptions: false },
);
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,
ThreadingSupport::Enabled { with_subscriptions: false },
);
assert_eq!(receipts.num_unread, 1);
assert_eq!(receipts.num_mentions, 0);
assert_eq!(receipts.num_notifications, 0);
}
}