| ... | ... | @@ -1,10 +1,12 @@ |
| 1 | | import { DownloadIcon, RotateCwIcon, TriangleAlertIcon, XIcon } from "lucide-react"; |
| 1 | import { DownloadIcon, RefreshCwIcon, RotateCwIcon, TriangleAlertIcon } from "lucide-react"; |
| 2 | 2 | import { useCallback, useState } from "react"; |
| 3 | 3 | import { isElectron } from "../../env"; |
| 4 | import { cn } from "../../lib/utils"; |
| 4 | 5 | import { ensureLocalApi } from "../../localApi"; |
| 5 | 6 | import { useDesktopUpdateState } from "../../state/desktopUpdate"; |
| 6 | 7 | import { stackedThreadToast, toastManager } from "../ui/toast"; |
| 7 | 8 | import { |
| 9 | canCheckForUpdate, |
| 8 | 10 | getArm64IntelBuildWarningDescription, |
| 9 | 11 | getDesktopUpdateActionError, |
| 10 | 12 | getDesktopUpdateButtonTooltip, |
| ... | ... | @@ -12,12 +14,12 @@ import { |
| 12 | 14 | isDesktopUpdateButtonDisabled, |
| 13 | 15 | resolveDesktopUpdateButtonAction, |
| 14 | 16 | shouldShowArm64IntelBuildWarning, |
| 15 | | shouldShowDesktopUpdateButton, |
| 16 | 17 | shouldToastDesktopUpdateActionResult, |
| 17 | 18 | } from "../desktopUpdate.logic"; |
| 18 | 19 | import { showDesktopUpdateDownloadedToast } from "../desktopUpdate.toast"; |
| 19 | 20 | import { Alert, AlertDescription, AlertTitle } from "../ui/alert"; |
| 20 | 21 | import { Separator } from "../ui/separator"; |
| 22 | import { SidebarMenuItem } from "../ui/sidebar"; |
| 21 | 23 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 22 | 24 | |
| 23 | 25 | function keyReleaseNoteItems(items: ReadonlyArray<string>) { |
| ... | ... | @@ -41,16 +43,29 @@ function SidebarUpdateReleaseNotesTooltip({ |
| 41 | 43 | } |
| 42 | 44 | |
| 43 | 45 | return ( |
| 44 | | <div className="w-120 max-w-[calc(100vw-2rem)] text-left"> |
| 46 | <div className="w-fit max-w-[min(24rem,calc(100vw-2rem))] text-left"> |
| 45 | 47 | <div className="px-1"> |
| 46 | | <div className="text-sm leading-5 font-medium">{tooltip}</div> |
| 48 | {state.status === "available" ? ( |
| 49 | <div> |
| 50 | <div className="whitespace-nowrap text-sm leading-5 font-medium"> |
| 51 | Update ready to download |
| 52 | </div> |
| 53 | {state.availableVersion ? ( |
| 54 | <div className="mt-0.5 text-xs leading-4 text-update-foreground"> |
| 55 | {state.availableVersion} |
| 56 | </div> |
| 57 | ) : null} |
| 58 | </div> |
| 59 | ) : ( |
| 60 | <div className="text-sm leading-5 font-medium">{tooltip}</div> |
| 61 | )} |
| 47 | 62 | </div> |
| 48 | 63 | <div className="max-h-[min(28rem,calc(100vh-6rem))] overflow-y-auto px-1 pt-4 pb-1"> |
| 49 | 64 | {state.releaseNotes.map((releaseNote, index) => ( |
| 50 | 65 | <div key={releaseNote.version}> |
| 51 | 66 | {index > 0 && <Separator className="my-3 bg-border/60" />} |
| 52 | 67 | <section> |
| 53 | | <h3 className="text-muted-foreground text-xs leading-4 font-semibold"> |
| 68 | <h3 className="text-foreground text-xs leading-4 font-semibold"> |
| 54 | 69 | {index === 0 ? "What's changed" : `Changes in ${releaseNote.version}`} |
| 55 | 70 | </h3> |
| 56 | 71 | <ul className="mt-2 space-y-1.5 pl-4 text-xs leading-5 text-popover-foreground/90"> |
| ... | ... | @@ -68,24 +83,50 @@ function SidebarUpdateReleaseNotesTooltip({ |
| 68 | 83 | ); |
| 69 | 84 | } |
| 70 | 85 | |
| 86 | export function SidebarUpdateArchitectureWarning() { |
| 87 | return isElectron ? <SidebarUpdateArchitectureWarningContent /> : null; |
| 88 | } |
| 89 | |
| 90 | function SidebarUpdateArchitectureWarningContent() { |
| 91 | const state = useDesktopUpdateState(); |
| 92 | const visible = shouldShowArm64IntelBuildWarning(state); |
| 93 | const description = state && visible ? getArm64IntelBuildWarningDescription(state) : null; |
| 94 | |
| 95 | if (!visible || !description) return null; |
| 96 | |
| 97 | return ( |
| 98 | <Alert variant="warning" className="rounded-2xl border-warning/40 bg-warning/8 text-xs"> |
| 99 | <TriangleAlertIcon /> |
| 100 | <AlertTitle>Intel build on Apple Silicon</AlertTitle> |
| 101 | <AlertDescription>{description}</AlertDescription> |
| 102 | </Alert> |
| 103 | ); |
| 104 | } |
| 105 | |
| 71 | 106 | export function SidebarUpdatePill() { |
| 107 | return isElectron ? <SidebarUpdateControl /> : null; |
| 108 | } |
| 109 | |
| 110 | function SidebarUpdateControl() { |
| 72 | 111 | const state = useDesktopUpdateState(); |
| 73 | | const [dismissed, setDismissed] = useState(false); |
| 74 | 112 | const [isActionPending, setIsActionPending] = useState(false); |
| 75 | 113 | |
| 76 | | const visible = isElectron && shouldShowDesktopUpdateButton(state) && !dismissed; |
| 77 | | const tooltip = state ? getDesktopUpdateButtonTooltip(state) : "Update available"; |
| 78 | | const disabled = isDesktopUpdateButtonDisabled(state); |
| 79 | 114 | const action = state ? resolveDesktopUpdateButtonAction(state) : "none"; |
| 80 | | |
| 81 | | const showArm64Warning = isElectron && shouldShowArm64IntelBuildWarning(state); |
| 82 | | const arm64Description = |
| 83 | | state && showArm64Warning ? getArm64IntelBuildWarningDescription(state) : null; |
| 115 | const isDownloading = state?.status === "downloading"; |
| 116 | const isUpdateState = action !== "none" || isDownloading; |
| 117 | const tooltip = isUpdateState |
| 118 | ? state |
| 119 | ? getDesktopUpdateButtonTooltip(state) |
| 120 | : "Update available" |
| 121 | : state?.status === "checking" |
| 122 | ? "Checking for updates…" |
| 123 | : "Check for updates"; |
| 124 | const disabled = isUpdateState ? isDesktopUpdateButtonDisabled(state) : !canCheckForUpdate(state); |
| 84 | 125 | |
| 85 | 126 | const handleAction = useCallback(async () => { |
| 86 | 127 | const bridge = window.desktopBridge; |
| 87 | 128 | if (!bridge || !state) return; |
| 88 | | if (disabled || action === "none" || isActionPending) return; |
| 129 | if (disabled || isActionPending) return; |
| 89 | 130 | |
| 90 | 131 | setIsActionPending(true); |
| 91 | 132 | |
| ... | ... | @@ -165,99 +206,92 @@ export function SidebarUpdatePill() { |
| 165 | 206 | ); |
| 166 | 207 | }) |
| 167 | 208 | .finally(() => setIsActionPending(false)); |
| 209 | return; |
| 168 | 210 | } |
| 169 | | }, [action, disabled, isActionPending, state]); |
| 170 | 211 | |
| 171 | | if (!visible && !showArm64Warning) return null; |
| 212 | void bridge |
| 213 | .checkForUpdate() |
| 214 | .then((result) => { |
| 215 | if (result.checked) return; |
| 216 | toastManager.add( |
| 217 | stackedThreadToast({ |
| 218 | type: "error", |
| 219 | title: "Could not check for updates", |
| 220 | description: |
| 221 | result.state.message ?? "Automatic updates are not available in this build.", |
| 222 | }), |
| 223 | ); |
| 224 | }) |
| 225 | .catch((error) => { |
| 226 | toastManager.add( |
| 227 | stackedThreadToast({ |
| 228 | type: "error", |
| 229 | title: "Could not check for updates", |
| 230 | description: error instanceof Error ? error.message : "Update check failed.", |
| 231 | }), |
| 232 | ); |
| 233 | }) |
| 234 | .finally(() => setIsActionPending(false)); |
| 235 | }, [action, disabled, isActionPending, state]); |
| 172 | 236 | |
| 173 | 237 | return ( |
| 174 | | <div className="flex flex-col gap-1"> |
| 175 | | {showArm64Warning && arm64Description && ( |
| 176 | | <Alert variant="warning" className="rounded-2xl border-warning/40 bg-warning/8 text-xs"> |
| 177 | | <TriangleAlertIcon /> |
| 178 | | <AlertTitle>Intel build on Apple Silicon</AlertTitle> |
| 179 | | <AlertDescription>{arm64Description}</AlertDescription> |
| 180 | | </Alert> |
| 181 | | )} |
| 182 | | {visible && ( |
| 183 | | <div |
| 184 | | className={`group/update relative flex h-7 w-full items-center rounded-lg bg-update-surface text-xs font-medium text-update-foreground ${ |
| 185 | | disabled ? " cursor-not-allowed opacity-60" : "" |
| 186 | | }`} |
| 187 | | > |
| 188 | | <div className="pointer-events-none absolute inset-0 rounded-lg transition-colors group-has-[button.update-main:hover]/update:bg-update/12" /> |
| 189 | | <Tooltip> |
| 190 | | <TooltipTrigger |
| 191 | | render={ |
| 192 | | <button |
| 193 | | type="button" |
| 194 | | aria-label={tooltip} |
| 195 | | aria-disabled={disabled || isActionPending || undefined} |
| 196 | | disabled={disabled || isActionPending} |
| 197 | | className="update-main relative flex h-full flex-1 items-center gap-2 px-2 enabled:cursor-pointer" |
| 198 | | onClick={handleAction} |
| 199 | | > |
| 200 | | {action === "install" ? ( |
| 201 | | <> |
| 202 | | <RotateCwIcon className="size-3.5" /> |
| 203 | | <span>Restart to update</span> |
| 204 | | </> |
| 205 | | ) : state?.status === "downloading" ? ( |
| 206 | | <> |
| 207 | | <DownloadIcon className="size-3.5" /> |
| 208 | | <span> |
| 209 | | Downloading |
| 210 | | {typeof state.downloadPercent === "number" |
| 211 | | ? ` (${Math.floor(state.downloadPercent)}%)` |
| 212 | | : "…"} |
| 213 | | </span> |
| 214 | | </> |
| 215 | | ) : ( |
| 216 | | <> |
| 217 | | <DownloadIcon className="size-3.5" /> |
| 218 | | <span>Update available</span> |
| 219 | | </> |
| 220 | | )} |
| 221 | | </button> |
| 222 | | } |
| 223 | | /> |
| 224 | | <TooltipPopup |
| 225 | | align="start" |
| 226 | | className={ |
| 227 | | state?.channel === "nightly" && state.releaseNotes.length > 0 |
| 228 | | ? // pointer-events-auto overrides the positioner's pointer-events-none so the |
| 229 | | // release notes stay open (and scrollable) when the cursor moves into them. |
| 230 | | "pointer-events-auto max-w-none text-balance" |
| 231 | | : undefined |
| 232 | | } |
| 233 | | side="top" |
| 238 | <SidebarMenuItem className="ml-auto shrink-0"> |
| 239 | <Tooltip> |
| 240 | <TooltipTrigger |
| 241 | render={ |
| 242 | <button |
| 243 | type="button" |
| 244 | aria-label={tooltip} |
| 245 | aria-disabled={disabled || isActionPending || undefined} |
| 246 | disabled={disabled || isActionPending} |
| 247 | 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 |
| 250 | ? "bg-update-surface text-update-foreground enabled:hover:bg-update/12" |
| 251 | : "text-[var(--sidebar-icon-color)] enabled:hover:bg-sidebar-row-hover enabled:hover:text-sidebar-foreground", |
| 252 | )} |
| 253 | onClick={handleAction} |
| 234 | 254 | > |
| 235 | | {state ? ( |
| 236 | | <SidebarUpdateReleaseNotesTooltip state={state} tooltip={tooltip} /> |
| 255 | {action === "install" ? ( |
| 256 | <RotateCwIcon className="size-4" /> |
| 257 | ) : isUpdateState ? ( |
| 258 | <DownloadIcon className="size-4" /> |
| 237 | 259 | ) : ( |
| 238 | | tooltip |
| 260 | <RefreshCwIcon |
| 261 | className={cn("size-4", state?.status === "checking" && "animate-spin")} |
| 262 | /> |
| 239 | 263 | )} |
| 240 | | </TooltipPopup> |
| 241 | | </Tooltip> |
| 242 | | {action === "download" && ( |
| 243 | | <Tooltip> |
| 244 | | <TooltipTrigger |
| 245 | | render={ |
| 246 | | <button |
| 247 | | type="button" |
| 248 | | aria-label="Dismiss update" |
| 249 | | className="mr-1 inline-flex size-5 items-center justify-center rounded-md text-update-foreground transition-colors" |
| 250 | | onClick={() => setDismissed(true)} |
| 251 | | > |
| 252 | | <XIcon className="size-3.5" /> |
| 253 | | </button> |
| 264 | </button> |
| 265 | } |
| 266 | /> |
| 267 | <TooltipPopup |
| 268 | align="center" |
| 269 | className={ |
| 270 | isUpdateState && state?.channel === "nightly" && state.releaseNotes.length > 0 |
| 271 | ? // pointer-events-auto overrides the positioner's pointer-events-none so the |
| 272 | // release notes stay open (and scrollable) when the cursor moves into them. |
| 273 | "pointer-events-auto max-w-none text-balance" |
| 274 | : undefined |
| 275 | } |
| 276 | side="top" |
| 277 | style={ |
| 278 | isUpdateState |
| 279 | ? { |
| 280 | background: |
| 281 | "color-mix(in srgb, var(--update) 18%, color-mix(in srgb, var(--popover) var(--glass-opacity), transparent))", |
| 282 | borderColor: "var(--update-foreground)", |
| 254 | 283 | } |
| 255 | | /> |
| 256 | | <TooltipPopup side="top">Dismiss until next launch</TooltipPopup> |
| 257 | | </Tooltip> |
| 284 | : undefined |
| 285 | } |
| 286 | variant={isUpdateState ? "glass" : "default"} |
| 287 | > |
| 288 | {isUpdateState && state ? ( |
| 289 | <SidebarUpdateReleaseNotesTooltip state={state} tooltip={tooltip} /> |
| 290 | ) : ( |
| 291 | tooltip |
| 258 | 292 | )} |
| 259 | | </div> |
| 260 | | )} |
| 261 | | </div> |
| 293 | </TooltipPopup> |
| 294 | </Tooltip> |
| 295 | </SidebarMenuItem> |
| 262 | 296 | ); |
| 263 | 297 | } |