From 9c3439a1aacfa08e6dda5fbd5a67535fabbe1111 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 17 Sep 2021 15:18:52 -0500 Subject: [PATCH] Show updated relation reply from edited message - v2 (#6817) Part of https://github.com/vector-im/element-web/issues/10391 When `m.relates_to` -> `m.in_reply_to` is provided in `m.new_content` for an edited message, use the updated reply. ex. ```json { "type": "m.room.message", "content": { "body": " * foo bar", "msgtype": "m.text", "m.new_content": { "body": "foo bar", "msgtype": "m.text", "m.relates_to": { "m.in_reply_to": { "event_id": "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og" } } }, "m.relates_to": { "rel_type": "m.replace", "event_id": "$lX9MRe9ZTFOOvnU8PRVbvr1wqGtYvNQ1rSot-iUTN5k" } } } ``` --- src/components/views/elements/ReplyThread.tsx | 8 +- .../views/elements/ReplyThread-test.js | 209 ++++++++++++++++++ 2 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 test/components/views/elements/ReplyThread-test.js diff --git a/src/components/views/elements/ReplyThread.tsx b/src/components/views/elements/ReplyThread.tsx index d061d52f46..59c827d5d8 100644 --- a/src/components/views/elements/ReplyThread.tsx +++ b/src/components/views/elements/ReplyThread.tsx @@ -88,7 +88,13 @@ export default class ReplyThread extends React.Component { // could be used here for replies as well... However, the helper // currently assumes the relation has a `rel_type`, which older replies // do not, so this block is left as-is for now. - const mRelatesTo = ev.getWireContent()['m.relates_to']; + // + // We're prefer ev.getContent() over ev.getWireContent() to make sure + // we grab the latest edit with potentially new relations. But we also + // can't just rely on ev.getContent() by itself because historically we + // still show the reply from the original message even though the edit + // event does not include the relation reply. + const mRelatesTo = ev.getContent()['m.relates_to'] || ev.getWireContent()['m.relates_to']; if (mRelatesTo && mRelatesTo['m.in_reply_to']) { const mInReplyTo = mRelatesTo['m.in_reply_to']; if (mInReplyTo && mInReplyTo['event_id']) return mInReplyTo['event_id']; diff --git a/test/components/views/elements/ReplyThread-test.js b/test/components/views/elements/ReplyThread-test.js new file mode 100644 index 0000000000..ee81c2f210 --- /dev/null +++ b/test/components/views/elements/ReplyThread-test.js @@ -0,0 +1,209 @@ +import "../../../skinned-sdk"; +import * as testUtils from '../../../test-utils'; +import ReplyThread from '../../../../src/components/views/elements/ReplyThread'; + +describe("ReplyThread", () => { + describe('getParentEventId', () => { + it('retrieves relation reply from unedited event', () => { + const originalEventWithRelation = testUtils.mkEvent({ + event: true, + type: "m.room.message", + content: { + "msgtype": "m.text", + "body": "> Reply to this message\n\n foo", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og", + }, + }, + }, + user: "some_other_user", + room: "room_id", + }); + + expect(ReplyThread.getParentEventId(originalEventWithRelation)) + .toStrictEqual('$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og'); + }); + + it('retrieves relation reply from original event when edited', () => { + const originalEventWithRelation = testUtils.mkEvent({ + event: true, + type: "m.room.message", + content: { + "msgtype": "m.text", + "body": "> Reply to this message\n\n foo", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og", + }, + }, + }, + user: "some_other_user", + room: "room_id", + }); + + const editEvent = testUtils.mkEvent({ + event: true, + type: "m.room.message", + content: { + "msgtype": "m.text", + "body": "> Reply to this message\n\n * foo bar", + "m.new_content": { + "msgtype": "m.text", + "body": "foo bar", + }, + "m.relates_to": { + "rel_type": "m.replace", + "event_id": originalEventWithRelation.event_id, + }, + }, + user: "some_other_user", + room: "room_id", + }); + + // The edit replaces the original event + originalEventWithRelation.makeReplaced(editEvent); + + // The relation should be pulled from the original event + expect(ReplyThread.getParentEventId(originalEventWithRelation)) + .toStrictEqual('$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og'); + }); + + it('retrieves relation reply from edit event when provided', () => { + const originalEvent = testUtils.mkEvent({ + event: true, + type: "m.room.message", + content: { + msgtype: "m.text", + body: "foo", + }, + user: "some_other_user", + room: "room_id", + }); + + const editEvent = testUtils.mkEvent({ + event: true, + type: "m.room.message", + content: { + "msgtype": "m.text", + "body": "> Reply to this message\n\n * foo bar", + "m.new_content": { + "msgtype": "m.text", + "body": "foo bar", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og", + }, + }, + }, + "m.relates_to": { + "rel_type": "m.replace", + "event_id": originalEvent.event_id, + }, + }, + user: "some_other_user", + room: "room_id", + }); + + // The edit replaces the original event + originalEvent.makeReplaced(editEvent); + + // The relation should be pulled from the edit event + expect(ReplyThread.getParentEventId(originalEvent)) + .toStrictEqual('$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og'); + }); + + it('prefers relation reply from edit event over original event', () => { + const originalEventWithRelation = testUtils.mkEvent({ + event: true, + type: "m.room.message", + content: { + "msgtype": "m.text", + "body": "> Reply to this message\n\n foo", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$111", + }, + }, + }, + user: "some_other_user", + room: "room_id", + }); + + const editEvent = testUtils.mkEvent({ + event: true, + type: "m.room.message", + content: { + "msgtype": "m.text", + "body": "> Reply to this message\n\n * foo bar", + "m.new_content": { + "msgtype": "m.text", + "body": "foo bar", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$999", + }, + }, + }, + "m.relates_to": { + "rel_type": "m.replace", + "event_id": originalEventWithRelation.event_id, + }, + }, + user: "some_other_user", + room: "room_id", + }); + + // The edit replaces the original event + originalEventWithRelation.makeReplaced(editEvent); + + // The relation should be pulled from the edit event + expect(ReplyThread.getParentEventId(originalEventWithRelation)).toStrictEqual('$999'); + }); + + it('able to clear relation reply from original event by providing empty relation field', () => { + const originalEventWithRelation = testUtils.mkEvent({ + event: true, + type: "m.room.message", + content: { + "msgtype": "m.text", + "body": "> Reply to this message\n\n foo", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$111", + }, + }, + }, + user: "some_other_user", + room: "room_id", + }); + + const editEvent = testUtils.mkEvent({ + event: true, + type: "m.room.message", + content: { + "msgtype": "m.text", + "body": "> Reply to this message\n\n * foo bar", + "m.new_content": { + "msgtype": "m.text", + "body": "foo bar", + // Clear the relation from the original event + "m.relates_to": {}, + }, + "m.relates_to": { + "rel_type": "m.replace", + "event_id": originalEventWithRelation.event_id, + }, + }, + user: "some_other_user", + room: "room_id", + }); + + // The edit replaces the original event + originalEventWithRelation.makeReplaced(editEvent); + + // The relation should be pulled from the edit event + expect(ReplyThread.getParentEventId(originalEventWithRelation)).toStrictEqual(undefined); + }); + }); +});