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