From fdb76218770f8180bffdbfbe297ea0f9576a0c16 Mon Sep 17 00:00:00 2001 From: Mirian Margiani Date: Thu, 5 Dec 2024 17:17:19 +0100 Subject: [PATCH] Refactor reply formatting to follow the 1.12 spec 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: - https://github.com/matrix-org/matrix-spec-proposals/pull/2781 - https://github.com/matrix-org/matrix-spec-proposals/pull/3676 - spec: https://github.com/matrix-org/matrix-spec/pull/1994 Signed-off-by: Mirian Margiani --- src/matrix/room/timeline/entries/reply.js | 69 +++++++++++++++++++---- 1 file changed, 58 insertions(+), 11 deletions(-) diff --git a/src/matrix/room/timeline/entries/reply.js b/src/matrix/room/timeline/entries/reply.js index 6039a0e092..92b573137b 100644 --- a/src/matrix/room/timeline/entries/reply.js +++ b/src/matrix/room/timeline/entries/reply.js @@ -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 are gone. + return formattedBody.replace(/[\s\S]*<\/mx-reply>/gi, ''); +} + function _createReplyContent(targetId, msgtype, body, formattedBody) { return { msgtype, @@ -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 = `
In reply to ${prefix}` + - `${name}
` + - `${formattedBody}
`; + 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 = + `` + + `
` + + `In reply to` + + `${prefix}${sender}` + + `
` + + `${cleanedFormattedBody}` + + `
` + + `
`; + const newFormattedBody = formattedFallback + htmlEscape(body).replaceAll('\n', '
'); + return _createReplyContent(entry.id, msgtype, newBody, newFormattedBody); }