69 lines
1.9 KiB
TypeScript
69 lines
1.9 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* Renders a message body from its entity spans.
|
|
*
|
|
* Nothing here parses markup — the server already turned the composer's
|
|
* markers into clean text plus spans, so a message cannot inject markup into
|
|
* anyone else's thread. This component only decorates ranges.
|
|
*/
|
|
|
|
import { buildEntityTree, type EntityNode } from "@/lib/chat-format";
|
|
import type { MessageEntity } from "@/lib/chat";
|
|
|
|
function renderNodes(nodes: EntityNode[], keyPrefix: string): React.ReactNode[] {
|
|
return nodes.map((node, i) => {
|
|
const key = `${keyPrefix}-${i}`;
|
|
if (node.kind === "text") return <span key={key}>{node.text}</span>;
|
|
|
|
const children = renderNodes(node.children, key);
|
|
switch (node.entity.type) {
|
|
case "bold":
|
|
return <strong key={key} className="font-semibold">{children}</strong>;
|
|
case "strike":
|
|
return <s key={key}>{children}</s>;
|
|
case "code":
|
|
return (
|
|
<code
|
|
key={key}
|
|
className="rounded bg-black/10 px-1 py-0.5 font-mono text-[0.9em] dark:bg-white/15"
|
|
>
|
|
{children}
|
|
</code>
|
|
);
|
|
case "url":
|
|
return (
|
|
<a
|
|
key={key}
|
|
href={node.entity.url}
|
|
target="_blank"
|
|
// noreferrer as well as noopener: the destination shouldn't learn
|
|
// which conversation the link was shared in.
|
|
rel="noopener noreferrer"
|
|
className="underline underline-offset-2 hover:opacity-80"
|
|
>
|
|
{children}
|
|
</a>
|
|
);
|
|
default:
|
|
return <span key={key}>{children}</span>;
|
|
}
|
|
});
|
|
}
|
|
|
|
export function MessageText({
|
|
body,
|
|
entities,
|
|
}: {
|
|
body: string;
|
|
entities: MessageEntity[];
|
|
}) {
|
|
// whitespace-pre-wrap keeps the sender's line breaks without turning the
|
|
// body into markup.
|
|
return (
|
|
<span className="whitespace-pre-wrap break-words">
|
|
{renderNodes(buildEntityTree(body, entities), "e")}
|
|
</span>
|
|
);
|
|
}
|