Files
buzz/desktop/src/features/settings/SidebarUpdateCard.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

120 lines
3.6 KiB
TypeScript

import * as React from "react";
import { openUrl } from "@tauri-apps/plugin-opener";
import { CircleArrowUp, ExternalLink } from "lucide-react";
import { useUpdaterContext } from "./hooks/UpdaterProvider";
import { shouldShowSidebarUpdateCard } from "./sidebarUpdateCardVisibility";
import { SidebarCompactActionCard } from "@/shared/ui/sidebar-action-card";
import { Spinner } from "@/shared/ui/spinner";
type SidebarUpdateCardProps = {
onDismiss: () => void;
};
type SidebarUpdateCompactCardProps = SidebarUpdateCardProps & {
actionTestId?: string;
testId?: string;
};
export function SidebarUpdateCompactCard({
actionTestId,
onDismiss,
testId = "sidebar-update-card-compact",
}: SidebarUpdateCompactCardProps) {
const { installAndRelaunch } = useUpdaterContext();
const [isUpdatePending, setIsUpdatePending] = React.useState(false);
const updatePendingRef = React.useRef(false);
const updateFrameRef = React.useRef<number | null>(null);
const updateTimeoutRef = React.useRef<number | null>(null);
React.useEffect(() => {
return () => {
if (updateFrameRef.current !== null) {
window.cancelAnimationFrame(updateFrameRef.current);
}
if (updateTimeoutRef.current !== null) {
window.clearTimeout(updateTimeoutRef.current);
}
updatePendingRef.current = false;
};
}, []);
const handleUpdate = React.useCallback(() => {
if (updatePendingRef.current) {
return;
}
updatePendingRef.current = true;
setIsUpdatePending(true);
updateFrameRef.current = window.requestAnimationFrame(() => {
updateFrameRef.current = null;
updateTimeoutRef.current = window.setTimeout(() => {
updateTimeoutRef.current = null;
void installAndRelaunch()
.catch((error) => {
console.error("[SidebarUpdateCard] update failed:", error);
})
.finally(() => {
updatePendingRef.current = false;
setIsUpdatePending(false);
});
}, 0);
});
}, [installAndRelaunch]);
return (
<SidebarCompactActionCard
actionAriaLabel="Update now"
actionDisabled={isUpdatePending}
actionTestId={actionTestId}
description={isUpdatePending ? "Updating" : "Click to update"}
dismissLabel="Dismiss update notification"
icon={
isUpdatePending ? (
<Spinner aria-hidden="true" className="h-5 w-5 border-2" />
) : (
<CircleArrowUp aria-hidden="true" className="h-5 w-5" />
)
}
iconKey={isUpdatePending ? "pending" : "idle"}
onAction={handleUpdate}
onDismiss={onDismiss}
testId={testId}
title="Ready to update!"
/>
);
}
export function SidebarUpdateCard({ onDismiss }: SidebarUpdateCardProps) {
const { status } = useUpdaterContext();
if (!shouldShowSidebarUpdateCard(status)) {
return null;
}
if (status.state === "manual-required") {
return (
<SidebarCompactActionCard
actionAriaLabel="Download update from GitHub"
actionTestId="sidebar-update-download-github"
description={`v${status.version} available — download from GitHub. Switch to AppImage for automatic updates.`}
dismissLabel="Dismiss update notification"
icon={<ExternalLink aria-hidden="true" className="h-5 w-5" />}
iconKey="manual"
onAction={() => void openUrl(status.releaseUrl)}
onDismiss={onDismiss}
testId="sidebar-update-card-manual"
title="Update available"
/>
);
}
return (
<SidebarUpdateCompactCard
actionTestId="sidebar-update-now"
onDismiss={onDismiss}
testId="sidebar-update-card"
/>
);
}