Skip to content

Commit 4cbc162

Browse files
committed
timeline: update replies when a message has been edited
1 parent 6c7acf6 commit 4cbc162

File tree

2 files changed

+155
-5
lines changed

2 files changed

+155
-5
lines changed

crates/matrix-sdk-ui/src/timeline/event_handler.rs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ use crate::{
6868
controller::PendingEdit,
6969
event_item::{ReactionInfo, ReactionStatus},
7070
reactions::PendingReaction,
71+
RepliedToEvent,
7172
},
7273
};
7374

@@ -511,9 +512,33 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
511512
) {
512513
if let Some((item_pos, item)) = rfind_event_by_id(self.items, &replacement.event_id) {
513514
let edit_json = self.ctx.flow.raw_event().cloned();
514-
if let Some(new_item) = self.apply_msg_edit(&item, replacement, edit_json) {
515+
if let Some(new_item) = self.apply_msg_edit(&item, replacement.new_content, edit_json) {
515516
trace!("Applied edit");
516-
self.items.set(item_pos, TimelineItem::new(new_item, item.internal_id.to_owned()));
517+
518+
let internal_id = item.internal_id.to_owned();
519+
520+
// Update all events that replied to this message with the edited content.
521+
self.items.for_each(|mut entry| {
522+
let Some(event_item) = entry.as_event() else { return };
523+
let Some(message) = event_item.content.as_message() else { return };
524+
let Some(in_reply_to) = message.in_reply_to() else { return };
525+
if replacement.event_id == in_reply_to.event_id {
526+
let in_reply_to = InReplyToDetails {
527+
event_id: in_reply_to.event_id.clone(),
528+
event: TimelineDetails::Ready(Box::new(
529+
RepliedToEvent::from_timeline_item(&new_item),
530+
)),
531+
};
532+
let new_reply_content =
533+
TimelineItemContent::Message(message.with_in_reply_to(in_reply_to));
534+
let new_reply_item =
535+
entry.with_kind(event_item.with_content(new_reply_content, None));
536+
ObservableVectorTransactionEntry::set(&mut entry, new_reply_item);
537+
}
538+
});
539+
540+
// Update the event itself.
541+
self.items.set(item_pos, TimelineItem::new(new_item, internal_id));
517542
self.result.items_updated += 1;
518543
}
519544
} else if let Flow::Remote { position, raw_event, .. } = &self.ctx.flow {
@@ -589,7 +614,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
589614
fn apply_msg_edit(
590615
&self,
591616
item: &EventTimelineItem,
592-
replacement: Replacement<RoomMessageEventContentWithoutRelation>,
617+
new_content: RoomMessageEventContentWithoutRelation,
593618
edit_json: Option<Raw<AnySyncTimelineEvent>>,
594619
) -> Option<EventTimelineItem> {
595620
if self.ctx.sender != item.sender() {
@@ -609,7 +634,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
609634
};
610635

611636
let mut new_msg = msg.clone();
612-
new_msg.apply_edit(replacement.new_content);
637+
new_msg.apply_edit(new_content);
613638

614639
let mut new_item = item.with_content(TimelineItemContent::Message(new_msg), edit_json);
615640

crates/matrix-sdk-ui/tests/integration/timeline/edit.rs

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ async fn test_send_reply_edit() {
410410
.mount(&server)
411411
.await;
412412

413-
timeline
413+
let edited = timeline
414414
.edit(
415415
&reply_item,
416416
EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain(
@@ -419,6 +419,7 @@ async fn test_send_reply_edit() {
419419
)
420420
.await
421421
.unwrap();
422+
assert!(edited);
422423

423424
// Let the send queue handle the event.
424425
yield_now().await;
@@ -444,6 +445,130 @@ async fn test_send_reply_edit() {
444445
server.verify().await;
445446
}
446447

448+
#[async_test]
449+
async fn test_edit_to_replied_updates_reply() {
450+
let room_id = room_id!("!a98sd12bjh:example.org");
451+
let (client, server) = logged_in_client_with_server().await;
452+
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
453+
454+
let mut sync_builder = SyncResponseBuilder::new();
455+
sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id));
456+
457+
mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
458+
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
459+
server.reset().await;
460+
461+
mock_encryption_state(&server, false).await;
462+
463+
let room = client.get_room(room_id).unwrap();
464+
let timeline = room.timeline().await.unwrap();
465+
let (_, mut timeline_stream) =
466+
timeline.subscribe_filter_map(|item| item.as_event().cloned()).await;
467+
468+
let f = EventFactory::new();
469+
let event_id = event_id!("$original_event");
470+
let user_id = client.user_id().unwrap();
471+
472+
// When a room has two messages, one is a reply to the other…
473+
sync_builder.add_joined_room(
474+
JoinedRoomBuilder::new(room_id)
475+
.add_timeline_event(f.text_msg("bonjour").sender(user_id).event_id(event_id))
476+
.add_timeline_event(f.text_msg("hi back").reply_to(event_id).sender(*ALICE))
477+
.add_timeline_event(f.text_msg("yo").reply_to(event_id).sender(*BOB)),
478+
);
479+
480+
mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
481+
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
482+
server.reset().await;
483+
484+
// (I see all the messages in the timeline.)
485+
let replied_to_item = assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => {
486+
assert_eq!(value.content().as_message().unwrap().body(), "bonjour");
487+
assert!(value.is_editable());
488+
value
489+
});
490+
491+
assert_next_matches!(timeline_stream, VectorDiff::PushBack { value: reply_item } => {
492+
let reply_message = reply_item.content().as_message().unwrap();
493+
assert_eq!(reply_message.body(), "hi back");
494+
495+
let in_reply_to = reply_message.in_reply_to().unwrap();
496+
assert_eq!(in_reply_to.event_id, event_id);
497+
498+
assert_let!(TimelineDetails::Ready(replied_to) = &in_reply_to.event);
499+
assert_eq!(replied_to.content().as_message().unwrap().body(), "bonjour");
500+
});
501+
502+
assert_next_matches!(timeline_stream, VectorDiff::PushBack { value: reply_item } => {
503+
let reply_message = reply_item.content().as_message().unwrap();
504+
assert_eq!(reply_message.body(), "yo");
505+
506+
let in_reply_to = reply_message.in_reply_to().unwrap();
507+
assert_eq!(in_reply_to.event_id, event_id);
508+
509+
assert_let!(TimelineDetails::Ready(replied_to) = &in_reply_to.event);
510+
assert_eq!(replied_to.content().as_message().unwrap().body(), "bonjour");
511+
});
512+
513+
mock_encryption_state(&server, false).await;
514+
Mock::given(method("PUT"))
515+
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*"))
516+
.respond_with(
517+
ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$edit_event" })),
518+
)
519+
.expect(1)
520+
.mount(&server)
521+
.await;
522+
523+
// If I edit the first message,…
524+
let edited = timeline
525+
.edit(
526+
&replied_to_item,
527+
EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain(
528+
"hello world",
529+
)),
530+
)
531+
.await
532+
.unwrap();
533+
assert!(edited);
534+
535+
yield_now().await; // let the send queue handle the edit.
536+
537+
// The reply events are updated with the edited replied-to content.
538+
assert_next_matches!(timeline_stream, VectorDiff::Set { index: 1, value } => {
539+
let reply_message = value.content().as_message().unwrap();
540+
assert_eq!(reply_message.body(), "hi back");
541+
assert!(!reply_message.is_edited());
542+
543+
let in_reply_to = reply_message.in_reply_to().unwrap();
544+
assert_eq!(in_reply_to.event_id, event_id);
545+
assert_let!(TimelineDetails::Ready(replied_to) = &in_reply_to.event);
546+
assert_eq!(replied_to.content().as_message().unwrap().body(), "hello world");
547+
});
548+
549+
assert_next_matches!(timeline_stream, VectorDiff::Set { index: 2, value } => {
550+
let reply_message = value.content().as_message().unwrap();
551+
assert_eq!(reply_message.body(), "yo");
552+
assert!(!reply_message.is_edited());
553+
554+
let in_reply_to = reply_message.in_reply_to().unwrap();
555+
assert_eq!(in_reply_to.event_id, event_id);
556+
assert_let!(TimelineDetails::Ready(replied_to) = &in_reply_to.event);
557+
assert_eq!(replied_to.content().as_message().unwrap().body(), "hello world");
558+
});
559+
560+
// And the edit happens.
561+
assert_next_matches!(timeline_stream, VectorDiff::Set { index: 0, value } => {
562+
let msg = value.content().as_message().unwrap();
563+
assert_eq!(msg.body(), "hello world");
564+
assert!(msg.is_edited());
565+
});
566+
567+
sleep(Duration::from_millis(200)).await;
568+
569+
server.verify().await;
570+
}
571+
447572
#[async_test]
448573
async fn test_send_edit_poll() {
449574
let room_id = room_id!("!a98sd12bjh:example.org");

0 commit comments

Comments
 (0)