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;