Render the formatting a message was sent with
A Matrix message carrying its own formatted body is shown with it rather than being run through the BBCode and markdown pass, which would mangle markup that is already markup. Media, code and quote extraction still read the plain body, so link previews keep working on a formatted message. The sanitiser gained the structural tags that formatting actually uses - paragraphs, lists, quotes, code, headings. Each needed a render case and not just a place on the whitelist: the walk has no default branch, so a tag allowed through without one renders as nothing at all, children included. None of them can carry behaviour, and everything outside the list still degrades to its text. Headings render as bold. A real heading inside a chat line would tower over the conversation around it. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+1
-1
Submodule nobilis updated: 2821bc926a...1e11ce222c
@@ -1914,6 +1914,46 @@ img.smilie {
|
||||
A row is "what it is" against "what you can do about it", with the name
|
||||
allowed to shrink: a room id is long, and the two buttons must stay
|
||||
reachable rather than being pushed off the edge by it. */
|
||||
/* Formatting a sender supplied themselves - Matrix's formatted_body.
|
||||
Deliberately restrained: a message is a line in a conversation, so lists
|
||||
and quotes get structure without the vertical space a document would take,
|
||||
and the first and last blocks lose their margins so a formatted message
|
||||
sits on the same baseline as a plain one beside it. */
|
||||
.message-body p {
|
||||
margin: 0 0 var(--spacing-xxs);
|
||||
}
|
||||
|
||||
.message-body p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.message-body ul,
|
||||
.message-body ol {
|
||||
margin: var(--spacing-xxs) 0;
|
||||
padding-left: 1.4em;
|
||||
}
|
||||
|
||||
.message-body blockquote {
|
||||
margin: var(--spacing-xxs) 0;
|
||||
padding-left: var(--spacing-s);
|
||||
border-left: 2px solid var(--outline-medium);
|
||||
color: var(--surface-variant-text);
|
||||
}
|
||||
|
||||
.message-body pre {
|
||||
margin: var(--spacing-xxs) 0;
|
||||
padding: var(--spacing-s);
|
||||
border-radius: var(--corner-radius-small);
|
||||
background: var(--surface-variant);
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.message-body code {
|
||||
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* The "X is typing" line. Reserves no height when empty: a row that
|
||||
appears and disappears under the composer would shove the message list
|
||||
up and down as people start and stop writing. */
|
||||
|
||||
@@ -127,17 +127,28 @@ export function MessageRow({
|
||||
body = stripQuoteBlocks(body)
|
||||
|
||||
return {
|
||||
html: formatMessage(body, {
|
||||
revealedSpoilers: revealed,
|
||||
isSockchat,
|
||||
smilies: smilieIndex,
|
||||
channels
|
||||
}),
|
||||
// A sender's own formatting, where the protocol carried any: Matrix
|
||||
// sends a formatted body alongside the plain one. Used instead of the
|
||||
// BBCode/markdown pass rather than as well as it - it is already
|
||||
// markup, and running it through a second syntax would mangle it. Not
|
||||
// trusted for being structured: RichText sanitises it exactly as it
|
||||
// sanitises everything else.
|
||||
//
|
||||
// Media, code and quote extraction still read the plain body above, so
|
||||
// link previews keep working on a formatted message.
|
||||
html:
|
||||
message.html ??
|
||||
formatMessage(body, {
|
||||
revealedSpoilers: revealed,
|
||||
isSockchat,
|
||||
smilies: smilieIndex,
|
||||
channels
|
||||
}),
|
||||
media,
|
||||
codeBlocks,
|
||||
quoteBlocks
|
||||
}
|
||||
}, [message.body, contentSniffing, sniffed, revealed, isSockchat, smilieIndex, channels])
|
||||
}, [message.body, message.html, contentSniffing, sniffed, revealed, isSockchat, smilieIndex, channels])
|
||||
|
||||
const entries: MenuEntry[] = [
|
||||
{ label: 'Reply', icon: 'reply', onClick: () => reply() },
|
||||
|
||||
@@ -15,7 +15,38 @@ import { MEDIA_SCHEME, resolveMediaUrl } from './util'
|
||||
* to its text content.
|
||||
*/
|
||||
|
||||
const ALLOWED_TAGS = new Set(['B', 'STRONG', 'I', 'EM', 'U', 'S', 'STRIKE', 'SPAN', 'A', 'IMG', 'BR'])
|
||||
const ALLOWED_TAGS = new Set([
|
||||
'B',
|
||||
'STRONG',
|
||||
'I',
|
||||
'EM',
|
||||
'U',
|
||||
'S',
|
||||
'STRIKE',
|
||||
'SPAN',
|
||||
'A',
|
||||
'IMG',
|
||||
'BR',
|
||||
// Structure, for the formatting Matrix senders actually use. None of
|
||||
// these can carry behaviour - they are containers, and every attribute is
|
||||
// filtered separately below - so allowing them widens what renders
|
||||
// without widening what can run. Anything still outside the list keeps
|
||||
// degrading to its text.
|
||||
'P',
|
||||
'CODE',
|
||||
'PRE',
|
||||
'BLOCKQUOTE',
|
||||
'UL',
|
||||
'OL',
|
||||
'LI',
|
||||
'DEL',
|
||||
'H1',
|
||||
'H2',
|
||||
'H3',
|
||||
'H4',
|
||||
'H5',
|
||||
'H6'
|
||||
])
|
||||
|
||||
/**
|
||||
* Tags whose text content is markup rather than prose. Everything else that
|
||||
@@ -197,8 +228,50 @@ function walk(node: Node, handlers: Handlers, depth: number): ReactNode[] {
|
||||
break
|
||||
case 'STRIKE':
|
||||
case 'S':
|
||||
// Matrix's own spelling of the same thing.
|
||||
case 'DEL':
|
||||
out.push(<s key={key}>{kids()}</s>)
|
||||
break
|
||||
|
||||
// Structure. These need cases of their own and not just a place on the
|
||||
// whitelist: the switch has no default, so a tag allowed through
|
||||
// without one renders as nothing at all - its children included.
|
||||
case 'P':
|
||||
out.push(<p key={key}>{kids()}</p>)
|
||||
break
|
||||
case 'CODE':
|
||||
out.push(
|
||||
<code key={key} className={className}>
|
||||
{kids()}
|
||||
</code>
|
||||
)
|
||||
break
|
||||
case 'PRE':
|
||||
out.push(<pre key={key}>{kids()}</pre>)
|
||||
break
|
||||
case 'BLOCKQUOTE':
|
||||
out.push(<blockquote key={key}>{kids()}</blockquote>)
|
||||
break
|
||||
case 'UL':
|
||||
out.push(<ul key={key}>{kids()}</ul>)
|
||||
break
|
||||
case 'OL':
|
||||
out.push(<ol key={key}>{kids()}</ol>)
|
||||
break
|
||||
case 'LI':
|
||||
out.push(<li key={key}>{kids()}</li>)
|
||||
break
|
||||
// A heading inside a chat line is emphasis, not document structure -
|
||||
// rendering one at heading size would tower over the conversation
|
||||
// around it.
|
||||
case 'H1':
|
||||
case 'H2':
|
||||
case 'H3':
|
||||
case 'H4':
|
||||
case 'H5':
|
||||
case 'H6':
|
||||
out.push(<b key={key}>{kids()}</b>)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -229,6 +229,13 @@ export interface Message {
|
||||
* their media as URLs inside the body, which link unfurling picks up.
|
||||
*/
|
||||
attachments?: Attachment[]
|
||||
/**
|
||||
* The sender's own formatted version of `body`, where the protocol carries
|
||||
* one (Matrix's `formatted_body`). Restricted HTML, and still untrusted -
|
||||
* render it through RichText's whitelist like anything else. `body` stays
|
||||
* the plain-text fallback and is what search reads.
|
||||
*/
|
||||
html?: string
|
||||
/** Matrix only - the sender's full MXID, for targeting moderation actions. */
|
||||
senderId?: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user