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;