Grow the companion, fix idle fallback, add update banner
Companion: bump the corner mascot ~12% (128→144) and make animation resolution robust — fall back to the idle clip when a mood has no clip of its own, and pin the always-asleep cat to one pose so its Lottie never reloads. This stops a bare emoji from replacing the cute companion on idle. Update notifications: serve a build id at /api/version (a hash of the embedded dist/index.html, which changes on every frontend deploy). The client records its load-time id, polls every 90s (and on tab focus), and floats a Mandarin-first "new version available" banner offering a refresh when the id changes. Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
52
web/src/hooks/useVersionWatch.ts
Normal file
52
web/src/hooks/useVersionWatch.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { api } from '../api/client'
|
||||
|
||||
// How often to ask the server whether a newer frontend has shipped. Gentle —
|
||||
// a deploy is rare, and the check is a tiny no-store GET.
|
||||
const POLL_MS = 90_000
|
||||
|
||||
// useVersionWatch records the build id the app loaded with, then quietly polls
|
||||
// /api/version. When the server reports a different id, a new version has been
|
||||
// deployed and `updateAvailable` flips true (and stays true) so the UI can
|
||||
// offer a refresh. Network blips are ignored — it only ever reacts to a real,
|
||||
// confirmed change. Returns false until the baseline is established.
|
||||
export function useVersionWatch(): boolean {
|
||||
const [updateAvailable, setUpdateAvailable] = useState(false)
|
||||
// The version we're currently running. Null until the first successful fetch.
|
||||
const baseline = useRef<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
|
||||
const check = async () => {
|
||||
try {
|
||||
const { version } = await api.version()
|
||||
if (!active || !version) return
|
||||
if (baseline.current === null) {
|
||||
baseline.current = version // first read: this is "us"
|
||||
} else if (version !== baseline.current) {
|
||||
setUpdateAvailable(true)
|
||||
}
|
||||
} catch {
|
||||
/* offline / server bounce — try again next tick, never alarm the user */
|
||||
}
|
||||
}
|
||||
|
||||
check()
|
||||
const id = setInterval(check, POLL_MS)
|
||||
// Re-check the moment she returns to the tab, so a deploy that happened while
|
||||
// she was away surfaces right away instead of up to POLL_MS later.
|
||||
const onVisible = () => {
|
||||
if (document.visibilityState === 'visible') check()
|
||||
}
|
||||
document.addEventListener('visibilitychange', onVisible)
|
||||
|
||||
return () => {
|
||||
active = false
|
||||
clearInterval(id)
|
||||
document.removeEventListener('visibilitychange', onVisible)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return updateAvailable
|
||||
}
|
||||
Reference in New Issue
Block a user