feat(ffi): introduce a ThreadSummary type within MsgLikeContent (#4933)

…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
This commit is contained in:
Stefan Ceriu
2025-04-16 09:11:31 +03:00
committed by GitHub
parent ed232df0b6
commit d36b2a6869
10 changed files with 160 additions and 64 deletions

View File

@@ -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<Reaction>,
/// Event ID of the thread root, if this is a threaded message.
pub thread_root: Option<String>,
/// The event this message is replying to, if any.
pub in_reply_to: Option<Arc<InReplyToDetails>>,
/// Event ID of the thread root, if this is a message in a thread.
pub thread_root: Option<String>,
/// Details about the thread this message is the root of.
pub thread_summary: Option<Arc<ThreadSummary>>,
}
#[derive(Clone, uniffi::Record)]
@@ -95,6 +101,8 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> 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::<MessageType>::try_into(message.msgtype().clone())
@@ -112,6 +120,7 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
reactions,
in_reply_to,
thread_root,
thread_summary,
}
}
Kind::Sticker(sticker) => {
@@ -134,6 +143,7 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
reactions,
in_reply_to,
thread_root,
thread_summary,
}
}
Kind::Poll(poll_state) => {
@@ -156,16 +166,22 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> 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<matrix_sdk_ui::timeline::ThreadSummary> 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 }
}
}

View File

@@ -542,6 +542,7 @@ mod tests {
ReactionsByKeyBySender::default(),
None,
None,
None,
),
event_kind,
true,

View File

@@ -1026,16 +1026,14 @@ impl<P: RoomDataProvider, D: Decryptor> TimelineController<P, D> {
};
// 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));

View File

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

View File

@@ -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);
}

View File

@@ -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<OwnedEventId>,
in_reply_to: Option<InReplyToDetails>,
thread_summary: Option<ThreadSummary>,
) -> 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<OwnedEventId> {
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<ThreadSummary> {
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.

View File

@@ -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<Box<ThreadSummaryLatestEvent>>,
}
#[derive(Clone, Debug)]
pub struct ThreadSummaryLatestEvent {
pub content: TimelineItemContent,
pub sender: OwnedUserId,
pub sender_profile: TimelineDetails<Profile>,
}
/// 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<OwnedEventId>,
/// The event this message is replying to, if any.
pub in_reply_to: Option<InReplyToDetails>,
/// Event ID of the thread root, if this is a message in a thread.
pub thread_root: Option<OwnedEventId>,
/// Information about the thread this message is the root of, if any.
pub thread_summary: Option<ThreadSummary>,
}
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<Message> {

View File

@@ -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,
})
}

View File

@@ -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,
};

View File

@@ -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},