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