From 93dc44ae90288c1e54f394d19b4d27ff4d88dbd2 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Wed, 12 Nov 2025 08:33:39 +0100 Subject: [PATCH 1/4] feat(timeline): utilize the cache and include common relations when focusing on an event without context Signed-off-by: Johannes Marbach --- crates/matrix-sdk-ui/CHANGELOG.md | 3 ++ .../src/timeline/controller/mod.rs | 48 ++++++++++++++----- .../tests/integration/timeline/mod.rs | 4 +- .../tests/integration/timeline/thread.rs | 2 +- 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/crates/matrix-sdk-ui/CHANGELOG.md b/crates/matrix-sdk-ui/CHANGELOG.md index 91ee1313f19..d093e6c3215 100644 --- a/crates/matrix-sdk-ui/CHANGELOG.md +++ b/crates/matrix-sdk-ui/CHANGELOG.md @@ -8,6 +8,9 @@ All notable changes to this project will be documented in this file. ### Features +- Utilize the cache and include common relations when focusing a timeline on an event without + requestion context. + ([#5858](https://github.com/matrix-org/matrix-rust-sdk/pull/5858)) - Add push actions to `NotificationItem`. ([#5835](https://github.com/matrix-org/matrix-rust-sdk/pull/5835)) - Add support for top level space ordering through [MSC3230](https://github.com/matrix-org/matrix-spec-proposals/pull/3230) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 342317d2edc..91c8bf29238 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -23,6 +23,7 @@ use imbl::Vector; #[cfg(test)] use matrix_sdk::Result; use matrix_sdk::{ + config::RequestConfig, deserialized_responses::TimelineEvent, event_cache::{RoomEventCache, RoomPaginationStatus}, paginators::{PaginationResult, PaginationToken, Paginator}, @@ -41,7 +42,7 @@ use ruma::{ poll::unstable_start::UnstablePollStartEventContent, reaction::ReactionEventContent, receipt::{Receipt, ReceiptThread, ReceiptType}, - relation::Annotation, + relation::{Annotation, RelationType}, room::message::{MessageType, Relation}, }, room_version_rules::RoomVersionRules, @@ -474,16 +475,40 @@ impl TimelineController

{ let event_paginator = Paginator::new(self.room_data_provider.clone()); - // Start a /context request so we can know if the event is in a thread or not, - // and know which kind of pagination we'll be using then. - let start_from_result = event_paginator - .start_from(event_id, (*num_context_events).into()) - .await - .map_err(PaginationError::Paginator)?; + let events = if *num_context_events == 0 { + // If no context is requested, try to load the event from the cache first and + // include common relations such as reactions and edits. + let request_config = Some(RequestConfig::default().retry_limit(3)); + let relations_filter = + Some(vec![RelationType::Annotation, RelationType::Replacement]); + + // Load the event from the cache or, failing that, the server. + match self + .room_data_provider + .load_event_with_relations(event_id, request_config, relations_filter) + .await + { + Ok((event, related_events)) => { + let mut events = vec![event]; + events.extend(related_events); + events + } + Err(err) => { + warn!("error when loading focussed event: {err}"); + vec![] + } + } + } else { + // Start a /context request to load the focussed event and surrounding events. + let start_from_result = event_paginator + .start_from(event_id, (*num_context_events).into()) + .await + .map_err(PaginationError::Paginator)?; + start_from_result.events + }; // Find the target event, and see if it's part of a thread. - let thread_root_event_id = start_from_result - .events + let thread_root_event_id = events .iter() .find( |event| { @@ -499,7 +524,7 @@ impl TimelineController

{ // Look if the thread root event is part of the /context response. This // allows us to spare some backwards pagination with // /relations. - let includes_root_event = start_from_result.events.iter().any(|event| { + let includes_root_event = events.iter().any(|event| { if let Some(id) = event.event_id() { id == root_id } else { false } }); @@ -522,8 +547,7 @@ impl TimelineController

{ }, }); - let has_events = !start_from_result.events.is_empty(); - let events = start_from_result.events; + let has_events = !events.is_empty(); match paginator.get().expect("Paginator was not instantiated") { AnyPaginator::Unthreaded { .. } => { diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs index 19d18604b0f..db40450a484 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs @@ -116,7 +116,7 @@ async fn test_timeline_is_threaded() { let timeline = TimelineBuilder::new(&room) .with_focus(TimelineFocus::Event { target: owned_event_id!("$target"), - num_context_events: 0, + num_context_events: 2, hide_threaded_events: true, }) .build() @@ -147,7 +147,7 @@ async fn test_timeline_is_threaded() { let timeline = TimelineBuilder::new(&room) .with_focus(TimelineFocus::Event { target: owned_event_id!("$target"), - num_context_events: 0, + num_context_events: 2, hide_threaded_events: true, }) .build() diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/thread.rs b/crates/matrix-sdk-ui/tests/integration/timeline/thread.rs index 8a1c38f79f0..8fb2c5a7ffb 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/thread.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/thread.rs @@ -1848,7 +1848,7 @@ async fn test_permalink_doesnt_listen_to_thread_sync() { let timeline = TimelineBuilder::new(&room) .with_focus(TimelineFocus::Event { target: owned_event_id!("$target"), - num_context_events: 0, + num_context_events: 2, hide_threaded_events: true, }) .build() From b4f5e1acee796a77749117c104701e04c3d9ab71 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Tue, 25 Nov 2025 14:25:15 +0100 Subject: [PATCH 2/4] fixup! feat(timeline): utilize the cache and include common relations when focusing on an event without context Switch to error and fallback to /context when loading single event fails Signed-off-by: Johannes Marbach --- .../src/timeline/controller/mod.rs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 20e3f5b1db9..6e323c24af1 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -468,6 +468,15 @@ impl TimelineController

{ let event_paginator = Paginator::new(self.room_data_provider.clone()); + let load_events_with_context = || async { + // Start a /context request to load the focussed event and surrounding events. + event_paginator + .start_from(event_id, (*num_context_events).into()) + .await + .map(|r| r.events) + .map_err(PaginationError::Paginator) + }; + let events = if *num_context_events == 0 { // If no context is requested, try to load the event from the cache first and // include common relations such as reactions and edits. @@ -487,17 +496,14 @@ impl TimelineController

{ events } Err(err) => { - warn!("error when loading focussed event: {err}"); - vec![] + error!("error when loading focussed event: {err}"); + // Fall back to load the focussed event using /context. + load_events_with_context().await? } } } else { // Start a /context request to load the focussed event and surrounding events. - let start_from_result = event_paginator - .start_from(event_id, (*num_context_events).into()) - .await - .map_err(PaginationError::Paginator)?; - start_from_result.events + load_events_with_context().await? }; // Find the target event, and see if it's part of a thread. From e27d0d5b7e189c2176d44753dbaefcb457427951 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Tue, 25 Nov 2025 15:26:35 +0100 Subject: [PATCH 3/4] fixup! feat(timeline): utilize the cache and include common relations when focusing on an event without context Add tests with num_context_events = 0 Signed-off-by: Johannes Marbach --- .../tests/integration/timeline/mod.rs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs index db40450a484..6a91522e739 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs @@ -97,6 +97,57 @@ async fn test_timeline_is_threaded() { assert!(timeline.is_threaded()); } + { + // An event-focused timeline, focused on a non-thread event, isn't threaded when + // no context is requested. + let f = EventFactory::new(); + let event = f + .text_msg("hello world") + .event_id(event_id!("$target")) + .room(room_id) + .sender(&ALICE) + .into_event(); + server.mock_room_event().match_event_id().ok(event).mock_once().mount().await; + + let timeline = TimelineBuilder::new(&room) + .with_focus(TimelineFocus::Event { + target: owned_event_id!("$target"), + num_context_events: 0, + hide_threaded_events: true, + }) + .build() + .await + .unwrap(); + assert!(timeline.is_threaded().not()); + } + + { + // But an event-focused timeline, focused on an in-thread event, is threaded + // when no context is requested \o/ + let f = EventFactory::new(); + let thread_root = event_id!("$thread_root"); + let event = f + .text_msg("hey to you too") + .event_id(event_id!("$target")) + .in_thread(thread_root, thread_root) + .room(room_id) + .sender(&ALICE) + .into_event(); + + server.mock_room_event().match_event_id().ok(event).mock_once().mount().await; + + let timeline = TimelineBuilder::new(&room) + .with_focus(TimelineFocus::Event { + target: owned_event_id!("$target"), + num_context_events: 0, + hide_threaded_events: true, + }) + .build() + .await + .unwrap(); + assert!(timeline.is_threaded()); + } + { // An event-focused timeline, focused on a non-thread event, isn't threaded. let f = EventFactory::new(); From 37d56be98fcdca5d6f4d2728e02697cbcbd7309e Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Tue, 25 Nov 2025 15:37:12 +0100 Subject: [PATCH 4/4] fixup! feat(timeline): utilize the cache and include common relations when focusing on an event without context Use different event ID to work around spurious test failure Signed-off-by: Johannes Marbach --- crates/matrix-sdk-ui/tests/integration/timeline/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs index 6a91522e739..ddb9a37bbeb 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs @@ -128,7 +128,7 @@ async fn test_timeline_is_threaded() { let thread_root = event_id!("$thread_root"); let event = f .text_msg("hey to you too") - .event_id(event_id!("$target")) + .event_id(event_id!("$thetarget")) .in_thread(thread_root, thread_root) .room(room_id) .sender(&ALICE) @@ -138,7 +138,7 @@ async fn test_timeline_is_threaded() { let timeline = TimelineBuilder::new(&room) .with_focus(TimelineFocus::Event { - target: owned_event_id!("$target"), + target: owned_event_id!("$thetarget"), num_context_events: 0, hide_threaded_events: true, })