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;