The variant pipeline is async, so right after upload all four URLs in
profile.avatar_urls are still null. The first wiring attempt called
pickAvatarUrl() which returned null, and nothing visible changed even
though the upload + PATCH succeeded.
Fixes:
- pickAvatarUrl: use the actual backend keys (small/medium/large/
original — there's no "thumbnail").
- After upload, when no variant URL is ready, fetch the raw object
via /api/v1/digital_objects/:id/content as a blob URL for immediate
display. Persist that URL to localStorage so the appbar's
useProfile() picks it up via the storage event.
- ProfileBootstrap: detect stale blob: URLs cached from previous
sessions, clear them, and refetch a fresh blob URL when variants
still aren't ready. Eventually the persistent variant URLs land
and overwrite.
- Force-remount AvatarImage via key={src} in the profile page and
appbar — base-ui's Avatar.Image keeps internal load state that
doesn't always reset on src change.
- Diagnostic logs in fetchDigitalObjectAsBlobUrl + the upload flow
to make next debug round one step easier (kept; cheap).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
80 lines
2.4 KiB
TypeScript
80 lines
2.4 KiB
TypeScript
// Arcadia profile API. Backed by /api/v1/profile (current user) — handles
|
|
// avatar wiring (avatar_digital_object_id + variant URLs) and the basic
|
|
// profile fields. The "profile" here is the per-tenant profile row, not
|
|
// the auth account.
|
|
|
|
import type { ArcadiaClient } from "@crema/arcadia-client"
|
|
|
|
export interface Profile {
|
|
id: string
|
|
user_id: string
|
|
tenant_id: string
|
|
avatar_url: string | null
|
|
avatar_digital_object_id: string | null
|
|
/**
|
|
* Variant URLs keyed by size (e.g. "thumbnail", "medium", "original").
|
|
* Shape depends on the storage backend; treat as best-effort.
|
|
*/
|
|
avatar_urls?: Record<string, string> | null
|
|
bio?: string | null
|
|
phone?: string | null
|
|
location?: string | null
|
|
timezone?: string | null
|
|
inserted_at?: string
|
|
updated_at?: string
|
|
}
|
|
|
|
export interface ProfileUpdateInput {
|
|
avatar_digital_object_id?: string | null
|
|
bio?: string | null
|
|
phone?: string | null
|
|
location?: string | null
|
|
timezone?: string | null
|
|
}
|
|
|
|
export async function getProfile(arcadia: ArcadiaClient): Promise<Profile> {
|
|
const res = await arcadia.GET<{ data: Profile } | Profile>("/api/v1/profile")
|
|
return "data" in (res as object)
|
|
? (res as { data: Profile }).data
|
|
: (res as Profile)
|
|
}
|
|
|
|
export async function updateProfile(
|
|
arcadia: ArcadiaClient,
|
|
input: ProfileUpdateInput,
|
|
): Promise<Profile> {
|
|
const res = await arcadia.PATCH<{ data: Profile } | Profile>("/api/v1/profile", {
|
|
body: { profile: input },
|
|
})
|
|
return "data" in (res as object)
|
|
? (res as { data: Profile }).data
|
|
: (res as Profile)
|
|
}
|
|
|
|
/**
|
|
* Pick the most appropriate avatar URL from a profile. Backend returns
|
|
* `avatar_urls = {small, medium, large, original}` keyed by size. The
|
|
* variants are populated async after image processing completes —
|
|
* before that, all four are `null` and we fall back to the legacy
|
|
* `avatar_url` string column (which is also usually null when uploads
|
|
* use the digital_object pipeline).
|
|
*
|
|
* Returns null when nothing is ready; caller should fall back to
|
|
* fetching the raw content as a blob URL.
|
|
*/
|
|
export function pickAvatarUrl(profile: Profile | null | undefined): string | null {
|
|
if (!profile) return null
|
|
const variants = profile.avatar_urls
|
|
if (variants && typeof variants === "object") {
|
|
return (
|
|
variants.small ||
|
|
variants.medium ||
|
|
variants.large ||
|
|
variants.original ||
|
|
profile.avatar_url ||
|
|
null
|
|
)
|
|
}
|
|
return profile.avatar_url ?? null
|
|
}
|