From efda26ef6fe579fa3755440403e6f855b8b75d14 Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Wed, 28 May 2025 19:40:45 +0200 Subject: [PATCH] feat(timeline): forward the `TimelineEvent::thread_summary` to timeline items Right now, we're not passing the latest event yet, but we could improve that later! --- .../src/deserialized_responses.rs | 9 +++ .../timeline/controller/state_transaction.rs | 11 +++- .../src/timeline/event_handler.rs | 31 ++++++--- .../src/timeline/event_item/content/reply.rs | 4 ++ .../src/timeline/event_item/mod.rs | 2 +- .../tests/integration/timeline/thread.rs | 66 +++++++++++++++++-- 6 files changed, 105 insertions(+), 18 deletions(-) diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index e1402ceeb..f37d6ca01 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -392,6 +392,15 @@ impl ThreadSummaryStatus { fn is_unknown(&self) -> bool { matches!(self, ThreadSummaryStatus::Unknown) } + + /// Transforms the [`ThreadSummaryStatus`] into an optional thread summary, + /// for cases where we don't care about distinguishing unknown and none. + pub fn summary(&self) -> Option<&ThreadSummary> { + match self { + ThreadSummaryStatus::Unknown | ThreadSummaryStatus::None => None, + ThreadSummaryStatus::Some(thread_summary) => Some(thread_summary), + } + } } /// Represents a Matrix room event that has been returned from `/sync`, diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs b/crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs index 4fa8a80fc..9ddfbef36 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state_transaction.rs @@ -37,7 +37,7 @@ use super::{ }; use crate::timeline::{ event_handler::{FailedToParseEvent, RemovedItem, TimelineAction}, - VirtualTimelineItem, + ThreadSummary, TimelineDetails, VirtualTimelineItem, }; pub(in crate::timeline) struct TimelineStateTransaction<'a> { @@ -205,6 +205,7 @@ impl<'a> TimelineStateTransaction<'a> { room_data_provider, None, None, + None, &self.items, &mut self.meta, ) @@ -555,7 +556,12 @@ impl<'a> TimelineStateTransaction<'a> { date_divider_adjuster: &mut DateDividerAdjuster, ) -> RemovedItem { // TODO: do something with the thread summary! - let TimelineEvent { push_actions, kind, thread_summary: _thread_summary } = event; + let TimelineEvent { push_actions, kind, thread_summary } = event; + + let thread_summary = thread_summary.summary().map(|_common_summary| { + // TODO: later, fill the latest event in the thread summary! + ThreadSummary { latest_event: TimelineDetails::Unavailable } + }); let encryption_info = kind.encryption_info().cloned(); @@ -584,6 +590,7 @@ impl<'a> TimelineStateTransaction<'a> { event, &raw, room_data_provider, + thread_summary, utd_info, bundled_edit_encryption_info, &self.items, diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index b98defa24..fff18125b 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -57,7 +57,8 @@ use super::{ }, traits::RoomDataProvider, EncryptedMessage, EventTimelineItem, InReplyToDetails, MsgLikeContent, MsgLikeKind, OtherState, - ReactionStatus, RepliedToEvent, Sticker, TimelineDetails, TimelineItem, TimelineItemContent, + ReactionStatus, RepliedToEvent, Sticker, ThreadSummary, TimelineDetails, TimelineItem, + TimelineItemContent, }; use crate::timeline::controller::aggregations::PendingEdit; @@ -151,6 +152,7 @@ pub(super) struct RemoteEventContext<'a> { raw_event: &'a Raw, relations: BundledMessageLikeRelations, bundled_edit_encryption_info: Option>, + thread_summary: Option, } /// An action that we want to cause on the timeline. @@ -194,6 +196,7 @@ impl TimelineAction { event: AnySyncTimelineEvent, raw_event: &Raw, room_data_provider: &P, + thread_summary: Option, unable_to_decrypt_info: Option, bundled_edit_encryption_info: Option>, timeline_items: &Vector>, @@ -257,6 +260,7 @@ impl TimelineAction { raw_event, relations: ev.relations(), bundled_edit_encryption_info, + thread_summary, }), timeline_items, meta, @@ -272,6 +276,7 @@ impl TimelineAction { raw_event, relations: ev.relations(), bundled_edit_encryption_info, + thread_summary, }), timeline_items, meta, @@ -371,7 +376,7 @@ impl TimelineAction { timeline_items, ); - if let Some(event_id) = remote_ctx.map(|ctx| ctx.event_id) { + if let Some(event_id) = remote_ctx.as_ref().map(|ctx| ctx.event_id) { Self::mark_response(meta, event_id, in_reply_to.as_ref()); } @@ -380,7 +385,7 @@ impl TimelineAction { reactions: Default::default(), thread_root, in_reply_to, - thread_summary: None, + thread_summary: remote_ctx.and_then(|ctx| ctx.thread_summary), })) } @@ -391,7 +396,7 @@ impl TimelineAction { Self::extract_reply_and_thread_root(c.relates_to.clone(), timeline_items); // Record the bundled edit in the aggregations set, if any. - if let Some(ctx) = remote_ctx { + let thread_summary = if let Some(ctx) = remote_ctx { if let Some(new_content) = extract_poll_edit_content(ctx.relations) { // It is replacing the current event. if let Some(edit_event_id) = @@ -417,7 +422,11 @@ impl TimelineAction { } Self::mark_response(meta, ctx.event_id, in_reply_to.as_ref()); - } + + ctx.thread_summary + } else { + None + }; let poll_state = PollState::new(c); @@ -427,7 +436,7 @@ impl TimelineAction { reactions: Default::default(), thread_root, in_reply_to, - thread_summary: None, + thread_summary, }), } } @@ -439,7 +448,7 @@ impl TimelineAction { ); // Record the bundled edit in the aggregations set, if any. - if let Some(ctx) = remote_ctx { + let thread_summary = if let Some(ctx) = remote_ctx { if let Some(new_content) = extract_room_msg_edit_content(ctx.relations) { // It is replacing the current event. if let Some(edit_event_id) = @@ -465,7 +474,11 @@ impl TimelineAction { } Self::mark_response(meta, ctx.event_id, in_reply_to.as_ref()); - } + + ctx.thread_summary + } else { + None + }; Self::AddItem { content: TimelineItemContent::message( @@ -474,7 +487,7 @@ impl TimelineAction { Default::default(), thread_root, in_reply_to, - None, + thread_summary, ), } } diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/reply.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/reply.rs index 54ea1bc18..6671a3875 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/reply.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/reply.rs @@ -121,11 +121,15 @@ impl RepliedToEvent { debug!(event_type = %event.event_type(), "got deserialized event"); + // We don't need to fill the thread information of an embedded reply. + let thread_summary = None; + let sender = event.sender().to_owned(); let action = TimelineAction::from_event( event, &raw_event, room_data_provider, + thread_summary, unable_to_decrypt_info, bundled_edit_encryption_info, timeline_items, diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs index c742cb4f1..29b46698a 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -693,7 +693,7 @@ impl TimelineDetails { } } - pub(crate) fn is_unavailable(&self) -> bool { + pub fn is_unavailable(&self) -> bool { matches!(self, Self::Unavailable) } diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/thread.rs b/crates/matrix-sdk-ui/tests/integration/timeline/thread.rs index 2fc40bef1..2bef70b22 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/thread.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/thread.rs @@ -15,10 +15,19 @@ use assert_matches2::assert_let; use eyeball_im::VectorDiff; use futures_util::StreamExt as _; -use matrix_sdk::test_utils::mocks::{MatrixMockServer, RoomRelationsResponseTemplate}; -use matrix_sdk_test::{async_test, event_factory::EventFactory}; -use matrix_sdk_ui::timeline::{TimelineBuilder, TimelineFocus}; -use ruma::{event_id, events::AnyTimelineEvent, owned_event_id, room_id, serde::Raw, user_id}; +use matrix_sdk::{ + assert_let_timeout, + test_utils::mocks::{MatrixMockServer, RoomRelationsResponseTemplate}, +}; +use matrix_sdk_test::{async_test, event_factory::EventFactory, JoinedRoomBuilder, ALICE}; +use matrix_sdk_ui::timeline::{RoomExt as _, TimelineBuilder, TimelineFocus}; +use ruma::{ + event_id, + events::{relation::BundledThread, AnyTimelineEvent, BundledMessageLikeRelations}, + owned_event_id, room_id, + serde::Raw, + uint, user_id, +}; use stream_assert::assert_pending; #[async_test] @@ -151,8 +160,6 @@ async fn test_thread_backpagination() { // events and the thread root assert_eq!(timeline_updates.len(), 5); - println!("Stefan: {timeline_updates:?}"); - // Check the timeline diffs assert_let!(VectorDiff::PushFront { value } = &timeline_updates[0]); assert_eq!(value.as_event().unwrap().event_id().unwrap(), event_id!("$2")); @@ -193,3 +200,50 @@ async fn test_thread_backpagination() { "Threaded event 4" ); } + +#[async_test] +async fn test_thread_summary() { + // A sync event that includes a bundled thread summary receives a + // `ThreadSummary` in the associated timeline content. + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let room_id = room_id!("!a:b.c"); + let room = server.sync_joined_room(&client, room_id).await; + + let timeline = room.timeline().await.unwrap(); + + let (initial_items, mut stream) = timeline.subscribe().await; + assert!(initial_items.is_empty()); + + let f = EventFactory::new().room(room_id).sender(&ALICE); + let thread_event_id = event_id!("$thread_root"); + let latest_event_id = event_id!("$latest_event"); + + let latest_thread_event = f.text_msg("the last one!").event_id(latest_event_id).into_raw(); + + let mut relations = BundledMessageLikeRelations::new(); + relations.thread = Some(Box::new(BundledThread::new(latest_thread_event, uint!(42), false))); + + let event = f + .text_msg("thready thread mcthreadface") + .bundled_relations(relations) + .event_id(thread_event_id); + + server.sync_room(&client, JoinedRoomBuilder::new(room_id).add_timeline_event(event)).await; + + assert_let_timeout!(Some(timeline_updates) = stream.next()); + // Message + day divider. + assert_eq!(timeline_updates.len(), 2); + + // Check the timeline diffs. + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[0]); + let event_item = value.as_event().unwrap(); + assert_eq!(event_item.event_id().unwrap(), thread_event_id); + assert_let!(Some(summary) = event_item.content().thread_summary()); + // Soon™, Stefan, soon™. + assert!(summary.latest_event.is_unavailable()); + + assert_let!(VectorDiff::PushFront { value } = &timeline_updates[1]); + assert!(value.is_date_divider()); +}