/** Build a blob URL for a profile image. Returns pre-resolved URLs as-is. */ export function blobUrl(did: string, blobRef: unknown): string | null { if (!blobRef) return null; if (typeof blobRef === "string") { if (blobRef.startsWith("http")) return blobRef; try { blobRef = JSON.parse(blobRef); } catch { return null; } } const p = blobRef as any; if (!p?.ref?.$link) return null; return `https://cdn.bsky.app/img/avatar/plain/${did}/${p.ref.$link}@jpeg`; } /** Truncate a DID for display. */ export function truncDid(did: string): string { if (did.length <= 24) return did; return did.slice(0, 12) + "\u2026" + did.slice(-6); } /** Compact relative time matching grain-native: now, 2m, 3h, 4d, 1w, then Mon DD. */ export function relativeTime(iso: string): string { const date = new Date(iso); const diff = (Date.now() - date.getTime()) / 1000; if (diff < 60) return "now"; if (diff < 3600) return `${Math.floor(diff / 60)}m`; if (diff < 86400) return `${Math.floor(diff / 3600)}h`; if (diff < 604800) return `${Math.floor(diff / 86400)}d`; if (diff < 2592000) return `${Math.floor(diff / 604800)}w`; return date.toLocaleDateString("en-US", { month: "short", day: "numeric" }); } /** Compact number formatting: 1K, 1.2M, etc. */ export function compactCount(n: number): string { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`; if (n >= 1_000) return `${(n / 1_000).toFixed(1).replace(/\.0$/, "")}K`; return String(n); } // ─── Links ────────────────────────────────────────────────────────── type Actor = { did?: string | null; handle?: string | null } | null | undefined; const rkeyOf = (uri: string) => uri.split("/").pop() ?? ""; /** * The segment that names an account in a URL. Handles are what people read * and share, so an account with one is linked by it. An account without one, * or with Bluesky's `handle.invalid` placeholder, or whose view fell back to * echoing its DID as the handle, is linked by DID instead. Every profile route * accepts either. */ export function actorSegment(p: Actor): string { const handle = p?.handle; if (handle && handle !== "handle.invalid" && !handle.startsWith("did:")) return handle; return p?.did ?? ""; } export const profilePath = (p: Actor) => `/profile/${actorSegment(p)}`; /** `/profile/:actor/gallery/:rkey`. Falls back to the DID in the URI when no creator view came along. */ export function galleryPath(g: { uri: string; creator?: Actor }): string { return `${profilePath(g.creator ?? { did: g.uri.split("/")[2] })}/gallery/${rkeyOf(g.uri)}`; } /** `/profile/:actor/story/:rkey`, same shape as `galleryPath`. */ export function storyPath(s: { uri: string; creator?: Actor }): string { return `${profilePath(s.creator ?? { did: s.uri.split("/")[2] })}/story/${rkeyOf(s.uri)}`; }