A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.
1import React, { useMemo } from 'react'; 2import { BlueskyPost, type BlueskyPostRendererInjectedProps, BLUESKY_POST_COLLECTION } from './BlueskyPost'; 3import { BlueskyPostRenderer } from '../renderers/BlueskyPostRenderer'; 4import type { FeedPostRecord } from '../types/bluesky'; 5import { parseAtUri } from '../utils/at-uri'; 6 7/** 8 * Props for rendering a Bluesky post that quotes another Bluesky post. 9 */ 10export interface BlueskyQuotePostProps { 11 /** 12 * DID of the repository that owns the parent post. 13 */ 14 did: string; 15 /** 16 * Record key of the parent post. 17 */ 18 rkey: string; 19 /** 20 * Preferred color scheme propagated to nested renders. 21 */ 22 colorScheme?: 'light' | 'dark' | 'system'; 23 /** 24 * Custom renderer override applied to the parent post. 25 */ 26 renderer?: React.ComponentType<BlueskyPostRendererInjectedProps>; 27 /** 28 * Fallback content rendered before any request completes. 29 */ 30 fallback?: React.ReactNode; 31 /** 32 * Loading indicator rendered while the parent post is resolving. 33 */ 34 loadingIndicator?: React.ReactNode; 35 /** 36 * Controls whether the Bluesky icon is shown. Defaults to `true`. 37 */ 38 showIcon?: boolean; 39 /** 40 * Placement for the Bluesky icon. Defaults to `'timestamp'`. 41 */ 42 iconPlacement?: 'cardBottomRight' | 'timestamp' | 'linkInline'; 43} 44 45/** 46 * Renders a Bluesky post while embedding its quoted post inline via a nested `BlueskyPost`. 47 * 48 * @param did - DID that owns the quoted parent post. 49 * @param rkey - Record key identifying the parent post. 50 * @param colorScheme - Preferred color scheme for both parent and quoted posts. 51 * @param renderer - Optional renderer override applied to the parent post. 52 * @param fallback - Node rendered before parent post data loads. 53 * @param loadingIndicator - Node rendered while the parent post request is in-flight. 54 * @param showIcon - Controls whether the Bluesky icon renders. Defaults to `true`. 55 * @param iconPlacement - Placement location for the icon. Defaults to `'timestamp'`. 56 * @returns A `BlueskyPost` element configured with an augmented renderer. 57 */ 58export const BlueskyQuotePost: React.FC<BlueskyQuotePostProps> = ({ did, rkey, colorScheme, renderer, fallback, loadingIndicator, showIcon = true, iconPlacement = 'timestamp' }) => { 59 const BaseRenderer = renderer ?? BlueskyPostRenderer; 60 const Renderer = useMemo(() => { 61 const QuoteRenderer: React.FC<BlueskyPostRendererInjectedProps> = (props) => { 62 const embedNode = createQuoteEmbed(props.record, props.colorScheme ?? colorScheme); 63 return <BaseRenderer {...props} embed={embedNode} />; 64 }; 65 QuoteRenderer.displayName = 'BlueskyQuotePostRenderer'; 66 return QuoteRenderer; 67 }, [BaseRenderer, colorScheme]); 68 69 return ( 70 <BlueskyPost 71 did={did} 72 rkey={rkey} 73 colorScheme={colorScheme} 74 renderer={Renderer} 75 fallback={fallback} 76 loadingIndicator={loadingIndicator} 77 showIcon={showIcon} 78 iconPlacement={iconPlacement} 79 /> 80 ); 81}; 82 83/** 84 * Builds the quoted post embed node when the parent record contains a record embed. 85 * 86 * @param record - Feed post containing a possible quote reference. 87 * @param colorScheme - Desired visual theme for the nested quote. 88 * @returns A nested `BlueskyPost` or `null` if no compatible embed exists. 89 */ 90function createQuoteEmbed(record: FeedPostRecord, colorScheme?: 'light' | 'dark' | 'system') { 91 const embed = record.embed as { $type?: string; record?: { uri?: string } } | undefined; 92 if (!embed || embed.$type !== 'app.bsky.embed.record') return null; 93 const quoted = embed.record; 94 const quotedUri = quoted?.uri; 95 const parsed = parseAtUri(quotedUri); 96 if (!parsed || parsed.collection !== BLUESKY_POST_COLLECTION) return null; 97 return ( 98 <div style={quoteWrapperStyle}> 99 <BlueskyPost did={parsed.did} rkey={parsed.rkey} colorScheme={colorScheme} showIcon={false} /> 100 </div> 101 ); 102} 103 104const quoteWrapperStyle: React.CSSProperties = { 105 display: 'flex', 106 flexDirection: 'column', 107 gap: 8 108}; 109 110export default BlueskyQuotePost;