'use client'; import { useEffect, useState } from 'react'; import PostPreview from '@/components/PostPreview'; import RecordPreview from '@/components/RecordPreview'; import { MarginAnnotationPreview, MarginBookmarkPreview, MarginCollectionItemPreview, MarginCollectionPreview, MarginHighlightPreview, MarginLikePreview, MarginReplyPreview, } from '@/components/margin'; import { getMarginLexiconType } from '@/utils/marginLexicons'; import { fetchPostThread, type GenericRecord, type PostThread, } from '@/utils/recordFetcher'; import type { AtRecord } from '@/utils/atproto/pdsClient'; type Props = { handle: string; did: string; collection: string; rkey: string; /** PDS-fetched record; used as the source for non-post / margin previews. */ record: AtRecord | null; /** * Optional action UI (e.g. the explorer's Edit button) rendered inside * the generic RecordPreview's footer next to the CID. Ignored by the * specialised post / margin previews — those have their own surrounds * and the explorer renders the action above them instead. */ footerActions?: import('react').ReactNode; }; /** * Returns true when the given collection will render through the generic * RecordPreview fallback (i.e. not a post, not a margin lexicon). The * explorer uses this to decide whether its edit button gets slotted into * the preview's footer or rendered as a standalone chip above the card. */ export function previewRendersGeneric(collection: string): boolean { if (collection === 'app.bsky.feed.post') return false; if (getMarginLexiconType(collection)) return false; return true; } /** * Renders the same rich record preview that universal link pages show — * PostPreview for Bluesky posts, the specialised margin previews for the * at.margin.* lexicons, and the generic RecordPreview otherwise. Returns * null when there's nothing to render yet (e.g. still fetching). * * Posts need a second fetch (AppView's getPostThread for author info, * embeds, engagement counts) because the PDS-only record we already have * is just the bare record value. */ export default function RichRecordPreview({ handle, did, collection, rkey, record, footerActions, }: Props) { const marginType = getMarginLexiconType(collection); const isPost = collection === 'app.bsky.feed.post'; const [postThread, setPostThread] = useState(null); useEffect(() => { if (!isPost) { setPostThread(null); return undefined; } let cancelled = false; const atUri = `at://${did}/${collection}/${rkey}`; fetchPostThread(atUri).then((thread) => { if (!cancelled) setPostThread(thread); }); return () => { cancelled = true; }; }, [isPost, did, collection, rkey]); // Posts: prefer the AppView thread (renders embeds, author, counts). if (isPost) { const post = postThread?.thread[0]?.value.post; if (!post) return null; return ; } if (!record) return null; const recordForLegacy: GenericRecord = { uri: record.uri, cid: record.cid, value: record.value, }; // Margin lexicons get their own renderers — close visual parity with the // universal link page so users get the same affordances. if (marginType === 'at.margin.annotation') { return ; } if (marginType === 'at.margin.bookmark') { return ; } if (marginType === 'at.margin.highlight') { return ; } if (marginType === 'at.margin.collection') { return ; } if (marginType === 'at.margin.collectionItem') { return ; } if (marginType === 'at.margin.reply') { return ; } if (marginType === 'at.margin.like') { return ; } return ( ); }