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}