11 KiB
Event replacements
{{% added-in v="1.4" %}}
Event replacements, or "message edit events", are events that use an event
relationship
with a rel_type
of m.replace
, which indicates that the original event is
intended to be replaced.
An example of a message edit event might look like this:
{
"type": "m.room.message",
"content": {
"body": "* Hello! My name is bar",
"msgtype": "m.text",
"m.new_content": {
"body": "Hello! My name is bar",
"msgtype": "m.text"
},
"m.relates_to": {
"rel_type": "m.replace",
"event_id": "$some_event_id"
}
},
// ... other fields required by events
}
The content
of the replacement must contain a m.new_content
property which
defines the replacement content
. The normal content
properties (body
,
msgtype
etc.) provide a fallback for clients which do not understand
replacement events.
m.new_content
can include any properties that would normally be found in
an event's content property, such as formatted_body
(see m.room.message
msgtypes
).
Validity of replacement events
There are a number of requirements on replacement events, which must be satisfied for the replacement to be considered valid:
-
As with all event relationships, the original event and replacement event must have the same
room_id
(i.e. you cannot send an event in one room and then an edited version in a different room). -
The original event and replacement event must have the same
sender
(i.e. you cannot edit someone else's messages). -
The replacement and original events must have the same
type
(i.e. you cannot change the original event's type). -
The replacement and original events must not have a
state_key
property (i.e. you cannot edit state events at all). -
The original event must not, itself, have a
rel_type
ofm.replace
(i.e. you cannot edit an edit — though you can send multiple edits for a single original event). -
The replacement event (once decrypted, if appropriate) must have an
m.new_content
property.
If any of these criteria are not satisfied, implementations should ignore the replacement event (the content of the original should not be replaced, and the edit should not be included in the server-side aggregation).
Note that the msgtype
property of replacement
m.room.message
events does not need to be the same as in the original event. For
example, it is legitimate to replace an m.text
event with an m.emote
.
Editing encrypted events
If the original event was encrypted, the replacement
should be too. In that case, m.new_content
is placed in the content of the
encrypted payload. As with all event relationships, the m.relates_to
property
must be sent in the unencrypted (cleartext) part of the event.
For example, a replacement for an encrypted event might look like this:
{
"type": "m.room.encrypted",
"content": {
"m.relates_to": {
"rel_type": "m.replace",
"event_id": "$some_event_id"
},
"algorithm": "m.megolm.v1.aes-sha2",
"sender_key": "<sender_curve25519_key>",
"device_id": "<sender_device_id>",
"session_id": "<outbound_group_session_id>",
"ciphertext": "<encrypted_payload_base_64>"
}
// irrelevant fields not shown
}
... and, once decrypted, the payload might look like this:
{
"type": "m.room.<event_type>",
"room_id": "!some_room_id",
"content": {
"body": "* Hello! My name is bar",
"msgtype": "m.text",
"m.new_content": {
"body": "Hello! My name is bar",
"msgtype": "m.text"
}
}
}
Note that:
- There is no
m.relates_to
property in the encrypted payload. If there was, it would be ignored. - There is no
m.new_content
property in the cleartext content of them.room.encrypted
event. As above, if there was then it would be ignored.
{{% boxes/note %}} The payload of an encrypted replacement event must be encrypted as normal, including ratcheting any Megolm session as normal. The original Megolm ratchet entry should not be re-used. {{% /boxes/note %}}
Applying m.new_content
When applying a replacement, the content
of the original event is treated as
being overwritten entirely by m.new_content
, with the exception of m.relates_to
,
which is left unchanged. Any m.relates_to
property within m.new_content
is ignored.
{{% boxes/note %}}
Note that server implementations must not actually overwrite
the original event's content
: instead the server presents it as being overwritten
when it is served over the client-server API. See Server-side replacement of content
below.
{{% /boxes/note %}}
For example, given a pair of events:
{
"event_id": "$original_event",
"type": "m.room.message",
"content": {
"body": "I really like cake",
"msgtype": "m.text",
"formatted_body": "I really like cake",
}
}
{
"event_id": "$edit_event",
"type": "m.room.message",
"content": {
"body": "* I really like *chocolate* cake",
"msgtype": "m.text",
"m.new_content": {
"body": "I really like *chocolate* cake",
"msgtype": "m.text",
"com.example.extension_property": "chocolate"
},
"m.relates_to": {
"rel_type": "m.replace",
"event_id": "$original_event_id"
}
}
}
... then the end result is an event as shown below:
{
"event_id": "$original_event",
"type": "m.room.message",
"content": {
"body": "I really like *chocolate* cake",
"msgtype": "m.text",
"com.example.extension_property": "chocolate"
}
}
Note that formatted_body
is now absent, because it was absent in the
replacement event.
Server behaviour
Server-side aggregation of m.replace
relationships
Note that there can be multiple events with an m.replace
relationship to a
given event (for example, if an event is edited multiple times). These should
be aggregated by the homeserver.
The aggregation format of m.replace
relationships gives the event_id
,
origin_server_ts
, and sender
of the most recent replacement event. The
most recent event is determined by comparing origin_server_ts
; if two or more
replacement events have identical origin_server_ts
, the event with the
lexicographically largest event_id
is treated as more recent.
This aggregation is bundled under the unsigned
property as m.relations
for any
event that is the target of an m.replace
relationship. For example:
{
"event_id": "$original_event_id",
// irrelevant fields not shown
"unsigned": {
"m.relations": {
"m.replace": {
"event_id": "$latest_edit_event_id",
"origin_server_ts": 1649772304313,
"sender": "@editing_user:localhost"
}
}
}
}
If the original event is
redacted, any
m.replace
relationship should not be bundled with it (whether or not any
subsequent replacements are themselves redacted). Note that this behaviour is
specific to the m.replace
relationship. See also redactions of edited
events below.
Server-side replacement of content
Whenever an m.replace
is to be bundled with an event as above, the server
should also modify the content of the original event according to the
m.new_content
of the most recent replacement event (determined as above).
An exception applies to GET /_matrix/client/v3/rooms/{roomId}/event/{eventId}
,
which should return the unmodified event (though the relationship should still
be bundled, as described above).
Client behaviour
Clients can often ignore m.replace
events, because any events returned
by the server to the client will be updated by the server to account for
subsequent edits.
However, clients should apply the replacement themselves when the server is unable to do so. This happens in the following situations:
-
The client has already received and stored the original event before the message edit event arrives.
-
The original event (and hence its replacement) are encrypted.
Client authors are reminded to take note of the requirements for Validity of replacement events, and to ignore any invalid replacement events that are received.
Permalinks
When creating links to events (also known as permalinks), clients build links which reference the event that the creator of the permalink is viewing at that point (which might be a message edit event).
The client viewing the permalink should resolve this reference to the original event, and then display the most recent version of that event.
Redactions of edited events
When an event using a rel_type
of m.replace
is redacted, it
removes that edit revision. This has little effect if there were subsequent
edits. However, if it was the most recent edit, the event is in effect
reverted to its content before the redacted edit.
Redacting the original message in effect removes the message, including all
subsequent edits, from the visible timeline. In this situation, homeservers
will return an empty content
for the original event as with any other
redacted event, and as
above the replacement
events will not be bundled with the original event. Note that the subsequent edits are
not actually redacted themselves: they simply serve no purpose within the visible timeline.
Edits of replies
Some particular constraints apply to events which replace a reply. In particular:
-
In contrast to the original reply, there should be no
m.in_reply_to
property in the them.relates_to
object, since it would be redundant (see Applyingm.new_content
above, which notes that the original event'sm.relates_to
is preserved), as well as being contrary to the spirit of the event relationships mechanism which expects only one "parent" per event. -
m.new_content
should not contain any reply fallback, since it is assumed that any client which can handle edits can also display replies natively. However, thecontent
of the replacement event should provide fallback content for clients which support neither rich replies nor edits.
An example of an edit to a reply is as follows:
{
"type": "m.room.message",
// irrelevant fields not shown
"content": {
"body": "> <@alice:example.org> question\n\n* reply",
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"formatted_body": "<mx-reply><blockquote><a href=\"https://matrix.to/#/!somewhere:example.org/$event:example.org\">In reply to</a> <a href=\"https://matrix.to/#/@alice:example.org\">@alice:example.org</a><br />question</blockquote></mx-reply>* reply",
"m.new_content": {
"body": "reply",
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"formatted_body": "reply"
},
"m.relates_to": {
"rel_type": "m.replace",
"event_id": "$original_reply_event"
}
}
}