-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathupdate-notification.tsx
More file actions
110 lines (94 loc) · 3.9 KB
/
Copy pathupdate-notification.tsx
File metadata and controls
110 lines (94 loc) · 3.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import { useCallback, useEffect, useRef } from 'react';
import { toast } from 'sonner';
import { UpdateStatus, useAutoUpdater } from '@/hooks/use-auto-updater';
import { useSaveHistoryPrompt } from '@/hooks/use-save-history-guard';
import { getElectron } from '@/lib/utils';
export function UpdateNotification() {
const { updateStatus, quitAndInstall } = useAutoUpdater();
const lastStatusRef = useRef<UpdateStatus | null>(null);
const downloadToastIdRef = useRef<string | number | null>(null);
/**
* Installing takes the app down, so it loses the interview exactly as closing does - and
* unlike a close it cannot be vetoed once started, because the installer is launched before
* the quit is requested. So the question is asked here, in front of it.
*
* `hasHistory` is read from main on the click rather than through `useSaveHistoryGuard`.
* That hook subscribes to the app state, which during an interview is a new object several
* times a second, and this component would then re-render - and re-arm its status effect -
* on every ASR partial to answer a question it only asks when a button is pressed. One round
* trip on the click also reads the flag where it is derived rather than a broadcast behind.
*/
const confirmThenInstall = useCallback(async () => {
const electron = getElectron();
const state = await electron?.appState.get();
if (state?.hasHistory) {
const proceed = await useSaveHistoryPrompt.getState().prompt('update');
if (!proceed) return;
}
await quitAndInstall();
}, [quitAndInstall]);
useEffect(() => {
if (!updateStatus) return;
const { status, info, progress, error } = updateStatus;
if (lastStatusRef.current === status) {
if (status === UpdateStatus.Downloading && downloadToastIdRef.current && progress) {
toast.loading(`Downloading update... ${progress.percent.toFixed(0)}%`, {
id: downloadToastIdRef.current,
description: `${(progress.transferred / 1024 / 1024).toFixed(1)} MB / ${(progress.total / 1024 / 1024).toFixed(1)} MB`,
});
}
return;
}
lastStatusRef.current = status;
switch (status) {
case UpdateStatus.Checking:
case UpdateStatus.NotAvailable:
break;
case UpdateStatus.Available:
if (info) {
toast.info(`Update Available: v${info.version}`, {
description: 'Download will start automatically in the background.',
duration: 5000,
});
}
break;
case UpdateStatus.Downloading:
if (progress) {
downloadToastIdRef.current = toast.loading(
`Downloading update... ${progress.percent.toFixed(0)}%`,
{
description: `${(progress.transferred / 1024 / 1024).toFixed(1)} MB / ${(progress.total / 1024 / 1024).toFixed(1)} MB`,
}
);
}
break;
case UpdateStatus.Downloaded:
if (downloadToastIdRef.current) {
toast.dismiss(downloadToastIdRef.current);
downloadToastIdRef.current = null;
}
if (info) {
const isMac = window.electronAPI?.platform === 'darwin';
toast.success(`Update Downloaded: v${info.version}`, {
description: isMac
? 'Click to open the installer, then drag it into Applications.'
: 'Click to restart and install the update.',
duration: Infinity,
action: {
label: isMac ? 'Open Installer' : 'Restart Now',
onClick: () => void confirmThenInstall(),
},
});
}
break;
case UpdateStatus.Error:
if (downloadToastIdRef.current) {
toast.dismiss(downloadToastIdRef.current);
downloadToastIdRef.current = null;
}
console.error('[UpdateNotification] Update error:', error);
break;
}
}, [updateStatus, confirmThenInstall]);
return null;
}