Files
buzz/desktop/src/features/settings/UpdateIndicator.tsx
Will Pfleger deed64a14f fix(desktop): require user action before applying updates (#1820)
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
2026-07-13 16:13:20 -04:00

118 lines
3.0 KiB
TypeScript

import { openUrl } from "@tauri-apps/plugin-opener";
import type { ComponentType } from "react";
import { ExternalLink, RefreshCcw, RotateCw } from "lucide-react";
import { Button } from "@/shared/ui/button";
import { Spinner } from "@/shared/ui/spinner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
import { useUpdaterContext } from "./hooks/UpdaterProvider";
import type { UpdateStatus } from "./hooks/use-updater";
const indicatorButtonClass =
"relative text-muted-foreground/80 hover:bg-muted/60 hover:text-foreground";
type IndicatorIcon = ComponentType<{
"aria-hidden"?: boolean;
className?: string;
}>;
const variants: Record<
"available" | "downloading" | "installing" | "manual-required" | "ready",
{
Icon: IndicatorIcon;
iconClassName?: string;
label: string;
badgeColor: string;
}
> = {
available: {
Icon: RefreshCcw,
label: "Update available",
badgeColor: "bg-primary",
},
downloading: {
Icon: Spinner,
iconClassName: "h-4 w-4 border-2",
label: "Downloading update\u2026",
badgeColor: "bg-primary",
},
installing: {
Icon: Spinner,
iconClassName: "h-4 w-4 border-2",
label: "Installing update\u2026",
badgeColor: "bg-primary",
},
"manual-required": {
Icon: ExternalLink,
label:
"Update available — download from GitHub (use AppImage for auto-updates)",
badgeColor: "bg-primary",
},
ready: {
Icon: RotateCw,
label: "Update now",
badgeColor: "bg-emerald-500",
},
};
function getVariant(state: UpdateStatus["state"]) {
if (
state === "available" ||
state === "downloading" ||
state === "installing" ||
state === "manual-required" ||
state === "ready"
) {
return variants[state];
}
return null;
}
export function UpdateIndicator({ className }: { className?: string }) {
const { status, installAndRelaunch } = useUpdaterContext();
const variant = getVariant(status.state);
if (!variant) {
return null;
}
const { Icon, iconClassName = "h-4 w-4", label, badgeColor } = variant;
const isActionable =
status.state === "ready" || status.state === "manual-required";
const handleClick =
status.state === "ready"
? installAndRelaunch
: status.state === "manual-required"
? () => {
void openUrl(status.releaseUrl);
}
: null;
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
aria-label={label}
className={`${indicatorButtonClass} ${className ?? ""}`}
disabled={!isActionable}
onClick={() => {
if (handleClick) {
void handleClick();
}
}}
size="icon"
type="button"
variant="ghost"
>
<Icon aria-hidden className={iconClassName} />
<span
className={`absolute right-1 top-1 h-1.5 w-1.5 rounded-full ${badgeColor} animate-pulse`}
/>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">{label}</TooltipContent>
</Tooltip>
);
}