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;