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