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 } 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}
20
21/**
22 * Internal context value shared across all AT Protocol hooks.
23 */
24interface AtProtoContextValue {
25 /** Service resolver for DID resolution and PDS endpoint discovery. */
26 resolver: ServiceResolver;
27 /** Normalized PLC directory base URL. */
28 plcDirectory: string;
29 /** Cache for DID documents and handle mappings. */
30 didCache: DidCache;
31 /** Cache for fetched blob data. */
32 blobCache: BlobCache;
33 /** Cache for fetched AT Protocol records. */
34 recordCache: RecordCache;
35}
36
37const AtProtoContext = createContext<AtProtoContextValue | undefined>(
38 undefined,
39);
40
41/**
42 * Context provider that supplies AT Protocol infrastructure to all child components.
43 *
44 * This provider initializes and shares:
45 * - Service resolver for DID and PDS endpoint resolution
46 * - DID cache for identity resolution
47 * - Blob cache for efficient media handling
48 *
49 * All AT Protocol components (`BlueskyPost`, `LeafletDocument`, etc.) must be wrapped
50 * in this provider to function correctly.
51 *
52 * @example
53 * ```tsx
54 * import { AtProtoProvider, BlueskyPost } from 'atproto-ui';
55 *
56 * function App() {
57 * return (
58 * <AtProtoProvider>
59 * <BlueskyPost did="did:plc:example" rkey="3k2aexample" />
60 * </AtProtoProvider>
61 * );
62 * }
63 * ```
64 *
65 * @example
66 * ```tsx
67 * // Using a custom PLC directory
68 * <AtProtoProvider plcDirectory="https://custom-plc.example.com">
69 * <YourComponents />
70 * </AtProtoProvider>
71 * ```
72 *
73 * @param children - Child components to render within the provider.
74 * @param plcDirectory - Optional PLC directory override (defaults to https://plc.directory).
75 * @returns Provider component that enables AT Protocol functionality.
76 */
77export function AtProtoProvider({
78 children,
79 plcDirectory,
80}: AtProtoProviderProps) {
81 const normalizedPlc = useMemo(
82 () =>
83 normalizeBaseUrl(
84 plcDirectory && plcDirectory.trim()
85 ? plcDirectory
86 : "https://plc.directory",
87 ),
88 [plcDirectory],
89 );
90 const resolver = useMemo(
91 () => new ServiceResolver({ plcDirectory: normalizedPlc }),
92 [normalizedPlc],
93 );
94 const cachesRef = useRef<{
95 didCache: DidCache;
96 blobCache: BlobCache;
97 recordCache: RecordCache;
98 } | null>(null);
99 if (!cachesRef.current) {
100 cachesRef.current = {
101 didCache: new DidCache(),
102 blobCache: new BlobCache(),
103 recordCache: new RecordCache(),
104 };
105 }
106
107 const value = useMemo<AtProtoContextValue>(
108 () => ({
109 resolver,
110 plcDirectory: normalizedPlc,
111 didCache: cachesRef.current!.didCache,
112 blobCache: cachesRef.current!.blobCache,
113 recordCache: cachesRef.current!.recordCache,
114 }),
115 [resolver, normalizedPlc],
116 );
117
118 return (
119 <AtProtoContext.Provider value={value}>
120 {children}
121 </AtProtoContext.Provider>
122 );
123}
124
125/**
126 * Hook that accesses the AT Protocol context provided by `AtProtoProvider`.
127 *
128 * This hook exposes the service resolver, DID cache, blob cache, and record cache
129 * for building custom AT Protocol functionality.
130 *
131 * @throws {Error} When called outside of an `AtProtoProvider`.
132 * @returns {AtProtoContextValue} Object containing resolver, caches, and PLC directory URL.
133 *
134 * @example
135 * ```tsx
136 * import { useAtProto } from 'atproto-ui';
137 *
138 * function MyCustomComponent() {
139 * const { resolver, didCache, blobCache, recordCache } = useAtProto();
140 * // Use the resolver and caches for custom AT Protocol operations
141 * }
142 * ```
143 */
144export function useAtProto() {
145 const ctx = useContext(AtProtoContext);
146 if (!ctx) throw new Error("useAtProto must be used within AtProtoProvider");
147 return ctx;
148}