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 * Optional when `record` is provided. 21 */ 22 rkey?: string; 23 /** 24 * Prefetched profile record. When provided, skips fetching the profile from the network. 25 */ 26 record?: ProfileRecord; 27 /** 28 * Optional renderer override for custom presentation. 29 */ 30 renderer?: React.ComponentType<BlueskyProfileRendererInjectedProps>; 31 /** 32 * Fallback node shown before a request begins yielding data. 33 */ 34 fallback?: React.ReactNode; 35 /** 36 * Loading indicator shown during in-flight fetches. 37 */ 38 loadingIndicator?: React.ReactNode; 39 /** 40 * Pre-resolved handle to display when available externally. 41 */ 42 handle?: string; 43 /** 44 * Preferred color scheme forwarded to renderer implementations. 45 */ 46 colorScheme?: "light" | "dark" | "system"; 47} 48 49/** 50 * Props injected into custom profile renderer implementations. 51 */ 52export type BlueskyProfileRendererInjectedProps = { 53 /** 54 * Loaded profile record value. 55 */ 56 record: ProfileRecord; 57 /** 58 * Indicates whether the record is currently being fetched. 59 */ 60 loading: boolean; 61 /** 62 * Any error encountered while fetching the profile. 63 */ 64 error?: Error; 65 /** 66 * DID associated with the profile. 67 */ 68 did: string; 69 /** 70 * Human-readable handle for the DID, when known. 71 */ 72 handle?: string; 73 /** 74 * Blob URL for the user's avatar, when available. 75 */ 76 avatarUrl?: string; 77 /** 78 * Preferred color scheme for theming downstream components. 79 */ 80 colorScheme?: "light" | "dark" | "system"; 81}; 82 83/** NSID for the canonical Bluesky profile collection. */ 84export const BLUESKY_PROFILE_COLLECTION = "app.bsky.actor.profile"; 85 86/** 87 * Fetches and renders a Bluesky actor profile, optionally injecting custom presentation 88 * and providing avatar resolution support. 89 * 90 * @param did - DID whose profile record should be fetched. 91 * @param rkey - Record key within the profile collection (default `'self'`). 92 * @param renderer - Optional component override for custom rendering. 93 * @param fallback - Node rendered prior to loading state initialization. 94 * @param loadingIndicator - Node rendered while the profile request is in-flight. 95 * @param handle - Optional pre-resolved handle to display. 96 * @param colorScheme - Preferred color scheme forwarded to the renderer. 97 * @returns A rendered profile component with loading/error states handled. 98 */ 99export const BlueskyProfile: React.FC<BlueskyProfileProps> = ({ 100 did: handleOrDid, 101 rkey = "self", 102 record, 103 renderer, 104 fallback, 105 loadingIndicator, 106 handle, 107 colorScheme, 108}) => { 109 const Component: React.ComponentType<BlueskyProfileRendererInjectedProps> = 110 renderer ?? ((props) => <BlueskyProfileRenderer {...props} />); 111 const { did, handle: resolvedHandle } = useDidResolution(handleOrDid); 112 const repoIdentifier = did ?? handleOrDid; 113 const effectiveHandle = 114 handle ?? 115 resolvedHandle ?? 116 (handleOrDid.startsWith("did:") 117 ? formatDidForLabel(repoIdentifier) 118 : handleOrDid); 119 120 const Wrapped: React.FC<{ 121 record: ProfileRecord; 122 loading: boolean; 123 error?: Error; 124 }> = (props) => { 125 const avatarCid = getAvatarCid(props.record); 126 const { url: avatarUrl } = useBlob(repoIdentifier, avatarCid); 127 return ( 128 <Component 129 {...props} 130 did={repoIdentifier} 131 handle={effectiveHandle} 132 avatarUrl={avatarUrl} 133 colorScheme={colorScheme} 134 /> 135 ); 136 }; 137 138 // When record is provided, pass it directly to skip fetching 139 if (record) { 140 return ( 141 <AtProtoRecord<ProfileRecord> 142 record={record} 143 renderer={Wrapped} 144 fallback={fallback} 145 loadingIndicator={loadingIndicator} 146 /> 147 ); 148 } 149 150 // Otherwise fetch the record using did, collection, and rkey 151 return ( 152 <AtProtoRecord<ProfileRecord> 153 did={repoIdentifier} 154 collection={BLUESKY_PROFILE_COLLECTION} 155 rkey={rkey} 156 renderer={Wrapped} 157 fallback={fallback} 158 loadingIndicator={loadingIndicator} 159 /> 160 ); 161}; 162 163export default BlueskyProfile;