Users may wish to edit previously sent messages, for example to correct typos. This can be done by sending a new message with an indication that it replaces the previously sent message.
This proposal is one in a series of proposals that defines a mechanism for events to relate to each other. Together, these proposals replace MSC1849.
- MSC2674 defines a standard shape for indicating events which relate to other events.
- MSC2675 defines APIs to let the server calculate the aggregations on behalf of the client, and so bundle the related events with the original event where appropriate.
- This proposal defines how users can edit messages using this mechanism.
- MSC2677 defines how users can annotate events, such as reacting to events with emoji, using this mechanism.
Element-Web (then Riot-Web) and Synapse both implemented initial support for message editing, following the proposals of MSC1849, in May 2019 (matrix-react-sdk, synapse). Element-Android and Element-iOS also added implementations around that time. Unfortunately, those implementations presented the feature as "production-ready", despite it not yet having been adopted into the Matrix specification.
The current situation is therefore that client or server implementations hoping to interact with Element users must simply follow the examples of that implementation. In other words, message edits form part of the de-facto spec despite not being formalised in the written spec. This is clearly a regrettable situation. Hopefully, processes have improved over the last three years so that this situation will not arise again. Nevertheless there is little we can do now other than formalise the status quo.
This MSC, along with the others mentioned above, therefore seeks primarily to do that. Although there is plenty of scope for improvement, we consider that better done in future MSCs, based on a shared understanding of the current implementation.
In short, this MSC prefers fidelity to the current implementations over elegance of design.
A new rel_type
of m.replace
is defined for use with the m.relates_to
field as defined in
MSC2674. This is
intended primarily for handling edits, and lets you define an event which
replaces an existing event.
Such an event, with rel_type: m.replace
, is referred to as a "message edit event".
The content
of a message edit event must contain a m.new_content
property
which defines the replacement content. (This allows the normal body
fields to
be used for a fallback for clients who do not understand replacement events.)
For instance, an m.room.message
which replaces an existing event might look like:
{
"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"
}
}
}
The m.new_content
can include any properties that would normally be found in
an event's content
property, such as formatted_body
.
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. The
m.relates_to
property remains unencrypted, as required by the
relationships
section of the Client-Server API specification.
For example, an encrypted replacement 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>"
}
}
... 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. (Any such property would be ignored.) - There is no
m.new_content
property in the cleartextcontent
of them.room.encrypted
event. (Again, any such property would be ignored.)
For clarity: the payload must be encrypted as normal, ratcheting the Megolm session as normal. The original Megolm ratchet entry should not be re-used.
When applying a replacement, the content
property of the original event is
replaced entirely by the 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.
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 <em>really</em> 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. Note that formatted_body
is now absent, because it was absent in the replacement event, but
m.relates_to
remains unchanged (ie, absent).
{
"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 the msgtype
property of m.room.message
events need not be the
same as in the original event. For example, if a user intended to send a
message beginning with "/me", but their client sends an m.emote
event
instead, they could edit the message to send be an m.text
event as they had
originally intended.
Some message edit events are defined to be invalid. To be considered valid, all of the following criteria must be satisfied:
- The replacement and original events must have the same
type
. - Neither the replacement nor original events can be state events (ie, neither
may have a
state_key
). - The original event must not, itself, have a
rel_type
ofm.replace
. - The original event and replacement event must have the same
sender
. - The replacement event (once decrypted, if appropriate) must have an
m.new_content
property.
The original event and replacement event must also have the same room_id
, as
required by the
relationships
section of the Client-Server API specification.
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 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 format of the aggregation for m.replace
simply gives gives the
event_id
, origin_server_ts
, and sender
of the most recent replacement
event (as determined by origin_server_ts
, falling back to a lexicographic
ordering of event_id
).
This aggregation is bundled into the unsigned/m.relations
property of any
event that is the target of an m.replace
relationship. For example:
{
"event_id": "$original_event_id",
// ...
"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 edits are themselves
redacted). Note that this behaviour is specific to the m.replace
relationship.
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 edit (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).
The endpoints where this behaviour takes place is the same as those where
aggregations are bundled, with the exception of
/room/{roomId}/event/{eventId}
. This includes:
GET /rooms/{roomId}/messages
GET /rooms/{roomId}/context/{eventId}
GET /rooms/{roomId}/relations/{eventId}
GET /rooms/{roomId}/relations/{eventId}/{relType}
GET /rooms/{roomId}/relations/{eventId}/{relType}/{eventType}
GET /sync
when the relevant section has alimited
value oftrue
POST /search
for any matching events underroom_events
.
Clients can often ignore message edit events, since any events the server returns via the C-S API 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 message edit events, and to ignore any invalid edit events that may be received.
Permalinks to edited events should capture the event ID 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 ID to the original event ID, and then display the most recent version of that event.
When a message 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. It must be noted that, although they are not immediately
visible in Element, subsequent edits remain unredacted and can be seen via API
calls. See Future considerations.
Some particular constraints apply to events which replace a reply. In particular:
-
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 MSC2674 which expects only one relationship 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.
An example of an edit to a reply is as follows:
{
"type": "m.room.message",
"content": {
"body": "> <@richvdh:sw1v.org> ab\n\n * ef",
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"formatted_body": "<mx-reply><blockquote><a href=\"https://matrix.to/#/!qOZKfwKPirAoSosXrf:matrix.org/$1652807718765kOVDf:sw1v.org?via=matrix.org&via=sw1v.org\">In reply to</a> <a href=\"https://matrix.to/#/@richvdh:sw1v.org\">@richvdh:sw1v.org</a><br>ab</blockquote></mx-reply> * ef",
"m.new_content": {
"body": "ef",
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"formatted_body": "ef"
},
"m.relates_to": {
"rel_type": "m.replace",
"event_id": "$original_reply_event"
}
}
}
In future we may wish to consider ordering replacements (or relations in
general) via a DAG rather than using origin_server_ts
to determine ordering -
particularly to mitigate potential abuse of edits applied by moderators.
Whatever, care must be taken by the server to ensure that if there are multiple
replacement events, the server must consistently choose the same one as all
other servers.
It is highly unintuitive that redacting the original event leaves subsequent edits visible to curious eyes even though they are hidden from the timeline. This is considered a bug which this MSC makes no attempt to resolve. See also element-web#11978 and synapse#5594.
There are various issues which would need to be resolved before edits to state
events could be supported. In particular, we would need to consider how the
semantically-meaningful fields of the content of a state event relate to
m.new_content
. Variation between implementations could easily lead to
security problems (See
element-web#21851 for
example.)
There is a usecase for users with sufficient power-level to edit other peoples' events. For now, no attempt is made to support this. If it is supported in the future, we would need to find a way to make it clear in the timeline.