A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.
1/* eslint-disable react-refresh/only-export-components */
2import React, {
3 createContext,
4 useContext,
5 useMemo,
6 useRef,
7} from "react";
8import { ServiceResolver, normalizeBaseUrl, DEFAULT_CONFIG } from "../utils/atproto-client";
9import { BlobCache, DidCache, RecordCache } from "../utils/cache";
10
11/**
12 * Props for the AT Protocol context provider.
13 */
14export interface AtProtoProviderProps {
15 /** Child components that will have access to the AT Protocol context. */
16 children: React.ReactNode;
17 /** Optional custom PLC directory URL. Defaults to https://plc.directory */
18 plcDirectory?: string;
19 /** Optional custom identity service URL. Defaults to https://public.api.bsky.app */
20 identityService?: string;
21 /** Optional custom Slingshot service URL. Defaults to https://slingshot.microcosm.blue */
22 slingshotBaseUrl?: string;
23 /** Optional custom Bluesky appview service URL. Defaults to https://public.api.bsky.app */
24 blueskyAppviewService?: string;
25 /** Optional custom Bluesky app base URL for links. Defaults to https://bsky.app */
26 blueskyAppBaseUrl?: string;
27 /** Optional custom Tangled base URL for links. Defaults to https://tangled.org */
28 tangledBaseUrl?: string;
29 /** Optional custom Constellation API URL for backlinks. Defaults to https://constellation.microcosm.blue */
30 constellationBaseUrl?: string;
31}
32
33/**
34 * Internal context value shared across all AT Protocol hooks.
35 */
36interface AtProtoContextValue {
37 /** Service resolver for DID resolution and PDS endpoint discovery. */
38 resolver: ServiceResolver;
39 /** Normalized PLC directory base URL. */
40 plcDirectory: string;
41 /** Normalized Bluesky appview service URL. */
42 blueskyAppviewService: string;
43 /** Normalized Bluesky app base URL for links. */
44 blueskyAppBaseUrl: string;
45 /** Normalized Tangled base URL for links. */
46 tangledBaseUrl: string;
47 /** Normalized Constellation API base URL for backlinks. */
48 constellationBaseUrl: string;
49 /** Cache for DID documents and handle mappings. */
50 didCache: DidCache;
51 /** Cache for fetched blob data. */
52 blobCache: BlobCache;
53 /** Cache for fetched AT Protocol records. */
54 recordCache: RecordCache;
55}
56
57const AtProtoContext = createContext<AtProtoContextValue | undefined>(
58 undefined,
59);
60
61/**
62 * Context provider that supplies AT Protocol infrastructure to all child components.
63 *
64 * This provider initializes and shares:
65 * - Service resolver for DID and PDS endpoint resolution
66 * - DID cache for identity resolution
67 * - Blob cache for efficient media handling
68 *
69 * All AT Protocol components (`BlueskyPost`, `LeafletDocument`, etc.) must be wrapped
70 * in this provider to function correctly.
71 *
72 * @example
73 * ```tsx
74 * import { AtProtoProvider, BlueskyPost } from 'atproto-ui';
75 *
76 * function App() {
77 * return (
78 * <AtProtoProvider>
79 * <BlueskyPost did="did:plc:example" rkey="3k2aexample" />
80 * </AtProtoProvider>
81 * );
82 * }
83 * ```
84 *
85 * @example
86 * ```tsx
87 * // Using a custom PLC directory
88 * <AtProtoProvider plcDirectory="https://custom-plc.example.com">
89 * <YourComponents />
90 * </AtProtoProvider>
91 * ```
92 *
93 * @param children - Child components to render within the provider.
94 * @param plcDirectory - Optional PLC directory override (defaults to https://plc.directory).
95 * @returns Provider component that enables AT Protocol functionality.
96 */
97export function AtProtoProvider({
98 children,
99 plcDirectory,
100 identityService,
101 slingshotBaseUrl,
102 blueskyAppviewService,
103 blueskyAppBaseUrl,
104 tangledBaseUrl,
105 constellationBaseUrl,
106}: AtProtoProviderProps) {
107 const normalizedPlc = useMemo(
108 () =>
109 normalizeBaseUrl(
110 plcDirectory && plcDirectory.trim()
111 ? plcDirectory
112 : DEFAULT_CONFIG.plcDirectory,
113 ),
114 [plcDirectory],
115 );
116 const normalizedIdentity = useMemo(
117 () =>
118 normalizeBaseUrl(
119 identityService && identityService.trim()
120 ? identityService
121 : DEFAULT_CONFIG.identityService,
122 ),
123 [identityService],
124 );
125 const normalizedSlingshot = useMemo(
126 () =>
127 normalizeBaseUrl(
128 slingshotBaseUrl && slingshotBaseUrl.trim()
129 ? slingshotBaseUrl
130 : DEFAULT_CONFIG.slingshotBaseUrl,
131 ),
132 [slingshotBaseUrl],
133 );
134 const normalizedAppview = useMemo(
135 () =>
136 normalizeBaseUrl(
137 blueskyAppviewService && blueskyAppviewService.trim()
138 ? blueskyAppviewService
139 : DEFAULT_CONFIG.blueskyAppviewService,
140 ),
141 [blueskyAppviewService],
142 );
143 const normalizedBlueskyApp = useMemo(
144 () =>
145 normalizeBaseUrl(
146 blueskyAppBaseUrl && blueskyAppBaseUrl.trim()
147 ? blueskyAppBaseUrl
148 : DEFAULT_CONFIG.blueskyAppBaseUrl,
149 ),
150 [blueskyAppBaseUrl],
151 );
152 const normalizedTangled = useMemo(
153 () =>
154 normalizeBaseUrl(
155 tangledBaseUrl && tangledBaseUrl.trim()
156 ? tangledBaseUrl
157 : DEFAULT_CONFIG.tangledBaseUrl,
158 ),
159 [tangledBaseUrl],
160 );
161 const normalizedConstellation = useMemo(
162 () =>
163 normalizeBaseUrl(
164 constellationBaseUrl && constellationBaseUrl.trim()
165 ? constellationBaseUrl
166 : DEFAULT_CONFIG.constellationBaseUrl,
167 ),
168 [constellationBaseUrl],
169 );
170 const resolver = useMemo(
171 () => new ServiceResolver({
172 plcDirectory: normalizedPlc,
173 identityService: normalizedIdentity,
174 slingshotBaseUrl: normalizedSlingshot,
175 }),
176 [normalizedPlc, normalizedIdentity, normalizedSlingshot],
177 );
178 const cachesRef = useRef<{
179 didCache: DidCache;
180 blobCache: BlobCache;
181 recordCache: RecordCache;
182 } | null>(null);
183 if (!cachesRef.current) {
184 cachesRef.current = {
185 didCache: new DidCache(),
186 blobCache: new BlobCache(),
187 recordCache: new RecordCache(),
188 };
189 }
190
191 const value = useMemo<AtProtoContextValue>(
192 () => ({
193 resolver,
194 plcDirectory: normalizedPlc,
195 blueskyAppviewService: normalizedAppview,
196 blueskyAppBaseUrl: normalizedBlueskyApp,
197 tangledBaseUrl: normalizedTangled,
198 constellationBaseUrl: normalizedConstellation,
199 didCache: cachesRef.current!.didCache,
200 blobCache: cachesRef.current!.blobCache,
201 recordCache: cachesRef.current!.recordCache,
202 }),
203 [resolver, normalizedPlc, normalizedAppview, normalizedBlueskyApp, normalizedTangled, normalizedConstellation],
204 );
205
206 return (
207 <AtProtoContext.Provider value={value}>
208 {children}
209 </AtProtoContext.Provider>
210 );
211}
212
213/**
214 * Hook that accesses the AT Protocol context provided by `AtProtoProvider`.
215 *
216 * This hook exposes the service resolver, DID cache, blob cache, and record cache
217 * for building custom AT Protocol functionality.
218 *
219 * @throws {Error} When called outside of an `AtProtoProvider`.
220 * @returns {AtProtoContextValue} Object containing resolver, caches, and PLC directory URL.
221 *
222 * @example
223 * ```tsx
224 * import { useAtProto } from 'atproto-ui';
225 *
226 * function MyCustomComponent() {
227 * const { resolver, didCache, blobCache, recordCache } = useAtProto();
228 * // Use the resolver and caches for custom AT Protocol operations
229 * }
230 * ```
231 */
232export function useAtProto() {
233 const ctx = useContext(AtProtoContext);
234 if (!ctx) throw new Error("useAtProto must be used within AtProtoProvider");
235 return ctx;
236}