Skip to content

Commit

Permalink
Refactor reply formatting to follow the 1.12 spec
Browse files Browse the repository at this point in the history
Due to invalid formatting, replies to replies became garbled,
causing display issues in some clients. Hydrogen itself managed
to display the replies correctly but other clients and bridges
struggled because they were actually using the fallbacks.

Current spec: https://spec.matrix.org/v1.12/client-server-api/#fallbacks-for-rich-replies

Reply fallbacks are actively being removed in the upcoming spec but
that doesn't mean that Hydrogen should keep the old bugged code in place.

Upcoming MSCs:
- matrix-org/matrix-spec-proposals#2781
- matrix-org/matrix-spec-proposals#3676
- spec: matrix-org/matrix-spec#1994

Signed-off-by: Mirian Margiani <[email protected]>
  • Loading branch information
ichthyosaurus committed Dec 5, 2024
1 parent 8dfea1f commit fdb7621
Showing 1 changed file with 58 additions and 11 deletions.
69 changes: 58 additions & 11 deletions src/matrix/room/timeline/entries/reply.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,41 @@ function fallbackPrefix(msgtype) {
return msgtype === "m.emote" ? "* " : "";
}

function _parsePlainBody(plainBody) {
// Strip any existing reply fallback and return an array of lines.

const bodyLines = plainBody.trim().split("\n");

return bodyLines
.map((elem, index, array) => {
if (index > 0 && array[index-1][0] !== '>') {
// stop stripping the fallback at the first line of non-fallback text
return elem;
} else if (elem[0] === '>' && elem[1] === ' ') {
return null;
} else {
return elem;
}
})
.filter((elem) => elem !== null)
// Join, trim, and split to remove any line breaks that were left between the
// fallback and the actual message body. Don't use trim() because that would
// also remove any other whitespace at the beginning of the message that the
// user added intentionally.
.join('\n')
.replace(/^\n+|\n+$/g, '')
.split('\n')
}

function _parseFormattedBody(formattedBody) {
// Strip any existing reply fallback and return a HTML string again.

// This is greedy and definitely not the most efficient way to do it.
// However, this function is only called when sending a reply (so: not too
// often) and it should make sure that all instances of <mx-reply> are gone.
return formattedBody.replace(/<mx-reply>[\s\S]*<\/mx-reply>/gi, '');
}

function _createReplyContent(targetId, msgtype, body, formattedBody) {
return {
msgtype,
Expand All @@ -48,28 +83,40 @@ function _createReplyContent(targetId, msgtype, body, formattedBody) {
"event_id": targetId
}
}
// TODO include user mentions
};
}

export function createReplyContent(entry, msgtype, body, permaLink) {
// TODO check for absense of sender / body / msgtype / etc?
// NOTE We assume sender, body, and msgtype are never invalid because they
// are required fields.
const nonTextual = fallbackForNonTextualMessage(entry.content.msgtype);
const prefix = fallbackPrefix(entry.content.msgtype);
const sender = entry.sender;
const name = entry.displayName || sender;

const formattedBody = nonTextual || entry.content.formatted_body ||
(entry.content.body && htmlEscape(entry.content.body)) || "";
const formattedFallback = `<mx-reply><blockquote>In reply to ${prefix}` +
`<a href="https://matrix.to/#/${sender}">${name}</a><br />` +
`${formattedBody}</blockquote></mx-reply>`;
const repliedToId = entry.id;
// TODO collect user mentions (sender and any previous mentions)

// Generate new plain body with plain reply fallback
const plainBody = nonTextual || entry.content.body || "";
const bodyLines = plainBody.split("\n");
const bodyLines = _parsePlainBody(plainBody);
bodyLines[0] = `> ${prefix}<${sender}> ${bodyLines[0]}`
const plainFallback = bodyLines.join("\n> ");

const newBody = plainFallback + '\n\n' + body;
const newFormattedBody = formattedFallback + htmlEscape(body);

// Generate new formatted body with formatted reply fallback
const formattedBody = nonTextual || entry.content.formatted_body ||
(entry.content.body && htmlEscape(entry.content.body)) || "";
const cleanedFormattedBody = _parseFormattedBody(formattedBody);
const formattedFallback =
`<mx-reply>` +
`<blockquote>` +
`<a href="${permaLink}">In reply to</a>` +
`${prefix}<a href="https://matrix.to/#/${sender}">${sender}</a>` +
`<br />` +
`${cleanedFormattedBody}` +
`</blockquote>` +
`</mx-reply>`;
const newFormattedBody = formattedFallback + htmlEscape(body).replaceAll('\n', '<br/>');

return _createReplyContent(entry.id, msgtype, newBody, newFormattedBody);
}

0 comments on commit fdb7621

Please sign in to comment.