Skip to content

Commit

Permalink
timeline: update replies when a message has been edited
Browse files Browse the repository at this point in the history
  • Loading branch information
bnjbvr committed Oct 7, 2024
1 parent 6c7acf6 commit 4cbc162
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 5 deletions.
33 changes: 29 additions & 4 deletions crates/matrix-sdk-ui/src/timeline/event_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ use crate::{
controller::PendingEdit,
event_item::{ReactionInfo, ReactionStatus},
reactions::PendingReaction,
RepliedToEvent,
},
};

Expand Down Expand Up @@ -511,9 +512,33 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
) {
if let Some((item_pos, item)) = rfind_event_by_id(self.items, &replacement.event_id) {
let edit_json = self.ctx.flow.raw_event().cloned();
if let Some(new_item) = self.apply_msg_edit(&item, replacement, edit_json) {
if let Some(new_item) = self.apply_msg_edit(&item, replacement.new_content, edit_json) {
trace!("Applied edit");
self.items.set(item_pos, TimelineItem::new(new_item, item.internal_id.to_owned()));

let internal_id = item.internal_id.to_owned();

// Update all events that replied to this message with the edited content.
self.items.for_each(|mut entry| {
let Some(event_item) = entry.as_event() else { return };
let Some(message) = event_item.content.as_message() else { return };
let Some(in_reply_to) = message.in_reply_to() else { return };
if replacement.event_id == in_reply_to.event_id {
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::Message(message.with_in_reply_to(in_reply_to));
let new_reply_item =
entry.with_kind(event_item.with_content(new_reply_content, None));
ObservableVectorTransactionEntry::set(&mut entry, new_reply_item);
}
});

// Update the event itself.
self.items.set(item_pos, TimelineItem::new(new_item, internal_id));
self.result.items_updated += 1;
}
} else if let Flow::Remote { position, raw_event, .. } = &self.ctx.flow {
Expand Down Expand Up @@ -589,7 +614,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
fn apply_msg_edit(
&self,
item: &EventTimelineItem,
replacement: Replacement<RoomMessageEventContentWithoutRelation>,
new_content: RoomMessageEventContentWithoutRelation,
edit_json: Option<Raw<AnySyncTimelineEvent>>,
) -> Option<EventTimelineItem> {
if self.ctx.sender != item.sender() {
Expand All @@ -609,7 +634,7 @@ impl<'a, 'o> TimelineEventHandler<'a, 'o> {
};

let mut new_msg = msg.clone();
new_msg.apply_edit(replacement.new_content);
new_msg.apply_edit(new_content);

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

Expand Down
127 changes: 126 additions & 1 deletion crates/matrix-sdk-ui/tests/integration/timeline/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ async fn test_send_reply_edit() {
.mount(&server)
.await;

timeline
let edited = timeline
.edit(
&reply_item,
EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain(
Expand All @@ -419,6 +419,7 @@ async fn test_send_reply_edit() {
)
.await
.unwrap();
assert!(edited);

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

#[async_test]
async fn test_edit_to_replied_updates_reply() {
let room_id = room_id!("!a98sd12bjh:example.org");
let (client, server) = logged_in_client_with_server().await;
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));

let mut sync_builder = SyncResponseBuilder::new();
sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id));

mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
server.reset().await;

mock_encryption_state(&server, false).await;

let room = client.get_room(room_id).unwrap();
let timeline = room.timeline().await.unwrap();
let (_, mut timeline_stream) =
timeline.subscribe_filter_map(|item| item.as_event().cloned()).await;

let f = EventFactory::new();
let event_id = event_id!("$original_event");
let user_id = client.user_id().unwrap();

// When a room has two messages, one is a reply to the other…
sync_builder.add_joined_room(
JoinedRoomBuilder::new(room_id)
.add_timeline_event(f.text_msg("bonjour").sender(user_id).event_id(event_id))
.add_timeline_event(f.text_msg("hi back").reply_to(event_id).sender(*ALICE))
.add_timeline_event(f.text_msg("yo").reply_to(event_id).sender(*BOB)),
);

mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
server.reset().await;

// (I see all the messages in the timeline.)
let replied_to_item = assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => {
assert_eq!(value.content().as_message().unwrap().body(), "bonjour");
assert!(value.is_editable());
value
});

assert_next_matches!(timeline_stream, VectorDiff::PushBack { value: reply_item } => {
let reply_message = reply_item.content().as_message().unwrap();
assert_eq!(reply_message.body(), "hi back");

let in_reply_to = reply_message.in_reply_to().unwrap();
assert_eq!(in_reply_to.event_id, event_id);

assert_let!(TimelineDetails::Ready(replied_to) = &in_reply_to.event);
assert_eq!(replied_to.content().as_message().unwrap().body(), "bonjour");
});

assert_next_matches!(timeline_stream, VectorDiff::PushBack { value: reply_item } => {
let reply_message = reply_item.content().as_message().unwrap();
assert_eq!(reply_message.body(), "yo");

let in_reply_to = reply_message.in_reply_to().unwrap();
assert_eq!(in_reply_to.event_id, event_id);

assert_let!(TimelineDetails::Ready(replied_to) = &in_reply_to.event);
assert_eq!(replied_to.content().as_message().unwrap().body(), "bonjour");
});

mock_encryption_state(&server, false).await;
Mock::given(method("PUT"))
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/.*"))
.respond_with(
ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$edit_event" })),
)
.expect(1)
.mount(&server)
.await;

// If I edit the first message,…
let edited = timeline
.edit(
&replied_to_item,
EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain(
"hello world",
)),
)
.await
.unwrap();
assert!(edited);

yield_now().await; // let the send queue handle the edit.

// The reply events are updated with the edited replied-to content.
assert_next_matches!(timeline_stream, VectorDiff::Set { index: 1, value } => {
let reply_message = value.content().as_message().unwrap();
assert_eq!(reply_message.body(), "hi back");
assert!(!reply_message.is_edited());

let in_reply_to = reply_message.in_reply_to().unwrap();
assert_eq!(in_reply_to.event_id, event_id);
assert_let!(TimelineDetails::Ready(replied_to) = &in_reply_to.event);
assert_eq!(replied_to.content().as_message().unwrap().body(), "hello world");
});

assert_next_matches!(timeline_stream, VectorDiff::Set { index: 2, value } => {
let reply_message = value.content().as_message().unwrap();
assert_eq!(reply_message.body(), "yo");
assert!(!reply_message.is_edited());

let in_reply_to = reply_message.in_reply_to().unwrap();
assert_eq!(in_reply_to.event_id, event_id);
assert_let!(TimelineDetails::Ready(replied_to) = &in_reply_to.event);
assert_eq!(replied_to.content().as_message().unwrap().body(), "hello world");
});

// And the edit happens.
assert_next_matches!(timeline_stream, VectorDiff::Set { index: 0, value } => {
let msg = value.content().as_message().unwrap();
assert_eq!(msg.body(), "hello world");
assert!(msg.is_edited());
});

sleep(Duration::from_millis(200)).await;

server.verify().await;
}

#[async_test]
async fn test_send_edit_poll() {
let room_id = room_id!("!a98sd12bjh:example.org");
Expand Down

0 comments on commit 4cbc162

Please sign in to comment.