1use std::{cmp::Ordering, collections::HashMap};
16
17use futures_core::Stream;
18use indexmap::IndexMap;
19use ruma::{
20 EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, UserId,
21 events::receipt::{Receipt, ReceiptEventContent, ReceiptThread, ReceiptType},
22};
23use tokio::sync::watch;
24use tokio_stream::wrappers::WatchStream;
25use tracing::{debug, error, instrument, trace, warn};
26
27use super::{
28 AllRemoteEvents, ObservableItemsTransaction, RelativePosition, RoomDataProvider,
29 TimelineMetadata, TimelineState, rfind_event_by_id,
30};
31use crate::timeline::{TimelineItem, controller::TimelineStateTransaction};
32
33#[derive(Clone, Debug, Default)]
35pub(super) struct ReadReceiptsState {
36 by_event: HashMap<OwnedEventId, IndexMap<OwnedUserId, Receipt>>,
40
41 latest_by_user: HashMap<OwnedUserId, HashMap<ReceiptType, (OwnedEventId, Receipt)>>,
46
47 own_user_read_receipts_changed_sender: watch::Sender<()>,
49}
50
51#[derive(Clone, Copy, PartialEq, Eq, Debug)]
61pub(super) enum ImplicitReadReceipts {
62 Include,
64 Exclude,
67}
68
69impl ReadReceiptsState {
70 pub(super) fn clear(&mut self) {
72 self.by_event.clear();
73 self.latest_by_user.clear();
74 }
75
76 pub(super) fn subscribe_own_user_read_receipts_changed(
78 &self,
79 ) -> impl Stream<Item = ()> + use<> {
80 let subscriber = self.own_user_read_receipts_changed_sender.subscribe();
81 WatchStream::from_changes(subscriber)
82 }
83
84 pub(crate) fn get_latest(
87 &self,
88 user_id: &UserId,
89 receipt_type: &ReceiptType,
90 ) -> Option<&(OwnedEventId, Receipt)> {
91 self.latest_by_user.get(user_id).and_then(|map| map.get(receipt_type))
92 }
93
94 fn upsert_latest(
97 &mut self,
98 user_id: OwnedUserId,
99 receipt_type: ReceiptType,
100 read_receipt: (OwnedEventId, Receipt),
101 ) {
102 self.latest_by_user.entry(user_id).or_default().insert(receipt_type, read_receipt);
103 }
104
105 #[instrument(skip_all, fields(user_id = %new_receipt.user_id, event_id = %new_receipt.event_id))]
115 fn maybe_update_read_receipt(
116 &mut self,
117 new_receipt: FullReceipt<'_>,
118 is_own_user_id: bool,
119 timeline_items: &mut ObservableItemsTransaction<'_>,
120 ) {
121 let all_events = timeline_items.all_remote_events();
122
123 let old_receipt = self.get_latest(new_receipt.user_id, &new_receipt.receipt_type);
125
126 if old_receipt
127 .as_ref()
128 .is_some_and(|(old_receipt_event_id, _)| old_receipt_event_id == new_receipt.event_id)
129 {
130 if !is_own_user_id {
132 trace!("receipt hasn't changed, nothing to do");
133 }
134 return;
135 }
136
137 let old_event_id = old_receipt.map(|(event_id, _)| event_id);
138
139 let mut old_receipt_pos = None;
141 let mut old_item_pos = None;
142 let mut old_item_event_id = None;
143 let mut new_receipt_pos = None;
144 let mut new_item_pos = None;
145 let mut new_item_event_id = None;
146
147 for (pos, event) in all_events.iter().rev().enumerate() {
148 if old_receipt_pos.is_none() && old_event_id == Some(&event.event_id) {
149 old_receipt_pos = Some(pos);
150 }
151
152 if old_receipt_pos.is_some()
155 && old_item_event_id.is_none()
156 && event.visible
157 && event.can_show_read_receipts
158 {
159 old_item_pos = event.timeline_item_index;
160 old_item_event_id = Some(event.event_id.clone());
161 }
162
163 if new_receipt_pos.is_none() && new_receipt.event_id == event.event_id {
164 new_receipt_pos = Some(pos);
165 }
166
167 if new_receipt_pos.is_some()
170 && new_item_event_id.is_none()
171 && event.visible
172 && event.can_show_read_receipts
173 {
174 new_item_pos = event.timeline_item_index;
175 new_item_event_id = Some(event.event_id.clone());
176 }
177
178 if old_item_event_id.is_some() && new_item_event_id.is_some() {
179 break;
181 }
182 }
183
184 if let Some(old_receipt_pos) = old_receipt_pos {
186 let Some(new_receipt_pos) = new_receipt_pos else {
187 if !is_own_user_id {
190 trace!(
191 "we had a previous read receipt, but couldn't find the event \
192 targeted by the new read receipt in the timeline, exiting"
193 );
194 }
195 return;
196 };
197
198 if old_receipt_pos < new_receipt_pos {
199 if !is_own_user_id {
201 trace!("the previous read receipt is more recent than the new one, exiting");
202 }
203 return;
204 }
205 }
206
207 if !is_own_user_id {
217 trace!(
218 from_event = ?old_event_id,
219 from_visible_event = ?old_item_event_id,
220 to_event = ?new_receipt.event_id,
221 to_visible_event = ?new_item_event_id,
222 ?old_item_pos,
223 ?new_item_pos,
224 "moving read receipt",
225 );
226
227 if let Some(old_event_id) = old_event_id.cloned() {
229 self.remove_event_receipt_for_user(&old_event_id, new_receipt.user_id);
230 }
231
232 self.add_event_receipt_for_user(
234 new_receipt.event_id.to_owned(),
235 new_receipt.user_id.to_owned(),
236 new_receipt.receipt.clone(),
237 );
238 }
239
240 self.upsert_latest(
242 new_receipt.user_id.to_owned(),
243 new_receipt.receipt_type,
244 (new_receipt.event_id.to_owned(), new_receipt.receipt.clone()),
245 );
246
247 if is_own_user_id {
248 self.own_user_read_receipts_changed_sender.send_replace(());
249 return;
251 }
252
253 if new_item_event_id == old_item_event_id {
254 return;
256 }
257
258 let timeline_update = ReadReceiptTimelineUpdate {
259 old_item_pos,
260 old_event_id: old_item_event_id,
261 new_item_pos,
262 new_event_id: new_item_event_id,
263 };
264
265 timeline_update.apply(
266 timeline_items,
267 new_receipt.user_id.to_owned(),
268 new_receipt.receipt.clone(),
269 );
270 }
271
272 fn get_event_receipts(&self, event_id: &EventId) -> Option<&IndexMap<OwnedUserId, Receipt>> {
274 self.by_event.get(event_id)
275 }
276
277 fn add_event_receipt_for_user(
279 &mut self,
280 event_id: OwnedEventId,
281 user_id: OwnedUserId,
282 receipt: Receipt,
283 ) {
284 self.by_event.entry(event_id).or_default().insert(user_id, receipt);
285 }
286
287 fn remove_event_receipt_for_user(&mut self, event_id: &EventId, user_id: &UserId) {
289 if let Some(map) = self.by_event.get_mut(event_id) {
290 map.swap_remove(user_id);
291 if map.is_empty() {
293 self.by_event.remove(event_id);
294 }
295 }
296 }
297
298 #[instrument(skip(self, timeline_items, at_end))]
303 pub(super) fn compute_event_receipts(
304 &self,
305 event_id: &EventId,
306 timeline_items: &mut ObservableItemsTransaction<'_>,
307 at_end: bool,
308 ) -> IndexMap<OwnedUserId, Receipt> {
309 let mut all_receipts = self.get_event_receipts(event_id).cloned().unwrap_or_default();
310
311 if at_end {
312 trace!(
314 "early return because @end, retrieved receipts: {}",
315 all_receipts.iter().map(|(u, _)| u.as_str()).collect::<Vec<_>>().join(", ")
316 );
317 return all_receipts;
318 }
319
320 trace!(
321 "loaded receipts: {}",
322 all_receipts.iter().map(|(u, _)| u.as_str()).collect::<Vec<_>>().join(", ")
323 );
324
325 let Some(current_event_index) = timeline_items.position_by_event_id(event_id) else {
336 warn!("Could not find event {event_id} in timeline");
337 return all_receipts;
338 };
339 let mut prev_events_iter = timeline_items.all_remote_events().range(0..current_event_index);
340 let previous_events_that_can_show_read_receipts =
341 prev_events_iter.by_ref().filter(|event| event.can_show_read_receipts).filter_map(
342 |event| event.timeline_item_index.map(|item_index| (&event.event_id, item_index)),
343 );
344
345 let prev_event_and_item_index = previous_events_that_can_show_read_receipts
350 .last()
351 .map(|(event_id, index)| (event_id.clone(), index));
352
353 let next_events_iter =
359 timeline_items.all_remote_events().range(current_event_index..).skip(1);
360 let mut hidden = Vec::new();
361 for hidden_receipt_event_meta in
362 next_events_iter.take_while(|meta| !meta.visible || !meta.can_show_read_receipts)
363 {
364 if let Some(event_receipts) =
365 self.get_event_receipts(&hidden_receipt_event_meta.event_id)
366 {
367 trace!(%hidden_receipt_event_meta.event_id, "found receipts on hidden event");
368 hidden.extend(event_receipts.clone());
369 }
370 }
371
372 if let Some((prev_event_id, prev_item_index)) = prev_event_and_item_index {
374 let prev_item = &timeline_items[prev_item_index];
375 if let Some(remote_prev_item) = prev_item.as_event() {
379 let prev_receipts = remote_prev_item.read_receipts().clone();
380 for (user_id, _) in &hidden {
381 if !prev_receipts.contains_key(user_id) {
382 continue;
383 }
384 let mut up = ReadReceiptTimelineUpdate {
385 old_item_pos: Some(prev_item_index),
386 old_event_id: Some(prev_event_id.clone()),
387 new_item_pos: None,
388 new_event_id: None,
389 };
390 up.remove_old_receipt(timeline_items, user_id);
391 }
392 }
393 }
394
395 all_receipts.extend(hidden);
396 trace!(
397 "computed receipts: {}",
398 all_receipts.iter().map(|(u, _)| u.as_str()).collect::<Vec<_>>().join(", ")
399 );
400 all_receipts
401 }
402}
403
404struct FullReceipt<'a> {
405 event_id: &'a EventId,
406 user_id: &'a UserId,
407 receipt_type: ReceiptType,
408 receipt: &'a Receipt,
409}
410
411#[derive(Clone, Debug, Default)]
413struct ReadReceiptTimelineUpdate {
414 old_item_pos: Option<usize>,
417 old_event_id: Option<OwnedEventId>,
419 new_item_pos: Option<usize>,
422 new_event_id: Option<OwnedEventId>,
424}
425
426impl ReadReceiptTimelineUpdate {
427 #[instrument(skip_all)]
429 fn remove_old_receipt(&mut self, items: &mut ObservableItemsTransaction<'_>, user_id: &UserId) {
430 let Some(event_id) = &self.old_event_id else {
431 return;
433 };
434
435 let item_pos = self.old_item_pos.or_else(|| {
436 items
437 .iter_remotes_region()
438 .rev()
439 .filter_map(|(nth, item)| Some((nth, item.as_event()?)))
440 .find_map(|(nth, event_item)| {
441 (event_item.event_id() == Some(event_id)).then_some(nth)
442 })
443 });
444
445 let Some(item_pos) = item_pos else {
446 debug!(%event_id, %user_id, "inconsistent state: old event item for read receipt was not found");
447 return;
448 };
449
450 self.old_item_pos = Some(item_pos);
451
452 let event_item = &items[item_pos];
453 let event_item_id = event_item.unique_id().to_owned();
454
455 let Some(mut event_item) = event_item.as_event().cloned() else {
456 warn!("received a read receipt for a virtual item, this should not be possible");
457 return;
458 };
459
460 if let Some(remote_event_item) = event_item.as_remote_mut() {
461 if remote_event_item.read_receipts.swap_remove(user_id).is_none() {
462 debug!(
463 %event_id, %user_id,
464 "inconsistent state: old event item for user's read \
465 receipt doesn't have a receipt for the user"
466 );
467 }
468 trace!(%user_id, %event_id, "removed read receipt from event item");
469 items.replace(item_pos, TimelineItem::new(event_item, event_item_id));
470 } else {
471 warn!("received a read receipt for a local item, this should not be possible");
472 }
473 }
474
475 #[instrument(skip_all)]
477 fn add_new_receipt(
478 self,
479 items: &mut ObservableItemsTransaction<'_>,
480 user_id: OwnedUserId,
481 receipt: Receipt,
482 ) {
483 let Some(event_id) = self.new_event_id else {
484 return;
486 };
487
488 let old_item_pos = self.old_item_pos.unwrap_or(0);
489
490 let item_pos = self.new_item_pos.or_else(|| {
491 items
492 .iter_remotes_region()
493 .skip_while(|(nth, _)| *nth < old_item_pos)
496 .find_map(|(nth, item)| {
497 if let Some(event_item) = item.as_event() {
498 (event_item.event_id() == Some(&event_id)).then_some(nth)
499 } else {
500 None
501 }
502 })
503 });
504
505 let Some(item_pos) = item_pos else {
506 debug!(
507 %event_id, %user_id,
508 "inconsistent state: new event item for read receipt was not found",
509 );
510 return;
511 };
512
513 debug_assert!(
514 item_pos >= self.old_item_pos.unwrap_or(0),
515 "The new receipt must be added on a timeline item that is _after_ the timeline item \
516 that was holding the old receipt"
517 );
518
519 let event_item = &items[item_pos];
520 let event_item_id = event_item.unique_id().to_owned();
521
522 let Some(mut event_item) = event_item.as_event().cloned() else {
523 warn!("received a read receipt for a virtual item, this should not be possible");
524 return;
525 };
526
527 if let Some(remote_event_item) = event_item.as_remote_mut() {
528 trace!(%user_id, %event_id, "added read receipt to event item");
529 remote_event_item.read_receipts.insert(user_id, receipt);
530 items.replace(item_pos, TimelineItem::new(event_item, event_item_id));
531 } else {
532 warn!("received a read receipt for a local item, this should not be possible");
533 }
534 }
535
536 fn apply(
538 mut self,
539 items: &mut ObservableItemsTransaction<'_>,
540 user_id: OwnedUserId,
541 receipt: Receipt,
542 ) {
543 self.remove_old_receipt(items, &user_id);
544 self.add_new_receipt(items, user_id, receipt);
545 }
546}
547
548impl<P: RoomDataProvider> TimelineStateTransaction<'_, P> {
549 pub(super) fn handle_explicit_read_receipts(
550 &mut self,
551 receipt_event_content: ReceiptEventContent,
552 own_user_id: &UserId,
553 ) {
554 trace!("handling explicit read receipts");
555
556 for (event_id, receipt_types) in receipt_event_content.0 {
557 for (receipt_type, receipts) in receipt_types {
558 if !matches!(receipt_type, ReceiptType::Read | ReceiptType::ReadPrivate) {
560 continue;
561 }
562
563 for (user_id, receipt) in receipts {
564 let is_own_user_id = user_id == own_user_id;
565 let full_receipt = FullReceipt {
566 event_id: &event_id,
567 user_id: &user_id,
568 receipt_type: receipt_type.clone(),
569 receipt: &receipt,
570 };
571
572 self.meta.read_receipts.maybe_update_read_receipt(
573 full_receipt,
574 is_own_user_id,
575 &mut self.items,
576 );
577 }
578 }
579 }
580 }
581
582 pub(super) async fn load_read_receipts_for_event(
586 &mut self,
587 event_id: &EventId,
588 room_data_provider: &P,
589 ) {
590 trace!(%event_id, "loading initial receipts for an event");
591
592 let receipt_thread = self.focus.receipt_thread();
593
594 let receipts = if matches!(receipt_thread, ReceiptThread::Unthreaded | ReceiptThread::Main)
595 {
596 let mut main_receipts =
602 room_data_provider.load_event_receipts(event_id, &ReceiptThread::Main).await;
603
604 let unthreaded_receipts =
606 room_data_provider.load_event_receipts(event_id, &ReceiptThread::Unthreaded).await;
607
608 main_receipts.extend(unthreaded_receipts);
612 main_receipts
613 } else {
614 room_data_provider.load_event_receipts(event_id, &receipt_thread).await
617 };
618
619 let own_user_id = room_data_provider.own_user_id();
620
621 for (user_id, receipt) in receipts {
624 let full_receipt = FullReceipt {
625 event_id,
626 user_id: &user_id,
627 receipt_type: ReceiptType::Read,
628 receipt: &receipt,
629 };
630
631 self.meta.read_receipts.maybe_update_read_receipt(
632 full_receipt,
633 user_id == own_user_id,
634 &mut self.items,
635 );
636 }
637 }
638
639 pub(super) fn maybe_add_implicit_read_receipt(
648 &mut self,
649 event_id: &EventId,
650 sender: Option<&UserId>,
651 timestamp: Option<MilliSecondsSinceUnixEpoch>,
652 ) {
653 let (Some(user_id), Some(timestamp)) = (sender, timestamp) else {
654 return;
656 };
657
658 trace!(%user_id, %event_id, "adding implicit read receipt");
659
660 let mut receipt = Receipt::new(timestamp);
661 receipt.thread = self.focus.receipt_thread();
662
663 let full_receipt =
664 FullReceipt { event_id, user_id, receipt_type: ReceiptType::Read, receipt: &receipt };
665
666 let is_own_event = sender.is_some_and(|sender| sender == self.meta.own_user_id);
667
668 self.meta.read_receipts.maybe_update_read_receipt(
669 full_receipt,
670 is_own_event,
671 &mut self.items,
672 );
673 }
674
675 #[instrument(skip(self))]
678 pub(super) fn maybe_update_read_receipts_of_prev_event(&mut self, event_id: &EventId) {
679 let Some(prev_event_meta) = self
681 .items
682 .all_remote_events()
683 .iter()
684 .rev()
685 .skip_while(|meta| meta.event_id != event_id)
687 .skip(1)
689 .find(|meta| meta.visible && meta.can_show_read_receipts)
691 else {
692 trace!("Couldn't find any previous visible event, exiting");
693 return;
694 };
695
696 let Some((prev_item_pos, prev_event_item)) =
697 rfind_event_by_id(&self.items, &prev_event_meta.event_id)
698 else {
699 error!("inconsistent state: timeline item of visible event was not found");
700 return;
701 };
702
703 let prev_event_item_id = prev_event_item.internal_id.to_owned();
704 let mut prev_event_item = prev_event_item.clone();
705
706 let Some(remote_prev_event_item) = prev_event_item.as_remote_mut() else {
707 warn!("loading read receipts for a local item, this should not be possible");
708 return;
709 };
710
711 let read_receipts = self.meta.read_receipts.compute_event_receipts(
712 &remote_prev_event_item.event_id,
713 &mut self.items,
714 false,
715 );
716
717 if read_receipts.len() == remote_prev_event_item.read_receipts.len() {
719 trace!("same count of read receipts, not doing anything");
720 return;
721 }
722
723 trace!("replacing read receipts with the new ones");
724 remote_prev_event_item.read_receipts = read_receipts;
725 self.items.replace(prev_item_pos, TimelineItem::new(prev_event_item, prev_event_item_id));
726 }
727}
728
729impl<P: RoomDataProvider> TimelineState<P> {
730 pub(super) async fn populate_initial_user_receipt(
733 &mut self,
734 room_data_provider: &P,
735 receipt_type: ReceiptType,
736 ) {
737 let own_user_id = room_data_provider.own_user_id().to_owned();
738
739 let receipt_thread = self.focus.receipt_thread();
740 let wants_unthreaded_receipts = receipt_thread == ReceiptThread::Unthreaded;
741
742 let mut read_receipt = room_data_provider
743 .load_user_receipt(receipt_type.clone(), &receipt_thread, &own_user_id)
744 .await;
745
746 if wants_unthreaded_receipts && read_receipt.is_none() {
747 read_receipt = room_data_provider
749 .load_user_receipt(receipt_type.clone(), &ReceiptThread::Main, &own_user_id)
750 .await;
751 }
752
753 if let Some(read_receipt) = read_receipt {
754 self.meta.read_receipts.upsert_latest(own_user_id, receipt_type, read_receipt);
755 }
756 }
757
758 pub(super) async fn latest_user_read_receipt(
762 &self,
763 user_id: &UserId,
764 receipt_thread: ReceiptThread,
765 room_data_provider: &P,
766 implicit_receipts: ImplicitReadReceipts,
767 ) -> Option<(OwnedEventId, Receipt)> {
768 let all_remote_events = self.items.all_remote_events();
769
770 let public_read_receipt = self
771 .meta
772 .user_receipt(
773 user_id,
774 ReceiptType::Read,
775 receipt_thread.clone(),
776 room_data_provider,
777 all_remote_events,
778 implicit_receipts,
779 )
780 .await;
781
782 let private_read_receipt = self
783 .meta
784 .user_receipt(
785 user_id,
786 ReceiptType::ReadPrivate,
787 receipt_thread,
788 room_data_provider,
789 all_remote_events,
790 implicit_receipts,
791 )
792 .await;
793
794 match TimelineMetadata::compare_optional_receipts(
798 public_read_receipt.as_ref(),
799 private_read_receipt.as_ref(),
800 all_remote_events,
801 ) {
802 Ordering::Greater => public_read_receipt,
803 Ordering::Less => private_read_receipt,
804 _ => unreachable!(),
805 }
806 }
807
808 pub(super) fn latest_user_read_receipt_timeline_event_id(
811 &self,
812 user_id: &UserId,
813 ) -> Option<OwnedEventId> {
814 let public_read_receipt = self.meta.read_receipts.get_latest(user_id, &ReceiptType::Read);
817 let private_read_receipt =
818 self.meta.read_receipts.get_latest(user_id, &ReceiptType::ReadPrivate);
819
820 let (latest_receipt_id, _) = match TimelineMetadata::compare_optional_receipts(
824 public_read_receipt,
825 private_read_receipt,
826 self.items.all_remote_events(),
827 ) {
828 Ordering::Greater => public_read_receipt?,
829 Ordering::Less => private_read_receipt?,
830 _ => unreachable!(),
831 };
832
833 self.items
835 .all_remote_events()
836 .iter()
837 .rev()
838 .skip_while(|ev| ev.event_id != *latest_receipt_id)
839 .find(|ev| ev.visible && ev.can_show_read_receipts)
840 .map(|ev| ev.event_id.clone())
841 }
842}
843
844impl TimelineMetadata {
845 pub(super) async fn user_receipt<P: RoomDataProvider>(
856 &self,
857 user_id: &UserId,
858 receipt_type: ReceiptType,
859 receipt_thread: ReceiptThread,
860 room_data_provider: &P,
861 all_remote_events: &AllRemoteEvents,
862 implicit_receipts: ImplicitReadReceipts,
863 ) -> Option<(OwnedEventId, Receipt)> {
864 if implicit_receipts == ImplicitReadReceipts::Include && let Some(receipt) = self.read_receipts.get_latest(user_id, &receipt_type)
866 {
867 return Some(receipt.clone());
869 }
870
871 if receipt_thread == ReceiptThread::Unthreaded {
872 let unthreaded_read_receipt = room_data_provider
875 .load_user_receipt(receipt_type.clone(), &ReceiptThread::Unthreaded, user_id)
876 .await;
877
878 let main_thread_read_receipt = room_data_provider
879 .load_user_receipt(receipt_type.clone(), &ReceiptThread::Main, user_id)
880 .await;
881
882 match Self::compare_optional_receipts(
885 main_thread_read_receipt.as_ref(),
886 unthreaded_read_receipt.as_ref(),
887 all_remote_events,
888 ) {
889 Ordering::Greater => main_thread_read_receipt,
890 Ordering::Less => unthreaded_read_receipt,
891 _ => unreachable!(),
892 }
893 } else {
894 room_data_provider
898 .load_user_receipt(receipt_type.clone(), &receipt_thread, user_id)
899 .await
900 }
901 }
902
903 fn compare_optional_receipts(
910 lhs: Option<&(OwnedEventId, Receipt)>,
911 rhs_or_default: Option<&(OwnedEventId, Receipt)>,
912 all_remote_events: &AllRemoteEvents,
913 ) -> Ordering {
914 let Some((lhs_event_id, lhs_receipt)) = lhs else {
916 return Ordering::Less;
917 };
918 let Some((rhs_event_id, rhs_receipt)) = rhs_or_default else {
919 return Ordering::Greater;
920 };
921
922 if let Some(relative_pos) =
924 Self::compare_events_positions(lhs_event_id, rhs_event_id, all_remote_events)
925 {
926 if relative_pos == RelativePosition::Before {
927 return Ordering::Greater;
928 }
929
930 return Ordering::Less;
931 }
932
933 if let Some((lhs_ts, rhs_ts)) = lhs_receipt.ts.zip(rhs_receipt.ts) {
935 if lhs_ts > rhs_ts {
936 return Ordering::Greater;
937 }
938
939 return Ordering::Less;
940 }
941
942 Ordering::Less
943 }
944}