|
| 1 | +import React, { useEffect, useState } from 'react' |
| 2 | + |
| 3 | +import { useTheme } from '../hooks/use-theme' |
| 4 | +import { useFreebuffSessionStore } from '../state/freebuff-session-store' |
| 5 | +import { IS_FREEBUFF } from '../utils/constants' |
| 6 | + |
| 7 | +const LOW_THRESHOLD_MS = 5 * 60_000 |
| 8 | +const CRITICAL_THRESHOLD_MS = 60_000 |
| 9 | + |
| 10 | +const formatRemaining = (ms: number): string => { |
| 11 | + if (ms <= 0) return 'expiring…' |
| 12 | + const totalSeconds = Math.ceil(ms / 1000) |
| 13 | + if (totalSeconds < 60) return `${totalSeconds}s left` |
| 14 | + const minutes = Math.floor(totalSeconds / 60) |
| 15 | + if (minutes < 60) return `${minutes}m left` |
| 16 | + const hours = Math.floor(minutes / 60) |
| 17 | + const rem = minutes % 60 |
| 18 | + return rem === 0 ? `${hours}h left` : `${hours}h ${rem}m left` |
| 19 | +} |
| 20 | + |
| 21 | +/** |
| 22 | + * Small countdown shown while a freebuff session is active. Renders the |
| 23 | + * time remaining until the server-issued `expiresAt` so users aren't |
| 24 | + * surprised when their seat is released. Returns null in non-freebuff |
| 25 | + * builds or when no active session exists — safe to always mount. |
| 26 | + */ |
| 27 | +export const FreebuffSessionCountdown: React.FC = () => { |
| 28 | + const theme = useTheme() |
| 29 | + const session = useFreebuffSessionStore((s) => s.session) |
| 30 | + const expiresAtMs = |
| 31 | + session?.status === 'active' ? Date.parse(session.expiresAt) : null |
| 32 | + |
| 33 | + const [now, setNow] = useState(() => Date.now()) |
| 34 | + useEffect(() => { |
| 35 | + if (!expiresAtMs) return |
| 36 | + const id = setInterval(() => setNow(Date.now()), 1000) |
| 37 | + return () => clearInterval(id) |
| 38 | + }, [expiresAtMs]) |
| 39 | + |
| 40 | + if (!IS_FREEBUFF || !expiresAtMs) return null |
| 41 | + |
| 42 | + const remainingMs = expiresAtMs - now |
| 43 | + const color = |
| 44 | + remainingMs < CRITICAL_THRESHOLD_MS |
| 45 | + ? theme.error |
| 46 | + : remainingMs < LOW_THRESHOLD_MS |
| 47 | + ? theme.warning |
| 48 | + : theme.muted |
| 49 | + |
| 50 | + return <span fg={color}>{formatRemaining(remainingMs)}</span> |
| 51 | +} |
| 52 | + |
| 53 | +/** True when the freebuff session countdown will render non-null content. |
| 54 | + * Used by the chat surface to keep the status bar visible while a |
| 55 | + * session is active, even when there's no streaming/queue activity. */ |
| 56 | +export const useHasActiveFreebuffSession = (): boolean => { |
| 57 | + return useFreebuffSessionStore( |
| 58 | + (s) => IS_FREEBUFF && s.session?.status === 'active', |
| 59 | + ) |
| 60 | +} |
0 commit comments