avatar: render immediately + survive reload
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>
This commit is contained in:
@@ -86,3 +86,45 @@ export async function deleteDigitalObject(
|
||||
): Promise<void> {
|
||||
await arcadia.DELETE(`/api/v1/digital_objects/${encodeURIComponent(id)}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the raw bytes of a digital object and return a browser blob URL
|
||||
* suitable for `<img src>`. Used as an immediate-display fallback when
|
||||
* the async variant URLs aren't ready yet (e.g. fresh avatar upload).
|
||||
*
|
||||
* Bypasses the arcadia-client because that client only parses JSON or
|
||||
* text — for binary we need response.blob(). Auth is injected manually
|
||||
* from sessionStorage to match the rest of the auth surface.
|
||||
*
|
||||
* The returned blob URL is per-page; it does NOT survive a reload.
|
||||
* Caller should not persist it to localStorage — only render in memory
|
||||
* until the persistent variant URLs come through (e.g. on next mount).
|
||||
*/
|
||||
export async function fetchDigitalObjectAsBlobUrl(
|
||||
baseUrl: string,
|
||||
id: string,
|
||||
token: string,
|
||||
tenantId?: string,
|
||||
): Promise<string> {
|
||||
const headers: Record<string, string> = {
|
||||
Accept: "*/*",
|
||||
Authorization: `Bearer ${token}`,
|
||||
}
|
||||
if (tenantId) headers["X-Tenant-ID"] = tenantId
|
||||
|
||||
const url = `${baseUrl.replace(/\/+$/, "")}/api/v1/digital_objects/${encodeURIComponent(
|
||||
id,
|
||||
)}/content`
|
||||
const res = await fetch(url, { headers })
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch digital object content: ${res.status} ${await res.text().catch(() => "")}`,
|
||||
)
|
||||
}
|
||||
const blob = await res.blob()
|
||||
// eslint-disable-next-line no-console
|
||||
console.info(
|
||||
`[digital-objects] fetched blob id=${id} type=${blob.type} size=${blob.size}B`,
|
||||
)
|
||||
return URL.createObjectURL(blob)
|
||||
}
|
||||
|
||||
@@ -52,18 +52,24 @@ export async function updateProfile(
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the most appropriate avatar URL from a profile. Prefers a small
|
||||
* variant (thumbnail / small / medium) if available; falls back to
|
||||
* `avatar_url`, then null.
|
||||
* 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.thumbnail ||
|
||||
variants.small ||
|
||||
variants.medium ||
|
||||
variants.large ||
|
||||
variants.original ||
|
||||
profile.avatar_url ||
|
||||
null
|
||||
|
||||
Reference in New Issue
Block a user