import { TriangleAlertIcon } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { isElectron } from "../../env"; import { useMediaQuery } from "../../hooks/useMediaQuery"; import { cn } from "../../lib/utils"; import { ensureLocalApi } from "../../localApi"; import { useDesktopUpdateState } from "../../state/desktopUpdate"; import { stackedThreadToast, toastManager } from "../ui/toast"; import { canCheckForUpdate, getArm64IntelBuildWarningDescription, getDesktopUpdateActionError, getDesktopUpdateButtonTooltip, getDesktopUpdateInstallConfirmationMessage, isDesktopUpdateButtonDisabled, resolveDesktopUpdateButtonAction, shouldShowArm64IntelBuildWarning, shouldToastDesktopUpdateActionResult, } from "../desktopUpdate.logic"; import { showDesktopUpdateDownloadedToast } from "../desktopUpdate.toast"; import { Alert, AlertDescription, AlertTitle } from "../ui/alert"; import { Separator } from "../ui/separator"; import { SidebarMenuItem } from "../ui/sidebar"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { DesktopUpdateStatusIcon, shouldContinueDesktopUpdateCheckAnimation, shouldShowDesktopUpdateCheckIcon, } from "./DesktopUpdateStatusIcon"; function resolveSidebarUpdatePresentation({ action, isDownloading, showCheckIcon, }: { readonly action: ReturnType; readonly isDownloading: boolean; readonly showCheckIcon: boolean; }) { const showUpdateDetails = action !== "none" || isDownloading; const iconStatus = showCheckIcon ? "checking" : action === "install" ? "downloaded" : isDownloading ? "downloading" : action === "download" ? "available" : "idle"; return { iconStatus, showUpdateDetails, showUpdateIconState: showUpdateDetails && !showCheckIcon, } as const; } function keyReleaseNoteItems(items: ReadonlyArray) { const occurrences = new Map(); return items.map((item) => { const occurrence = occurrences.get(item) ?? 0; occurrences.set(item, occurrence + 1); return { item, key: JSON.stringify([item, occurrence]) }; }); } function SidebarUpdateReleaseNotesTooltip({ state, tooltip, }: { readonly state: NonNullable>; readonly tooltip: string; }) { if (state.channel !== "nightly" || state.releaseNotes.length === 0) { return <>{tooltip}; } return (
{state.status === "available" ? (
Update ready to download
{state.availableVersion ? (
{state.availableVersion}
) : null}
) : (
{tooltip}
)}
{state.releaseNotes.map((releaseNote, index) => (
{index > 0 && }

{index === 0 ? "What's changed" : `Changes in ${releaseNote.version}`}

    {keyReleaseNoteItems(releaseNote.items).map(({ item, key }) => (
  • {item}
  • ))}
))}
); } export function SidebarUpdateArchitectureWarning() { return isElectron ? : null; } function SidebarUpdateArchitectureWarningContent() { const state = useDesktopUpdateState(); const visible = shouldShowArm64IntelBuildWarning(state); const description = state && visible ? getArm64IntelBuildWarningDescription(state) : null; if (!visible || !description) return null; return ( Intel build on Apple Silicon {description} ); } export function SidebarUpdatePill() { return isElectron ? : null; } function SidebarUpdateControl() { const state = useDesktopUpdateState(); const [isActionPending, setIsActionPending] = useState(false); const [checkAnimationKey, setCheckAnimationKey] = useState(0); const [isCheckAnimationLatched, setIsCheckAnimationLatched] = useState(false); const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)"); useEffect(() => { if (prefersReducedMotion) { setIsCheckAnimationLatched(false); } else if (state?.status === "checking") { setIsCheckAnimationLatched(true); } }, [prefersReducedMotion, state?.status]); const action = state ? resolveDesktopUpdateButtonAction(state) : "none"; const isDownloading = state?.status === "downloading"; const showCheckIcon = shouldShowDesktopUpdateCheckIcon({ isAnimationLatched: isCheckAnimationLatched, isChecking: state?.status === "checking", prefersReducedMotion, }); const { iconStatus, showUpdateDetails, showUpdateIconState } = resolveSidebarUpdatePresentation({ action, isDownloading, showCheckIcon, }); const tooltip = showUpdateDetails ? state ? getDesktopUpdateButtonTooltip(state) : "Update available" : showCheckIcon ? "Checking for updates…" : "Check for updates"; const disabled = showCheckIcon ? true : showUpdateDetails ? isDesktopUpdateButtonDisabled(state) : !canCheckForUpdate(state); const handleAction = useCallback(async () => { const bridge = window.desktopBridge; if (!bridge || !state) return; if (disabled || isActionPending) return; setIsActionPending(true); if (action === "download") { void bridge .downloadUpdate() .then((result) => { if (result.completed) { showDesktopUpdateDownloadedToast(bridge, result.state); } if (!shouldToastDesktopUpdateActionResult(result)) return; const actionError = getDesktopUpdateActionError(result); if (!actionError) return; toastManager.add( stackedThreadToast({ type: "error", title: "Could not download update", description: actionError, }), ); }) .catch((error) => { toastManager.add( stackedThreadToast({ type: "error", title: "Could not start update download", description: error instanceof Error ? error.message : "An unexpected error occurred.", }), ); }) .finally(() => setIsActionPending(false)); return; } if (action === "install") { let confirmed = false; try { confirmed = await ensureLocalApi().dialogs.confirm( getDesktopUpdateInstallConfirmationMessage(state), ); } catch (error) { setIsActionPending(false); toastManager.add( stackedThreadToast({ type: "error", title: "Could not confirm update", description: error instanceof Error ? error.message : "Update confirmation failed.", }), ); return; } if (!confirmed) { setIsActionPending(false); return; } void bridge .installUpdate() .then((result) => { if (!shouldToastDesktopUpdateActionResult(result)) return; const actionError = getDesktopUpdateActionError(result); if (!actionError) return; toastManager.add( stackedThreadToast({ type: "error", title: "Could not install update", description: actionError, }), ); }) .catch((error) => { toastManager.add( stackedThreadToast({ type: "error", title: "Could not install update", description: error instanceof Error ? error.message : "An unexpected error occurred.", }), ); }) .finally(() => setIsActionPending(false)); return; } if (!prefersReducedMotion) { setIsCheckAnimationLatched(true); setCheckAnimationKey((key) => key + 1); } void bridge .checkForUpdate() .then((result) => { if (result.checked) return; toastManager.add( stackedThreadToast({ type: "error", title: "Could not check for updates", description: result.state.message ?? "Automatic updates are not available in this build.", }), ); }) .catch((error) => { toastManager.add( stackedThreadToast({ type: "error", title: "Could not check for updates", description: error instanceof Error ? error.message : "Update check failed.", }), ); }) .finally(() => setIsActionPending(false)); }, [action, disabled, isActionPending, prefersReducedMotion, state]); const handleCheckAnimationIteration = useCallback(() => { setIsCheckAnimationLatched( shouldContinueDesktopUpdateCheckAnimation({ isChecking: state?.status === "checking", prefersReducedMotion, }), ); }, [prefersReducedMotion, state?.status]); return ( } /> 0 ? // pointer-events-auto overrides the positioner's pointer-events-none so the // release notes stay open (and scrollable) when the cursor moves into them. "pointer-events-auto max-w-none text-balance" : undefined } side="top" style={ showUpdateDetails ? { background: "color-mix(in srgb, var(--update) 18%, color-mix(in srgb, var(--popover) var(--glass-opacity), transparent))", borderColor: "var(--update-foreground)", } : undefined } variant={showUpdateDetails ? "glass" : "default"} > {showUpdateDetails && state ? ( ) : ( tooltip )} ); }