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 */
21 rkey?: string;
22 /**
23 * Optional renderer override for custom presentation.
24 */
25 renderer?: React.ComponentType<BlueskyProfileRendererInjectedProps>;
26 /**
27 * Fallback node shown before a request begins yielding data.
28 */
29 fallback?: React.ReactNode;
30 /**
31 * Loading indicator shown during in-flight fetches.
32 */
33 loadingIndicator?: React.ReactNode;
34 /**
35 * Pre-resolved handle to display when available externally.
36 */
37 handle?: string;
38 /**
39 * Preferred color scheme forwarded to renderer implementations.
40 */
41 colorScheme?: "light" | "dark" | "system";
42}
43
44/**
45 * Props injected into custom profile renderer implementations.
46 */
47export type BlueskyProfileRendererInjectedProps = {
48 /**
49 * Loaded profile record value.
50 */
51 record: ProfileRecord;
52 /**
53 * Indicates whether the record is currently being fetched.
54 */
55 loading: boolean;
56 /**
57 * Any error encountered while fetching the profile.
58 */
59 error?: Error;
60 /**
61 * DID associated with the profile.
62 */
63 did: string;
64 /**
65 * Human-readable handle for the DID, when known.
66 */
67 handle?: string;
68 /**
69 * Blob URL for the user's avatar, when available.
70 */
71 avatarUrl?: string;
72 /**
73 * Preferred color scheme for theming downstream components.
74 */
75 colorScheme?: "light" | "dark" | "system";
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 * @param colorScheme - Preferred color scheme forwarded to the renderer.
92 * @returns A rendered profile component with loading/error states handled.
93 */
94export const BlueskyProfile: React.FC<BlueskyProfileProps> = ({
95 did: handleOrDid,
96 rkey = "self",
97 renderer,
98 fallback,
99 loadingIndicator,
100 handle,
101 colorScheme,
102}) => {
103 const Component: React.ComponentType<BlueskyProfileRendererInjectedProps> =
104 renderer ?? ((props) => <BlueskyProfileRenderer {...props} />);
105 const { did, handle: resolvedHandle } = useDidResolution(handleOrDid);
106 const repoIdentifier = did ?? handleOrDid;
107 const effectiveHandle =
108 handle ??
109 resolvedHandle ??
110 (handleOrDid.startsWith("did:")
111 ? formatDidForLabel(repoIdentifier)
112 : handleOrDid);
113
114 const Wrapped: React.FC<{
115 record: ProfileRecord;
116 loading: boolean;
117 error?: Error;
118 }> = (props) => {
119 const avatarCid = getAvatarCid(props.record);
120 const { url: avatarUrl } = useBlob(repoIdentifier, avatarCid);
121 return (
122 <Component
123 {...props}
124 did={repoIdentifier}
125 handle={effectiveHandle}
126 avatarUrl={avatarUrl}
127 colorScheme={colorScheme}
128 />
129 );
130 };
131 return (
132 <AtProtoRecord<ProfileRecord>
133 did={repoIdentifier}
134 collection={BLUESKY_PROFILE_COLLECTION}
135 rkey={rkey}
136 renderer={Wrapped}
137 fallback={fallback}
138 loadingIndicator={loadingIndicator}
139 />
140 );
141};
142
143export default BlueskyProfile;