A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.
at main 4.4 kB view raw
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} 46 47/** 48 * Props injected into custom profile renderer implementations. 49 */ 50export type BlueskyProfileRendererInjectedProps = { 51 /** 52 * Loaded profile record value. 53 */ 54 record: ProfileRecord; 55 /** 56 * Indicates whether the record is currently being fetched. 57 */ 58 loading: boolean; 59 /** 60 * Any error encountered while fetching the profile. 61 */ 62 error?: Error; 63 /** 64 * DID associated with the profile. 65 */ 66 did: string; 67 /** 68 * Human-readable handle for the DID, when known. 69 */ 70 handle?: string; 71 /** 72 * Blob URL for the user's avatar, when available. 73 */ 74 avatarUrl?: string; 75 76}; 77 78/** NSID for the canonical Bluesky profile collection. */ 79export const BLUESKY_PROFILE_COLLECTION = "app.bsky.actor.profile"; 80 81/** 82 * Fetches and renders a Bluesky actor profile, optionally injecting custom presentation 83 * and providing avatar resolution support. 84 * 85 * @param did - DID whose profile record should be fetched. 86 * @param rkey - Record key within the profile collection (default `'self'`). 87 * @param renderer - Optional component override for custom rendering. 88 * @param fallback - Node rendered prior to loading state initialization. 89 * @param loadingIndicator - Node rendered while the profile request is in-flight. 90 * @param handle - Optional pre-resolved handle to display. 91 * @returns A rendered profile component with loading/error states handled. 92 */ 93export const BlueskyProfile: React.FC<BlueskyProfileProps> = React.memo(({ 94 did: handleOrDid, 95 rkey = "self", 96 record, 97 renderer, 98 fallback, 99 loadingIndicator, 100 handle, 101}) => { 102 const Component: React.ComponentType<BlueskyProfileRendererInjectedProps> = 103 renderer ?? ((props) => <BlueskyProfileRenderer {...props} />); 104 const { did, handle: resolvedHandle } = useDidResolution(handleOrDid); 105 const repoIdentifier = did ?? handleOrDid; 106 const effectiveHandle = 107 handle ?? 108 resolvedHandle ?? 109 (handleOrDid.startsWith("did:") 110 ? formatDidForLabel(repoIdentifier) 111 : handleOrDid); 112 113 const Wrapped: React.FC<{ 114 record: ProfileRecord; 115 loading: boolean; 116 error?: Error; 117 }> = (props) => { 118 // Check if the avatar has a CDN URL from the appview (preferred) 119 const avatar = props.record?.avatar; 120 const avatarCdnUrl = isBlobWithCdn(avatar) ? avatar.cdnUrl : undefined; 121 const avatarCid = avatarCdnUrl ? undefined : getAvatarCid(props.record); 122 const { url: avatarUrlFromBlob } = useBlob(repoIdentifier, avatarCid); 123 const avatarUrl = avatarCdnUrl || avatarUrlFromBlob; 124 125 return ( 126 <Component 127 {...props} 128 did={repoIdentifier} 129 handle={effectiveHandle} 130 avatarUrl={avatarUrl} 131 /> 132 ); 133 }; 134 135 if (record !== undefined) { 136 return ( 137 <AtProtoRecord<ProfileRecord> 138 record={record} 139 renderer={Wrapped} 140 fallback={fallback} 141 loadingIndicator={loadingIndicator} 142 /> 143 ); 144 } 145 146 return ( 147 <AtProtoRecord<ProfileRecord> 148 did={repoIdentifier} 149 collection={BLUESKY_PROFILE_COLLECTION} 150 rkey={rkey} 151 renderer={Wrapped} 152 fallback={fallback} 153 loadingIndicator={loadingIndicator} 154 /> 155 ); 156}); 157 158export default BlueskyProfile;