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 * 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 renderer - Optional renderer override applied to the parent post. 50 * @param fallback - Node rendered before parent post data loads. 51 * @param loadingIndicator - Node rendered while the parent post request is in-flight. 52 * @param showIcon - Controls whether the Bluesky icon renders. Defaults to `true`. 53 * @param iconPlacement - Placement location for the icon. Defaults to `'timestamp'`. 54 * @returns A `BlueskyPost` element configured with an augmented renderer. 55 */ 56const BlueskyQuotePostComponent: React.FC<BlueskyQuotePostProps> = ({ 57 did, 58 rkey, 59 renderer, 60 fallback, 61 loadingIndicator, 62 showIcon = true, 63 iconPlacement = "timestamp", 64}) => { 65 const BaseRenderer = renderer ?? BlueskyPostRenderer; 66 const Renderer = useMemo(() => { 67 const QuoteRenderer: React.FC<BlueskyPostRendererInjectedProps> = ( 68 props, 69 ) => { 70 const embedSource = props.record.embed as 71 | QuoteRecordEmbed 72 | undefined; 73 const embedNode = useMemo( 74 () => createQuoteEmbed(embedSource), 75 [embedSource], 76 ); 77 return <BaseRenderer isQuotePost={true} {...props} embed={embedNode} />; 78 }; 79 QuoteRenderer.displayName = "BlueskyQuotePostRenderer"; 80 const MemoizedQuoteRenderer = memo(QuoteRenderer); 81 MemoizedQuoteRenderer.displayName = "BlueskyQuotePostRenderer"; 82 return MemoizedQuoteRenderer; 83 }, [BaseRenderer]); 84 85 return ( 86 <BlueskyPost 87 did={did} 88 rkey={rkey} 89 renderer={Renderer} 90 fallback={fallback} 91 loadingIndicator={loadingIndicator} 92 showIcon={showIcon} 93 iconPlacement={iconPlacement} 94 /> 95 ); 96}; 97 98BlueskyQuotePostComponent.displayName = "BlueskyQuotePost"; 99 100export const BlueskyQuotePost: NamedExoticComponent<BlueskyQuotePostProps> = 101 memo(BlueskyQuotePostComponent); 102BlueskyQuotePost.displayName = "BlueskyQuotePost"; 103 104/** 105 * Builds the quoted post embed node when the parent record contains a record embed. 106 * 107 * @param embed - Embed payload containing a possible quote reference. 108 * @returns A nested `BlueskyPost` or `null` if no compatible embed exists. 109 */ 110type QuoteRecordEmbed = { $type?: string; record?: { uri?: string } }; 111 112function createQuoteEmbed( 113 embed: QuoteRecordEmbed | undefined, 114) { 115 if (!embed || embed.$type !== "app.bsky.embed.record") return null; 116 const quoted = embed.record; 117 const quotedUri = quoted?.uri; 118 const parsed = parseAtUri(quotedUri); 119 if (!parsed || parsed.collection !== BLUESKY_POST_COLLECTION) return null; 120 return ( 121 <div style={quoteWrapperStyle}> 122 <BlueskyPost 123 did={parsed.did} 124 rkey={parsed.rkey} 125 showIcon={false} 126 /> 127 </div> 128 ); 129} 130 131const quoteWrapperStyle: React.CSSProperties = { 132 display: "flex", 133 flexDirection: "column", 134 gap: 8, 135}; 136 137export default BlueskyQuotePost;