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}