import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import type { Facet, FacetFeature } from '../api/types';
import HandleHoverCard from '../components/HandleHoverCard';
function byteOffsetToStrIndex(text: string, byteOffset: number): number {
const encoder = new TextEncoder();
let strIdx = 0;
let bytePos = 0;
while (bytePos < byteOffset && strIdx < text.length) {
const codePoint = text.codePointAt(strIdx)!;
const byteLen = encoder.encode(String.fromCodePoint(codePoint)).length;
bytePos += byteLen;
strIdx += codePoint > 0xffff ? 2 : 1;
}
return strIdx;
}
export interface PostBodyProps {
text: string;
facets?: Facet[];
className?: string;
}
export default function PostBody({ text, facets, className }: PostBodyProps) {
const navigate = useNavigate();
const handleMentionClick = useCallback((did: string) => (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
navigate(`/profile/${encodeURIComponent(did)}`);
}, [navigate]);
const handleHashtagClick = useCallback((tag: string) => (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
navigate(`/search?q=${encodeURIComponent('#' + tag)}`);
}, [navigate]);
if (facets && facets.length > 0) {
return renderWithFacets(text, facets, className, handleMentionClick, handleHashtagClick);
}
return renderWithRegex(text, className, handleMentionClick, handleHashtagClick);
}
interface Segment {
start: number;
end: number;
feature: FacetFeature;
}
function renderWithFacets(
text: string,
facets: Facet[],
className?: string,
onMention?: (did: string) => (e: React.MouseEvent) => void,
onHashtag?: (tag: string) => (e: React.MouseEvent) => void,
): React.ReactNode {
const segments: Segment[] = [];
for (const facet of facets) {
const start = byteOffsetToStrIndex(text, facet.index.byteStart);
const end = byteOffsetToStrIndex(text, facet.index.byteEnd);
for (const feature of facet.features) {
const t = feature.$type;
if (
t === 'app.bsky.richtext.facet#mention' ||
t === 'app.bsky.richtext.facet#link' ||
t === 'app.bsky.richtext.facet#tag'
) {
segments.push({ start, end, feature });
break;
}
}
}
segments.sort((a, b) => a.start - b.start);
const nodes: React.ReactNode[] = [];
let cursor = 0;
for (const seg of segments) {
if (seg.start < cursor || seg.end > text.length || seg.start >= seg.end) continue;
if (seg.start > cursor) {
nodes.push(text.slice(cursor, seg.start));
}
const matchedText = text.slice(seg.start, seg.end);
const f = seg.feature;
if (f.$type === 'app.bsky.richtext.facet#mention') {
const did = (f as { did: string }).did;
const mentionHandle = matchedText.replace(/^@/, '');
nodes.push(