import React, { useMemo, useRef } from 'react'; import { useColorScheme, type ColorSchemePreference } from '../hooks/useColorScheme'; import { useDidResolution } from '../hooks/useDidResolution'; import { useBlob } from '../hooks/useBlob'; 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; colorScheme?: ColorSchemePreference; did: string; rkey: string; canonicalUrl?: string; publicationBaseUrl?: string; publicationRecord?: LeafletPublicationRecord; } export const LeafletDocumentRenderer: React.FC = ({ record, loading, error, colorScheme = 'system', did, rkey, canonicalUrl, publicationBaseUrl, publicationRecord }) => { const scheme = useColorScheme(colorScheme); const palette = scheme === 'dark' ? theme.dark : theme.light; 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 ? `https://bsky.app/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 = `https://bsky.app/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 ? `https://bsky.app/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; colorScheme: 'light' | 'dark' }> = ({ page, documentDid, colorScheme }) => { if (!page.blocks?.length) return null; return (
{page.blocks.map((blockWrapper, idx) => ( ))}
); }; interface LeafletBlockRendererProps { wrapper: LeafletLinearDocumentBlock; documentDid: string; colorScheme: 'light' | 'dark'; isFirst?: boolean; } const LeafletBlockRenderer: React.FC = ({ wrapper, documentDid, colorScheme, 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']; colorScheme: 'light' | 'dark'; isFirst?: boolean }> = ({ block, alignment, colorScheme, 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 palette = colorScheme === 'dark' ? theme.dark : theme.light; const style: React.CSSProperties = { ...base.paragraph, ...palette.paragraph, ...(alignment ? { textAlign: alignment } : undefined), ...(isFirst ? { marginTop: 0 } : undefined) }; return (

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

); }; const LeafletHeaderBlockView: React.FC<{ block: LeafletHeaderBlock; alignment?: React.CSSProperties['textAlign']; colorScheme: 'light' | 'dark'; isFirst?: boolean }> = ({ block, alignment, colorScheme, isFirst }) => { const palette = colorScheme === 'dark' ? theme.dark : theme.light; 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 headingStyles = palette.heading[normalizedLevel]; const style: React.CSSProperties = { ...base.heading, ...headingStyles, ...(alignment ? { textAlign: alignment } : undefined), ...(isFirst ? { marginTop: 0 } : undefined) }; return React.createElement( headingTag, { style }, segments.map((segment, idx) => ( {renderSegment(segment, colorScheme)} )) ); }; const LeafletBlockquoteBlockView: React.FC<{ block: LeafletBlockquoteBlock; alignment?: React.CSSProperties['textAlign']; colorScheme: 'light' | 'dark'; isFirst?: boolean }> = ({ block, alignment, colorScheme, 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 palette = colorScheme === 'dark' ? theme.dark : theme.light; return (
{segments.map((segment, idx) => ( {renderSegment(segment, colorScheme)} ))}
); }; const LeafletImageBlockView: React.FC<{ block: LeafletImageBlock; alignment?: React.CSSProperties['textAlign']; documentDid: string; colorScheme: 'light' | 'dark' }> = ({ block, alignment, documentDid, colorScheme }) => { const palette = colorScheme === 'dark' ? theme.dark : theme.light; 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; colorScheme: 'light' | 'dark' }> = ({ block, alignment, documentDid, colorScheme }) => { const palette = colorScheme === 'dark' ? theme.dark : theme.light; return (
    {block.children?.map((child, idx) => ( ))}
); }; const LeafletListItemRenderer: React.FC<{ item: LeafletListItem; documentDid: string; colorScheme: 'light' | 'dark'; alignment?: React.CSSProperties['textAlign'] }> = ({ item, documentDid, colorScheme, alignment }) => { return (
  • {item.children && item.children.length > 0 && (
      {item.children.map((child, idx) => ( ))}
    )}
  • ); }; const LeafletInlineBlock: React.FC<{ block: LeafletBlock; colorScheme: 'light' | 'dark'; documentDid: string; alignment?: React.CSSProperties['textAlign'] }> = ({ block, colorScheme, 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; colorScheme: 'light' | 'dark' }> = ({ block, alignment, documentDid, colorScheme }) => { const palette = colorScheme === 'dark' ? theme.dark : theme.light; 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 (