mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
fix(electron): defer update dialogs until app is active
This commit is contained in:
1 parent
22938e4922
commit
e482a675a0
2 files changed
+123
-63
No files matched your search
@@ -32,6 +32,29 @@ type DialogCall = {
|
||||
message?: string
|
||||
detail?: string
|
||||
buttons?: string[]
|
||||
parent?: unknown
|
||||
}
|
||||
|
||||
class FakeUpdateWindow extends EventEmitter {
|
||||
visible = true
|
||||
focused = true
|
||||
destroyed = false
|
||||
|
||||
constructor(readonly webContents: { send: (...args: unknown[]) => void }) {
|
||||
super()
|
||||
}
|
||||
|
||||
isVisible(): boolean {
|
||||
return this.visible
|
||||
}
|
||||
|
||||
isFocused(): boolean {
|
||||
return this.focused
|
||||
}
|
||||
|
||||
isDestroyed(): boolean {
|
||||
return this.destroyed
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUpdaterModule() {
|
||||
@@ -45,8 +68,12 @@ async function loadUpdaterModule() {
|
||||
isPackaged: true,
|
||||
},
|
||||
dialog: {
|
||||
async showMessageBox(options: DialogCall) {
|
||||
dialogCalls.push(options)
|
||||
async showMessageBox(parentOrOptions: unknown, maybeOptions?: DialogCall) {
|
||||
const options = maybeOptions ?? (parentOrOptions as DialogCall)
|
||||
dialogCalls.push({
|
||||
...options,
|
||||
parent: maybeOptions ? parentOrOptions : undefined,
|
||||
})
|
||||
return { response: 1 }
|
||||
},
|
||||
},
|
||||
@@ -90,20 +117,20 @@ async function loadUpdaterModule() {
|
||||
return {
|
||||
autoUpdater,
|
||||
dialogCalls,
|
||||
checkUpdate: mod.checkUpdate as (win: { webContents: { send: (...args: unknown[]) => void } }) => void,
|
||||
checkUpdate: mod.checkUpdate as unknown as (win: FakeUpdateWindow) => void,
|
||||
manualCheckForUpdates: mod.manualCheckForUpdates as () => void,
|
||||
}
|
||||
}
|
||||
|
||||
test('automatic stable updates download silently before showing install prompt', async () => {
|
||||
test('automatic stable updates defer the install prompt until the app window is active', async () => {
|
||||
const { autoUpdater, dialogCalls, checkUpdate, manualCheckForUpdates } = await loadUpdaterModule()
|
||||
const sent: unknown[][] = []
|
||||
|
||||
checkUpdate({
|
||||
webContents: {
|
||||
send: (...args: unknown[]) => sent.push(args),
|
||||
},
|
||||
const win = new FakeUpdateWindow({
|
||||
send: (...args: unknown[]) => sent.push(args),
|
||||
})
|
||||
win.focused = false
|
||||
|
||||
checkUpdate(win)
|
||||
|
||||
autoUpdater.emit('update-available', { version: '0.28.2' })
|
||||
await Promise.resolve()
|
||||
@@ -116,16 +143,24 @@ test('automatic stable updates download silently before showing install prompt',
|
||||
autoUpdater.emit('update-downloaded', { version: '0.28.2' })
|
||||
await Promise.resolve()
|
||||
|
||||
assert.equal(dialogCalls.length, 0)
|
||||
|
||||
win.focused = true
|
||||
win.emit('focus')
|
||||
await new Promise<void>((resolve) => setImmediate(resolve))
|
||||
|
||||
assert.equal(dialogCalls.length, 1)
|
||||
assert.equal(dialogCalls[0]?.title, 'update.downloadComplete')
|
||||
assert.equal(dialogCalls[0]?.message, 'update.readyToInstall')
|
||||
assert.equal(dialogCalls[0]?.parent, win)
|
||||
|
||||
manualCheckForUpdates()
|
||||
autoUpdater.emit('update-available', { version: '0.28.3' })
|
||||
await Promise.resolve()
|
||||
await new Promise<void>((resolve) => setImmediate(resolve))
|
||||
|
||||
assert.equal(dialogCalls.length, 2)
|
||||
assert.equal(dialogCalls[1]?.title, 'update.newVersionTitle:0.28.3')
|
||||
assert.equal(dialogCalls[1]?.message, 'update.newVersionMessage:0.28.3')
|
||||
assert.equal(dialogCalls[1]?.parent, win)
|
||||
assert.equal(autoUpdater.downloadCalls, 1)
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { dialog, app } from 'electron'
|
||||
import { dialog, app, type BrowserWindow } from 'electron'
|
||||
import { autoUpdater } from 'electron-updater'
|
||||
import { platform } from '@electron-toolkit/utils'
|
||||
import { logger } from '../logger'
|
||||
@@ -8,24 +8,37 @@ import { configureUpdateProxy, handleUpdateError, resetToDefaultSource } from '.
|
||||
|
||||
type AppWithQuitFlag = typeof app & { isQuiting?: boolean }
|
||||
|
||||
interface UpdateWindow {
|
||||
webContents: {
|
||||
send(channel: string, ...args: unknown[]): void
|
||||
}
|
||||
}
|
||||
|
||||
const appWithQuitFlag = app as AppWithQuitFlag
|
||||
|
||||
let isFirstShow = true
|
||||
let isManualCheck = false
|
||||
|
||||
export function checkUpdate(win: UpdateWindow): void {
|
||||
function runWhenWindowActive(win: BrowserWindow, callback: () => void): void {
|
||||
const tryRun = (): boolean => {
|
||||
if (win.isDestroyed()) {
|
||||
win.removeListener('focus', tryRun)
|
||||
return true
|
||||
}
|
||||
if (!win.isVisible() || !win.isFocused()) return false
|
||||
|
||||
win.removeListener('focus', tryRun)
|
||||
callback()
|
||||
return true
|
||||
}
|
||||
|
||||
if (!tryRun()) {
|
||||
win.on('focus', tryRun)
|
||||
}
|
||||
}
|
||||
|
||||
export function checkUpdate(win: BrowserWindow): void {
|
||||
configureUpdateProxy()
|
||||
|
||||
autoUpdater.autoDownload = false
|
||||
autoUpdater.autoInstallOnAppQuit = false
|
||||
|
||||
let showUpdateMessageBox = false
|
||||
let showInstallMessageBox = false
|
||||
let isDownloadingUpdate = false
|
||||
|
||||
const startDownloadUpdate = (): void => {
|
||||
@@ -63,23 +76,27 @@ export function checkUpdate(win: UpdateWindow): void {
|
||||
}
|
||||
|
||||
showUpdateMessageBox = true
|
||||
dialog
|
||||
.showMessageBox({
|
||||
title: t('update.newVersionTitle', { version: info.version }),
|
||||
message: t('update.newVersionMessage', { version: info.version }),
|
||||
detail: t('update.newVersionDetail'),
|
||||
buttons: [t('update.downloadNow'), t('update.cancel')],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
type: 'question',
|
||||
noLink: true,
|
||||
})
|
||||
.then((result) => {
|
||||
showUpdateMessageBox = false
|
||||
if (result.response === 0) {
|
||||
startDownloadUpdate()
|
||||
}
|
||||
})
|
||||
runWhenWindowActive(win, () => {
|
||||
void dialog
|
||||
.showMessageBox(win, {
|
||||
title: t('update.newVersionTitle', { version: info.version }),
|
||||
message: t('update.newVersionMessage', { version: info.version }),
|
||||
detail: t('update.newVersionDetail'),
|
||||
buttons: [t('update.downloadNow'), t('update.cancel')],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
type: 'question',
|
||||
noLink: true,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.response === 0) {
|
||||
startDownloadUpdate()
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
showUpdateMessageBox = false
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
autoUpdater.on('download-progress', (progress) => {
|
||||
@@ -88,36 +105,44 @@ export function checkUpdate(win: UpdateWindow): void {
|
||||
})
|
||||
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
dialog
|
||||
.showMessageBox({
|
||||
title: t('update.downloadComplete'),
|
||||
message: t('update.readyToInstall'),
|
||||
buttons: [t('update.install'), t('update.remindLater')],
|
||||
defaultId: 1,
|
||||
cancelId: 1,
|
||||
type: 'question',
|
||||
})
|
||||
.then(async (result) => {
|
||||
if (result.response !== 0) return
|
||||
if (showInstallMessageBox) return
|
||||
|
||||
win.webContents.send('begin-install')
|
||||
appWithQuitFlag.isQuiting = true
|
||||
showInstallMessageBox = true
|
||||
runWhenWindowActive(win, () => {
|
||||
void dialog
|
||||
.showMessageBox(win, {
|
||||
title: t('update.downloadComplete'),
|
||||
message: t('update.readyToInstall'),
|
||||
buttons: [t('update.install'), t('update.remindLater')],
|
||||
defaultId: 1,
|
||||
cancelId: 1,
|
||||
type: 'question',
|
||||
})
|
||||
.then(async (result) => {
|
||||
if (result.response !== 0) return
|
||||
|
||||
if (platform.isWindows) {
|
||||
logger.info('[Update] Windows: Closing worker before installing...')
|
||||
try {
|
||||
await closeWorkerAsync()
|
||||
} catch (error) {
|
||||
logger.error(`[Update] Failed to close worker: ${error}`)
|
||||
win.webContents.send('begin-install')
|
||||
appWithQuitFlag.isQuiting = true
|
||||
|
||||
if (platform.isWindows) {
|
||||
logger.info('[Update] Windows: Closing worker before installing...')
|
||||
try {
|
||||
await closeWorkerAsync()
|
||||
} catch (error) {
|
||||
logger.error(`[Update] Failed to close worker: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setImmediate(() => {
|
||||
autoUpdater.quitAndInstall(true, true)
|
||||
})
|
||||
}, 100)
|
||||
})
|
||||
setTimeout(() => {
|
||||
setImmediate(() => {
|
||||
autoUpdater.quitAndInstall(true, true)
|
||||
})
|
||||
}, 100)
|
||||
})
|
||||
.finally(() => {
|
||||
showInstallMessageBox = false
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
autoUpdater.on('update-not-available', () => {
|
||||
@@ -155,8 +180,8 @@ export function manualCheckForUpdates(): void {
|
||||
})
|
||||
}
|
||||
|
||||
export function simulateUpdateDialog(_win: UpdateWindow): void {
|
||||
void dialog.showMessageBox({
|
||||
export function simulateUpdateDialog(win: BrowserWindow): void {
|
||||
void dialog.showMessageBox(win, {
|
||||
title: t('update.newVersionTitle', { version: '9.9.9' }),
|
||||
message: t('update.newVersionMessage', { version: '9.9.9' }),
|
||||
detail: t('update.newVersionDetail'),
|
||||
|
||||
Reference in new issue
Block a user