Skip to content

Commit 766772f

Browse files
committed
feat(timeline): insert the start of the timeline in places where it's required
1 parent f5b6767 commit 766772f

File tree

11 files changed

+144
-32
lines changed

11 files changed

+144
-32
lines changed

crates/matrix-sdk-ui/src/timeline/controller/metadata.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ pub(in crate::timeline) struct TimelineMetadata {
4141
/// This value is constant over the lifetime of the metadata.
4242
internal_id_prefix: Option<String>,
4343

44-
/// The `count` value for the `Skip higher-order stream used by the
44+
/// The `count` value for the `Skip` higher-order stream used by the
4545
/// `TimelineSubscriber`. See its documentation to learn more.
4646
pub(super) subscriber_skip_count: SkipCount,
4747

crates/matrix-sdk-ui/src/timeline/controller/mod.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ use super::{
7171
traits::{Decryptor, RoomDataProvider},
7272
DateDividerMode, Error, EventSendState, EventTimelineItem, InReplyToDetails, Message,
7373
PaginationError, Profile, RepliedToEvent, TimelineDetails, TimelineEventItemId, TimelineFocus,
74-
TimelineItem, TimelineItemContent, TimelineItemKind,
74+
TimelineItem, TimelineItemContent, TimelineItemKind, VirtualTimelineItem,
7575
};
7676
use crate::{
7777
timeline::{
@@ -1309,6 +1309,18 @@ impl<P: RoomDataProvider, D: Decryptor> TimelineController<P, D> {
13091309
}
13101310
}
13111311
}
1312+
1313+
/// Insert a timeline start item at the beginning of the room, if it's
1314+
/// missing.
1315+
pub async fn insert_timeline_start_if_missing(&self) {
1316+
let mut state = self.state.write().await;
1317+
let mut txn = state.transaction();
1318+
if txn.items.get(0).is_some_and(|item| item.is_timeline_start()) {
1319+
return;
1320+
}
1321+
txn.items.push_front(txn.meta.new_timeline_item(VirtualTimelineItem::TimelineStart), None);
1322+
txn.commit();
1323+
}
13121324
}
13131325

13141326
impl TimelineController {

crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ use super::{
3434
ObservableItems, ObservableItemsTransaction, TimelineFocusKind, TimelineMetadata,
3535
TimelineSettings,
3636
};
37-
use crate::events::SyncTimelineEventWithoutContent;
37+
use crate::{events::SyncTimelineEventWithoutContent, timeline::VirtualTimelineItem};
3838

3939
pub(in crate::timeline) struct TimelineStateTransaction<'a> {
4040
/// A vector transaction over the items themselves. Holds temporary state
@@ -484,9 +484,16 @@ impl<'a> TimelineStateTransaction<'a> {
484484
// `VectorDiff::Clear` should be much more efficient to process for
485485
// subscribers.
486486
if has_local_echoes {
487-
// Remove all remote events and the read marker
487+
// Remove all remote events and virtual items that aren't date dividers.
488488
self.items.for_each(|entry| {
489-
if entry.is_remote_event() || entry.is_read_marker() {
489+
if entry.is_remote_event()
490+
|| entry.as_virtual().is_some_and(|vitem| match vitem {
491+
VirtualTimelineItem::DateDivider(_) => false,
492+
VirtualTimelineItem::ReadMarker | VirtualTimelineItem::TimelineStart => {
493+
true
494+
}
495+
})
496+
{
490497
ObservableItemsTransactionEntry::remove(entry);
491498
}
492499
});

crates/matrix-sdk-ui/src/timeline/date_dividers.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -402,16 +402,21 @@ impl DateDividerAdjuster {
402402
};
403403

404404
// Assert invariants.
405-
// 1. The timeline starts with a date divider.
406-
if let Some(item) = items.get(0) {
407-
if item.is_read_marker() {
408-
if let Some(next_item) = items.get(1) {
409-
if !next_item.is_date_divider() {
410-
report.errors.push(DateDividerInsertError::FirstItemNotDateDivider);
405+
// 1. The timeline starts with a date divider, if it's not only virtual items.
406+
{
407+
let mut i = 0;
408+
while let Some(item) = items.get(i) {
409+
if let Some(virt) = item.as_virtual() {
410+
if matches!(virt, VirtualTimelineItem::DateDivider(_)) {
411+
// We found a date divider among the first virtual items: stop here.
412+
break;
411413
}
414+
} else {
415+
// We found an event, but we didn't have a date divider: report an error.
416+
report.errors.push(DateDividerInsertError::FirstItemNotDateDivider);
417+
break;
412418
}
413-
} else if !item.is_date_divider() {
414-
report.errors.push(DateDividerInsertError::FirstItemNotDateDivider);
419+
i += 1;
415420
}
416421
}
417422

crates/matrix-sdk-ui/src/timeline/event_handler.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1128,7 +1128,17 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
11281128
(!timeline_item.as_event()?.is_local_echo())
11291129
.then_some(timeline_item_index + 1)
11301130
})
1131-
.unwrap_or(0)
1131+
.unwrap_or_else(|| {
1132+
// We don't have any local echo, so we could insert at 0. However, in
1133+
// the case of an insertion caused by a pagination, we
1134+
// may have already pushed the start of the timeline item, so we need
1135+
// to check if the first item is that, and insert after it otherwise.
1136+
if self.items.get(0).is_some_and(|item| item.is_timeline_start()) {
1137+
1
1138+
} else {
1139+
0
1140+
}
1141+
})
11321142
});
11331143

11341144
trace!(
@@ -1225,6 +1235,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
12251235
/// `transaction_id` of `new_event_timeline_item` if it exists.
12261236
// Note: this method doesn't take `&mut self` to avoid a borrow checker
12271237
// conflict with `TimelineEventHandler::add_item`.
1238+
// TODO(bnjbvr): refactor
12281239
fn deduplicate_local_timeline_item(
12291240
items: &mut ObservableItemsTransaction<'_>,
12301241
new_event_timeline_item: &mut EventTimelineItem,

crates/matrix-sdk-ui/src/timeline/item.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,19 @@ impl TimelineItem {
106106
matches!(&self.kind, TimelineItemKind::Event(_))
107107
}
108108

109-
/// Check whether this item is a date divider.
110-
#[must_use]
109+
/// Check whether this item is a (virtual) date divider.
111110
pub fn is_date_divider(&self) -> bool {
112111
matches!(self.kind, TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(_)))
113112
}
114113

115114
pub(crate) fn is_read_marker(&self) -> bool {
116115
matches!(self.kind, TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker))
117116
}
117+
118+
/// Check whether this item is a (virtual) timeline start item.
119+
pub fn is_timeline_start(&self) -> bool {
120+
matches!(self.kind, TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart))
121+
}
118122
}
119123

120124
impl Deref for TimelineItem {

crates/matrix-sdk-ui/src/timeline/pagination.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ impl super::Timeline {
7575
// As an exceptional contract, restart the back-pagination if we received an
7676
// empty chunk.
7777
if outcome.reached_start || !outcome.events.is_empty() {
78+
if outcome.reached_start {
79+
self.controller.insert_timeline_start_if_missing().await;
80+
}
7881
return Ok(outcome.reached_start);
7982
}
8083
}
@@ -111,10 +114,22 @@ impl super::Timeline {
111114

112115
let current_value = status.next_now();
113116

117+
let controller = self.controller.clone();
114118
let stream = Box::pin(stream! {
115119
let status_stream = status.dedup();
120+
116121
pin_mut!(status_stream);
122+
117123
while let Some(state) = status_stream.next().await {
124+
match state {
125+
RoomPaginationStatus::Idle { hit_timeline_start } => {
126+
if hit_timeline_start {
127+
controller.insert_timeline_start_if_missing().await;
128+
}
129+
}
130+
RoomPaginationStatus::Paginating => {}
131+
}
132+
118133
yield state;
119134
}
120135
});

crates/matrix-sdk-ui/tests/integration/timeline/decryption.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,21 +318,29 @@ async fn test_an_utd_from_the_event_cache_as_a_paginated_item_is_decrypted() {
318318
// let's test everything :-).
319319
assert!(reached_start);
320320

321+
assert_next_matches_with_timeout!(updates_stream, 250, updates => {
322+
assert_eq!(updates.len(), 1, "We get the start of timeline item");
323+
324+
assert_matches!(&updates[0], VectorDiff::PushFront { value } => {
325+
assert!(value.is_timeline_start());
326+
});
327+
});
328+
321329
// Now, let's look at the updates. We must observe an update reflecting the UTD
322330
// has entered the `Timeline`.
323331
assert_next_matches_with_timeout!(updates_stream, 250, updates => {
324332
assert_eq!(updates.len(), 2, "Expecting 2 updates from the `Timeline`");
325333

326334
// UTD! UTD!
327-
assert_matches!(&updates[0], VectorDiff::Insert { index: 1, value: event } => {
335+
assert_matches!(&updates[0], VectorDiff::Insert { index: 2, value: event } => {
328336
assert_matches!(event.as_event(), Some(event) => {
329337
assert_eq!(event.event_id().unwrap().as_str(), "$ev0");
330338
assert!(event.content().is_unable_to_decrypt());
331339
});
332340
});
333341

334342
// UTD is decrypted now!
335-
assert_matches!(&updates[1], VectorDiff::Set { index: 1, value: event } => {
343+
assert_matches!(&updates[1], VectorDiff::Set { index: 2, value: event } => {
336344
assert_matches!(event.as_event(), Some(event) => {
337345
assert_eq!(event.event_id().unwrap().as_str(), "$ev0");
338346
assert_matches!(event.content().as_message(), Some(message) => {

crates/matrix-sdk-ui/tests/integration/timeline/pagination.rs

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,14 @@ async fn test_back_pagination() {
158158
RoomPaginationStatus::Idle { hit_timeline_start: true }
159159
);
160160

161+
// Timeline start is inserted.
162+
{
163+
assert_let!(Some(timeline_updates) = timeline_stream.next().await);
164+
assert_eq!(timeline_updates.len(), 1);
165+
assert_let!(VectorDiff::PushFront { value } = &timeline_updates[0]);
166+
assert!(value.is_timeline_start());
167+
}
168+
161169
assert_pending!(timeline_stream);
162170
}
163171

@@ -493,9 +501,9 @@ async fn test_timeline_reset_while_paginating() {
493501
// field.
494502
assert!(hit_start);
495503

496-
// No events in back-pagination responses, date divider + event from latest
497-
// sync is present
498-
assert_eq!(timeline.items().await.len(), 2);
504+
// No events in back-pagination responses, start of timeline + date divider +
505+
// event from latest sync is present
506+
assert_eq!(timeline.items().await.len(), 3);
499507

500508
// Make sure both pagination mocks were called
501509
server.verify().await;
@@ -781,14 +789,20 @@ async fn test_until_num_items_with_empty_chunk() {
781789
assert!(date_divider.is_date_divider());
782790
}
783791

784-
timeline.paginate_backwards(10).await.unwrap();
792+
let reached_start = timeline.paginate_backwards(10).await.unwrap();
793+
assert!(reached_start);
785794

786795
assert_let!(Some(timeline_updates) = timeline_stream.next().await);
796+
assert_eq!(timeline_updates.len(), 1);
797+
assert_let!(VectorDiff::PushFront { value } = &timeline_updates[0]);
798+
assert!(value.is_timeline_start());
787799

788800
// `m.room.name`: “hello room then”
789801
{
790-
assert_let!(VectorDiff::Insert { index, value: message } = &timeline_updates[0]);
791-
assert_eq!(*index, 1);
802+
assert_let!(Some(timeline_updates) = timeline_stream.next().await);
803+
assert_eq!(timeline_updates.len(), 1);
804+
805+
assert_let!(VectorDiff::Insert { index: 2, value: message } = &timeline_updates[0]);
792806
assert_let!(TimelineItemContent::Message(msg) = message.as_event().unwrap().content());
793807
assert_let!(MessageType::Text(text) = msg.msgtype());
794808
assert_eq!(text.body, "hello room then");
@@ -1073,13 +1087,21 @@ async fn test_lazy_back_pagination() {
10731087

10741088
assert!(hit_end_of_timeline);
10751089

1090+
// The start of the timeline is inserted as its own timeline update.
1091+
assert_timeline_stream! {
1092+
[timeline_stream]
1093+
prepend --- timeline start ---;
1094+
};
1095+
10761096
// Receive 3 new items.
10771097
//
1078-
// They are inserted after the date divider, hence the indices 1 and 2.
1098+
// They are inserted after the date divider and start of timeline, hence the
1099+
// indices 2 and 3.
1100+
10791101
assert_timeline_stream! {
10801102
[timeline_stream]
1081-
insert[1] "$ev201";
1082-
insert[2] "$ev200";
1103+
insert[2] "$ev201";
1104+
insert[3] "$ev200";
10831105
};
10841106
// So cool.
10851107
assert_pending!(timeline_stream);

crates/matrix-sdk-ui/tests/integration/timeline/read_receipts.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,18 +1198,20 @@ async fn test_no_duplicate_receipt_after_backpagination() {
11981198
.mount()
11991199
.await;
12001200

1201-
timeline.paginate_backwards(42).await.unwrap();
1201+
let reached_start = timeline.paginate_backwards(42).await.unwrap();
1202+
assert!(reached_start);
12021203

12031204
yield_now().await;
12041205

12051206
// Check that the receipts are at the correct place.
12061207
let timeline_items = timeline.items().await;
1207-
assert_eq!(timeline_items.len(), 3);
1208+
assert_eq!(timeline_items.len(), 4);
12081209

1209-
assert!(timeline_items[0].is_date_divider());
1210+
assert!(timeline_items[0].is_timeline_start());
1211+
assert!(timeline_items[1].is_date_divider());
12101212

12111213
{
1212-
let event1 = timeline_items[1].as_event().unwrap();
1214+
let event1 = timeline_items[2].as_event().unwrap();
12131215
// Sanity check: this is the edited event from Alice.
12141216
assert_eq!(event1.event_id().unwrap(), eid1);
12151217

@@ -1232,7 +1234,7 @@ async fn test_no_duplicate_receipt_after_backpagination() {
12321234
}
12331235

12341236
{
1235-
let event2 = timeline_items[2].as_event().unwrap();
1237+
let event2 = timeline_items[3].as_event().unwrap();
12361238

12371239
// Sanity check: this is Bob's event.
12381240
assert_eq!(event2.event_id().unwrap(), eid2);

crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,32 @@ macro_rules! assert_timeline_stream {
145145
)
146146
};
147147

148+
149+
// `prepend --- timeline start ---`
150+
( @_ [ $iterator:ident ] [ prepend --- timeline start --- ; $( $rest:tt )* ] [ $( $accumulator:tt )* ] ) => {
151+
assert_timeline_stream!(
152+
@_
153+
[ $iterator ]
154+
[ $( $rest )* ]
155+
[
156+
$( $accumulator )*
157+
{
158+
assert_matches!(
159+
$iterator .next(),
160+
Some(eyeball_im::VectorDiff::PushFront { value }) => {
161+
assert_matches!(
162+
&**value,
163+
matrix_sdk_ui::timeline::TimelineItemKind::Virtual(
164+
matrix_sdk_ui::timeline::VirtualTimelineItem::TimelineStart
165+
) => {}
166+
);
167+
}
168+
);
169+
}
170+
]
171+
)
172+
};
173+
148174
// `prepend "$event_id"`
149175
( @_ [ $iterator:ident ] [ prepend $event_id:literal ; $( $rest:tt )* ] [ $( $accumulator:tt )* ] ) => {
150176
assert_timeline_stream!(

0 commit comments

Comments
 (0)