A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.
1import React from "react";
2import { AtProtoRecord } from "../core/AtProtoRecord";
3import { BlueskyProfileRenderer } from "../renderers/BlueskyProfileRenderer";
4import type { ProfileRecord } from "../types/bluesky";
5import { useBlob } from "../hooks/useBlob";
6import { getAvatarCid } from "../utils/profile";
7import { useDidResolution } from "../hooks/useDidResolution";
8import { formatDidForLabel } from "../utils/at-uri";
9
10/**
11 * Props used to render a Bluesky actor profile record.
12 */
13export interface BlueskyProfileProps {
14 /**
15 * DID of the target actor whose profile should be loaded.
16 */
17 did: string;
18 /**
19 * Record key within the profile collection. Typically `'self'`.
20 * Optional when `record` is provided.
21 */
22 rkey?: string;
23 /**
24 * Prefetched profile record. When provided, skips fetching the profile from the network.
25 */
26 record?: ProfileRecord;
27 /**
28 * Optional renderer override for custom presentation.
29 */
30 renderer?: React.ComponentType<BlueskyProfileRendererInjectedProps>;
31 /**
32 * Fallback node shown before a request begins yielding data.
33 */
34 fallback?: React.ReactNode;
35 /**
36 * Loading indicator shown during in-flight fetches.
37 */
38 loadingIndicator?: React.ReactNode;
39 /**
40 * Pre-resolved handle to display when available externally.
41 */
42 handle?: string;
43 /**
44 * Preferred color scheme forwarded to renderer implementations.
45 */
46 colorScheme?: "light" | "dark" | "system";
47}
48
49/**
50 * Props injected into custom profile renderer implementations.
51 */
52export type BlueskyProfileRendererInjectedProps = {
53 /**
54 * Loaded profile record value.
55 */
56 record: ProfileRecord;
57 /**
58 * Indicates whether the record is currently being fetched.
59 */
60 loading: boolean;
61 /**
62 * Any error encountered while fetching the profile.
63 */
64 error?: Error;
65 /**
66 * DID associated with the profile.
67 */
68 did: string;
69 /**
70 * Human-readable handle for the DID, when known.
71 */
72 handle?: string;
73 /**
74 * Blob URL for the user's avatar, when available.
75 */
76 avatarUrl?: string;
77 /**
78 * Preferred color scheme for theming downstream components.
79 */
80 colorScheme?: "light" | "dark" | "system";
81};
82
83/** NSID for the canonical Bluesky profile collection. */
84export const BLUESKY_PROFILE_COLLECTION = "app.bsky.actor.profile";
85
86/**
87 * Fetches and renders a Bluesky actor profile, optionally injecting custom presentation
88 * and providing avatar resolution support.
89 *
90 * @param did - DID whose profile record should be fetched.
91 * @param rkey - Record key within the profile collection (default `'self'`).
92 * @param renderer - Optional component override for custom rendering.
93 * @param fallback - Node rendered prior to loading state initialization.
94 * @param loadingIndicator - Node rendered while the profile request is in-flight.
95 * @param handle - Optional pre-resolved handle to display.
96 * @param colorScheme - Preferred color scheme forwarded to the renderer.
97 * @returns A rendered profile component with loading/error states handled.
98 */
99export const BlueskyProfile: React.FC<BlueskyProfileProps> = ({
100 did: handleOrDid,
101 rkey = "self",
102 record,
103 renderer,
104 fallback,
105 loadingIndicator,
106 handle,
107 colorScheme,
108}) => {
109 const Component: React.ComponentType<BlueskyProfileRendererInjectedProps> =
110 renderer ?? ((props) => <BlueskyProfileRenderer {...props} />);
111 const { did, handle: resolvedHandle } = useDidResolution(handleOrDid);
112 const repoIdentifier = did ?? handleOrDid;
113 const effectiveHandle =
114 handle ??
115 resolvedHandle ??
116 (handleOrDid.startsWith("did:")
117 ? formatDidForLabel(repoIdentifier)
118 : handleOrDid);
119
120 const Wrapped: React.FC<{
121 record: ProfileRecord;
122 loading: boolean;
123 error?: Error;
124 }> = (props) => {
125 const avatarCid = getAvatarCid(props.record);
126 const { url: avatarUrl } = useBlob(repoIdentifier, avatarCid);
127 return (
128 <Component
129 {...props}
130 did={repoIdentifier}
131 handle={effectiveHandle}
132 avatarUrl={avatarUrl}
133 colorScheme={colorScheme}
134 />
135 );
136 };
137
138 if (record !== undefined) {
139 return (
140 <AtProtoRecord<ProfileRecord>
141 record={record}
142 renderer={Wrapped}
143 fallback={fallback}
144 loadingIndicator={loadingIndicator}
145 />
146 );
147 }
148
149 return (
150 <AtProtoRecord<ProfileRecord>
151 did={repoIdentifier}
152 collection={BLUESKY_PROFILE_COLLECTION}
153 rkey={rkey}
154 renderer={Wrapped}
155 fallback={fallback}
156 loadingIndicator={loadingIndicator}
157 />
158 );
159};
160
161export default BlueskyProfile;