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