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>
91 lines
3.0 KiB
TypeScript
91 lines
3.0 KiB
TypeScript
// Fetches the arcadia profile on app boot (and after login) and caches
|
|
// the resolved avatar URL in localStorage so the appbar's <Avatar> shows
|
|
// immediately, without waiting for the user to navigate to /profile.
|
|
|
|
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"
|
|
|
|
export function ProfileBootstrap() {
|
|
const arcadia = useArcadiaClient()
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
|
|
const tryBootstrap = async () => {
|
|
const token =
|
|
typeof window !== "undefined"
|
|
? sessionStorage.getItem("arcadia_access_token")
|
|
: null
|
|
if (!token) return
|
|
try {
|
|
const p = await getProfile(arcadia)
|
|
if (cancelled) return
|
|
|
|
const persistentUrl = pickAvatarUrl(p)
|
|
const current = loadProfile()
|
|
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.
|
|
}
|
|
}
|
|
|
|
void tryBootstrap()
|
|
|
|
const onSessionChange = () => void tryBootstrap()
|
|
window.addEventListener("crema:session-change", onSessionChange)
|
|
return () => {
|
|
cancelled = true
|
|
window.removeEventListener("crema:session-change", onSessionChange)
|
|
}
|
|
}, [arcadia])
|
|
|
|
return null
|
|
}
|