| ... | ... | @@ -1,6 +1,7 @@ |
| 1 | | import { DownloadIcon, RefreshCwIcon, RotateCwIcon, TriangleAlertIcon } from "lucide-react"; |
| 2 | | import { useCallback, useState } from "react"; |
| 1 | import { TriangleAlertIcon } from "lucide-react"; |
| 2 | import { useCallback, useEffect, useState } from "react"; |
| 3 | 3 | import { isElectron } from "../../env"; |
| 4 | import { useMediaQuery } from "../../hooks/useMediaQuery"; |
| 4 | 5 | import { cn } from "../../lib/utils"; |
| 5 | 6 | import { ensureLocalApi } from "../../localApi"; |
| 6 | 7 | import { useDesktopUpdateState } from "../../state/desktopUpdate"; |
| ... | ... | @@ -21,6 +22,38 @@ import { Alert, AlertDescription, AlertTitle } from "../ui/alert"; |
| 21 | 22 | import { Separator } from "../ui/separator"; |
| 22 | 23 | import { SidebarMenuItem } from "../ui/sidebar"; |
| 23 | 24 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 25 | import { |
| 26 | DesktopUpdateStatusIcon, |
| 27 | shouldContinueDesktopUpdateCheckAnimation, |
| 28 | shouldShowDesktopUpdateCheckIcon, |
| 29 | } from "./DesktopUpdateStatusIcon"; |
| 30 | |
| 31 | function resolveSidebarUpdatePresentation({ |
| 32 | action, |
| 33 | isDownloading, |
| 34 | showCheckIcon, |
| 35 | }: { |
| 36 | readonly action: ReturnType<typeof resolveDesktopUpdateButtonAction>; |
| 37 | readonly isDownloading: boolean; |
| 38 | readonly showCheckIcon: boolean; |
| 39 | }) { |
| 40 | const showUpdateDetails = action !== "none" || isDownloading; |
| 41 | const iconStatus = showCheckIcon |
| 42 | ? "checking" |
| 43 | : action === "install" |
| 44 | ? "downloaded" |
| 45 | : isDownloading |
| 46 | ? "downloading" |
| 47 | : action === "download" |
| 48 | ? "available" |
| 49 | : "idle"; |
| 50 | |
| 51 | return { |
| 52 | iconStatus, |
| 53 | showUpdateDetails, |
| 54 | showUpdateIconState: showUpdateDetails && !showCheckIcon, |
| 55 | } as const; |
| 56 | } |
| 24 | 57 | |
| 25 | 58 | function keyReleaseNoteItems(items: ReadonlyArray<string>) { |
| 26 | 59 | const occurrences = new Map<string, number>(); |
| ... | ... | @@ -110,18 +143,42 @@ export function SidebarUpdatePill() { |
| 110 | 143 | function SidebarUpdateControl() { |
| 111 | 144 | const state = useDesktopUpdateState(); |
| 112 | 145 | const [isActionPending, setIsActionPending] = useState(false); |
| 146 | const [checkAnimationKey, setCheckAnimationKey] = useState(0); |
| 147 | const [isCheckAnimationLatched, setIsCheckAnimationLatched] = useState(false); |
| 148 | const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)"); |
| 149 | |
| 150 | useEffect(() => { |
| 151 | if (prefersReducedMotion) { |
| 152 | setIsCheckAnimationLatched(false); |
| 153 | } else if (state?.status === "checking") { |
| 154 | setIsCheckAnimationLatched(true); |
| 155 | } |
| 156 | }, [prefersReducedMotion, state?.status]); |
| 113 | 157 | |
| 114 | 158 | const action = state ? resolveDesktopUpdateButtonAction(state) : "none"; |
| 115 | 159 | const isDownloading = state?.status === "downloading"; |
| 116 | | const isUpdateState = action !== "none" || isDownloading; |
| 117 | | const tooltip = isUpdateState |
| 160 | const showCheckIcon = shouldShowDesktopUpdateCheckIcon({ |
| 161 | isAnimationLatched: isCheckAnimationLatched, |
| 162 | isChecking: state?.status === "checking", |
| 163 | prefersReducedMotion, |
| 164 | }); |
| 165 | const { iconStatus, showUpdateDetails, showUpdateIconState } = resolveSidebarUpdatePresentation({ |
| 166 | action, |
| 167 | isDownloading, |
| 168 | showCheckIcon, |
| 169 | }); |
| 170 | const tooltip = showUpdateDetails |
| 118 | 171 | ? state |
| 119 | 172 | ? getDesktopUpdateButtonTooltip(state) |
| 120 | 173 | : "Update available" |
| 121 | | : state?.status === "checking" |
| 174 | : showCheckIcon |
| 122 | 175 | ? "Checking for updates…" |
| 123 | 176 | : "Check for updates"; |
| 124 | | const disabled = isUpdateState ? isDesktopUpdateButtonDisabled(state) : !canCheckForUpdate(state); |
| 177 | const disabled = showCheckIcon |
| 178 | ? true |
| 179 | : showUpdateDetails |
| 180 | ? isDesktopUpdateButtonDisabled(state) |
| 181 | : !canCheckForUpdate(state); |
| 125 | 182 | |
| 126 | 183 | const handleAction = useCallback(async () => { |
| 127 | 184 | const bridge = window.desktopBridge; |
| ... | ... | @@ -209,6 +266,10 @@ function SidebarUpdateControl() { |
| 209 | 266 | return; |
| 210 | 267 | } |
| 211 | 268 | |
| 269 | if (!prefersReducedMotion) { |
| 270 | setIsCheckAnimationLatched(true); |
| 271 | setCheckAnimationKey((key) => key + 1); |
| 272 | } |
| 212 | 273 | void bridge |
| 213 | 274 | .checkForUpdate() |
| 214 | 275 | .then((result) => { |
| ... | ... | @@ -232,7 +293,16 @@ function SidebarUpdateControl() { |
| 232 | 293 | ); |
| 233 | 294 | }) |
| 234 | 295 | .finally(() => setIsActionPending(false)); |
| 235 | | }, [action, disabled, isActionPending, state]); |
| 296 | }, [action, disabled, isActionPending, prefersReducedMotion, state]); |
| 297 | |
| 298 | const handleCheckAnimationIteration = useCallback(() => { |
| 299 | setIsCheckAnimationLatched( |
| 300 | shouldContinueDesktopUpdateCheckAnimation({ |
| 301 | isChecking: state?.status === "checking", |
| 302 | prefersReducedMotion, |
| 303 | }), |
| 304 | ); |
| 305 | }, [prefersReducedMotion, state?.status]); |
| 236 | 306 | |
| 237 | 307 | return ( |
| 238 | 308 | <SidebarMenuItem className="ml-auto shrink-0"> |
| ... | ... | @@ -245,29 +315,28 @@ function SidebarUpdateControl() { |
| 245 | 315 | aria-disabled={disabled || isActionPending || undefined} |
| 246 | 316 | disabled={disabled || isActionPending} |
| 247 | 317 | className={cn( |
| 248 | | "inline-flex size-8 items-center justify-center rounded-full outline-hidden ring-ring transition-colors enabled:cursor-pointer focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-60", |
| 249 | | isUpdateState |
| 318 | "inline-flex size-8 items-center justify-center rounded-full outline-hidden ring-ring transition-colors enabled:cursor-pointer focus-visible:ring-2 disabled:cursor-not-allowed", |
| 319 | showUpdateIconState |
| 250 | 320 | ? "bg-update-surface text-update-foreground enabled:hover:bg-update/12" |
| 251 | 321 | : "text-[var(--sidebar-icon-color)] enabled:hover:bg-sidebar-row-hover enabled:hover:text-sidebar-foreground", |
| 322 | disabled && !showUpdateIconState && "opacity-60", |
| 252 | 323 | )} |
| 253 | 324 | onClick={handleAction} |
| 254 | 325 | > |
| 255 | | {action === "install" ? ( |
| 256 | | <RotateCwIcon className="size-4" /> |
| 257 | | ) : isUpdateState ? ( |
| 258 | | <DownloadIcon className="size-4" /> |
| 259 | | ) : ( |
| 260 | | <RefreshCwIcon |
| 261 | | className={cn("size-4", state?.status === "checking" && "animate-spin")} |
| 262 | | /> |
| 263 | | )} |
| 326 | <DesktopUpdateStatusIcon |
| 327 | key={showCheckIcon ? checkAnimationKey : iconStatus} |
| 328 | downloadPercent={state?.downloadPercent ?? null} |
| 329 | isCheckAnimating={showCheckIcon && !prefersReducedMotion} |
| 330 | onCheckAnimationIteration={handleCheckAnimationIteration} |
| 331 | status={iconStatus} |
| 332 | /> |
| 264 | 333 | </button> |
| 265 | 334 | } |
| 266 | 335 | /> |
| 267 | 336 | <TooltipPopup |
| 268 | 337 | align="center" |
| 269 | 338 | className={ |
| 270 | | isUpdateState && state?.channel === "nightly" && state.releaseNotes.length > 0 |
| 339 | showUpdateDetails && state?.channel === "nightly" && state.releaseNotes.length > 0 |
| 271 | 340 | ? // pointer-events-auto overrides the positioner's pointer-events-none so the |
| 272 | 341 | // release notes stay open (and scrollable) when the cursor moves into them. |
| 273 | 342 | "pointer-events-auto max-w-none text-balance" |
| ... | ... | @@ -275,7 +344,7 @@ function SidebarUpdateControl() { |
| 275 | 344 | } |
| 276 | 345 | side="top" |
| 277 | 346 | style={ |
| 278 | | isUpdateState |
| 347 | showUpdateDetails |
| 279 | 348 | ? { |
| 280 | 349 | background: |
| 281 | 350 | "color-mix(in srgb, var(--update) 18%, color-mix(in srgb, var(--popover) var(--glass-opacity), transparent))", |
| ... | ... | @@ -283,9 +352,9 @@ function SidebarUpdateControl() { |
| 283 | 352 | } |
| 284 | 353 | : undefined |
| 285 | 354 | } |
| 286 | | variant={isUpdateState ? "glass" : "default"} |
| 355 | variant={showUpdateDetails ? "glass" : "default"} |
| 287 | 356 | > |
| 288 | | {isUpdateState && state ? ( |
| 357 | {showUpdateDetails && state ? ( |
| 289 | 358 | <SidebarUpdateReleaseNotesTooltip state={state} tooltip={tooltip} /> |
| 290 | 359 | ) : ( |
| 291 | 360 | tooltip |