A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.
1import { useBlueskyAppview } from "./useBlueskyAppview"; 2import type { ProfileRecord } from "../types/bluesky"; 3 4/** 5 * Minimal profile fields returned by the Bluesky actor profile endpoint. 6 */ 7export interface BlueskyProfileData { 8 /** Actor DID. */ 9 did: string; 10 /** Actor handle. */ 11 handle: string; 12 /** Display name configured by the actor. */ 13 displayName?: string; 14 /** Profile description/bio. */ 15 description?: string; 16 /** Avatar blob (CID reference). */ 17 avatar?: string; 18 /** Banner image blob (CID reference). */ 19 banner?: string; 20 /** Creation timestamp for the profile. */ 21 createdAt?: string; 22} 23 24/** 25 * Fetches a Bluesky actor profile for a DID and exposes loading/error state. 26 * 27 * Uses a three-tier fallback strategy: 28 * 1. Try Bluesky appview API (app.bsky.actor.getProfile) - CIDs are extracted from CDN URLs 29 * 2. Fall back to Slingshot getRecord 30 * 3. Finally query the PDS directly 31 * 32 * When using the appview, avatar/banner CDN URLs (e.g., https://cdn.bsky.app/img/avatar/plain/did:plc:xxx/bafkreixxx@jpeg) 33 * are automatically parsed to extract CIDs and convert them to standard Blob format for compatibility. 34 * 35 * @param did - Actor DID whose profile should be retrieved. 36 * @returns {{ data: BlueskyProfileData | undefined; loading: boolean; error: Error | undefined }} Object exposing the profile payload, loading flag, and any error. 37 */ 38export function useBlueskyProfile(did: string | undefined) { 39 const { record, loading, error } = useBlueskyAppview<ProfileRecord>({ 40 did, 41 collection: "app.bsky.actor.profile", 42 rkey: "self", 43 }); 44 45 // Convert ProfileRecord to BlueskyProfileData 46 // Note: avatar and banner are Blob objects in the record (from all sources) 47 // The appview response is converted to ProfileRecord format by extracting CIDs from CDN URLs 48 const data: BlueskyProfileData | undefined = record 49 ? { 50 did: did || "", 51 handle: "", 52 displayName: record.displayName, 53 description: record.description, 54 avatar: extractCidFromProfileBlob(record.avatar), 55 banner: extractCidFromProfileBlob(record.banner), 56 createdAt: record.createdAt, 57 } 58 : undefined; 59 60 return { data, loading, error }; 61} 62 63/** 64 * Helper to extract CID from profile blob (avatar or banner). 65 */ 66function extractCidFromProfileBlob(blob: unknown): string | undefined { 67 if (typeof blob !== "object" || blob === null) return undefined; 68 69 const blobObj = blob as { 70 ref?: { $link?: string }; 71 cid?: string; 72 }; 73 74 if (typeof blobObj.cid === "string") return blobObj.cid; 75 if (typeof blobObj.ref === "object" && blobObj.ref !== null) { 76 const link = blobObj.ref.$link; 77 if (typeof link === "string") return link; 78 } 79 80 return undefined; 81}