diff --git a/apps/web/src/components/sidebar/SidebarProviderUpdatePill.tsx b/apps/web/src/components/sidebar/SidebarProviderUpdatePill.tsx index d5b1b9bddd4..0635ea64ebb 100644 --- a/apps/web/src/components/sidebar/SidebarProviderUpdatePill.tsx +++ b/apps/web/src/components/sidebar/SidebarProviderUpdatePill.tsx @@ -1,7 +1,16 @@ import { useNavigate } from "@tanstack/react-router"; import type { ServerProvider } from "@t3tools/contracts"; import { CircleCheckIcon, DownloadIcon, LoaderIcon, TriangleAlertIcon, XIcon } from "lucide-react"; -import { useCallback, useEffect, useState, type CSSProperties } from "react"; +import { + useCallback, + useEffect, + useRef, + useState, + type CSSProperties, + type Dispatch, + type SetStateAction, + type TransitionEvent, +} from "react"; import { useServerProviders } from "../../rpc/serverState"; import { @@ -37,48 +46,36 @@ function latestProviderCheckedAt( ); } -export function SidebarProviderUpdatePill() { - const navigate = useNavigate(); - const providers = useServerProviders(); - 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, - }); +function useProviderUpdateVisibleAfterIso( + providers: ReadonlyArray>, +): string | undefined { + const visibleAfterIsoRef = useRef(undefined); - useEffect(() => { - if (visibleAfterIso === undefined && effectiveVisibleAfterIso !== undefined) { - setVisibleAfterIso(effectiveVisibleAfterIso); - } - }, [effectiveVisibleAfterIso, visibleAfterIso]); + if (visibleAfterIsoRef.current === undefined) { + visibleAfterIsoRef.current = latestProviderCheckedAt(providers); + } - const openProviderSettings = useCallback(() => { - void navigate({ to: "/settings/providers" }); - }, [navigate]); - const displayedView = renderedView ?? view; - const dismissAfterVisibleMs = displayedView?.dismissAfterVisibleMs; - const viewKey = displayedView?.key ?? null; - const showDismissProgress = - dismissAfterVisibleMs !== undefined && - displayedView?.tone !== "loading" && - exitingKey !== viewKey; + return visibleAfterIsoRef.current; +} + +function useDisplayedProviderUpdatePill(input: { + view: ProviderUpdateSidebarPillView | null; + setDismissedKeys: Dispatch>>; +}) { + const { view, setDismissedKeys } = input; + const [renderedView, setRenderedView] = useState(view); + const [exitingKey, setExitingKey] = useState(null); + const pendingViewRef = useRef(null); + const dismissAfterExitKeyRef = useRef(null); const startExit = useCallback( (key: string, nextView: ProviderUpdateSidebarPillView | null, dismissKey?: string) => { if (exitingKey === key) { return; } - setPendingView(nextView); + pendingViewRef.current = nextView; + dismissAfterExitKeyRef.current = dismissKey ?? null; setExitingKey(key); - setDismissAfterExitKey(dismissKey ?? null); }, [exitingKey], ); @@ -103,6 +100,46 @@ export function SidebarProviderUpdatePill() { } }, [exitingKey, renderedView, startExit, view]); + const displayedView = renderedView ?? view; + const onTransitionEnd = useCallback( + (event: TransitionEvent) => { + if (event.target !== event.currentTarget) { + return; + } + if (!displayedView || exitingKey !== displayedView.key) { + return; + } + if (dismissAfterExitKeyRef.current === displayedView.key) { + setDismissedKeys((previous) => new Set(previous).add(displayedView.key)); + } + setRenderedView(pendingViewRef.current); + pendingViewRef.current = null; + dismissAfterExitKeyRef.current = null; + setExitingKey(null); + }, + [displayedView, exitingKey, setDismissedKeys], + ); + + return { + displayedView, + exitingKey, + onTransitionEnd, + startExit, + }; +} + +function useProviderUpdateAutoDismiss(input: { + dismissAfterVisibleMs: number | undefined; + exitingKey: string | null; + startExit: ( + key: string, + nextView: ProviderUpdateSidebarPillView | null, + dismissKey?: string, + ) => void; + viewKey: string | null; +}) { + const { dismissAfterVisibleMs, exitingKey, startExit, viewKey } = input; + useEffect(() => { if (!dismissAfterVisibleMs || !viewKey) { return; @@ -116,6 +153,40 @@ export function SidebarProviderUpdatePill() { return () => window.clearTimeout(timeoutId); }, [dismissAfterVisibleMs, exitingKey, startExit, viewKey]); +} + +export function SidebarProviderUpdatePill() { + const navigate = useNavigate(); + const providers = useServerProviders(); + const [dismissedKeys, setDismissedKeys] = useState>(() => new Set()); + const effectiveVisibleAfterIso = useProviderUpdateVisibleAfterIso(providers); + const view = getProviderUpdateSidebarPillView(providers, { + ...(effectiveVisibleAfterIso !== undefined + ? { visibleAfterIso: effectiveVisibleAfterIso } + : {}), + dismissedKeys, + }); + + const openProviderSettings = useCallback(() => { + void navigate({ to: "/settings/providers" }); + }, [navigate]); + const { displayedView, exitingKey, onTransitionEnd, startExit } = useDisplayedProviderUpdatePill({ + view, + setDismissedKeys, + }); + const dismissAfterVisibleMs = displayedView?.dismissAfterVisibleMs; + const viewKey = displayedView?.key ?? null; + const showDismissProgress = + dismissAfterVisibleMs !== undefined && + displayedView?.tone !== "loading" && + exitingKey !== viewKey; + + useProviderUpdateAutoDismiss({ + dismissAfterVisibleMs, + exitingKey, + startExit, + viewKey, + }); if (!displayedView) { return null; @@ -130,21 +201,7 @@ export function SidebarProviderUpdatePill() { ? "pointer-events-none translate-y-1.5 opacity-0" : "translate-y-0 opacity-100" }`} - onTransitionEnd={(event) => { - if (event.target !== event.currentTarget) { - return; - } - if (!displayedView || exitingKey !== displayedView.key) { - return; - } - if (dismissAfterExitKey === displayedView.key) { - setDismissedKeys((previous) => new Set(previous).add(displayedView.key)); - } - setRenderedView(pendingView); - setPendingView(null); - setExitingKey(null); - setDismissAfterExitKey(null); - }} + onTransitionEnd={onTransitionEnd} > {showDismissProgress ? (