import React, { useMemo, useRef } from "react"; import { useDidResolution } from "../hooks/useDidResolution"; import { useBlob } from "../hooks/useBlob"; import { useAtProto } from "../providers/AtProtoProvider"; import { parseAtUri, formatDidForLabel, toBlueskyPostUrl, leafletRkeyUrl, normalizeLeafletBasePath, } from "../utils/at-uri"; import { BlueskyPost } from "../components/BlueskyPost"; import type { LeafletDocumentRecord, LeafletLinearDocumentPage, LeafletLinearDocumentBlock, LeafletBlock, LeafletTextBlock, LeafletHeaderBlock, LeafletBlockquoteBlock, LeafletImageBlock, LeafletUnorderedListBlock, LeafletListItem, LeafletWebsiteBlock, LeafletIFrameBlock, LeafletMathBlock, LeafletCodeBlock, LeafletBskyPostBlock, LeafletAlignmentValue, LeafletRichTextFacet, LeafletRichTextFeature, LeafletPublicationRecord, } from "../types/leaflet"; export interface LeafletDocumentRendererProps { record: LeafletDocumentRecord; loading: boolean; error?: Error; did: string; rkey: string; canonicalUrl?: string; publicationBaseUrl?: string; publicationRecord?: LeafletPublicationRecord; } export const LeafletDocumentRenderer: React.FC< LeafletDocumentRendererProps > = ({ record, loading, error, did, rkey, canonicalUrl, publicationBaseUrl, publicationRecord, }) => { const { blueskyAppBaseUrl } = useAtProto(); const authorDid = record.author?.startsWith("did:") ? record.author : undefined; const publicationUri = useMemo( () => parseAtUri(record.publication), [record.publication], ); const postUrl = useMemo(() => { const postRefUri = record.postRef?.uri; if (!postRefUri) return undefined; const parsed = parseAtUri(postRefUri); return parsed ? toBlueskyPostUrl(parsed) : undefined; }, [record.postRef?.uri]); const { handle: publicationHandle } = useDidResolution(publicationUri?.did); const fallbackAuthorLabel = useAuthorLabel(record.author, authorDid); const resolvedPublicationLabel = publicationRecord?.name?.trim() ?? (publicationHandle ? `@${publicationHandle}` : publicationUri ? formatDidForLabel(publicationUri.did) : undefined); const authorLabel = resolvedPublicationLabel ?? fallbackAuthorLabel; const authorHref = publicationUri ? `${blueskyAppBaseUrl}/profile/${publicationUri.did}` : undefined; if (error) return (
Failed to load leaflet.
); if (loading && !record) return
Loading leaflet…
; if (!record) return (
Leaflet record missing.
); const publishedAt = record.publishedAt ? new Date(record.publishedAt) : undefined; const publishedLabel = publishedAt ? publishedAt.toLocaleString(undefined, { dateStyle: "long", timeStyle: "short", }) : undefined; const fallbackLeafletUrl = `${blueskyAppBaseUrl}/leaflet/${encodeURIComponent(did)}/${encodeURIComponent(rkey)}`; const publicationRoot = publicationBaseUrl ?? publicationRecord?.base_path ?? undefined; const resolvedPublicationRoot = publicationRoot ? normalizeLeafletBasePath(publicationRoot) : undefined; const publicationLeafletUrl = leafletRkeyUrl(publicationRoot, rkey); const viewUrl = canonicalUrl ?? publicationLeafletUrl ?? postUrl ?? (publicationUri ? `${blueskyAppBaseUrl}/profile/${publicationUri.did}` : undefined) ?? fallbackLeafletUrl; const metaItems: React.ReactNode[] = []; if (authorLabel) { const authorNode = authorHref ? ( {authorLabel} ) : ( authorLabel ); metaItems.push(By {authorNode}); } if (publishedLabel) metaItems.push( , ); if (resolvedPublicationRoot) { metaItems.push( {resolvedPublicationRoot.replace(/^https?:\/\//, "")} , ); } if (viewUrl) { metaItems.push( View source , ); } return (

{record.title}

{record.description && (

{record.description}

)}
{metaItems.map((item, idx) => ( {idx > 0 && ( )} {item} ))}
{record.pages?.map((page, pageIndex) => ( ))}
); }; const LeafletPageRenderer: React.FC<{ page: LeafletLinearDocumentPage; documentDid: string; }> = ({ page, documentDid }) => { if (!page.blocks?.length) return null; return (
{page.blocks.map((blockWrapper, idx) => ( ))}
); }; interface LeafletBlockRendererProps { wrapper: LeafletLinearDocumentBlock; documentDid: string; isFirst?: boolean; } const LeafletBlockRenderer: React.FC = ({ wrapper, documentDid, isFirst, }) => { const block = wrapper.block; if (!block || !("$type" in block) || !block.$type) { return null; } const alignment = alignmentValue(wrapper.alignment); switch (block.$type) { case "pub.leaflet.blocks.header": return ( ); case "pub.leaflet.blocks.blockquote": return ( ); case "pub.leaflet.blocks.image": return ( ); case "pub.leaflet.blocks.unorderedList": return ( ); case "pub.leaflet.blocks.website": return ( ); case "pub.leaflet.blocks.iframe": return ( ); case "pub.leaflet.blocks.math": return ( ); case "pub.leaflet.blocks.code": return ( ); case "pub.leaflet.blocks.horizontalRule": return ( ); case "pub.leaflet.blocks.bskyPost": return ( ); case "pub.leaflet.blocks.text": default: return ( ); } }; const LeafletTextBlockView: React.FC<{ block: LeafletTextBlock; alignment?: React.CSSProperties["textAlign"]; isFirst?: boolean; }> = ({ block, alignment, isFirst }) => { const segments = useMemo( () => createFacetedSegments(block.plaintext, block.facets), [block.plaintext, block.facets], ); const textContent = block.plaintext ?? ""; if (!textContent.trim() && segments.length === 0) { return null; } const style: React.CSSProperties = { ...base.paragraph, color: `var(--atproto-color-text)`, ...(alignment ? { textAlign: alignment } : undefined), ...(isFirst ? { marginTop: 0 } : undefined), }; return (

{segments.map((segment, idx) => ( {renderSegment(segment)} ))}

); }; const LeafletHeaderBlockView: React.FC<{ block: LeafletHeaderBlock; alignment?: React.CSSProperties["textAlign"]; isFirst?: boolean; }> = ({ block, alignment, isFirst }) => { const level = block.level && block.level >= 1 && block.level <= 6 ? block.level : 2; const segments = useMemo( () => createFacetedSegments(block.plaintext, block.facets), [block.plaintext, block.facets], ); const normalizedLevel = Math.min(Math.max(level, 1), 6) as | 1 | 2 | 3 | 4 | 5 | 6; const headingTag = (["h1", "h2", "h3", "h4", "h5", "h6"] as const)[ normalizedLevel - 1 ]; const style: React.CSSProperties = { ...base.heading, color: `var(--atproto-color-text)`, fontSize: normalizedLevel === 1 ? 30 : normalizedLevel === 2 ? 28 : normalizedLevel === 3 ? 24 : normalizedLevel === 4 ? 20 : normalizedLevel === 5 ? 18 : 16, ...(alignment ? { textAlign: alignment } : undefined), ...(isFirst ? { marginTop: 0 } : undefined), }; return React.createElement( headingTag, { style }, segments.map((segment, idx) => ( {renderSegment(segment)} )), ); }; const LeafletBlockquoteBlockView: React.FC<{ block: LeafletBlockquoteBlock; alignment?: React.CSSProperties["textAlign"]; isFirst?: boolean; }> = ({ block, alignment, isFirst }) => { const segments = useMemo( () => createFacetedSegments(block.plaintext, block.facets), [block.plaintext, block.facets], ); const textContent = block.plaintext ?? ""; if (!textContent.trim() && segments.length === 0) { return null; } return (
{segments.map((segment, idx) => ( {renderSegment(segment)} ))}
); }; const LeafletImageBlockView: React.FC<{ block: LeafletImageBlock; alignment?: React.CSSProperties["textAlign"]; documentDid: string; }> = ({ block, alignment, documentDid }) => { const cid = block.image?.ref?.$link ?? block.image?.cid; const { url, loading, error } = useBlob(documentDid, cid); const aspectRatio = block.aspectRatio?.height && block.aspectRatio?.width ? `${block.aspectRatio.width} / ${block.aspectRatio.height}` : undefined; return (
{url && !error ? ( {block.alt ) : (
{loading ? "Loading image…" : error ? "Image unavailable" : "No image"}
)}
{block.alt && block.alt.trim().length > 0 && (
{block.alt}
)}
); }; const LeafletListBlockView: React.FC<{ block: LeafletUnorderedListBlock; alignment?: React.CSSProperties["textAlign"]; documentDid: string; }> = ({ block, alignment, documentDid }) => { return (
    {block.children?.map((child, idx) => ( ))}
); }; const LeafletListItemRenderer: React.FC<{ item: LeafletListItem; documentDid: string; alignment?: React.CSSProperties["textAlign"]; }> = ({ item, documentDid, alignment }) => { return (
  • {item.children && item.children.length > 0 && (
      {item.children.map((child, idx) => ( ))}
    )}
  • ); }; const LeafletInlineBlock: React.FC<{ block: LeafletBlock; documentDid: string; alignment?: React.CSSProperties["textAlign"]; }> = ({ block, documentDid, alignment }) => { switch (block.$type) { case "pub.leaflet.blocks.header": return ( ); case "pub.leaflet.blocks.blockquote": return ( ); case "pub.leaflet.blocks.image": return ( ); default: return ( ); } }; const LeafletWebsiteBlockView: React.FC<{ block: LeafletWebsiteBlock; alignment?: React.CSSProperties["textAlign"]; documentDid: string; }> = ({ block, alignment, documentDid }) => { const previewCid = block.previewImage?.ref?.$link ?? block.previewImage?.cid; const { url, loading, error } = useBlob(documentDid, previewCid); return ( {url && !error ? ( {block.title ) : (
    {loading ? "Loading preview…" : "Open link"}
    )}
    {block.title && ( {block.title} )} {block.description && (

    {block.description}

    )} {block.src}
    ); }; const LeafletIframeBlockView: React.FC<{ block: LeafletIFrameBlock; alignment?: React.CSSProperties["textAlign"]; }> = ({ block, alignment }) => { return (