// The tray card: store status at a glance next to the tray icon. It only draws the main window's snapshot (card_get / // `card-snapshot`) and sends its buttons back (card_action); Refresh is the main window's own Refresh all. import { ArrowsClockwiseIcon } from '@phosphor-icons/react/dist/csr/ArrowsClockwise'; import { ArrowSquareOutIcon } from '@phosphor-icons/react/dist/csr/ArrowSquareOut'; import { BellSlashIcon } from '@phosphor-icons/react/dist/csr/BellSlash'; import { BellIcon } from '@phosphor-icons/react/dist/csr/Bell'; import { DotsThreeIcon } from '@phosphor-icons/react/dist/csr/DotsThree'; import { PowerIcon } from '@phosphor-icons/react/dist/csr/Power'; import { StarIcon } from '@phosphor-icons/react/dist/csr/Star'; import { WarningOctagonIcon } from '@phosphor-icons/react/dist/csr/WarningOctagon'; import { PulseIcon } from '@phosphor-icons/react/dist/csr/Pulse'; import { AppWindowIcon } from '@phosphor-icons/react/dist/csr/AppWindow'; import { useCallback, useEffect, useRef, useState, type KeyboardEvent, type ReactNode } from 'react'; import type { CardApp, CardRelease, CardSnapshot, CardStore } from './model'; import { ago, cardT, type CardKey, type CardLang } from './text'; export interface CardHost { get: () => Promise; onSnapshot: (fn: (s: CardSnapshot) => void) => () => void; onShown: (fn: () => void) => () => void; action: (action: 'hide' | 'open' | 'route' | 'refresh' | 'mute' | 'quit' | 'external' | 'ready', arg?: string) => void; } const route = (app: string, tab: 'releases' | 'reviews' | 'health') => JSON.stringify({ screen: 'app', appId: app, tab }); export function Card({ host, lang }: { host: CardHost; lang: CardLang }) { const T = useCallback((k: CardKey, v?: Record) => cardT(lang, k, v), [lang]); const [s, setS] = useState(null); const [menu, setMenu] = useState(false); const [now, setNow] = useState(() => Date.now()); const [shown, setShown] = useState(0); const root = useRef(null); useEffect(() => { host.get().then(x => { if (x) setS(x); }).catch(() => {}); const a = host.onSnapshot(setS); const b = host.onShown(() => { setNow(Date.now()); setMenu(false); setShown(n => n + 1); }); host.action('ready'); const tm = setInterval(() => setNow(Date.now()), 30_000); return () => { a(); b(); clearInterval(tm); }; }, [host]); // every show: fade in, focus the first control (keyboard users start at Refresh) useEffect(() => { (root.current?.querySelector('button:not([disabled])') ?? root.current)?.focus(); }, [shown, !!s]); const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault(); if (menu) setMenu(false); else host.action('hide'); return; } if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { const all = [...(root.current?.querySelectorAll('button:not([disabled])') ?? [])]; const i = all.indexOf(document.activeElement as HTMLElement); const n = all[(i + (e.key === 'ArrowDown' ? 1 : -1) + all.length) % all.length]; if (n) { e.preventDefault(); n.focus(); } } }; const store = (st: CardStore) => T(st === 'ios' ? 'appStore' : 'playStore'); const phase = (r: CardRelease) => (r.phase === 'rollout' ? (r.fraction != null && r.fraction < 1 ? T('rolloutPct', { pct: Math.round(r.fraction * 100) }) : T('rollout')) : T(r.phase)); const link = (key: string): CardApp | undefined => s?.links.find(l => l.key === key); const busy = !!s?.busy; const calm = s && !s.rejections.length && !s.releases.length && !s.health.length && !s.reviews.low.length; return (
{T('title')} {s?.muted && {T('muted')}}{T('lastRead', { ago: ago(lang, s?.at ?? null, now) })}
{menu && (

)}
{!s &&

{T('noData')}

} {s && calm &&

{s.at ? T('calm') : T('noData')}

} {!!s?.rejections.length && (
{s.rejections.map(r => ( host.action('route', route(r.app, 'releases'))} openLabel={T('openApp', { name: r.name })} line={<>{T('rejected')} · {store(r.store)}{r.version ? ` ${r.version}` : ''}} sub={r.reason} consoles={consoles(link(r.app), r.store, host, T)} /> ))}
)} {!!s?.releases.length && (
{s.releases.map(r => ( host.action('route', route(r.app, 'releases'))} openLabel={T('openApp', { name: r.name })} line={<>{phase(r)} · {store(r.store)}{r.version ? ` ${r.version}` : ''}} bar={r.phase === 'rollout' && r.fraction != null && r.fraction < 1 ? r.fraction : null} consoles={consoles(link(r.app), r.store, host, T)} /> ))}
)} {!!s?.health.length && (
{s.health.map(h => ( host.action('route', route(h.app, 'health'))} openLabel={T('openApp', { name: h.name })} line={<>{h.kinds.length > 1 ? T('crashAnr') : T(h.kinds[0]!)}} consoles={consoles(link(h.app), 'android', host, T)} /> ))}
)} {s && s.at && (
{s.reviews.low.map(r => ( ))}
)}
); } function consoles(l: CardApp | undefined, st: CardStore, host: CardHost, T: (k: CardKey) => string): ReactNode { const url = st === 'ios' ? l?.asc : l?.play; if (!url) return null; return ; } function Section({ title, tone, aside, children }: { title: string; tone?: 'bad' | 'warn'; aside?: string; children: ReactNode }) { return (

{title}{aside && {aside}}

{children}
); } function Row({ hue, name, line, sub, tone, bar, onOpen, openLabel, consoles }: { hue: number; name: string; line: ReactNode; sub?: string | null; tone: string; bar?: number | null; onOpen: () => void; openLabel: string; consoles: ReactNode }) { return (
{consoles}
); } const initials = (n: string) => n.split(/\s+/).filter(Boolean).slice(0, 2).map(w => w[0]!.toUpperCase()).join('') || '?';