230 lines · 8.3 KB · TypeScript
RawDownload
import { DownloadIcon, RotateCwIcon, TriangleAlertIcon, XIcon } from "lucide-react";import { useCallback, useState } from "react";import { isElectron } from "../../env";import { useDesktopUpdateState } from "../../state/desktopUpdate";import { stackedThreadToast, toastManager } from "../ui/toast";import {  getArm64IntelBuildWarningDescription,  getDesktopUpdateActionError,  getDesktopUpdateButtonTooltip,  getDesktopUpdateInstallConfirmationMessage,  isDesktopUpdateButtonDisabled,  resolveDesktopUpdateButtonAction,  shouldShowArm64IntelBuildWarning,  shouldShowDesktopUpdateButton,  shouldToastDesktopUpdateActionResult,} from "../desktopUpdate.logic";import { showDesktopUpdateDownloadedToast } from "../desktopUpdate.toast";import { Alert, AlertDescription, AlertTitle } from "../ui/alert";import { Separator } from "../ui/separator";import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; function SidebarUpdateReleaseNotesTooltip({  state,  tooltip,}: {  readonly state: NonNullable<ReturnType<typeof useDesktopUpdateState>>;  readonly tooltip: string;}) {  if (state.channel !== "nightly" || state.releaseNotes.length === 0) {    return <>{tooltip}</>;  }   return (    <div className="w-120 max-w-[calc(100vw-2rem)] text-left">      <div className="px-1">        <div className="text-sm leading-5 font-medium">{tooltip}</div>      </div>      <div className="pointer-events-auto max-h-[min(28rem,calc(100vh-6rem))] overflow-y-auto px-1 pt-4 pb-1">        {state.releaseNotes.map((releaseNote, index) => (          <div key={releaseNote.version}>            {index > 0 && <Separator className="my-3 bg-border/60" />}            <section>              <h3 className="text-muted-foreground text-xs leading-4 font-semibold">                {index === 0 ? "What's changed" : `Changes in ${releaseNote.version}`}              </h3>              <ul className="mt-2 space-y-1.5 pl-4 text-xs leading-5 text-popover-foreground/90">                {releaseNote.items.map((item, itemIndex) => (                  <li className="list-disc break-words" key={`${releaseNote.version}-${itemIndex}`}>                    {item}                  </li>                ))}              </ul>            </section>          </div>        ))}      </div>    </div>  );} export function SidebarUpdatePill() {  const state = useDesktopUpdateState();  const [dismissed, setDismissed] = useState(false);   const visible = isElectron && shouldShowDesktopUpdateButton(state) && !dismissed;  const tooltip = state ? getDesktopUpdateButtonTooltip(state) : "Update available";  const disabled = isDesktopUpdateButtonDisabled(state);  const action = state ? resolveDesktopUpdateButtonAction(state) : "none";   const showArm64Warning = isElectron && shouldShowArm64IntelBuildWarning(state);  const arm64Description =    state && showArm64Warning ? getArm64IntelBuildWarningDescription(state) : null;   const handleAction = useCallback(() => {    const bridge = window.desktopBridge;    if (!bridge || !state) return;    if (disabled || action === "none") return;     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.",            }),          );        });      return;    }     if (action === "install") {      const confirmed = window.confirm(        getDesktopUpdateInstallConfirmationMessage(state, navigator.platform),      );      if (!confirmed) 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.",            }),          );        });    }  }, [action, disabled, state]);   if (!visible && !showArm64Warning) return null;   return (    <div className="flex flex-col gap-1">      {showArm64Warning && arm64Description && (        <Alert variant="warning" className="rounded-2xl border-warning/40 bg-warning/8 text-xs">          <TriangleAlertIcon />          <AlertTitle>Intel build on Apple Silicon</AlertTitle>          <AlertDescription>{arm64Description}</AlertDescription>        </Alert>      )}      {visible && (        <div          className={`group/update relative flex h-7 w-full items-center rounded-lg bg-primary/15 text-xs font-medium text-primary ${            disabled ? " cursor-not-allowed opacity-60" : ""          }`}        >          <div className="pointer-events-none absolute inset-0 rounded-lg transition-colors group-has-[button.update-main:hover]/update:bg-primary/22" />          <Tooltip>            <TooltipTrigger              render={                <button                  type="button"                  aria-label={tooltip}                  aria-disabled={disabled || undefined}                  disabled={disabled}                  className="update-main relative flex h-full flex-1 items-center gap-2 px-2 enabled:cursor-pointer"                  onClick={handleAction}                >                  {action === "install" ? (                    <>                      <RotateCwIcon className="size-3.5" />                      <span>Restart to update</span>                    </>                  ) : state?.status === "downloading" ? (                    <>                      <DownloadIcon className="size-3.5" />                      <span>                        Downloading                        {typeof state.downloadPercent === "number"                          ? ` (${Math.floor(state.downloadPercent)}%)`                          : "…"}                      </span>                    </>                  ) : (                    <>                      <DownloadIcon className="size-3.5" />                      <span>Update available</span>                    </>                  )}                </button>              }            />            <TooltipPopup              align="start"              className={                state?.channel === "nightly" && state.releaseNotes.length > 0                  ? "max-w-none text-balance"                  : undefined              }              side="top"            >              {state ? (                <SidebarUpdateReleaseNotesTooltip state={state} tooltip={tooltip} />              ) : (                tooltip              )}            </TooltipPopup>          </Tooltip>          {action === "download" && (            <Tooltip>              <TooltipTrigger                render={                  <button                    type="button"                    aria-label="Dismiss update"                    className="mr-1 inline-flex size-5 items-center justify-center rounded-md text-primary/60 transition-colors hover:text-primary"                    onClick={() => setDismissed(true)}                  >                    <XIcon className="size-3.5" />                  </button>                }              />              <TooltipPopup side="top">Dismiss until next launch</TooltipPopup>            </Tooltip>          )}        </div>      )}    </div>  );}