import React from "react"; import { AtProtoRecord } from "../core/AtProtoRecord"; import { BlueskyProfileRenderer } from "../renderers/BlueskyProfileRenderer"; import type { ProfileRecord } from "../types/bluesky"; import { useBlob } from "../hooks/useBlob"; import { getAvatarCid } from "../utils/profile"; import { useDidResolution } from "../hooks/useDidResolution"; import { formatDidForLabel } from "../utils/at-uri"; import { isBlobWithCdn } from "../utils/blob"; /** * Props used to render a Bluesky actor profile record. */ export interface BlueskyProfileProps { /** * DID of the target actor whose profile should be loaded. */ did: string; /** * Record key within the profile collection. Typically `'self'`. * Optional when `record` is provided. */ rkey?: string; /** * Prefetched profile record. When provided, skips fetching the profile from the network. */ record?: ProfileRecord; /** * Optional renderer override for custom presentation. */ renderer?: React.ComponentType; /** * Fallback node shown before a request begins yielding data. */ fallback?: React.ReactNode; /** * Loading indicator shown during in-flight fetches. */ loadingIndicator?: React.ReactNode; /** * Pre-resolved handle to display when available externally. */ handle?: string; } /** * Props injected into custom profile renderer implementations. */ export type BlueskyProfileRendererInjectedProps = { /** * Loaded profile record value. */ record: ProfileRecord; /** * Indicates whether the record is currently being fetched. */ loading: boolean; /** * Any error encountered while fetching the profile. */ error?: Error; /** * DID associated with the profile. */ did: string; /** * Human-readable handle for the DID, when known. */ handle?: string; /** * Blob URL for the user's avatar, when available. */ avatarUrl?: string; }; /** NSID for the canonical Bluesky profile collection. */ export const BLUESKY_PROFILE_COLLECTION = "app.bsky.actor.profile"; /** * Fetches and renders a Bluesky actor profile, optionally injecting custom presentation * and providing avatar resolution support. * * @param did - DID whose profile record should be fetched. * @param rkey - Record key within the profile collection (default `'self'`). * @param renderer - Optional component override for custom rendering. * @param fallback - Node rendered prior to loading state initialization. * @param loadingIndicator - Node rendered while the profile request is in-flight. * @param handle - Optional pre-resolved handle to display. * @returns A rendered profile component with loading/error states handled. */ export const BlueskyProfile: React.FC = React.memo(({ did: handleOrDid, rkey = "self", record, renderer, fallback, loadingIndicator, handle, }) => { const Component: React.ComponentType = renderer ?? ((props) => ); const { did, handle: resolvedHandle } = useDidResolution(handleOrDid); const repoIdentifier = did ?? handleOrDid; const effectiveHandle = handle ?? resolvedHandle ?? (handleOrDid.startsWith("did:") ? formatDidForLabel(repoIdentifier) : handleOrDid); const Wrapped: React.FC<{ record: ProfileRecord; loading: boolean; error?: Error; }> = (props) => { // Check if the avatar has a CDN URL from the appview (preferred) const avatar = props.record?.avatar; const avatarCdnUrl = isBlobWithCdn(avatar) ? avatar.cdnUrl : undefined; const avatarCid = avatarCdnUrl ? undefined : getAvatarCid(props.record); const { url: avatarUrlFromBlob } = useBlob(repoIdentifier, avatarCid); const avatarUrl = avatarCdnUrl || avatarUrlFromBlob; return ( ); }; if (record !== undefined) { return ( record={record} renderer={Wrapped} fallback={fallback} loadingIndicator={loadingIndicator} /> ); } return ( did={repoIdentifier} collection={BLUESKY_PROFILE_COLLECTION} rkey={rkey} renderer={Wrapped} fallback={fallback} loadingIndicator={loadingIndicator} /> ); }); export default BlueskyProfile;