diff --git a/apps/web/src/components/sidebar/SidebarProviderUpdatePill.tsx b/apps/web/src/components/sidebar/SidebarProviderUpdatePill.tsx index b28b967eefa..747d3488e64 100644 --- a/apps/web/src/components/sidebar/SidebarProviderUpdatePill.tsx +++ b/apps/web/src/components/sidebar/SidebarProviderUpdatePill.tsx @@ -38,85 +38,91 @@ function latestProviderCheckedAt( ); } -export function SidebarProviderUpdatePill() { - const navigate = useNavigate(); - const providers = useAtomValue(primaryServerProvidersAtom); - const [dismissedKeys, setDismissedKeys] = useState>(() => new Set()); - const [renderedView, setRenderedView] = useState(null); - const [pendingView, setPendingView] = useState(null); - const [exitingKey, setExitingKey] = useState(null); - const [dismissAfterExitKey, setDismissAfterExitKey] = useState(null); - const [visibleAfterIso, setVisibleAfterIso] = useState(); - const effectiveVisibleAfterIso = visibleAfterIso ?? latestProviderCheckedAt(providers); - const view = getProviderUpdateSidebarPillView(providers, { - ...(effectiveVisibleAfterIso !== undefined - ? { visibleAfterIso: effectiveVisibleAfterIso } - : {}), - dismissedKeys, - }); +interface SidebarProviderUpdatePillContentProps { + readonly providers: ReadonlyArray; + readonly onOpenProviderSettings: () => void; +} - useEffect(() => { - if (visibleAfterIso === undefined && effectiveVisibleAfterIso !== undefined) { - setVisibleAfterIso(effectiveVisibleAfterIso); - } - }, [effectiveVisibleAfterIso, visibleAfterIso]); +interface ProviderUpdatePillExitState { + readonly view: ProviderUpdateSidebarPillView; + readonly dismissAfterExitKey: string | null; +} - const openProviderSettings = useCallback(() => { - void navigate({ to: "/settings/providers" }); - }, [navigate]); - const displayedView = renderedView ?? view; +function useProviderUpdatePillAutoDismiss({ + displayedView, + exitingKey, + startExit, +}: { + readonly displayedView: ProviderUpdateSidebarPillView | null; + readonly exitingKey: string | null; + readonly startExit: (view: ProviderUpdateSidebarPillView, dismissKey?: string) => void; +}) { const dismissAfterVisibleMs = displayedView?.dismissAfterVisibleMs; const viewKey = displayedView?.key ?? null; - const showDismissProgress = - dismissAfterVisibleMs !== undefined && - displayedView?.tone !== "loading" && - exitingKey !== viewKey; - - const startExit = useCallback( - (key: string, nextView: ProviderUpdateSidebarPillView | null, dismissKey?: string) => { - if (exitingKey === key) { - return; - } - setPendingView(nextView); - setExitingKey(key); - setDismissAfterExitKey(dismissKey ?? null); - }, - [exitingKey], - ); - - useEffect(() => { - if (exitingKey !== null) { - return; - } - if (!renderedView) { - if (view) { - setRenderedView(view); - } - return; - } - if (!view) { - startExit(renderedView.key, null); - return; - } - if (view.key !== renderedView.key) { - startExit(renderedView.key, view); - return; - } - }, [exitingKey, renderedView, startExit, view]); useEffect(() => { - if (!dismissAfterVisibleMs || !viewKey) { + if (!displayedView || !dismissAfterVisibleMs || !viewKey) { return; } if (exitingKey === viewKey) { return; } const timeoutId = window.setTimeout(() => { - startExit(viewKey, null, viewKey); + startExit(displayedView, viewKey); }, dismissAfterVisibleMs); return () => window.clearTimeout(timeoutId); - }, [dismissAfterVisibleMs, exitingKey, startExit, viewKey]); + }, [dismissAfterVisibleMs, displayedView, exitingKey, startExit, viewKey]); +} + +export function SidebarProviderUpdatePill() { + const navigate = useNavigate(); + const providers = useAtomValue(primaryServerProvidersAtom); + const openProviderSettings = useCallback(() => { + void navigate({ to: "/settings/providers" }); + }, [navigate]); + + if (providers.length === 0) { + return null; + } + + return ( + + ); +} + +export function SidebarProviderUpdatePillContent({ + providers, + onOpenProviderSettings, +}: SidebarProviderUpdatePillContentProps) { + const [dismissedKeys, setDismissedKeys] = useState>(() => new Set()); + const [visibleAfterIso] = useState(() => latestProviderCheckedAt(providers)); + const view = getProviderUpdateSidebarPillView(providers, { + ...(visibleAfterIso !== undefined ? { visibleAfterIso } : {}), + dismissedKeys, + }); + const [exitState, setExitState] = useState(null); + const displayedView = exitState?.view ?? view; + const exitingKey = exitState?.view.key ?? null; + const dismissAfterVisibleMs = displayedView?.dismissAfterVisibleMs; + const viewKey = displayedView?.key ?? null; + const showDismissProgress = + dismissAfterVisibleMs !== undefined && + displayedView?.tone !== "loading" && + exitingKey !== viewKey; + + const startExit = useCallback((exitView: ProviderUpdateSidebarPillView, dismissKey?: string) => { + setExitState((current) => + current?.view.key === exitView.key + ? current + : { view: exitView, dismissAfterExitKey: dismissKey ?? null }, + ); + }, []); + + useProviderUpdatePillAutoDismiss({ displayedView, exitingKey, startExit }); if (!displayedView) { return null; @@ -138,13 +144,10 @@ export function SidebarProviderUpdatePill() { if (!displayedView || exitingKey !== displayedView.key) { return; } - if (dismissAfterExitKey === displayedView.key) { + if (exitState?.dismissAfterExitKey === displayedView.key) { setDismissedKeys((previous) => new Set(previous).add(displayedView.key)); } - setRenderedView(pendingView); - setPendingView(null); - setExitingKey(null); - setDismissAfterExitKey(null); + setExitState(null); }} > {showDismissProgress ? ( @@ -169,7 +172,7 @@ export function SidebarProviderUpdatePill() { type="button" aria-label={displayedView.description} className="provider-update-main relative z-[1] flex h-full flex-1 items-center gap-2 px-2 text-left" - onClick={openProviderSettings} + onClick={onOpenProviderSettings} > {displayedView.tone === "loading" ? ( @@ -194,7 +197,7 @@ export function SidebarProviderUpdatePill() { type="button" aria-label="Dismiss provider update notice" className="relative z-[1] mr-1 inline-flex size-5 items-center justify-center rounded-md opacity-70 transition-opacity hover:opacity-100" - onClick={() => startExit(displayedView.key, null, displayedView.key)} + onClick={() => startExit(displayedView, displayedView.key)} >