matrix_sdk_ui/timeline/
date_dividers.rs

1// Copyright 2024 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Algorithm to adjust (insert/replace/remove) date dividers after new events
16//! have been received from any source.
17
18use std::{fmt::Display, sync::Arc};
19
20use chrono::{Datelike, Local, TimeZone};
21use ruma::MilliSecondsSinceUnixEpoch;
22use tracing::{error, event_enabled, instrument, trace, warn, Level};
23
24use super::{
25    controller::{ObservableItemsTransaction, TimelineMetadata},
26    DateDividerMode, TimelineItem, TimelineItemKind, VirtualTimelineItem,
27};
28
29#[derive(Debug, PartialEq)]
30struct Date {
31    year: i32,
32    month: u32,
33    day: u32,
34}
35
36impl Date {
37    fn is_same_month_as(&self, date: Date) -> bool {
38        self.year == date.year && self.month == date.month
39    }
40}
41
42/// Converts a timestamp since Unix Epoch to a year, month and day.
43fn timestamp_to_date(ts: MilliSecondsSinceUnixEpoch) -> Date {
44    let datetime = Local
45        .timestamp_millis_opt(ts.0.into())
46        // Only returns `None` if date is after Dec 31, 262143 BCE.
47        .single()
48        // Fallback to the current date to avoid issues with malicious
49        // homeservers.
50        .unwrap_or_else(Local::now);
51
52    Date { year: datetime.year(), month: datetime.month(), day: datetime.day() }
53}
54
55/// Algorithm ensuring that date dividers are adjusted correctly, according to
56/// new items that have been inserted.
57pub(super) struct DateDividerAdjuster {
58    /// The list of recorded operations to apply, after analyzing the latest
59    /// items.
60    ops: Vec<DateDividerOperation>,
61
62    /// A boolean indicating whether the struct has been used and thus must be
63    /// mark unused manually by calling [`Self::run`].
64    consumed: bool,
65
66    mode: DateDividerMode,
67}
68
69impl Drop for DateDividerAdjuster {
70    fn drop(&mut self) {
71        // Only run the assert if we're not currently panicking.
72        if !std::thread::panicking() && !self.consumed {
73            error!("a DateDividerAdjuster has not been consumed with run()");
74        }
75    }
76}
77
78/// A descriptor for a previous item.
79struct PrevItemDesc<'a> {
80    /// The index of the item in the `self.items` array.
81    item_index: usize,
82
83    /// The previous timeline item.
84    item: &'a Arc<TimelineItem>,
85
86    // The insert position of the operation in the `ops` array.
87    insert_op_at: usize,
88}
89
90impl DateDividerAdjuster {
91    pub fn new(mode: DateDividerMode) -> Self {
92        Self {
93            ops: Default::default(),
94            // The adjuster starts as consumed, and it will be marked no consumed iff it's used
95            // with `mark_used`.
96            consumed: true,
97            mode,
98        }
99    }
100
101    /// Marks this [`DateDividerAdjuster`] as used, which means it'll require a
102    /// call to [`DateDividerAdjuster::run`] before getting dropped.
103    pub fn mark_used(&mut self) {
104        // Mark the adjuster as needing to be consumed.
105        self.consumed = false;
106    }
107
108    /// Ensures that date separators are properly inserted/removed when needs
109    /// be.
110    #[instrument(skip_all)]
111    pub fn run(&mut self, items: &mut ObservableItemsTransaction<'_>, meta: &mut TimelineMetadata) {
112        // We're going to record vector operations like inserting, replacing and
113        // removing date dividers. Since we may remove or insert new items,
114        // recorded offsets will change as we're iterating over the array. The
115        // only way this is possible is because we're recording operations
116        // happening in non-decreasing order of the indices, i.e. we can't do an
117        // operation on index I and then on any index J<I later.
118        //
119        // Note we can't "just" iterate in reverse order, because we may have a
120        // `Remove(i)` followed by a `Replace((i+1) -1)`, which wouldn't do what
121        // we want, if running in reverse order.
122        //
123        // Also note that we can remove a few items at position J, then later decide to
124        // replace/remove an item (in `handle_event`) at position I, with I<J. That
125        // would break the above invariant (that operations happen in
126        // non-decreasing order of the indices), so we must record the insert
127        // position for an operation related to the previous item.
128
129        let mut prev_item: Option<PrevItemDesc<'_>> = None;
130        let mut latest_event_ts = None;
131
132        for (i, item) in items.iter_remotes_and_locals_regions() {
133            match item.kind() {
134                TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(ts)) => {
135                    // Record what the last alive item pair is only if we haven't removed the date
136                    // divider.
137                    if !self.handle_date_divider(i, *ts, prev_item.as_ref().map(|desc| desc.item)) {
138                        prev_item = Some(PrevItemDesc {
139                            item_index: i,
140                            item,
141                            insert_op_at: self.ops.len(),
142                        });
143                    }
144                }
145
146                TimelineItemKind::Event(event) => {
147                    let ts = event.timestamp();
148
149                    self.handle_event(i, ts, prev_item, latest_event_ts);
150
151                    prev_item =
152                        Some(PrevItemDesc { item_index: i, item, insert_op_at: self.ops.len() });
153                    latest_event_ts = Some(ts);
154                }
155
156                TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker)
157                | TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart) => {
158                    // Nothing to do.
159                }
160            }
161        }
162
163        // Also chase trailing date dividers explicitly, by iterating from the end to
164        // the start. Since they wouldn't be the prev_item of anything, we
165        // wouldn't analyze them in the previous loop.
166        for (i, item) in items.iter_remotes_and_locals_regions().rev() {
167            if item.is_date_divider() {
168                // The item is a trailing date divider: remove it, if it wasn't already
169                // scheduled for deletion.
170                if !self
171                    .ops
172                    .iter()
173                    .any(|op| matches!(op, DateDividerOperation::Remove(j) if i == *j))
174                {
175                    trace!("removing trailing date divider @ {i}");
176
177                    // Find the index at which to insert the removal operation. It must be before
178                    // any other operation on a bigger index, to maintain the
179                    // non-decreasing invariant.
180                    let index =
181                        self.ops.iter().position(|op| op.index() > i).unwrap_or(self.ops.len());
182
183                    self.ops.insert(index, DateDividerOperation::Remove(i));
184                }
185            }
186
187            if item.is_event() {
188                // Stop as soon as we run into the first (trailing) event.
189                break;
190            }
191        }
192
193        // Only record the initial state if we've enabled the trace log level, and not
194        // otherwise.
195        let initial_state = if event_enabled!(Level::TRACE) {
196            Some(
197                items
198                    .iter_remotes_and_locals_regions()
199                    .map(|(_i, timeline_item)| timeline_item.clone())
200                    .collect(),
201            )
202        } else {
203            None
204        };
205
206        self.process_ops(items, meta);
207
208        // Then check invariants.
209        if let Some(report) = self.check_invariants(items, initial_state) {
210            warn!("Errors encountered when checking invariants.");
211            warn!("{report}");
212            #[cfg(any(debug_assertions, test))]
213            panic!("There was an error checking date separator invariants");
214        }
215
216        self.consumed = true;
217    }
218
219    /// Decides what to do with a date divider.
220    ///
221    /// Returns whether it's been removed or not.
222    #[inline]
223    fn handle_date_divider(
224        &mut self,
225        i: usize,
226        ts: MilliSecondsSinceUnixEpoch,
227        prev_item: Option<&Arc<TimelineItem>>,
228    ) -> bool {
229        let Some(prev_item) = prev_item else {
230            // No interesting item prior to the date divider: it must be the first one,
231            // nothing to do.
232            return false;
233        };
234
235        match prev_item.kind() {
236            TimelineItemKind::Event(event) => {
237                // This date divider is preceded by an event.
238                if self.is_same_date_divider_group_as(event.timestamp(), ts) {
239                    // The event has the same date as the date divider: remove the current date
240                    // divider.
241                    trace!("removing date divider following event with same timestamp @ {i}");
242                    self.ops.push(DateDividerOperation::Remove(i));
243                    return true;
244                }
245            }
246
247            TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(_)) => {
248                trace!("removing duplicate date divider @ {i}");
249                // This date divider is preceded by another one: remove the current one.
250                self.ops.push(DateDividerOperation::Remove(i));
251                return true;
252            }
253
254            TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker)
255            | TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart) => {
256                // Nothing to do.
257            }
258        }
259
260        false
261    }
262
263    #[inline]
264    fn handle_event(
265        &mut self,
266        i: usize,
267        ts: MilliSecondsSinceUnixEpoch,
268        prev_item_desc: Option<PrevItemDesc<'_>>,
269        latest_event_ts: Option<MilliSecondsSinceUnixEpoch>,
270    ) {
271        let Some(PrevItemDesc { item_index, insert_op_at, item }) = prev_item_desc else {
272            // The event was the first item, so there wasn't any date divider before it:
273            // insert one.
274            trace!("inserting the first date divider @ {}", i);
275            self.ops.push(DateDividerOperation::Insert(i, ts));
276            return;
277        };
278
279        match item.kind() {
280            TimelineItemKind::Event(prev_event) => {
281                // The event is preceded by another event. If they're not the same date,
282                // insert a date divider.
283                let prev_ts = prev_event.timestamp();
284
285                if !self.is_same_date_divider_group_as(prev_ts, ts) {
286                    trace!(
287                        "inserting date divider @ {} between two events with different dates",
288                        i
289                    );
290                    self.ops.push(DateDividerOperation::Insert(i, ts));
291                }
292            }
293
294            TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(prev_ts)) => {
295                let event_date = timestamp_to_date(ts);
296
297                // The event is preceded by a date divider.
298                if timestamp_to_date(*prev_ts) != event_date {
299                    // The date divider is wrong. Should we replace it with the correct value, or
300                    // remove it entirely?
301                    if let Some(last_event_ts) = latest_event_ts {
302                        if timestamp_to_date(last_event_ts) == event_date {
303                            // There's a previous event with the same date: remove the divider.
304                            trace!("removed date divider @ {item_index} between two events that have the same date");
305                            self.ops.insert(insert_op_at, DateDividerOperation::Remove(item_index));
306                            return;
307                        }
308                    }
309
310                    // There's no previous event or there's one with a different date: replace
311                    // the current divider.
312                    trace!("replacing date divider @ {item_index} with new timestamp from event");
313                    self.ops.insert(insert_op_at, DateDividerOperation::Replace(item_index, ts));
314                }
315            }
316
317            TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker)
318            | TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart) => {
319                // Nothing to do.
320            }
321        }
322    }
323
324    fn process_ops(&self, items: &mut ObservableItemsTransaction<'_>, meta: &mut TimelineMetadata) {
325        // Record the deletion offset.
326        let mut offset = 0i64;
327        // Remember what the maximum index was, so we can assert that it's
328        // non-decreasing.
329        let mut max_i = 0;
330
331        for op in &self.ops {
332            match *op {
333                DateDividerOperation::Insert(i, ts) => {
334                    assert!(i >= max_i, "trying to insert at {i} < max_i={max_i}");
335
336                    let at = (i64::try_from(i).unwrap() + offset)
337                        .min(i64::try_from(items.len()).unwrap());
338                    assert!(at >= 0);
339                    let at = at as usize;
340
341                    items.push_date_divider(
342                        at,
343                        meta.new_timeline_item(VirtualTimelineItem::DateDivider(ts)),
344                    );
345
346                    offset += 1;
347                    max_i = i;
348                }
349
350                DateDividerOperation::Replace(i, ts) => {
351                    assert!(i >= max_i, "trying to replace at {i} < max_i={max_i}");
352
353                    let at = i64::try_from(i).unwrap() + offset;
354                    assert!(at >= 0);
355                    let at = at as usize;
356
357                    let replaced = &items[at];
358                    if !replaced.is_date_divider() {
359                        error!("we replaced a non date-divider @ {i}: {:?}", replaced.kind());
360                    }
361
362                    let unique_id = replaced.unique_id();
363                    let item = TimelineItem::new(
364                        VirtualTimelineItem::DateDivider(ts),
365                        unique_id.to_owned(),
366                    );
367
368                    items.replace(at, item);
369                    max_i = i;
370                }
371
372                DateDividerOperation::Remove(i) => {
373                    assert!(i >= max_i, "trying to replace at {i} < max_i={max_i}");
374
375                    let at = i64::try_from(i).unwrap() + offset;
376                    assert!(at >= 0);
377
378                    let removed = items.remove(at as usize);
379                    if !removed.is_date_divider() {
380                        error!("we removed a non date-divider @ {i}: {:?}", removed.kind());
381                    }
382
383                    offset -= 1;
384                    max_i = i;
385                }
386            }
387        }
388    }
389
390    /// Checks the invariants that must hold at any time after inserting date
391    /// dividers.
392    ///
393    /// Returns a report if and only if there was at least one error.
394    fn check_invariants<'a, 'o>(
395        &mut self,
396        items: &'a ObservableItemsTransaction<'o>,
397        initial_state: Option<Vec<Arc<TimelineItem>>>,
398    ) -> Option<DateDividerInvariantsReport<'a, 'o>> {
399        let mut report = DateDividerInvariantsReport {
400            initial_state,
401            errors: Vec::new(),
402            operations: std::mem::take(&mut self.ops),
403            final_state: items,
404        };
405
406        // Assert invariants.
407        // 1. The timeline starts with a date divider, if it's not only virtual items.
408        {
409            let mut i = items.first_remotes_region_index();
410            while let Some(item) = items.get(i) {
411                if let Some(virt) = item.as_virtual() {
412                    if matches!(virt, VirtualTimelineItem::DateDivider(_)) {
413                        // We found a date divider among the first virtual items: stop here.
414                        break;
415                    }
416                } else {
417                    // We found an event, but we didn't have a date divider: report an error.
418                    report.errors.push(DateDividerInsertError::FirstItemNotDateDivider);
419                    break;
420                }
421                i += 1;
422            }
423        }
424
425        // 2. There are no two date dividers following each other.
426        {
427            let mut prev_was_date_divider = false;
428            for (i, item) in items.iter_remotes_and_locals_regions() {
429                if item.is_date_divider() {
430                    if prev_was_date_divider {
431                        report.errors.push(DateDividerInsertError::DuplicateDateDivider { at: i });
432                    }
433                    prev_was_date_divider = true;
434                } else {
435                    prev_was_date_divider = false;
436                }
437            }
438        };
439
440        // 3. There's no trailing date divider.
441        if let Some(last) = items.last() {
442            if last.is_date_divider() {
443                report.errors.push(DateDividerInsertError::TrailingDateDivider);
444            }
445        }
446
447        // 4. Items are properly separated with date dividers.
448        {
449            let mut prev_event_ts = None;
450            let mut prev_date_divider_ts = None;
451
452            for (i, item) in items.iter_remotes_and_locals_regions() {
453                if let Some(ev) = item.as_event() {
454                    let ts = ev.timestamp();
455
456                    // We have the same date as the previous event we've seen.
457                    if let Some(prev_ts) = prev_event_ts {
458                        if !self.is_same_date_divider_group_as(prev_ts, ts) {
459                            report.errors.push(
460                                DateDividerInsertError::MissingDateDividerBetweenEvents { at: i },
461                            );
462                        }
463                    }
464
465                    // There is a date divider before us, and it's the same date as our timestamp.
466                    if let Some(prev_ts) = prev_date_divider_ts {
467                        if !self.is_same_date_divider_group_as(prev_ts, ts) {
468                            report.errors.push(
469                                DateDividerInsertError::InconsistentDateAfterPreviousDateDivider {
470                                    at: i,
471                                },
472                            );
473                        }
474                    } else {
475                        report
476                            .errors
477                            .push(DateDividerInsertError::MissingDateDividerBeforeEvent { at: i });
478                    }
479
480                    prev_event_ts = Some(ts);
481                } else if let TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(ts)) =
482                    item.kind()
483                {
484                    // The previous date divider is for a different date.
485                    if let Some(prev_ts) = prev_date_divider_ts {
486                        if self.is_same_date_divider_group_as(prev_ts, *ts) {
487                            report
488                                .errors
489                                .push(DateDividerInsertError::DuplicateDateDivider { at: i });
490                        }
491                    }
492
493                    prev_event_ts = None;
494                    prev_date_divider_ts = Some(*ts);
495                }
496            }
497        }
498
499        // 5. If there was a read marker at the beginning, there should be one at the
500        //    end.
501        if let Some(state) = &report.initial_state {
502            if state.iter().any(|item| item.is_read_marker())
503                && !report
504                    .final_state
505                    .iter_remotes_and_locals_regions()
506                    .any(|(_i, item)| item.is_read_marker())
507            {
508                report.errors.push(DateDividerInsertError::ReadMarkerDisappeared);
509            }
510        }
511
512        if report.errors.is_empty() {
513            None
514        } else {
515            Some(report)
516        }
517    }
518
519    /// Returns whether the two dates for the given timestamps are the same or
520    /// not.
521    fn is_same_date_divider_group_as(
522        &self,
523        lhs: MilliSecondsSinceUnixEpoch,
524        rhs: MilliSecondsSinceUnixEpoch,
525    ) -> bool {
526        match self.mode {
527            DateDividerMode::Daily => timestamp_to_date(lhs) == timestamp_to_date(rhs),
528            DateDividerMode::Monthly => {
529                timestamp_to_date(lhs).is_same_month_as(timestamp_to_date(rhs))
530            }
531        }
532    }
533}
534
535#[derive(Debug)]
536enum DateDividerOperation {
537    Insert(usize, MilliSecondsSinceUnixEpoch),
538    Replace(usize, MilliSecondsSinceUnixEpoch),
539    Remove(usize),
540}
541
542impl DateDividerOperation {
543    fn index(&self) -> usize {
544        match self {
545            DateDividerOperation::Insert(i, _)
546            | DateDividerOperation::Replace(i, _)
547            | DateDividerOperation::Remove(i) => *i,
548        }
549    }
550}
551
552/// A report returned by [`DateDividerAdjuster::check_invariants`].
553struct DateDividerInvariantsReport<'a, 'o> {
554    /// Initial state before inserting the items.
555    initial_state: Option<Vec<Arc<TimelineItem>>>,
556    /// The operations that have been applied on the list.
557    operations: Vec<DateDividerOperation>,
558    /// Final state after inserting the date dividers.
559    final_state: &'a ObservableItemsTransaction<'o>,
560    /// Errors encountered in the algorithm.
561    errors: Vec<DateDividerInsertError>,
562}
563
564impl Display for DateDividerInvariantsReport<'_, '_> {
565    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
566        // Write all the items of a slice of timeline items.
567        fn write_items(
568            f: &mut std::fmt::Formatter<'_>,
569            items: &[Arc<TimelineItem>],
570        ) -> std::fmt::Result {
571            for (i, item) in items.iter().enumerate() {
572                if let TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(ts)) = item.kind()
573                {
574                    writeln!(f, "#{i} --- {}", ts.0)?;
575                } else if let Some(event) = item.as_event() {
576                    // id: timestamp
577                    writeln!(
578                        f,
579                        "#{i} {}: {}",
580                        event
581                            .event_id()
582                            .map_or_else(|| "(no event id)".to_owned(), |id| id.to_string()),
583                        event.timestamp().0
584                    )?;
585                } else {
586                    writeln!(f, "#{i} (other virtual item)")?;
587                }
588            }
589
590            Ok(())
591        }
592
593        if let Some(initial_state) = &self.initial_state {
594            writeln!(f, "Initial state:")?;
595            write_items(f, initial_state)?;
596
597            writeln!(f, "\nOperations to apply:")?;
598            for op in &self.operations {
599                match *op {
600                    DateDividerOperation::Insert(i, ts) => writeln!(f, "insert @ {i}: {}", ts.0)?,
601                    DateDividerOperation::Replace(i, ts) => writeln!(f, "replace @ {i}: {}", ts.0)?,
602                    DateDividerOperation::Remove(i) => writeln!(f, "remove @ {i}")?,
603                }
604            }
605
606            writeln!(f, "\nFinal state:")?;
607            write_items(
608                f,
609                self.final_state
610                    .iter_remotes_and_locals_regions()
611                    .map(|(_i, item)| item.clone())
612                    .collect::<Vec<_>>()
613                    .as_slice(),
614            )?;
615
616            writeln!(f)?;
617        }
618
619        for err in &self.errors {
620            writeln!(f, "{err}")?;
621        }
622
623        Ok(())
624    }
625}
626
627#[derive(Debug, thiserror::Error)]
628enum DateDividerInsertError {
629    /// The first item isn't a date divider.
630    #[error("The first item isn't a date divider")]
631    FirstItemNotDateDivider,
632
633    /// There are two date dividers for the same date.
634    #[error("Duplicate date divider @ {at}.")]
635    DuplicateDateDivider { at: usize },
636
637    /// The last item is a date divider.
638    #[error("The last item is a date divider.")]
639    TrailingDateDivider,
640
641    /// Two events are following each other but they have different dates
642    /// without a date divider between them.
643    #[error("Missing date divider between events @ {at}")]
644    MissingDateDividerBetweenEvents { at: usize },
645
646    /// Some event is missing a date divider before it.
647    #[error("Missing date divider before event @ {at}")]
648    MissingDateDividerBeforeEvent { at: usize },
649
650    /// An event and the previous date divider aren't focused on the same date.
651    #[error("Event @ {at} and the previous date divider aren't targeting the same date")]
652    InconsistentDateAfterPreviousDateDivider { at: usize },
653
654    /// The read marker has been removed.
655    #[error("The read marker has been removed")]
656    ReadMarkerDisappeared,
657}
658
659#[cfg(test)]
660mod tests {
661    use assert_matches2::assert_let;
662    use ruma::{owned_event_id, owned_user_id, uint, MilliSecondsSinceUnixEpoch};
663
664    use super::{super::controller::ObservableItems, DateDividerAdjuster};
665    use crate::timeline::{
666        controller::TimelineMetadata,
667        date_dividers::timestamp_to_date,
668        event_item::{EventTimelineItemKind, RemoteEventTimelineItem},
669        DateDividerMode, EventTimelineItem, MsgLikeContent, TimelineItemContent,
670        VirtualTimelineItem,
671    };
672
673    fn event_with_ts(timestamp: MilliSecondsSinceUnixEpoch) -> EventTimelineItem {
674        let event_kind = EventTimelineItemKind::Remote(RemoteEventTimelineItem {
675            event_id: owned_event_id!("$1"),
676            transaction_id: None,
677            read_receipts: Default::default(),
678            is_own: false,
679            is_highlighted: false,
680            encryption_info: None,
681            original_json: None,
682            latest_edit_json: None,
683            origin: crate::timeline::event_item::RemoteEventOrigin::Sync,
684        });
685        EventTimelineItem::new(
686            owned_user_id!("@alice:example.org"),
687            crate::timeline::TimelineDetails::Pending,
688            timestamp,
689            TimelineItemContent::MsgLike(MsgLikeContent::redacted()),
690            event_kind,
691            false,
692        )
693    }
694
695    fn test_metadata() -> TimelineMetadata {
696        TimelineMetadata::new(owned_user_id!("@a:b.c"), ruma::RoomVersionId::V11, None, None, false)
697    }
698
699    #[test]
700    fn test_no_trailing_date_divider() {
701        let mut items = ObservableItems::new();
702        let mut txn = items.transaction();
703
704        let mut meta = test_metadata();
705
706        let timestamp = MilliSecondsSinceUnixEpoch(uint!(42));
707        let timestamp_next_day =
708            MilliSecondsSinceUnixEpoch((42 + 3600 * 24 * 1000).try_into().unwrap());
709
710        txn.push_back(meta.new_timeline_item(event_with_ts(timestamp)), None);
711        txn.push_back(
712            meta.new_timeline_item(VirtualTimelineItem::DateDivider(timestamp_next_day)),
713            None,
714        );
715        txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker), None);
716
717        let mut adjuster = DateDividerAdjuster::new(DateDividerMode::Daily);
718        adjuster.run(&mut txn, &mut meta);
719
720        txn.commit();
721
722        let mut iter = items.iter();
723
724        assert_let!(Some(item) = iter.next());
725        assert!(item.is_date_divider());
726
727        assert_let!(Some(item) = iter.next());
728        assert!(item.is_remote_event());
729
730        assert_let!(Some(item) = iter.next());
731        assert!(item.is_read_marker());
732
733        assert!(iter.next().is_none());
734    }
735
736    #[test]
737    fn test_read_marker_in_between_event_and_date_divider() {
738        let mut items = ObservableItems::new();
739        let mut txn = items.transaction();
740
741        let mut meta = test_metadata();
742
743        let timestamp = MilliSecondsSinceUnixEpoch(uint!(42));
744        let timestamp_next_day =
745            MilliSecondsSinceUnixEpoch((42 + 3600 * 24 * 1000).try_into().unwrap());
746        assert_ne!(timestamp_to_date(timestamp), timestamp_to_date(timestamp_next_day));
747
748        let event = event_with_ts(timestamp);
749        txn.push_back(meta.new_timeline_item(event.clone()), None);
750        txn.push_back(
751            meta.new_timeline_item(VirtualTimelineItem::DateDivider(timestamp_next_day)),
752            None,
753        );
754        txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker), None);
755        txn.push_back(meta.new_timeline_item(event), None);
756
757        let mut adjuster = DateDividerAdjuster::new(DateDividerMode::Daily);
758        adjuster.run(&mut txn, &mut meta);
759
760        txn.commit();
761
762        let mut iter = items.iter();
763
764        assert!(iter.next().unwrap().is_date_divider());
765        assert!(iter.next().unwrap().is_remote_event());
766        assert!(iter.next().unwrap().is_read_marker());
767        assert!(iter.next().unwrap().is_remote_event());
768        assert!(iter.next().is_none());
769    }
770
771    #[test]
772    fn test_read_marker_in_between_date_dividers() {
773        let mut items = ObservableItems::new();
774        let mut txn = items.transaction();
775
776        let mut meta = test_metadata();
777
778        let timestamp = MilliSecondsSinceUnixEpoch(uint!(42));
779        let timestamp_next_day =
780            MilliSecondsSinceUnixEpoch((42 + 3600 * 24 * 1000).try_into().unwrap());
781        assert_ne!(timestamp_to_date(timestamp), timestamp_to_date(timestamp_next_day));
782
783        txn.push_back(meta.new_timeline_item(event_with_ts(timestamp)), None);
784        txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DateDivider(timestamp)), None);
785        txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DateDivider(timestamp)), None);
786        txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker), None);
787        txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DateDivider(timestamp)), None);
788        txn.push_back(meta.new_timeline_item(event_with_ts(timestamp_next_day)), None);
789
790        let mut adjuster = DateDividerAdjuster::new(DateDividerMode::Daily);
791        adjuster.run(&mut txn, &mut meta);
792
793        txn.commit();
794
795        let mut iter = items.iter();
796
797        assert!(iter.next().unwrap().is_date_divider());
798        assert!(iter.next().unwrap().is_remote_event());
799        assert!(iter.next().unwrap().is_read_marker());
800        assert!(iter.next().unwrap().is_date_divider());
801        assert!(iter.next().unwrap().is_remote_event());
802        assert!(iter.next().is_none());
803    }
804
805    #[test]
806    fn test_remove_all_date_dividers() {
807        let mut items = ObservableItems::new();
808        let mut txn = items.transaction();
809
810        let mut meta = test_metadata();
811
812        let timestamp = MilliSecondsSinceUnixEpoch(uint!(42));
813        let timestamp_next_day =
814            MilliSecondsSinceUnixEpoch((42 + 3600 * 24 * 1000).try_into().unwrap());
815        assert_ne!(timestamp_to_date(timestamp), timestamp_to_date(timestamp_next_day));
816
817        txn.push_back(meta.new_timeline_item(event_with_ts(timestamp_next_day)), None);
818        txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DateDivider(timestamp)), None);
819        txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DateDivider(timestamp)), None);
820        txn.push_back(meta.new_timeline_item(event_with_ts(timestamp_next_day)), None);
821
822        let mut adjuster = DateDividerAdjuster::new(DateDividerMode::Daily);
823        adjuster.run(&mut txn, &mut meta);
824
825        txn.commit();
826
827        let mut iter = items.iter();
828
829        assert!(iter.next().unwrap().is_date_divider());
830        assert!(iter.next().unwrap().is_remote_event());
831        assert!(iter.next().unwrap().is_remote_event());
832        assert!(iter.next().is_none());
833    }
834
835    #[test]
836    fn test_event_read_marker_spurious_date_divider() {
837        let mut items = ObservableItems::new();
838        let mut txn = items.transaction();
839
840        let mut meta = test_metadata();
841
842        let timestamp = MilliSecondsSinceUnixEpoch(uint!(42));
843
844        txn.push_back(meta.new_timeline_item(event_with_ts(timestamp)), None);
845        txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker), None);
846        txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DateDivider(timestamp)), None);
847
848        let mut adjuster = DateDividerAdjuster::new(DateDividerMode::Daily);
849        adjuster.run(&mut txn, &mut meta);
850
851        txn.commit();
852
853        let mut iter = items.iter();
854
855        assert!(iter.next().unwrap().is_date_divider());
856        assert!(iter.next().unwrap().is_remote_event());
857        assert!(iter.next().unwrap().is_read_marker());
858        assert!(iter.next().is_none());
859    }
860
861    #[test]
862    fn test_multiple_trailing_date_dividers() {
863        let mut items = ObservableItems::new();
864        let mut txn = items.transaction();
865
866        let mut meta = test_metadata();
867
868        let timestamp = MilliSecondsSinceUnixEpoch(uint!(42));
869
870        txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker), None);
871        txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DateDivider(timestamp)), None);
872        txn.push_back(meta.new_timeline_item(VirtualTimelineItem::DateDivider(timestamp)), None);
873
874        let mut adjuster = DateDividerAdjuster::new(DateDividerMode::Daily);
875        adjuster.run(&mut txn, &mut meta);
876
877        txn.commit();
878
879        let mut iter = items.iter();
880
881        assert!(iter.next().unwrap().is_read_marker());
882        assert!(iter.next().is_none());
883    }
884
885    #[test]
886    fn test_start_with_read_marker() {
887        let mut items = ObservableItems::new();
888        let mut txn = items.transaction();
889
890        let mut meta = test_metadata();
891        let timestamp = MilliSecondsSinceUnixEpoch(uint!(42));
892
893        txn.push_back(meta.new_timeline_item(VirtualTimelineItem::ReadMarker), None);
894        txn.push_back(meta.new_timeline_item(event_with_ts(timestamp)), None);
895
896        let mut adjuster = DateDividerAdjuster::new(DateDividerMode::Daily);
897        adjuster.run(&mut txn, &mut meta);
898
899        txn.commit();
900
901        let mut iter = items.iter();
902
903        assert!(iter.next().unwrap().is_read_marker());
904        assert!(iter.next().unwrap().is_date_divider());
905        assert!(iter.next().unwrap().is_remote_event());
906        assert!(iter.next().is_none());
907    }
908
909    #[test]
910    fn test_daily_divider_mode() {
911        let mut items = ObservableItems::new();
912        let mut txn = items.transaction();
913
914        let mut meta = test_metadata();
915
916        txn.push_back(
917            meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch(uint!(0)))),
918            None,
919        );
920        txn.push_back(
921            meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch(uint!(86_400_000)))), // One day later
922            None,
923        );
924        txn.push_back(
925            meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch(uint!(2_678_400_000)))), // One month later
926            None,
927        );
928
929        let mut adjuster = DateDividerAdjuster::new(DateDividerMode::Daily);
930        adjuster.run(&mut txn, &mut meta);
931
932        txn.commit();
933
934        let mut iter = items.iter();
935
936        assert!(iter.next().unwrap().is_date_divider());
937        assert!(iter.next().unwrap().is_remote_event());
938        assert!(iter.next().unwrap().is_date_divider());
939        assert!(iter.next().unwrap().is_remote_event());
940        assert!(iter.next().unwrap().is_date_divider());
941        assert!(iter.next().unwrap().is_remote_event());
942        assert!(iter.next().is_none());
943    }
944
945    #[test]
946    fn test_monthly_divider_mode() {
947        let mut items = ObservableItems::new();
948        let mut txn = items.transaction();
949
950        let mut meta = test_metadata();
951
952        txn.push_back(
953            // Start one day later than the origin, to make this test pass on all timezones.
954            // Let's call this time T.
955            meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch(uint!(86_400_000)))),
956            None,
957        );
958        txn.push_back(
959            // One day later (T+1 day).
960            meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch(uint!(172_800_000)))),
961            None,
962        );
963        txn.push_back(
964            // One month later (T+31 days).
965            meta.new_timeline_item(event_with_ts(MilliSecondsSinceUnixEpoch(uint!(2_764_800_000)))),
966            None,
967        );
968
969        let mut adjuster = DateDividerAdjuster::new(DateDividerMode::Monthly);
970        adjuster.run(&mut txn, &mut meta);
971
972        txn.commit();
973
974        let mut iter = items.iter();
975
976        assert!(iter.next().unwrap().is_date_divider());
977        assert!(iter.next().unwrap().is_remote_event());
978        assert!(iter.next().unwrap().is_remote_event());
979        assert!(iter.next().unwrap().is_date_divider());
980        assert!(iter.next().unwrap().is_remote_event());
981        assert!(iter.next().is_none());
982    }
983}