feat(room): make load_event_with_relations also load relations when falling back to the network
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
This commit is contained in:
committed by
Andy Balaam
parent
238e4e8a87
commit
9105db3d82
@@ -13,6 +13,9 @@ All notable changes to this project will be documented in this file.
|
||||
`EventTimelineItem::from_latest_event`, and `Timeline::latest_event`. See the
|
||||
documentation of `matrix_sdk::latest_event` to learn about the new API.
|
||||
[#5624](https://github.com/matrix-org/matrix-rust-sdk/pull/5624/)
|
||||
- `Room::load_event_with_relations` now also calls `/relations` to fetch related events when falling back
|
||||
to network mode after a cache miss.
|
||||
([#5930](https://github.com/matrix-org/matrix-rust-sdk/pull/5930))
|
||||
|
||||
## [0.16.0] - 2025-12-04
|
||||
|
||||
|
||||
@@ -15,9 +15,15 @@
|
||||
use std::{fmt::Formatter, sync::Arc};
|
||||
|
||||
use futures_util::{StreamExt, stream};
|
||||
use matrix_sdk::{BoxFuture, Room, SendOutsideWasm, SyncOutsideWasm, config::RequestConfig};
|
||||
use matrix_sdk::{
|
||||
BoxFuture, Room, SendOutsideWasm, SyncOutsideWasm,
|
||||
config::RequestConfig,
|
||||
room::{IncludeRelations, RelationsOptions},
|
||||
};
|
||||
use matrix_sdk_base::deserialized_responses::TimelineEvent;
|
||||
use ruma::{EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, events::relation::RelationType};
|
||||
use ruma::{
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, events::relation::RelationType, uint,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{debug, warn};
|
||||
@@ -180,7 +186,28 @@ impl PinnedEventsRoom for Room {
|
||||
}
|
||||
|
||||
debug!("Loading pinned event {event_id} from HS");
|
||||
self.event(event_id, request_config).await.map(|e| (e, Vec::new()))
|
||||
let event = self.event(event_id, request_config).await?;
|
||||
|
||||
debug!("Loading relations of pinned event {event_id} from HS");
|
||||
let mut related_events = Vec::new();
|
||||
let mut opts = RelationsOptions {
|
||||
include_relations: IncludeRelations::AllRelations,
|
||||
recurse: true,
|
||||
limit: Some(uint!(1000)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
loop {
|
||||
let relations = self.relations(event_id.to_owned(), opts.clone()).await?;
|
||||
related_events.extend(relations.chunk);
|
||||
if let Some(next_from) = relations.next_batch_token {
|
||||
opts.from = Some(next_from);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((event, related_events))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ use matrix_sdk::{
|
||||
config::SyncSettings,
|
||||
test_utils::{
|
||||
logged_in_client_with_server,
|
||||
mocks::{MatrixMockServer, RoomMessagesResponseTemplate},
|
||||
mocks::{MatrixMockServer, RoomMessagesResponseTemplate, RoomRelationsResponseTemplate},
|
||||
},
|
||||
};
|
||||
use matrix_sdk_base::deserialized_responses::TimelineEvent;
|
||||
@@ -22,7 +22,7 @@ use ruma::{
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, RoomId, UserId, assign,
|
||||
event_id,
|
||||
events::{
|
||||
AnySyncTimelineEvent,
|
||||
AnySyncTimelineEvent, AnyTimelineEvent,
|
||||
room::{
|
||||
encrypted::{
|
||||
EncryptedEventScheme, MegolmV1AesSha2ContentInit, RoomEncryptedEventContent,
|
||||
@@ -52,15 +52,26 @@ async fn test_new_pinned_events_are_not_added_on_sync() {
|
||||
let room_id = room_id!("!test:localhost");
|
||||
|
||||
let f = EventFactory::new().room(room_id).sender(*BOB);
|
||||
let event_id = event_id!("$1");
|
||||
let event_1 = f
|
||||
.text_msg("in the end")
|
||||
.event_id(event_id!("$1"))
|
||||
.event_id(event_id)
|
||||
.server_ts(MilliSecondsSinceUnixEpoch::now())
|
||||
.into_raw_sync();
|
||||
|
||||
// Mock /event endpoint for a timeline event
|
||||
// Mock /event endpoint for event $1.
|
||||
mock_events_endpoint(&server, room_id, vec![event_1]).await;
|
||||
|
||||
// Mock /relations for event $1.
|
||||
server
|
||||
.mock_room_relations()
|
||||
.match_target_event(event_id.to_owned())
|
||||
.match_limit(1000)
|
||||
.ok(RoomRelationsResponseTemplate::default().events(Vec::<Raw<AnyTimelineEvent>>::new()))
|
||||
.mock_once()
|
||||
.mount()
|
||||
.await;
|
||||
|
||||
// Load initial timeline items: a `m.room.pinned_events` with events $1 and $2
|
||||
// pinned
|
||||
let room = PinnedEventsSync::new(room_id)
|
||||
@@ -103,6 +114,140 @@ async fn test_new_pinned_events_are_not_added_on_sync() {
|
||||
assert_pending!(timeline_stream);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_pinned_event_with_reaction() {
|
||||
let server = MatrixMockServer::new().await;
|
||||
let client = server.client_builder().build().await;
|
||||
let room_id = room_id!("!test:localhost");
|
||||
|
||||
let f = EventFactory::new().room(room_id).sender(*BOB);
|
||||
let event_id = event_id!("$1");
|
||||
let event_1 = f
|
||||
.text_msg("in the end")
|
||||
.event_id(event_id)
|
||||
.server_ts(MilliSecondsSinceUnixEpoch::now())
|
||||
.into_raw_sync();
|
||||
|
||||
let reaction = f.reaction(event_id, "👀").into_raw_timeline();
|
||||
|
||||
// Mock /event endpoint for event $1.
|
||||
mock_events_endpoint(&server, room_id, vec![event_1]).await;
|
||||
|
||||
// Mock /relations for event $1.
|
||||
server
|
||||
.mock_room_relations()
|
||||
.match_target_event(event_id.to_owned())
|
||||
.match_limit(1000)
|
||||
.ok(RoomRelationsResponseTemplate::default().events(vec![reaction]))
|
||||
.mock_once()
|
||||
.mount()
|
||||
.await;
|
||||
|
||||
// Load initial timeline items: a `m.room.pinned_events` with event $1 pinned
|
||||
let room = PinnedEventsSync::new(room_id)
|
||||
.with_pinned_event_ids(vec!["$1"])
|
||||
.mock_and_sync(&client, &server)
|
||||
.await
|
||||
.expect("Room should be synced");
|
||||
let timeline =
|
||||
TimelineBuilder::new(&room).with_focus(pinned_events_focus(100)).build().await.unwrap();
|
||||
|
||||
assert!(
|
||||
timeline.live_back_pagination_status().await.is_none(),
|
||||
"there should be no live back-pagination status for a pinned events timeline"
|
||||
);
|
||||
|
||||
// Load timeline items
|
||||
let (items, mut timeline_stream) = timeline.subscribe().await;
|
||||
|
||||
// Verify that the timeline contains the pinned event and its reaction.
|
||||
assert_eq!(items.len(), 1 + 1); // event item + a date divider
|
||||
assert!(items[0].is_date_divider());
|
||||
assert_eq!(items[1].as_event().unwrap().content().as_message().unwrap().body(), "in the end");
|
||||
let reactions = items[1]
|
||||
.as_event()
|
||||
.unwrap()
|
||||
.content()
|
||||
.reactions()
|
||||
.expect("pinned event should have reactions");
|
||||
assert_eq!(reactions.len(), 1);
|
||||
assert!(reactions.get("👀").is_some());
|
||||
assert_pending!(timeline_stream);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_pinned_event_with_paginated_reactions() {
|
||||
let server = MatrixMockServer::new().await;
|
||||
let client = server.client_builder().build().await;
|
||||
let room_id = room_id!("!test:localhost");
|
||||
|
||||
let f = EventFactory::new().room(room_id).sender(*BOB);
|
||||
let event_id = event_id!("$1");
|
||||
let event_1 = f
|
||||
.text_msg("in the end")
|
||||
.event_id(event_id)
|
||||
.server_ts(MilliSecondsSinceUnixEpoch::now())
|
||||
.into_raw_sync();
|
||||
|
||||
let reaction_1 = f.reaction(event_id, "👀").into_raw_timeline();
|
||||
let reaction_2 = f.reaction(event_id, "🤔").into_raw_timeline();
|
||||
|
||||
// Mock /event endpoint for event $1.
|
||||
mock_events_endpoint(&server, room_id, vec![event_1]).await;
|
||||
|
||||
// Mock /relations for event $1, split across two pages.
|
||||
let token = "foo";
|
||||
server
|
||||
.mock_room_relations()
|
||||
.match_target_event(event_id.to_owned())
|
||||
.match_limit(1000)
|
||||
.ok(RoomRelationsResponseTemplate::default().events(vec![reaction_1]).next_batch(token))
|
||||
.mock_once()
|
||||
.mount()
|
||||
.await;
|
||||
server
|
||||
.mock_room_relations()
|
||||
.match_target_event(event_id.to_owned())
|
||||
.match_limit(1000)
|
||||
.match_from(token)
|
||||
.ok(RoomRelationsResponseTemplate::default().events(vec![reaction_2]))
|
||||
.mock_once()
|
||||
.mount()
|
||||
.await;
|
||||
|
||||
// Load initial timeline items: a `m.room.pinned_events` with event $1 pinned
|
||||
let room = PinnedEventsSync::new(room_id)
|
||||
.with_pinned_event_ids(vec!["$1"])
|
||||
.mock_and_sync(&client, &server)
|
||||
.await
|
||||
.expect("Room should be synced");
|
||||
let timeline =
|
||||
TimelineBuilder::new(&room).with_focus(pinned_events_focus(100)).build().await.unwrap();
|
||||
|
||||
assert!(
|
||||
timeline.live_back_pagination_status().await.is_none(),
|
||||
"there should be no live back-pagination status for a pinned events timeline"
|
||||
);
|
||||
|
||||
// Load timeline items
|
||||
let (items, mut timeline_stream) = timeline.subscribe().await;
|
||||
|
||||
// Verify that the timeline contains the pinned event and its reactions.
|
||||
assert_eq!(items.len(), 1 + 1); // event item + a date divider
|
||||
assert!(items[0].is_date_divider());
|
||||
assert_eq!(items[1].as_event().unwrap().content().as_message().unwrap().body(), "in the end");
|
||||
let reactions = items[1]
|
||||
.as_event()
|
||||
.unwrap()
|
||||
.content()
|
||||
.reactions()
|
||||
.expect("pinned event should have reactions");
|
||||
assert_eq!(reactions.len(), 2);
|
||||
assert!(reactions.get("👀").is_some());
|
||||
assert!(reactions.get("🤔").is_some());
|
||||
assert_pending!(timeline_stream);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_new_pinned_event_ids_reload_the_timeline() {
|
||||
let server = MatrixMockServer::new().await;
|
||||
@@ -110,20 +255,42 @@ async fn test_new_pinned_event_ids_reload_the_timeline() {
|
||||
let room_id = room_id!("!test:localhost");
|
||||
|
||||
let f = EventFactory::new().room(room_id).sender(*BOB);
|
||||
let event_id_1 = event_id!("$1");
|
||||
let event_1 = f
|
||||
.text_msg("in the end")
|
||||
.event_id(event_id!("$1"))
|
||||
.event_id(event_id_1)
|
||||
.server_ts(MilliSecondsSinceUnixEpoch::now())
|
||||
.into_raw_sync();
|
||||
let event_id_2 = event_id!("$2");
|
||||
let event_2 = f
|
||||
.text_msg("it doesn't even matter")
|
||||
.event_id(event_id!("$2"))
|
||||
.event_id(event_id_2)
|
||||
.server_ts(MilliSecondsSinceUnixEpoch::now())
|
||||
.into_raw_sync();
|
||||
|
||||
// Mock /event endpoint for events $1 and $2.
|
||||
mock_events_endpoint(&server, room_id, vec![event_1.clone(), event_2.clone()]).await;
|
||||
|
||||
// Mock /relations endpoint for events $1 and $2.
|
||||
server
|
||||
.mock_room_relations()
|
||||
.match_target_event(event_id_1.to_owned())
|
||||
.match_limit(1000)
|
||||
.ok(RoomRelationsResponseTemplate::default().events(Vec::<Raw<AnyTimelineEvent>>::new()))
|
||||
.mock_once()
|
||||
.mount()
|
||||
.await;
|
||||
server
|
||||
.mock_room_relations()
|
||||
.match_target_event(event_id_2.to_owned())
|
||||
.match_limit(1000)
|
||||
.ok(RoomRelationsResponseTemplate::default().events(Vec::<Raw<AnyTimelineEvent>>::new()))
|
||||
.mock_once()
|
||||
.mount()
|
||||
.await;
|
||||
|
||||
// Load initial timeline items: 2 text messages and a `m.room.pinned_events`
|
||||
// with event $1 and $2 pinned
|
||||
mock_events_endpoint(&server, room_id, vec![event_1.clone(), event_2.clone()]).await;
|
||||
let room = PinnedEventsSync::new(room_id)
|
||||
.with_timeline_events(vec![event_2.clone()])
|
||||
.with_pinned_event_ids(vec!["$1"])
|
||||
@@ -225,15 +392,26 @@ async fn test_cached_events_are_kept_for_different_room_instances() {
|
||||
event_cache.subscribe().unwrap();
|
||||
|
||||
let f = EventFactory::new().room(room_id).sender(*BOB);
|
||||
let event_id = event_id!("$1");
|
||||
let pinned_event = f
|
||||
.text_msg("in the end")
|
||||
.event_id(event_id!("$1"))
|
||||
.event_id(event_id)
|
||||
.server_ts(MilliSecondsSinceUnixEpoch::now())
|
||||
.into_raw_sync();
|
||||
|
||||
// Mock /event for some timeline events
|
||||
// Mock /event for the pinned event.
|
||||
mock_events_endpoint(&server, room_id, vec![pinned_event]).await;
|
||||
|
||||
// Mock /relations for the pinned event.
|
||||
server
|
||||
.mock_room_relations()
|
||||
.match_target_event(event_id.to_owned())
|
||||
.match_limit(1000)
|
||||
.ok(RoomRelationsResponseTemplate::default().events(Vec::<Raw<AnyTimelineEvent>>::new()))
|
||||
.mock_once()
|
||||
.mount()
|
||||
.await;
|
||||
|
||||
// Load initial timeline items: a `m.room.pinned_events` with event $1 and $2
|
||||
// pinned
|
||||
let room = PinnedEventsSync::new(room_id)
|
||||
@@ -427,6 +605,14 @@ async fn test_pinned_timeline_with_pinned_utd_on_sync_contains_it() {
|
||||
// Mock encrypted pinned event for which we don't have keys (an UTD)
|
||||
let utd_event = create_utd(room_id, &sender_id, event_id);
|
||||
mock_events_endpoint(&server, room_id, vec![utd_event]).await;
|
||||
server
|
||||
.mock_room_relations()
|
||||
.match_target_event(event_id.to_owned())
|
||||
.match_limit(1000)
|
||||
.ok(RoomRelationsResponseTemplate::default().events(Vec::<Raw<AnyTimelineEvent>>::new()))
|
||||
.mock_once()
|
||||
.mount()
|
||||
.await;
|
||||
|
||||
let timeline =
|
||||
TimelineBuilder::new(&room).with_focus(pinned_events_focus(1)).build().await.unwrap();
|
||||
@@ -445,15 +631,26 @@ async fn test_edited_events_are_reflected_in_sync() {
|
||||
let room_id = room_id!("!test:localhost");
|
||||
|
||||
let f = EventFactory::new().room(room_id).sender(*BOB);
|
||||
let event_id = event_id!("$1");
|
||||
let pinned_event = f
|
||||
.text_msg("in the end")
|
||||
.event_id(event_id!("$1"))
|
||||
.event_id(event_id)
|
||||
.server_ts(MilliSecondsSinceUnixEpoch::now())
|
||||
.into_raw_sync();
|
||||
|
||||
// Mock /event for some timeline events.
|
||||
// Mock /event for the pinned event.
|
||||
mock_events_endpoint(&server, room_id, vec![pinned_event]).await;
|
||||
|
||||
// Mock /relations for the pinned event.
|
||||
server
|
||||
.mock_room_relations()
|
||||
.match_target_event(event_id.to_owned())
|
||||
.match_limit(1000)
|
||||
.ok(RoomRelationsResponseTemplate::default().events(Vec::<Raw<AnyTimelineEvent>>::new()))
|
||||
.mock_once()
|
||||
.mount()
|
||||
.await;
|
||||
|
||||
// Load initial timeline items: a text message and a `m.room.pinned_events` with
|
||||
// event $1.
|
||||
let room = PinnedEventsSync::new(room_id)
|
||||
@@ -517,15 +714,26 @@ async fn test_redacted_events_are_reflected_in_sync() {
|
||||
let room_id = room_id!("!test:localhost");
|
||||
|
||||
let f = EventFactory::new().room(room_id).sender(*BOB);
|
||||
let event_id = event_id!("$1");
|
||||
let pinned_event = f
|
||||
.text_msg("in the end")
|
||||
.event_id(event_id!("$1"))
|
||||
.server_ts(MilliSecondsSinceUnixEpoch::now())
|
||||
.into_raw_sync();
|
||||
|
||||
// Mock /event for some timeline events
|
||||
// Mock /event for the pinned timeline event.
|
||||
mock_events_endpoint(&server, room_id, vec![pinned_event]).await;
|
||||
|
||||
// Mock /relations for pinned timeline event.
|
||||
server
|
||||
.mock_room_relations()
|
||||
.match_target_event(event_id.to_owned())
|
||||
.match_limit(1000)
|
||||
.ok(RoomRelationsResponseTemplate::default().events(Vec::<Raw<AnyTimelineEvent>>::new()))
|
||||
.mock_once()
|
||||
.mount()
|
||||
.await;
|
||||
|
||||
// Load initial timeline items: a text message and a `m.room.pinned_events` with
|
||||
// event $1
|
||||
let room = PinnedEventsSync::new(room_id)
|
||||
|
||||
Reference in New Issue
Block a user