diff --git a/bindings/matrix-sdk-ffi/src/timeline.rs b/bindings/matrix-sdk-ffi/src/timeline.rs index 49720bfd1..8aa1c8a5b 100644 --- a/bindings/matrix-sdk-ffi/src/timeline.rs +++ b/bindings/matrix-sdk-ffi/src/timeline.rs @@ -160,6 +160,7 @@ impl TimelineItem { } Item::Virtual(VItem::ReadMarker) => Some(VirtualTimelineItem::ReadMarker), Item::Virtual(VItem::LoadingIndicator) => Some(VirtualTimelineItem::LoadingIndicator), + Item::Virtual(VItem::TimelineStart) => Some(VirtualTimelineItem::TimelineStart), Item::Event(_) => None, } } @@ -596,6 +597,12 @@ pub enum VirtualTimelineItem { /// A loading indicator for a pagination request. LoadingIndicator, + + /// The beginning of the visible timeline. + /// + /// There might be earlier events the user is not allowed to see due to + /// history visibility. + TimelineStart, } #[extension_trait] diff --git a/crates/matrix-sdk/src/room/timeline/event_handler.rs b/crates/matrix-sdk/src/room/timeline/event_handler.rs index 82c2f34d1..6a7ede3de 100644 --- a/crates/matrix-sdk/src/room/timeline/event_handler.rs +++ b/crates/matrix-sdk/src/room/timeline/event_handler.rs @@ -525,7 +525,10 @@ impl<'a, 'i> TimelineEventHandler<'a, 'i> { // divider at position 1 and the new event at 2 rather than 0 and 1. let offset = match self.timeline_items.first().and_then(|item| item.as_virtual()) { - Some(VirtualTimelineItem::LoadingIndicator) => 1, + Some( + VirtualTimelineItem::LoadingIndicator + | VirtualTimelineItem::TimelineStart, + ) => 1, _ => 0, }; diff --git a/crates/matrix-sdk/src/room/timeline/inner.rs b/crates/matrix-sdk/src/room/timeline/inner.rs index c4cbd5cdb..ab6dcac85 100644 --- a/crates/matrix-sdk/src/room/timeline/inner.rs +++ b/crates/matrix-sdk/src/room/timeline/inner.rs @@ -156,15 +156,19 @@ impl TimelineInner { lock.insert_cloned(0, Arc::new(TimelineItem::loading_indicator())); } - #[instrument(skip_all)] - pub(super) fn remove_loading_indicator(&self) { + #[instrument(skip(self))] + pub(super) fn remove_loading_indicator(&self, more_messages: bool) { let mut lock = self.items.lock_mut(); if !lock.first().map_or(false, |item| item.is_loading_indicator()) { warn!("There is no loading indicator"); return; } - lock.remove(0); + if more_messages { + lock.remove(0); + } else { + lock.set_cloned(0, Arc::new(TimelineItem::timeline_start())) + } } pub(super) async fn handle_fully_read(&self, raw: Raw) { diff --git a/crates/matrix-sdk/src/room/timeline/mod.rs b/crates/matrix-sdk/src/room/timeline/mod.rs index 1f08f3540..a4853271c 100644 --- a/crates/matrix-sdk/src/room/timeline/mod.rs +++ b/crates/matrix-sdk/src/room/timeline/mod.rs @@ -182,8 +182,9 @@ impl Timeline { num_updates += self.inner.handle_back_paginated_event(room_ev, own_user_id).await; } - self.inner.remove_loading_indicator(); - let outcome = PaginationOutcome { more_messages: messages.end.is_some(), num_updates }; + let more_messages = messages.end.is_some(); + self.inner.remove_loading_indicator(more_messages); + let outcome = PaginationOutcome { more_messages, num_updates }; *start_lock = messages.end; Ok(outcome) @@ -343,6 +344,10 @@ impl TimelineItem { Self::Virtual(VirtualTimelineItem::LoadingIndicator) } + fn timeline_start() -> Self { + Self::Virtual(VirtualTimelineItem::TimelineStart) + } + fn is_read_marker(&self) -> bool { matches!(self, Self::Virtual(VirtualTimelineItem::ReadMarker)) } diff --git a/crates/matrix-sdk/src/room/timeline/virtual_item.rs b/crates/matrix-sdk/src/room/timeline/virtual_item.rs index 2bd5c7aa8..ff8b21ffe 100644 --- a/crates/matrix-sdk/src/room/timeline/virtual_item.rs +++ b/crates/matrix-sdk/src/room/timeline/virtual_item.rs @@ -34,4 +34,10 @@ pub enum VirtualTimelineItem { /// A loading indicator for a pagination request. LoadingIndicator, + + /// The beginning of the visible timeline. + /// + /// There might be earlier events the user is not allowed to see due to + /// history visibility. + TimelineStart, } diff --git a/crates/matrix-sdk/tests/integration/room/timeline.rs b/crates/matrix-sdk/tests/integration/room/timeline.rs index 049b3504a..7b32b3bec 100644 --- a/crates/matrix-sdk/tests/integration/room/timeline.rs +++ b/crates/matrix-sdk/tests/integration/room/timeline.rs @@ -260,6 +260,7 @@ async fn back_pagination() { .await; timeline.paginate_backwards(uint!(10)).await.unwrap(); + server.reset().await; let loading = assert_matches!( timeline_stream.next().await, @@ -297,6 +298,34 @@ async fn back_pagination() { // Removal of the loading indicator assert_matches!(timeline_stream.next().await, Some(VecDiff::RemoveAt { index: 0 })); + + Mock::given(method("GET")) + .and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$")) + .and(header("authorization", "Bearer 1234")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + // Usually there would be a few events here, but we just want to test + // that the timeline start item is added when there is no end token + "chunk": [], + "start": "t47409-4357353_219380_26003_2269" + }))) + .expect(1) + .named("messages_batch_1") + .mount(&server) + .await; + + timeline.paginate_backwards(uint!(10)).await.unwrap(); + + let loading = assert_matches!( + timeline_stream.next().await, + Some(VecDiff::InsertAt { index: 0, value }) => value + ); + assert_matches!(loading.as_virtual().unwrap(), VirtualTimelineItem::LoadingIndicator); + + let loading = assert_matches!( + timeline_stream.next().await, + Some(VecDiff::UpdateAt { index: 0, value }) => value + ); + assert_matches!(loading.as_virtual().unwrap(), VirtualTimelineItem::TimelineStart); } #[async_test]