Files
2026-08-29 03:49:58 +05:30

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>
);
}