0
2
/** 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)}`;
}