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