fix(ui): populate the thread_root and in_reply_to fields for stickers and polls

They have never been set and there was no way of telling if stickers and polls belong on a thread or come in reply of any other message.

This patch also exposes methods for setting these relations on the event factory level.
This commit is contained in:
Stefan Ceriu
2025-05-15 14:52:20 +03:00
committed by Stefan Ceriu
parent 69b8878890
commit da2dda0e45
4 changed files with 134 additions and 6 deletions

View File

@@ -31,7 +31,8 @@ use ruma::{
receipt::Receipt,
relation::Replacement,
room::message::{
Relation, RoomMessageEventContent, RoomMessageEventContentWithoutRelation,
Relation, RelationWithoutReplacement, RoomMessageEventContent,
RoomMessageEventContentWithoutRelation,
},
AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncStateEvent,
AnySyncTimelineEvent, BundledMessageLikeRelations, EventContent, FullStateEventContent,
@@ -354,11 +355,44 @@ impl TimelineAction {
}
AnyMessageLikeEventContent::Sticker(content) => {
let mut replied_to_event_id = None;
let mut thread_root = None;
let in_reply_to_details =
content.relates_to.as_ref().and_then(|relation| match relation {
Relation::Reply { in_reply_to } => {
replied_to_event_id = Some(in_reply_to.event_id.clone());
Some(InReplyToDetails::new(
in_reply_to.event_id.clone(),
timeline_items,
))
}
Relation::Thread(thread) => {
thread_root = Some(thread.event_id.clone());
thread.in_reply_to.as_ref().map(|in_reply_to| {
replied_to_event_id = Some(in_reply_to.event_id.clone());
InReplyToDetails::new(in_reply_to.event_id.clone(), timeline_items)
})
}
_ => None,
});
// If this message is a reply to another message, add an entry in the
// inverted mapping.
if let Some(event_id) = event_id {
if let Some(replied_to_event_id) = replied_to_event_id {
// This is a reply! Add an entry.
meta.replies
.entry(replied_to_event_id)
.or_default()
.insert(event_id.to_owned());
}
}
Self::add_item(TimelineItemContent::MsgLike(MsgLikeContent {
kind: MsgLikeKind::Sticker(Sticker { content }),
reactions: Default::default(),
thread_root: None,
in_reply_to: None,
thread_root,
in_reply_to: in_reply_to_details,
thread_summary: None,
}))
}
@@ -393,6 +427,39 @@ impl TimelineAction {
.or(pending_edit)
.unzip();
let mut replied_to_event_id = None;
let mut thread_root = None;
let in_reply_to_details =
c.relates_to.as_ref().and_then(|relation| match relation {
RelationWithoutReplacement::Reply { in_reply_to } => {
replied_to_event_id = Some(in_reply_to.event_id.clone());
Some(InReplyToDetails::new(
in_reply_to.event_id.clone(),
timeline_items,
))
}
RelationWithoutReplacement::Thread(thread) => {
thread_root = Some(thread.event_id.clone());
thread.in_reply_to.as_ref().map(|in_reply_to| {
replied_to_event_id = Some(in_reply_to.event_id.clone());
InReplyToDetails::new(in_reply_to.event_id.clone(), timeline_items)
})
}
_ => None,
});
// If this message is a reply to another message, add an entry in the
// inverted mapping.
if let Some(event_id) = event_id {
if let Some(replied_to_event_id) = replied_to_event_id {
// This is a reply! Add an entry.
meta.replies
.entry(replied_to_event_id)
.or_default()
.insert(event_id.to_owned());
}
}
let poll_state = PollState::new(c, edit_content);
let edit_json = edit_json.flatten();
@@ -401,8 +468,8 @@ impl TimelineAction {
content: TimelineItemContent::MsgLike(MsgLikeContent {
kind: MsgLikeKind::Poll(poll_state),
reactions: Default::default(),
thread_root: None,
in_reply_to: None,
thread_root,
in_reply_to: in_reply_to_details,
thread_summary: None,
}),
edit_json,

View File

@@ -22,6 +22,7 @@ use matrix_sdk_test::{
async_test, event_factory::PreviousMembership, sync_timeline_event, ALICE, BOB, CAROL,
};
use ruma::{
event_id,
events::{
receipt::{Receipt, ReceiptThread, ReceiptType},
room::{
@@ -141,6 +142,13 @@ async fn test_sticker() {
"w": 394
},
"url": "mxc://server.name/JWEIFJgwEIhweiWJE",
"m.relates_to": {
"rel_type": "m.thread",
"event_id": "$thread_root",
"m.in_reply_to": {
"event_id": "$in_reply_to"
}
}
},
"event_id": "$143273582443PhrSn",
"origin_server_ts": 143273582,
@@ -151,6 +159,11 @@ async fn test_sticker() {
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
assert!(item.content().is_sticker());
assert_eq!(item.content().thread_root(), Some(event_id!("$thread_root").to_owned()));
assert_let!(Some(details) = item.content().in_reply_to());
assert_eq!(details.event_id, event_id!("$in_reply_to").to_owned())
}
#[async_test]

View File

@@ -1,6 +1,8 @@
use assert_matches2::assert_let;
use fakes::poll_a2;
use matrix_sdk_test::{async_test, ALICE, BOB};
use ruma::{
event_id,
events::{
poll::{
unstable_end::UnstablePollEndEventContent,
@@ -230,6 +232,28 @@ async fn test_ending_poll_doesnt_clear_latest_json_edit() {
assert!(timeline.event_items().await[0].latest_edit_json().is_some());
}
#[async_test]
async fn test_poll_contains_relations() {
let timeline = TestTimeline::new();
let thread_root_id = event_id!("$thread_root");
let in_reply_to_id = event_id!("$in_reply_to");
let poll_start = poll_a2(&timeline.factory)
.in_thread(thread_root_id, in_reply_to_id)
.sender(&ALICE)
.event_id(event_id!("$poll"));
timeline.handle_live_event(poll_start).await;
let poll = timeline.event_items().await[0].clone();
assert_eq!(poll.content().thread_root(), Some(thread_root_id.to_owned()));
assert_let!(Some(details) = poll.content().in_reply_to());
assert_eq!(details.event_id, in_reply_to_id);
}
impl TestTimeline {
async fn event_items(&self) -> Vec<EventTimelineItem> {
self.controller.items().await.iter().filter_map(|item| item.as_event().cloned()).collect()

View File

@@ -45,7 +45,8 @@ use ruma::{
member::{MembershipState, RoomMemberEventContent},
message::{
FormattedBody, ImageMessageEventContent, MessageType, Relation,
RoomMessageEventContent, RoomMessageEventContentWithoutRelation,
RelationWithoutReplacement, RoomMessageEventContent,
RoomMessageEventContentWithoutRelation,
},
name::RoomNameEventContent,
power_levels::RoomPowerLevelsEventContent,
@@ -360,6 +361,29 @@ impl EventBuilder<RoomMessageEventContent> {
}
}
impl EventBuilder<UnstablePollStartEventContent> {
/// Adds a reply relation to the current event.
pub fn reply_to(mut self, event_id: &EventId) -> Self {
if let UnstablePollStartEventContent::New(content) = &mut self.content {
content.relates_to = Some(RelationWithoutReplacement::Reply {
in_reply_to: InReplyTo::new(event_id.to_owned()),
});
};
self
}
/// Adds a thread relation to the root event, setting the reply to
/// event id as well.
pub fn in_thread(mut self, root: &EventId, reply_to_event_id: &EventId) -> Self {
let thread = Thread::reply(root.to_owned(), reply_to_event_id.to_owned());
if let UnstablePollStartEventContent::New(content) = &mut self.content {
content.relates_to = Some(RelationWithoutReplacement::Thread(thread));
};
self
}
}
impl<E: EventContent> From<EventBuilder<E>> for Raw<AnySyncTimelineEvent>
where
E::EventType: Serialize,