mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(desktop): silent auto-download updates with non-blocking UI (#444)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { RouterProvider } from "@tanstack/react-router";
|
||||
import { useCallback, useLayoutEffect } from "react";
|
||||
|
||||
import { router } from "@/app/router";
|
||||
import { UpdaterProvider } from "@/features/settings/hooks/UpdaterProvider";
|
||||
import { useAppOnboardingState } from "@/features/onboarding/hooks";
|
||||
import { OnboardingFlow } from "@/features/onboarding/ui/OnboardingFlow";
|
||||
import { useWorkspaceInit } from "@/features/workspaces/useWorkspaceInit";
|
||||
@@ -92,5 +93,9 @@ export function App() {
|
||||
return <AppLoadingGate />;
|
||||
}
|
||||
|
||||
return <AppReady key={workspaceKey} />;
|
||||
return (
|
||||
<UpdaterProvider>
|
||||
<AppReady key={workspaceKey} />
|
||||
</UpdaterProvider>
|
||||
);
|
||||
}
|
||||
|
||||
+191
-196
@@ -51,7 +51,6 @@ import {
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
} from "@/shared/ui/sidebar";
|
||||
import { UpdaterProvider } from "@/features/settings/hooks/UpdaterProvider";
|
||||
import { UpdateIndicator } from "@/features/settings/UpdateIndicator";
|
||||
|
||||
type AppView = "home" | "channel" | "agents" | "workflows" | "pulse";
|
||||
@@ -453,209 +452,205 @@ export function AppShell() {
|
||||
}, [handleCloseSettings, handleOpenSettings, settingsOpen]);
|
||||
|
||||
return (
|
||||
<UpdaterProvider>
|
||||
<ChannelNavigationProvider channels={channels}>
|
||||
<AppShellProvider
|
||||
value={{
|
||||
markChannelRead,
|
||||
openChannelManagement: () => {
|
||||
setIsChannelManagementOpen(true);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<HuddleProvider>
|
||||
<div className="flex h-dvh flex-col overflow-hidden overscroll-none">
|
||||
<SidebarProvider className="min-h-0 flex-1 overflow-hidden">
|
||||
<div className="fixed left-[80px] top-[8px] z-50 flex items-center gap-1.5">
|
||||
<SidebarTrigger className="h-6 w-6 text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground" />
|
||||
<Button
|
||||
aria-label="Go back"
|
||||
className="h-6 w-6 text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground"
|
||||
data-testid="global-back"
|
||||
disabled={!canGoBack}
|
||||
onClick={goBack}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Go forward"
|
||||
className="h-6 w-6 text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground"
|
||||
data-testid="global-forward"
|
||||
disabled={!canGoForward}
|
||||
onClick={goForward}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="fixed right-[16px] top-[8px] z-50">
|
||||
<UpdateIndicator
|
||||
onOpenUpdates={() => handleOpenSettings("updates")}
|
||||
/>
|
||||
</div>
|
||||
<AppSidebar
|
||||
activeWorkspace={workspacesHook.activeWorkspace}
|
||||
channels={sidebarChannels}
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
errorMessage={
|
||||
channelsQuery.error instanceof Error
|
||||
? channelsQuery.error.message
|
||||
: undefined
|
||||
}
|
||||
fallbackDisplayName={identityQuery.data?.displayName}
|
||||
homeBadgeCount={homeBadgeCount}
|
||||
isAddWorkspaceOpen={isAddWorkspaceOpen}
|
||||
isCreatingChannel={createChannelMutation.isPending}
|
||||
isCreatingForum={createForumMutation.isPending}
|
||||
isLoading={channelsQuery.isLoading}
|
||||
isOpeningDm={openDmMutation.isPending}
|
||||
isNewDmOpen={isNewDmOpen}
|
||||
isPresencePending={presenceSession.isPending}
|
||||
onAddWorkspace={(workspace) => {
|
||||
const id = workspacesHook.addWorkspace(workspace);
|
||||
workspacesHook.switchWorkspace(id);
|
||||
}}
|
||||
onAddWorkspaceOpenChange={setIsAddWorkspaceOpen}
|
||||
onNewDmOpenChange={setIsNewDmOpen}
|
||||
onOpenAddWorkspace={() => setIsAddWorkspaceOpen(true)}
|
||||
onUpdateWorkspace={workspacesHook.updateWorkspace}
|
||||
onRemoveWorkspace={workspacesHook.removeWorkspace}
|
||||
onSwitchWorkspace={workspacesHook.switchWorkspace}
|
||||
selfPresenceStatus={presenceSession.currentStatus}
|
||||
workspaces={workspacesHook.workspaces}
|
||||
onCreateChannel={async ({
|
||||
description,
|
||||
name,
|
||||
visibility,
|
||||
ttlSeconds,
|
||||
}) => {
|
||||
const createdChannel =
|
||||
await createChannelMutation.mutateAsync({
|
||||
name,
|
||||
description,
|
||||
channelType: "stream",
|
||||
visibility,
|
||||
ttlSeconds,
|
||||
});
|
||||
|
||||
await goChannel(createdChannel.id);
|
||||
}}
|
||||
onCreateForum={async ({
|
||||
description,
|
||||
name,
|
||||
visibility,
|
||||
ttlSeconds,
|
||||
}) => {
|
||||
const createdForum = await createForumMutation.mutateAsync({
|
||||
<ChannelNavigationProvider channels={channels}>
|
||||
<AppShellProvider
|
||||
value={{
|
||||
markChannelRead,
|
||||
openChannelManagement: () => {
|
||||
setIsChannelManagementOpen(true);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<HuddleProvider>
|
||||
<div className="flex h-dvh flex-col overflow-hidden overscroll-none">
|
||||
<SidebarProvider className="min-h-0 flex-1 overflow-hidden">
|
||||
<div className="fixed left-[80px] top-[8px] z-50 flex items-center gap-1.5">
|
||||
<SidebarTrigger className="h-6 w-6 text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground" />
|
||||
<Button
|
||||
aria-label="Go back"
|
||||
className="h-6 w-6 text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground"
|
||||
data-testid="global-back"
|
||||
disabled={!canGoBack}
|
||||
onClick={goBack}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Go forward"
|
||||
className="h-6 w-6 text-muted-foreground/70 hover:bg-muted/60 hover:text-foreground"
|
||||
data-testid="global-forward"
|
||||
disabled={!canGoForward}
|
||||
onClick={goForward}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="fixed right-[16px] top-[8px] z-50">
|
||||
<UpdateIndicator
|
||||
onOpenUpdates={() => handleOpenSettings("updates")}
|
||||
/>
|
||||
</div>
|
||||
<AppSidebar
|
||||
activeWorkspace={workspacesHook.activeWorkspace}
|
||||
channels={sidebarChannels}
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
errorMessage={
|
||||
channelsQuery.error instanceof Error
|
||||
? channelsQuery.error.message
|
||||
: undefined
|
||||
}
|
||||
fallbackDisplayName={identityQuery.data?.displayName}
|
||||
homeBadgeCount={homeBadgeCount}
|
||||
isAddWorkspaceOpen={isAddWorkspaceOpen}
|
||||
isCreatingChannel={createChannelMutation.isPending}
|
||||
isCreatingForum={createForumMutation.isPending}
|
||||
isLoading={channelsQuery.isLoading}
|
||||
isOpeningDm={openDmMutation.isPending}
|
||||
isNewDmOpen={isNewDmOpen}
|
||||
isPresencePending={presenceSession.isPending}
|
||||
onAddWorkspace={(workspace) => {
|
||||
const id = workspacesHook.addWorkspace(workspace);
|
||||
workspacesHook.switchWorkspace(id);
|
||||
}}
|
||||
onAddWorkspaceOpenChange={setIsAddWorkspaceOpen}
|
||||
onNewDmOpenChange={setIsNewDmOpen}
|
||||
onOpenAddWorkspace={() => setIsAddWorkspaceOpen(true)}
|
||||
onUpdateWorkspace={workspacesHook.updateWorkspace}
|
||||
onRemoveWorkspace={workspacesHook.removeWorkspace}
|
||||
onSwitchWorkspace={workspacesHook.switchWorkspace}
|
||||
selfPresenceStatus={presenceSession.currentStatus}
|
||||
workspaces={workspacesHook.workspaces}
|
||||
onCreateChannel={async ({
|
||||
description,
|
||||
name,
|
||||
visibility,
|
||||
ttlSeconds,
|
||||
}) => {
|
||||
const createdChannel =
|
||||
await createChannelMutation.mutateAsync({
|
||||
name,
|
||||
description,
|
||||
channelType: "forum",
|
||||
channelType: "stream",
|
||||
visibility,
|
||||
ttlSeconds,
|
||||
});
|
||||
|
||||
await goChannel(createdForum.id);
|
||||
}}
|
||||
onHideDm={handleHideDm}
|
||||
onOpenBrowseChannels={handleOpenBrowseChannels}
|
||||
onOpenBrowseForums={handleOpenBrowseForums}
|
||||
onOpenDm={async ({ pubkeys }) => {
|
||||
const directMessage = await openDmMutation.mutateAsync({
|
||||
pubkeys,
|
||||
});
|
||||
await goChannel(directMessage.id);
|
||||
}}
|
||||
onOpenSearch={handleOpenSearch}
|
||||
onSelectAgents={() => {
|
||||
void goAgents();
|
||||
}}
|
||||
onSelectChannel={(channelId) => {
|
||||
void goChannel(channelId);
|
||||
}}
|
||||
onSelectHome={() => {
|
||||
void goHome();
|
||||
}}
|
||||
onSelectPulse={() => {
|
||||
void goPulse();
|
||||
}}
|
||||
onSelectSettings={handleOpenSettings}
|
||||
onSelectWorkflows={() => {
|
||||
void goWorkflows();
|
||||
}}
|
||||
onSetPresenceStatus={(status) =>
|
||||
presenceSession.setStatus(status)
|
||||
}
|
||||
profile={profileQuery.data}
|
||||
selectedChannelId={selectedChannelId}
|
||||
selectedView={selectedView}
|
||||
unreadChannelIds={unreadChannelIds}
|
||||
/>
|
||||
await goChannel(createdChannel.id);
|
||||
}}
|
||||
onCreateForum={async ({
|
||||
description,
|
||||
name,
|
||||
visibility,
|
||||
ttlSeconds,
|
||||
}) => {
|
||||
const createdForum = await createForumMutation.mutateAsync({
|
||||
name,
|
||||
description,
|
||||
channelType: "forum",
|
||||
visibility,
|
||||
ttlSeconds,
|
||||
});
|
||||
|
||||
<SidebarInset className="min-h-0 min-w-0 overflow-hidden">
|
||||
<Outlet />
|
||||
</SidebarInset>
|
||||
await goChannel(createdForum.id);
|
||||
}}
|
||||
onHideDm={handleHideDm}
|
||||
onOpenBrowseChannels={handleOpenBrowseChannels}
|
||||
onOpenBrowseForums={handleOpenBrowseForums}
|
||||
onOpenDm={async ({ pubkeys }) => {
|
||||
const directMessage = await openDmMutation.mutateAsync({
|
||||
pubkeys,
|
||||
});
|
||||
await goChannel(directMessage.id);
|
||||
}}
|
||||
onOpenSearch={handleOpenSearch}
|
||||
onSelectAgents={() => {
|
||||
void goAgents();
|
||||
}}
|
||||
onSelectChannel={(channelId) => {
|
||||
void goChannel(channelId);
|
||||
}}
|
||||
onSelectHome={() => {
|
||||
void goHome();
|
||||
}}
|
||||
onSelectPulse={() => {
|
||||
void goPulse();
|
||||
}}
|
||||
onSelectSettings={handleOpenSettings}
|
||||
onSelectWorkflows={() => {
|
||||
void goWorkflows();
|
||||
}}
|
||||
onSetPresenceStatus={(status) =>
|
||||
presenceSession.setStatus(status)
|
||||
}
|
||||
profile={profileQuery.data}
|
||||
selectedChannelId={selectedChannelId}
|
||||
selectedView={selectedView}
|
||||
unreadChannelIds={unreadChannelIds}
|
||||
/>
|
||||
|
||||
<AppShellOverlays
|
||||
activeChannel={activeChannel}
|
||||
browseDialogType={browseDialogType}
|
||||
channels={channels}
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
isChannelManagementOpen={isChannelManagementOpen}
|
||||
isSearchOpen={isSearchOpen}
|
||||
onBrowseChannelJoin={handleBrowseChannelJoin}
|
||||
onBrowseDialogOpenChange={handleBrowseDialogOpenChange}
|
||||
onChannelManagementOpenChange={setIsChannelManagementOpen}
|
||||
onDeleteActiveChannel={() => {
|
||||
setIsChannelManagementOpen(false);
|
||||
void goHome({ replace: true });
|
||||
}}
|
||||
onOpenSearchResult={handleOpenSearchResult}
|
||||
onSearchOpenChange={setIsSearchOpen}
|
||||
onSelectChannel={(channelId) => {
|
||||
void goChannel(channelId);
|
||||
}}
|
||||
/>
|
||||
<SidebarInset className="min-h-0 min-w-0 overflow-hidden">
|
||||
<Outlet />
|
||||
</SidebarInset>
|
||||
|
||||
{settingsOpen ? (
|
||||
<React.Suspense fallback={null}>
|
||||
<LazySettingsScreen
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
fallbackDisplayName={identityQuery.data?.displayName}
|
||||
isUpdatingDesktopNotifications={
|
||||
notificationSettings.isUpdatingDesktopEnabled
|
||||
}
|
||||
notificationErrorMessage={
|
||||
notificationSettings.errorMessage
|
||||
}
|
||||
notificationPermission={notificationSettings.permission}
|
||||
notificationSettings={notificationSettings.settings}
|
||||
onClose={handleCloseSettings}
|
||||
onSectionChange={setSettingsSection}
|
||||
onSetDesktopNotificationsEnabled={
|
||||
notificationSettings.setDesktopEnabled
|
||||
}
|
||||
onSetHomeBadgeEnabled={
|
||||
notificationSettings.setHomeBadgeEnabled
|
||||
}
|
||||
onSetMentionNotificationsEnabled={
|
||||
notificationSettings.setMentionsEnabled
|
||||
}
|
||||
onSetNeedsActionNotificationsEnabled={
|
||||
notificationSettings.setNeedsActionEnabled
|
||||
}
|
||||
section={settingsSection}
|
||||
/>
|
||||
</React.Suspense>
|
||||
) : null}
|
||||
</SidebarProvider>
|
||||
<HuddleBar />
|
||||
</div>
|
||||
</HuddleProvider>
|
||||
</AppShellProvider>
|
||||
</ChannelNavigationProvider>
|
||||
</UpdaterProvider>
|
||||
<AppShellOverlays
|
||||
activeChannel={activeChannel}
|
||||
browseDialogType={browseDialogType}
|
||||
channels={channels}
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
isChannelManagementOpen={isChannelManagementOpen}
|
||||
isSearchOpen={isSearchOpen}
|
||||
onBrowseChannelJoin={handleBrowseChannelJoin}
|
||||
onBrowseDialogOpenChange={handleBrowseDialogOpenChange}
|
||||
onChannelManagementOpenChange={setIsChannelManagementOpen}
|
||||
onDeleteActiveChannel={() => {
|
||||
setIsChannelManagementOpen(false);
|
||||
void goHome({ replace: true });
|
||||
}}
|
||||
onOpenSearchResult={handleOpenSearchResult}
|
||||
onSearchOpenChange={setIsSearchOpen}
|
||||
onSelectChannel={(channelId) => {
|
||||
void goChannel(channelId);
|
||||
}}
|
||||
/>
|
||||
|
||||
{settingsOpen ? (
|
||||
<React.Suspense fallback={null}>
|
||||
<LazySettingsScreen
|
||||
currentPubkey={identityQuery.data?.pubkey}
|
||||
fallbackDisplayName={identityQuery.data?.displayName}
|
||||
isUpdatingDesktopNotifications={
|
||||
notificationSettings.isUpdatingDesktopEnabled
|
||||
}
|
||||
notificationErrorMessage={notificationSettings.errorMessage}
|
||||
notificationPermission={notificationSettings.permission}
|
||||
notificationSettings={notificationSettings.settings}
|
||||
onClose={handleCloseSettings}
|
||||
onSectionChange={setSettingsSection}
|
||||
onSetDesktopNotificationsEnabled={
|
||||
notificationSettings.setDesktopEnabled
|
||||
}
|
||||
onSetHomeBadgeEnabled={
|
||||
notificationSettings.setHomeBadgeEnabled
|
||||
}
|
||||
onSetMentionNotificationsEnabled={
|
||||
notificationSettings.setMentionsEnabled
|
||||
}
|
||||
onSetNeedsActionNotificationsEnabled={
|
||||
notificationSettings.setNeedsActionEnabled
|
||||
}
|
||||
section={settingsSection}
|
||||
/>
|
||||
</React.Suspense>
|
||||
) : null}
|
||||
</SidebarProvider>
|
||||
<HuddleBar />
|
||||
</div>
|
||||
</HuddleProvider>
|
||||
</AppShellProvider>
|
||||
</ChannelNavigationProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { useUpdaterContext } from "./hooks/UpdaterProvider";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
|
||||
export function UpdateChecker() {
|
||||
const { status, checkForUpdate, downloadAndInstall, relaunch } =
|
||||
useUpdaterContext();
|
||||
const { status, checkForUpdate, relaunch } = useUpdaterContext();
|
||||
|
||||
return (
|
||||
<section className="min-w-0">
|
||||
@@ -44,14 +42,7 @@ export function UpdateChecker() {
|
||||
)}
|
||||
|
||||
{status.state === "available" && (
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-foreground">
|
||||
Version <Badge variant="info">{status.version}</Badge> is available.
|
||||
</p>
|
||||
<Button size="sm" onClick={downloadAndInstall}>
|
||||
Download & Install
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Preparing update...</p>
|
||||
)}
|
||||
|
||||
{status.state === "downloading" && (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Download, RefreshCw } from "lucide-react";
|
||||
import { Download, Loader2, RefreshCw } from "lucide-react";
|
||||
|
||||
import { Button } from "@/shared/ui/button";
|
||||
|
||||
@@ -11,23 +11,47 @@ const indicatorButtonClass =
|
||||
const iconClass = "h-3.5 w-3.5";
|
||||
|
||||
const variants: Record<
|
||||
"available" | "ready",
|
||||
{ Icon: typeof Download; label: string; badgeColor: string }
|
||||
"available" | "downloading" | "installing" | "ready",
|
||||
{
|
||||
Icon: typeof Download;
|
||||
label: string;
|
||||
badgeColor: string;
|
||||
iconClass: string;
|
||||
}
|
||||
> = {
|
||||
available: {
|
||||
Icon: Download,
|
||||
label: "Update available",
|
||||
badgeColor: "bg-primary",
|
||||
iconClass: iconClass,
|
||||
},
|
||||
downloading: {
|
||||
Icon: Loader2,
|
||||
label: "Downloading update\u2026",
|
||||
badgeColor: "bg-primary",
|
||||
iconClass: `${iconClass} animate-spin`,
|
||||
},
|
||||
installing: {
|
||||
Icon: Loader2,
|
||||
label: "Installing update\u2026",
|
||||
badgeColor: "bg-primary",
|
||||
iconClass: `${iconClass} animate-spin`,
|
||||
},
|
||||
ready: {
|
||||
Icon: RefreshCw,
|
||||
label: "Restart to update",
|
||||
badgeColor: "bg-emerald-500",
|
||||
iconClass: iconClass,
|
||||
},
|
||||
};
|
||||
|
||||
function getVariant(state: UpdateStatus["state"]) {
|
||||
if (state === "available" || state === "ready") {
|
||||
if (
|
||||
state === "available" ||
|
||||
state === "downloading" ||
|
||||
state === "installing" ||
|
||||
state === "ready"
|
||||
) {
|
||||
return variants[state];
|
||||
}
|
||||
return null;
|
||||
@@ -45,7 +69,7 @@ export function UpdateIndicator({
|
||||
return null;
|
||||
}
|
||||
|
||||
const { Icon, label, badgeColor } = variant;
|
||||
const { Icon, label, badgeColor, iconClass: variantIconClass } = variant;
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -55,7 +79,7 @@ export function UpdateIndicator({
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
>
|
||||
<Icon className={iconClass} />
|
||||
<Icon className={variantIconClass} />
|
||||
{label}
|
||||
<span
|
||||
className={`absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full ${badgeColor} animate-pulse`}
|
||||
|
||||
@@ -13,7 +13,6 @@ export type UpdateStatus =
|
||||
| { state: "ready" }
|
||||
| { state: "error"; message: string };
|
||||
|
||||
const TOAST_ID = "update-available";
|
||||
const BACKGROUND_UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
||||
const BACKGROUND_BLOCKED_STATES = new Set<UpdateStatus["state"]>([
|
||||
"checking",
|
||||
@@ -43,6 +42,7 @@ export function useUpdater() {
|
||||
const statusRef = useRef<UpdateStatus>({ state: "idle" });
|
||||
const updateRef = useRef<Update | null>(null);
|
||||
const checkInFlightRef = useRef(false);
|
||||
const downloadInFlightRef = useRef(false);
|
||||
const manualResultRequestedRef = useRef(false);
|
||||
|
||||
const setStatus = useCallback((nextStatus: UpdateStatus) => {
|
||||
@@ -51,6 +51,9 @@ export function useUpdater() {
|
||||
}, []);
|
||||
|
||||
const closeUpdate = useCallback(async () => {
|
||||
if (downloadInFlightRef.current) {
|
||||
return;
|
||||
}
|
||||
const current = updateRef.current;
|
||||
if (current) {
|
||||
updateRef.current = null;
|
||||
@@ -59,13 +62,17 @@ export function useUpdater() {
|
||||
}, []);
|
||||
|
||||
const downloadAndInstall = useCallback(async () => {
|
||||
if (downloadInFlightRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
downloadInFlightRef.current = true;
|
||||
try {
|
||||
const update = updateRef.current;
|
||||
if (!update) {
|
||||
return;
|
||||
}
|
||||
|
||||
toast.dismiss(TOAST_ID);
|
||||
setStatus({ state: "downloading" });
|
||||
|
||||
await update.downloadAndInstall((event) => {
|
||||
@@ -76,8 +83,14 @@ export function useUpdater() {
|
||||
|
||||
updateRef.current = null;
|
||||
setStatus({ state: "ready" });
|
||||
toast("Update ready", {
|
||||
description: "Restart when you're ready to apply the update.",
|
||||
duration: 8000,
|
||||
});
|
||||
} catch (err) {
|
||||
setStatus({ state: "error", message: toErrorMessage(err) });
|
||||
} finally {
|
||||
downloadInFlightRef.current = false;
|
||||
}
|
||||
}, [setStatus]);
|
||||
|
||||
@@ -112,15 +125,8 @@ export function useUpdater() {
|
||||
if (update) {
|
||||
updateRef.current = update;
|
||||
setStatus({ state: "available", version: update.version });
|
||||
toast("Update Available", {
|
||||
id: TOAST_ID,
|
||||
description: `Version ${update.version} is ready to download.`,
|
||||
duration: Infinity,
|
||||
action: {
|
||||
label: "Download & install",
|
||||
onClick: () => downloadAndInstall(),
|
||||
},
|
||||
});
|
||||
// Start download automatically — user sees "restart" when done
|
||||
void downloadAndInstall();
|
||||
} else if (shouldShowQuietResult) {
|
||||
setStatus({ state: "up-to-date" });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user