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:
jules
2026-05-05 10:33:14 +10:00
parent 2ab183596c
commit c968ac0735
5 changed files with 154 additions and 14 deletions

View File

@@ -5,6 +5,7 @@
import { useEffect } from "react"
import { useArcadiaClient } from "@crema/arcadia-client"
import { fetchDigitalObjectAsBlobUrl } from "~/lib/arcadia/digital-objects"
import { getProfile, pickAvatarUrl } from "~/lib/arcadia/profiles"
import { loadProfile, saveProfile } from "~/lib/profile"
@@ -23,11 +24,53 @@ export function ProfileBootstrap() {
try {
const p = await getProfile(arcadia)
if (cancelled) return
const url = pickAvatarUrl(p)
if (!url) return
const persistentUrl = pickAvatarUrl(p)
const current = loadProfile()
if (current.avatarUrl === url) return
saveProfile({ ...current, avatarUrl: url })
const cachedIsStaleBlob = current.avatarUrl?.startsWith("blob:") ?? false
if (persistentUrl) {
if (current.avatarUrl !== persistentUrl) {
saveProfile({ ...current, avatarUrl: persistentUrl })
}
return
}
// No persistent variant yet but the user has an avatar — fetch
// the raw bytes as a blob URL. This also covers the "stale blob
// URL from previous session" case: replace it with a fresh one.
if (p.avatar_digital_object_id) {
if (cachedIsStaleBlob) {
// Clear the stale URL immediately so the appbar drops back
// to initials while we refetch (better than a broken image).
saveProfile({ ...current, avatarUrl: "" })
}
try {
const baseUrl =
(import.meta.env.VITE_ARCADIA_URL as string | undefined) ??
"http://localhost:4000"
const tenantId =
(import.meta.env.VITE_ARCADIA_TENANT as string | undefined) ??
"default"
const blobUrl = await fetchDigitalObjectAsBlobUrl(
baseUrl,
p.avatar_digital_object_id,
token,
tenantId,
)
if (cancelled) return
const fresh = loadProfile()
saveProfile({ ...fresh, avatarUrl: blobUrl })
} catch {
// Best-effort; appbar will show initials until processing completes.
}
return
}
// No avatar at all — clear any stale URL the cache might still hold.
if (current.avatarUrl) {
saveProfile({ ...current, avatarUrl: "" })
}
} catch {
// 401 / network — silently skip; will retry on next session change.
}