Unified · Split
| @@ -28,62 +28,19 @@ const PROVIDER_UPDATE_PILL_PROGRESS_STYLES = { · 27 unmodified lines | |||
| 28 | error: "bg-destructive/14", | 28 | error: "bg-destructive/14", |
| 29 | } as const; | 29 | } as const; |
| 30 | 30 | ||
| 31 | function latestProviderCheckedAt( | 31 | function useMountedIsoTimestamp(): string { |
| 32 | providers: ReadonlyArray<Pick<ServerProvider, "checkedAt">>, | 32 | const [mountedIsoTimestamp] = useState(() => new Date().toISOString()); |
| 33 | ): string | undefined { | 33 | return mountedIsoTimestamp; |
| 34 | return providers.reduce<string | undefined>( | ||
| 35 | (latest, provider) => | ||
| 36 | latest === undefined || provider.checkedAt > latest ? provider.checkedAt : latest, | ||
| 37 | undefined, | ||
| 38 | ); | ||
| 39 | } | 34 | } |
| 40 | 35 | ||
| 41 | export function SidebarProviderUpdatePill() { | 36 | function useProviderUpdateRenderedViewSynchronization(input: { |
| 42 | const navigate = useNavigate(); | 37 | readonly exitingKey: string | null; |
| 43 | const providers = useAtomValue(primaryServerProvidersAtom); | 38 | readonly renderedView: ProviderUpdateSidebarPillView | null; |
| 44 | const [dismissedKeys, setDismissedKeys] = useState<ReadonlySet<string>>(() => new Set()); | 39 | readonly setRenderedView: (view: ProviderUpdateSidebarPillView | null) => void; |
| 45 | const [renderedView, setRenderedView] = useState<ProviderUpdateSidebarPillView | null>(null); | 40 | readonly startExit: (key: string, nextView: ProviderUpdateSidebarPillView | null) => void; |
| 46 | const [pendingView, setPendingView] = useState<ProviderUpdateSidebarPillView | null>(null); | 41 | readonly view: ProviderUpdateSidebarPillView | null; |
| 47 | const [exitingKey, setExitingKey] = useState<string | null>(null); | 42 | }) { |
| 48 | const [dismissAfterExitKey, setDismissAfterExitKey] = useState<string | null>(null); | 43 | const { exitingKey, renderedView, setRenderedView, startExit, view } = input; |
| 49 | const [visibleAfterIso, setVisibleAfterIso] = useState<string | undefined>(); | ||
| 50 | const effectiveVisibleAfterIso = visibleAfterIso ?? latestProviderCheckedAt(providers); | ||
| 51 | const view = getProviderUpdateSidebarPillView(providers, { | ||
| 52 | ...(effectiveVisibleAfterIso !== undefined | ||
| 53 | ? { visibleAfterIso: effectiveVisibleAfterIso } | ||
| 54 | : {}), | ||
| 55 | dismissedKeys, | ||
| 56 | }); | ||
| 57 | |||
| 58 | useEffect(() => { | ||
| 59 | if (visibleAfterIso === undefined && effectiveVisibleAfterIso !== undefined) { | ||
| 60 | setVisibleAfterIso(effectiveVisibleAfterIso); | ||
| 61 | } | ||
| 62 | }, [effectiveVisibleAfterIso, visibleAfterIso]); | ||
| 63 | |||
| 64 | const openProviderSettings = useCallback(() => { | ||
| 65 | void navigate({ to: "/settings/providers" }); | ||
| 66 | }, [navigate]); | ||
| 67 | const displayedView = renderedView ?? view; | ||
| 68 | const dismissAfterVisibleMs = displayedView?.dismissAfterVisibleMs; | ||
| 69 | const viewKey = displayedView?.key ?? null; | ||
| 70 | const showDismissProgress = | ||
| 71 | dismissAfterVisibleMs !== undefined && | ||
| 72 | displayedView?.tone !== "loading" && | ||
| 73 | exitingKey !== viewKey; | ||
| 74 | |||
| 75 | const startExit = useCallback( | ||
| 76 | (key: string, nextView: ProviderUpdateSidebarPillView | null, dismissKey?: string) => { | ||
| 77 | if (exitingKey === key) { | ||
| 78 | return; | ||
| 79 | } | ||
| 80 | setPendingView(nextView); | ||
| 81 | setExitingKey(key); | ||
| 82 | setDismissAfterExitKey(dismissKey ?? null); | ||
| 83 | }, | ||
| 84 | [exitingKey], | ||
| 85 | ); | ||
| 86 | |||
| 87 | useEffect(() => { | 44 | useEffect(() => { |
| 88 | if (exitingKey !== null) { | 45 | if (exitingKey !== null) { |
| 89 | return; | 46 | return; |
| @@ -102,8 +59,20 @@ export function SidebarProviderUpdatePill() { · 12 unmodified lines | |||
| 102 | startExit(renderedView.key, view); | 59 | startExit(renderedView.key, view); |
| 103 | return; | 60 | return; |
| 104 | } | 61 | } |
| 105 | }, [exitingKey, renderedView, startExit, view]); | 62 | }, [exitingKey, renderedView, setRenderedView, startExit, view]); |
| 63 | } | ||
| 106 | 64 | ||
| 65 | function useProviderUpdateAutoDismiss(input: { | ||
| 66 | readonly dismissAfterVisibleMs: number | undefined; | ||
| 67 | readonly exitingKey: string | null; | ||
| 68 | readonly startExit: ( | ||
| 69 | key: string, | ||
| 70 | nextView: ProviderUpdateSidebarPillView | null, | ||
| 71 | dismissKey?: string, | ||
| 72 | ) => void; | ||
| 73 | readonly viewKey: string | null; | ||
| 74 | }) { | ||
| 75 | const { dismissAfterVisibleMs, exitingKey, startExit, viewKey } = input; | ||
| 107 | useEffect(() => { | 76 | useEffect(() => { |
| 108 | if (!dismissAfterVisibleMs || !viewKey) { | 77 | if (!dismissAfterVisibleMs || !viewKey) { |
| 109 | return; | 78 | return; |
| @@ -117,6 +86,60 @@ export function SidebarProviderUpdatePill() { · 7 unmodified lines | |||
| 117 | 86 | ||
| 118 | return () => window.clearTimeout(timeoutId); | 87 | return () => window.clearTimeout(timeoutId); |
| 119 | }, [dismissAfterVisibleMs, exitingKey, startExit, viewKey]); | 88 | }, [dismissAfterVisibleMs, exitingKey, startExit, viewKey]); |
| 89 | } | ||
| 90 | |||
| 91 | export function SidebarProviderUpdatePill() { | ||
| 92 | const navigate = useNavigate(); | ||
| 93 | const providers = useAtomValue(primaryServerProvidersAtom); | ||
| 94 | const [dismissedKeys, setDismissedKeys] = useState<ReadonlySet<string>>(() => new Set()); | ||
| 95 | const visibleAfterIso = useMountedIsoTimestamp(); | ||
| 96 | const view = getProviderUpdateSidebarPillView(providers, { | ||
| 97 | visibleAfterIso, | ||
| 98 | dismissedKeys, | ||
| 99 | }); | ||
| 100 | const [renderedView, setRenderedView] = useState<ProviderUpdateSidebarPillView | null>( | ||
| 101 | () => view, | ||
| 102 | ); | ||
| 103 | const [pendingView, setPendingView] = useState<ProviderUpdateSidebarPillView | null>(null); | ||
| 104 | const [exitingKey, setExitingKey] = useState<string | null>(null); | ||
| 105 | const [dismissAfterExitKey, setDismissAfterExitKey] = useState<string | null>(null); | ||
| 106 | |||
| 107 | const openProviderSettings = useCallback(() => { | ||
| 108 | void navigate({ to: "/settings/providers" }); | ||
| 109 | }, [navigate]); | ||
| 110 | const displayedView = renderedView ?? view; | ||
| 111 | const dismissAfterVisibleMs = displayedView?.dismissAfterVisibleMs; | ||
| 112 | const viewKey = displayedView?.key ?? null; | ||
| 113 | const showDismissProgress = | ||
| 114 | dismissAfterVisibleMs !== undefined && | ||
| 115 | displayedView?.tone !== "loading" && | ||
| 116 | exitingKey !== viewKey; | ||
| 117 | |||
| 118 | const startExit = useCallback( | ||
| 119 | (key: string, nextView: ProviderUpdateSidebarPillView | null, dismissKey?: string) => { | ||
| 120 | if (exitingKey === key) { | ||
| 121 | return; | ||
| 122 | } | ||
| 123 | setPendingView(nextView); | ||
| 124 | setExitingKey(key); | ||
| 125 | setDismissAfterExitKey(dismissKey ?? null); | ||
| 126 | }, | ||
| 127 | [exitingKey], | ||
| 128 | ); | ||
| 129 | |||
| 130 | useProviderUpdateRenderedViewSynchronization({ | ||
| 131 | exitingKey, | ||
| 132 | renderedView, | ||
| 133 | setRenderedView, | ||
| 134 | startExit, | ||
| 135 | view, | ||
| 136 | }); | ||
| 137 | useProviderUpdateAutoDismiss({ | ||
| 138 | dismissAfterVisibleMs, | ||
| 139 | exitingKey, | ||
| 140 | startExit, | ||
| 141 | viewKey, | ||
| 142 | }); | ||
| 120 | 143 | ||
| 121 | if (!displayedView) { | 144 | if (!displayedView) { |
| 122 | return null; | 145 | return null; |