From d36b2a6869d7e5960bf652daecba5ea2de8f1cbf Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 16 Apr 2025 09:11:31 +0300 Subject: [PATCH] feat(ffi): introduce a `ThreadSummary` type within `MsgLikeContent` (#4933) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …that holds information on the thread the given item is the root of - it holds the latest event content and sender at the moment but will hold more information in the future e.g. number of replies, if it's unread etc. - the field is not currently being populate but is delivered earlier so it can power shipping the UI side on the embedders --- .../matrix-sdk-ffi/src/timeline/msg_like.rs | 69 +++++++++++++++++-- .../controller/decryption_retry_task.rs | 1 + .../src/timeline/controller/mod.rs | 12 ++-- .../timeline/controller/observable_items.rs | 1 + .../src/timeline/event_handler.rs | 63 ++++++++--------- .../src/timeline/event_item/content/mod.rs | 17 ++++- .../timeline/event_item/content/msg_like.rs | 30 ++++++-- .../src/timeline/event_item/content/reply.rs | 25 ++++--- .../src/timeline/event_item/mod.rs | 3 +- crates/matrix-sdk-ui/src/timeline/mod.rs | 3 +- 10 files changed, 160 insertions(+), 64 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/timeline/msg_like.rs b/bindings/matrix-sdk-ffi/src/timeline/msg_like.rs index cfcae5f72..18ad4a6d0 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/msg_like.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/msg_like.rs @@ -15,13 +15,17 @@ use std::{collections::HashMap, sync::Arc}; use matrix_sdk::crypto::types::events::UtdCause; +use matrix_sdk_ui::timeline::TimelineDetails; use ruma::events::{room::MediaSource as RumaMediaSource, EventContent}; -use super::{content::Reaction, reply::InReplyToDetails}; +use super::{ + content::{Reaction, TimelineItemContent}, + reply::InReplyToDetails, +}; use crate::{ error::ClientError, ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind}, - timeline::content::ReactionSenderData, + timeline::{content::ReactionSenderData, ProfileDetails}, utils::Timestamp, }; @@ -56,10 +60,12 @@ pub enum MsgLikeKind { pub struct MsgLikeContent { pub kind: MsgLikeKind, pub reactions: Vec, - /// Event ID of the thread root, if this is a threaded message. - pub thread_root: Option, /// The event this message is replying to, if any. pub in_reply_to: Option>, + /// Event ID of the thread root, if this is a message in a thread. + pub thread_root: Option, + /// Details about the thread this message is the root of. + pub thread_summary: Option>, } #[derive(Clone, uniffi::Record)] @@ -95,6 +101,8 @@ impl TryFrom for MsgLikeContent { let thread_root = value.thread_root.map(|id| id.to_string()); + let thread_summary = value.thread_summary.map(|t| Arc::new(t.into())); + Ok(match value.kind { Kind::Message(message) => { let msg_type = TryInto::::try_into(message.msgtype().clone()) @@ -112,6 +120,7 @@ impl TryFrom for MsgLikeContent { reactions, in_reply_to, thread_root, + thread_summary, } } Kind::Sticker(sticker) => { @@ -134,6 +143,7 @@ impl TryFrom for MsgLikeContent { reactions, in_reply_to, thread_root, + thread_summary, } } Kind::Poll(poll_state) => { @@ -156,16 +166,22 @@ impl TryFrom for MsgLikeContent { reactions, in_reply_to, thread_root, + thread_summary, } } - Kind::Redacted => { - Self { kind: MsgLikeKind::Redacted, reactions, in_reply_to, thread_root } - } + Kind::Redacted => Self { + kind: MsgLikeKind::Redacted, + reactions, + in_reply_to, + thread_root, + thread_summary, + }, Kind::UnableToDecrypt(msg) => Self { kind: MsgLikeKind::UnableToDecrypt { msg: EncryptedMessage::new(&msg) }, reactions, in_reply_to, thread_root, + thread_summary, }, }) } @@ -222,3 +238,42 @@ pub struct PollAnswer { pub id: String, pub text: String, } + +#[derive(Clone, uniffi::Object)] +pub struct ThreadSummary { + pub latest_event: ThreadSummaryLatestEventDetails, +} + +#[matrix_sdk_ffi_macros::export] +impl ThreadSummary { + pub fn latest_event(&self) -> ThreadSummaryLatestEventDetails { + self.latest_event.clone() + } +} + +#[derive(Clone, uniffi::Enum)] +pub enum ThreadSummaryLatestEventDetails { + Unavailable, + Pending, + Ready { sender: String, sender_profile: ProfileDetails, content: TimelineItemContent }, + Error { message: String }, +} + +impl From for ThreadSummary { + fn from(value: matrix_sdk_ui::timeline::ThreadSummary) -> Self { + let latest_event = match value.latest_event { + TimelineDetails::Unavailable => ThreadSummaryLatestEventDetails::Unavailable, + TimelineDetails::Pending => ThreadSummaryLatestEventDetails::Pending, + TimelineDetails::Ready(event) => ThreadSummaryLatestEventDetails::Ready { + content: event.content.into(), + sender: event.sender.to_string(), + sender_profile: (&event.sender_profile).into(), + }, + TimelineDetails::Error(message) => { + ThreadSummaryLatestEventDetails::Error { message: message.to_string() } + } + }; + + Self { latest_event } + } +} diff --git a/crates/matrix-sdk-ui/src/timeline/controller/decryption_retry_task.rs b/crates/matrix-sdk-ui/src/timeline/controller/decryption_retry_task.rs index 4b93246de..4a15e59c0 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/decryption_retry_task.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/decryption_retry_task.rs @@ -542,6 +542,7 @@ mod tests { ReactionsByKeyBySender::default(), None, None, + None, ), event_kind, true, diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 6b74aa7a7..ef5b07480 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -1026,16 +1026,14 @@ impl TimelineController { }; // Replace the local-related state (kind) and the content state. - let prev_reactions = prev_item.content().reactions(); - let prev_thread_root = prev_item.content().thread_root(); - let prev_in_reply_to = prev_item.content().in_reply_to(); let new_item = TimelineItem::new( prev_item.with_kind(ti_kind).with_content(TimelineItemContent::message( content, None, - prev_reactions, - prev_thread_root, - prev_in_reply_to, + prev_item.content().reactions(), + prev_item.content().thread_root(), + prev_item.content().in_reply_to(), + prev_item.content().thread_summary(), )), prev_item.internal_id.to_owned(), ); @@ -1388,6 +1386,7 @@ impl TimelineController { reactions, thread_root, in_reply_to, + thread_summary, }) = item.content().clone() else { info!("Event is no longer a message (redacted?)"); @@ -1408,6 +1407,7 @@ impl TimelineController { reactions, thread_root, in_reply_to: Some(InReplyToDetails { event_id: in_reply_to.event_id, event }), + thread_summary, })); state.items.replace(index, TimelineItem::new(item, internal_id)); diff --git a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs index 68bb353ae..3b47920eb 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/observable_items.rs @@ -408,6 +408,7 @@ mod observable_items_tests { reactions: Default::default(), thread_root: None, in_reply_to: None, + thread_summary: None, }), EventTimelineItemKind::Remote(RemoteEventTimelineItem { event_id: event_id.parse().unwrap(), diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index feb43bcec..49c76b675 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -418,6 +418,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { reactions: Default::default(), thread_root: None, in_reply_to: None, + thread_summary: None, }), None, ); @@ -622,6 +623,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { Default::default(), thread_root, in_reply_to_details, + None, ), edit_json, ); @@ -739,13 +741,15 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { return None; } - let TimelineItemContent::MsgLike(MsgLikeContent { - kind: MsgLikeKind::Message(msg), - reactions, - thread_root, - in_reply_to, - }) = item.content() - else { + let TimelineItemContent::MsgLike(content) = item.content() else { + info!( + "Edit of message event applies to {:?}, discarding", + item.content().debug_string(), + ); + return None; + }; + + let MsgLikeContent { kind: MsgLikeKind::Message(msg), .. } = content else { info!( "Edit of message event applies to {:?}, discarding", item.content().debug_string(), @@ -757,12 +761,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { new_msg.apply_edit(new_content); let mut new_item = item.with_content_and_latest_edit( - TimelineItemContent::MsgLike(MsgLikeContent { - kind: MsgLikeKind::Message(new_msg), - reactions: reactions.clone(), - thread_root: thread_root.clone(), - in_reply_to: in_reply_to.clone(), - }), + TimelineItemContent::MsgLike(content.with_kind(MsgLikeKind::Message(new_msg))), edit_json, ); @@ -852,24 +851,20 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { return None; } - let TimelineItemContent::MsgLike(MsgLikeContent { - kind: MsgLikeKind::Poll(poll_state), - reactions, - thread_root, - in_reply_to, - }) = &item.content() - else { + let TimelineItemContent::MsgLike(content) = &item.content() else { + info!("Edit of poll event applies to {}, discarding", item.content().debug_string(),); + return None; + }; + + let MsgLikeContent { kind: MsgLikeKind::Poll(poll_state), .. } = content else { info!("Edit of poll event applies to {}, discarding", item.content().debug_string(),); return None; }; let new_content = match poll_state.edit(replacement.new_content) { - Some(edited_poll_state) => TimelineItemContent::MsgLike(MsgLikeContent { - kind: MsgLikeKind::Poll(edited_poll_state), - reactions: reactions.clone(), - thread_root: thread_root.clone(), - in_reply_to: in_reply_to.clone(), - }), + Some(edited_poll_state) => TimelineItemContent::MsgLike( + content.with_kind(MsgLikeKind::Poll(edited_poll_state)), + ), None => { info!("Not applying edit to a poll that's already ended"); return None; @@ -921,6 +916,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { reactions: Default::default(), thread_root: None, in_reply_to: None, + thread_summary: None, }), edit_json, ); @@ -1391,19 +1387,18 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> { let Some(in_reply_to) = msglike.in_reply_to.as_ref() else { continue }; trace!(reply_event_id = ?event_item.identifier(), "Updating response to updated event"); - let in_reply_to = Some(InReplyToDetails { + let in_reply_to = InReplyToDetails { event_id: in_reply_to.event_id.clone(), event: TimelineDetails::Ready(Box::new(RepliedToEvent::from_timeline_item( new_item, ))), - }); + }; - let new_reply_content = TimelineItemContent::MsgLike(MsgLikeContent { - kind: MsgLikeKind::Message(message.clone()), - reactions: msglike.reactions.clone(), - thread_root: msglike.thread_root.clone(), - in_reply_to, - }); + let new_reply_content = TimelineItemContent::MsgLike( + msglike + .with_in_reply_to(in_reply_to) + .with_kind(MsgLikeKind::Message(message.clone())), + ); let new_reply_item = item.with_kind(event_item.with_content(new_reply_content)); items.replace(timeline_item_index, new_reply_item); } diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs index c5ef6bff0..700349adf 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs @@ -74,7 +74,7 @@ pub(in crate::timeline) use self::message::{ }; pub use self::{ message::Message, - msg_like::{MsgLikeContent, MsgLikeKind}, + msg_like::{MsgLikeContent, MsgLikeKind, ThreadSummary, ThreadSummaryLatestEvent}, polls::{PollResult, PollState}, reply::{InReplyToDetails, RepliedToEvent}, }; @@ -198,6 +198,7 @@ impl TimelineItemContent { let reactions = Default::default(); let thread_root = None; let in_reply_to = None; + let thread_summary = None; let msglike = MsgLikeContent { kind: MsgLikeKind::Message(Message::from_event( @@ -208,6 +209,7 @@ impl TimelineItemContent { reactions, thread_root, in_reply_to, + thread_summary, }; TimelineItemContent::MsgLike(msglike) @@ -251,12 +253,14 @@ impl TimelineItemContent { let reactions = Default::default(); let thread_root = None; let in_reply_to = None; + let thread_summary = None; let msglike = MsgLikeContent { kind: MsgLikeKind::Sticker(Sticker { content: event_content }), reactions, thread_root, in_reply_to, + thread_summary, }; TimelineItemContent::MsgLike(msglike) @@ -292,6 +296,7 @@ impl TimelineItemContent { let reactions = Default::default(); let thread_root = None; let in_reply_to = None; + let thread_summary = None; let msglike = MsgLikeContent { kind: MsgLikeKind::Poll(PollState::new( @@ -301,6 +306,7 @@ impl TimelineItemContent { reactions, thread_root, in_reply_to, + thread_summary, }; TimelineItemContent::MsgLike(msglike) @@ -408,6 +414,7 @@ impl TimelineItemContent { reactions: ReactionsByKeyBySender, thread_root: Option, in_reply_to: Option, + thread_summary: Option, ) -> Self { let remove_reply_fallback = if in_reply_to.is_some() { RemoveReplyFallback::Yes } else { RemoveReplyFallback::No }; @@ -417,6 +424,7 @@ impl TimelineItemContent { reactions, thread_root, in_reply_to, + thread_summary, }) } @@ -511,7 +519,7 @@ impl TimelineItemContent { } } - /// Event ID of the thread root, if this is a threaded message. + /// Event ID of the thread root, if this is a message in a thread. pub fn thread_root(&self) -> Option { as_variant!(self, Self::MsgLike)?.thread_root.clone() } @@ -540,6 +548,11 @@ impl TimelineItemContent { } } + /// Information about the thread this item is the root for. + pub fn thread_summary(&self) -> Option { + as_variant!(self, Self::MsgLike)?.thread_summary.clone() + } + /// Return a mutable handle to the reactions of this item. /// /// See also [`Self::reactions()`] to explain the optional return type. diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/msg_like.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/msg_like.rs index 5e479b099..0464c8778 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/msg_like.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/msg_like.rs @@ -13,10 +13,10 @@ // limitations under the License. use as_variant::as_variant; -use ruma::OwnedEventId; +use ruma::{OwnedEventId, OwnedUserId}; -use super::{EncryptedMessage, InReplyToDetails, Message, PollState, Sticker}; -use crate::timeline::ReactionsByKeyBySender; +use super::{EncryptedMessage, InReplyToDetails, Message, PollState, Sticker, TimelineItemContent}; +use crate::timeline::{Profile, ReactionsByKeyBySender, TimelineDetails}; #[derive(Clone, Debug)] pub enum MsgLikeKind { @@ -36,6 +36,18 @@ pub enum MsgLikeKind { UnableToDecrypt(EncryptedMessage), } +#[derive(Clone, Debug)] +pub struct ThreadSummary { + pub latest_event: TimelineDetails>, +} + +#[derive(Clone, Debug)] +pub struct ThreadSummaryLatestEvent { + pub content: TimelineItemContent, + pub sender: OwnedUserId, + pub sender_profile: TimelineDetails, +} + /// A special kind of [`super::TimelineItemContent`] that groups together /// different room message types with their respective reactions and thread /// information. @@ -43,10 +55,12 @@ pub enum MsgLikeKind { pub struct MsgLikeContent { pub kind: MsgLikeKind, pub reactions: ReactionsByKeyBySender, - /// Event ID of the thread root, if this is a threaded message. - pub thread_root: Option, /// The event this message is replying to, if any. pub in_reply_to: Option, + /// Event ID of the thread root, if this is a message in a thread. + pub thread_root: Option, + /// Information about the thread this message is the root of, if any. + pub thread_summary: Option, } impl MsgLikeContent { @@ -67,6 +81,7 @@ impl MsgLikeContent { reactions: Default::default(), thread_root: None, in_reply_to: None, + thread_summary: None, } } @@ -76,6 +91,7 @@ impl MsgLikeContent { reactions: Default::default(), thread_root: None, in_reply_to: None, + thread_summary: None, } } @@ -88,6 +104,10 @@ impl MsgLikeContent { Self { in_reply_to: Some(in_reply_to), ..self.clone() } } + pub fn with_kind(&self, kind: MsgLikeKind) -> Self { + Self { kind, ..self.clone() } + } + /// If `kind` is of the [`MsgLikeKind`][MsgLikeKind::Message] variant, /// return the inner [`Message`]. pub fn as_message(&self) -> Option { 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 2ee253925..e4fc55a66 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 @@ -135,11 +135,13 @@ impl RepliedToEvent { let content = match event.original_content() { Some(content) => match content { AnyMessageLikeEventContent::RoomMessage(c) => { - // Assume we're not interested in reactions and thread info in this context: + // Assume we're not interested in aggregations in this context: // this is information for an embedded (replied-to) event, that will usually not // include detailed information like reactions. let reactions = ReactionsByKeyBySender::default(); let thread_root = None; + let in_reply_to = None; + let thread_summary = None; TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(Message::from_event( @@ -149,21 +151,25 @@ impl RepliedToEvent { )), reactions, thread_root, - in_reply_to: None, + in_reply_to, + thread_summary, }) } AnyMessageLikeEventContent::Sticker(content) => { - // Assume we're not interested in reactions or thread info in this context. + // Assume we're not interested in aggregations in this context. // (See above an explanation as to why that's the case.) - let reactions = ReactionsByKeyBySender::default(); + let reactions = Default::default(); let thread_root = None; + let in_reply_to = None; + let thread_summary = None; TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Sticker(Sticker { content }), reactions, thread_root, - in_reply_to: None, + in_reply_to, + thread_summary, }) } @@ -185,10 +191,12 @@ impl RepliedToEvent { AnyMessageLikeEventContent::UnstablePollStart( UnstablePollStartEventContent::New(content), ) => { - // Assume we're not interested in reactions or thread info in this context. + // Assume we're not interested in aggregations in this context. // (See above an explanation as to why that's the case.) - let reactions = ReactionsByKeyBySender::default(); + let reactions = Default::default(); let thread_root = None; + let in_reply_to = None; + let thread_summary = None; // TODO: could we provide the bundled edit here? let poll_state = PollState::new(content, None); @@ -196,7 +204,8 @@ impl RepliedToEvent { kind: MsgLikeKind::Poll(poll_state), reactions, thread_root, - in_reply_to: None, + in_reply_to, + thread_summary, }) } 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 85b646f89..ba17a8f75 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -53,7 +53,8 @@ pub use self::{ content::{ AnyOtherFullStateEventContent, EncryptedMessage, InReplyToDetails, MemberProfileChange, MembershipChange, Message, MsgLikeContent, MsgLikeKind, OtherState, PollResult, PollState, - RepliedToEvent, RoomMembershipChange, RoomPinnedEventsChange, Sticker, TimelineItemContent, + RepliedToEvent, RoomMembershipChange, RoomPinnedEventsChange, Sticker, ThreadSummary, + ThreadSummaryLatestEvent, TimelineItemContent, }, local::EventSendState, }; diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 1483eea23..d130ad613 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -83,7 +83,8 @@ pub use self::{ EventTimelineItem, InReplyToDetails, MemberProfileChange, MembershipChange, Message, MsgLikeContent, MsgLikeKind, OtherState, PollResult, PollState, Profile, ReactionInfo, ReactionStatus, ReactionsByKeyBySender, RepliedToEvent, RoomMembershipChange, - RoomPinnedEventsChange, Sticker, TimelineDetails, TimelineEventItemId, TimelineItemContent, + RoomPinnedEventsChange, Sticker, ThreadSummary, ThreadSummaryLatestEvent, TimelineDetails, + TimelineEventItemId, TimelineItemContent, }, event_type_filter::TimelineEventTypeFilter, item::{TimelineItem, TimelineItemKind, TimelineUniqueId},