feat(timeline): utilize the cache and include common relations when focusing on an event without context

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
This commit is contained in:
Johannes Marbach
2025-11-12 08:33:39 +01:00
committed by Ivan Enderlin
parent 9b15400936
commit dfd607f195
4 changed files with 120 additions and 18 deletions

View File

@@ -18,6 +18,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))
- [**breaking**] `EventTimelineItem::get_shield` now returns a new type,
`TimelineEventShieldState`, which extends the old `ShieldState` with a code
for `SentInClear`, now that the latter has been removed from `ShieldState`.

View File

@@ -22,6 +22,7 @@ use imbl::Vector;
#[cfg(test)]
use matrix_sdk::Result;
use matrix_sdk::{
config::RequestConfig,
deserialized_responses::TimelineEvent,
event_cache::{DecryptionRetryRequest, RoomEventCache, RoomPaginationStatus},
paginators::{PaginationResult, PaginationToken, Paginator},
@@ -40,7 +41,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,
@@ -469,16 +470,46 @@ impl<P: RoomDataProvider> TimelineController<P> {
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 load_events_with_context = || async {
// Start a /context request to load the focused 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.
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) => {
error!("error when loading focussed event: {err}");
// Fall back to load the focused event using /context.
load_events_with_context().await?
}
}
} else {
// Start a /context request to load the focussed event and surrounding events.
load_events_with_context().await?
};
// 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| {
@@ -494,7 +525,7 @@ impl<P: RoomDataProvider> TimelineController<P> {
// 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 }
});
@@ -517,8 +548,7 @@ impl<P: RoomDataProvider> TimelineController<P> {
},
});
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 { .. } => {

View File

@@ -20,7 +20,9 @@ use eyeball_im::VectorDiff;
use futures_util::StreamExt;
use matrix_sdk::{
linked_chunk::{ChunkIdentifier, LinkedChunkId, Position, Update},
test_utils::mocks::{MatrixMockServer, RoomContextResponseTemplate},
test_utils::mocks::{
MatrixMockServer, RoomContextResponseTemplate, RoomRelationsResponseTemplate,
},
};
use matrix_sdk_test::{
ALICE, BOB, JoinedRoomBuilder, RoomAccountDataTestEvent, StateTestEvent, async_test,
@@ -37,13 +39,15 @@ use matrix_sdk_ui::{
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, event_id,
events::{
MessageLikeEventType, TimelineEventType,
AnyTimelineEvent, MessageLikeEventType, TimelineEventType,
room::{
encryption::RoomEncryptionEventContent,
message::{RedactedRoomMessageEventContent, RoomMessageEventContent},
},
},
owned_event_id, room_id, user_id,
owned_event_id, room_id,
serde::Raw,
user_id,
};
use serde_json::json;
use sliding_sync::assert_timeline_stream;
@@ -97,6 +101,71 @@ 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_id = event_id!("$target");
let event =
f.text_msg("hello world").event_id(event_id).room(room_id).sender(&ALICE).into_event();
server.mock_room_event().match_event_id().ok(event).mock_once().mount().await;
server
.mock_room_relations()
.match_target_event(event_id.to_owned())
.ok(RoomRelationsResponseTemplate::default()
.events(Vec::<Raw<AnyTimelineEvent>>::new()))
.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_id = event_id!("$thetarget");
let event = f
.text_msg("hey to you too")
.event_id(event_id)
.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;
server
.mock_room_relations()
.match_target_event(event_id.to_owned())
.ok(RoomRelationsResponseTemplate::default()
.events(Vec::<Raw<AnyTimelineEvent>>::new()))
.mock_once()
.mount()
.await;
let timeline = TimelineBuilder::new(&room)
.with_focus(TimelineFocus::Event {
target: owned_event_id!("$thetarget"),
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();
@@ -116,7 +185,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 +216,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()

View File

@@ -1859,7 +1859,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()