Skip to content

Commit 2cee62b

Browse files
Improve desktop app updater messages and actions (#8315)
- Regularly check for updates, not only on launch - When update is downloaded, notification to restart & install - also display version number in notification
1 parent 337a0ab commit 2cee62b

5 files changed

Lines changed: 105 additions & 28 deletions

File tree

newIDE/app/src/MainFrame/Preferences/PreferencesProvider.js

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ import {
2929
selectLanguageOrLocale,
3030
} from '../../Utils/Language';
3131
import { type GamesDashboardOrderBy } from '../../GameDashboard/GamesList';
32-
import { CHECK_APP_UPDATES_TIMEOUT } from '../../Utils/GlobalFetchTimeouts';
32+
import {
33+
CHECK_APP_UPDATES_TIMEOUT,
34+
PERIODIC_APP_UPDATES_TIMEOUT,
35+
} from '../../Utils/GlobalFetchTimeouts';
3336
const electron = optionalRequire('electron');
3437
const ipcRenderer = electron ? electron.ipcRenderer : null;
3538

@@ -158,6 +161,9 @@ const getPreferences = (): PreferencesValues => {
158161
};
159162

160163
export default class PreferencesProvider extends React.Component<Props, State> {
164+
_periodicUpdateCheckTimeout: ?TimeoutID = null;
165+
_periodicUpdateCheckInterval: ?IntervalID = null;
166+
161167
// $FlowFixMe[missing-local-annot]
162168
state = {
163169
values: (getPreferences(): PreferencesValues),
@@ -172,7 +178,8 @@ export default class PreferencesProvider extends React.Component<Props, State> {
172178
// $FlowFixMe[method-unbinding]
173179
setAutoDownloadUpdates: (this._setAutoDownloadUpdates.bind(this): any),
174180
// $FlowFixMe[method-unbinding]
175-
checkUpdates: (this._checkUpdates.bind(this): any),
181+
checkUpdates: ((forceDownload?: boolean) =>
182+
this._checkUpdates(forceDownload, true): any),
176183
// $FlowFixMe[method-unbinding]
177184
setAutoDisplayChangelog: (this._setAutoDisplayChangelog.bind(this): any),
178185
// $FlowFixMe[method-unbinding]
@@ -402,7 +409,19 @@ export default class PreferencesProvider extends React.Component<Props, State> {
402409
};
403410

404411
componentDidMount() {
405-
setTimeout(() => this._checkUpdates(), CHECK_APP_UPDATES_TIMEOUT);
412+
this._periodicUpdateCheckTimeout = setTimeout(
413+
() => this._checkUpdates(),
414+
CHECK_APP_UPDATES_TIMEOUT
415+
);
416+
this._periodicUpdateCheckInterval = setInterval(
417+
() => this._checkUpdates(),
418+
PERIODIC_APP_UPDATES_TIMEOUT
419+
);
420+
}
421+
422+
componentWillUnmount() {
423+
clearTimeout(this._periodicUpdateCheckTimeout);
424+
clearInterval(this._periodicUpdateCheckInterval);
406425
}
407426

408427
_setMultipleValues(updates: ProjectSpecificPreferencesValues) {
@@ -778,17 +797,17 @@ export default class PreferencesProvider extends React.Component<Props, State> {
778797
);
779798
}
780799

781-
_checkUpdates(forceDownload?: boolean) {
800+
_checkUpdates(forceDownload?: boolean, explicit?: boolean) {
782801
// Checking for updates is only done on Electron.
783802
// Note: This could be abstracted away later if other updates mechanisms
784803
// should be supported.
785804
const { disableCheckForUpdates } = this.props;
786805
if (!ipcRenderer || disableCheckForUpdates) return;
787806

788807
if (!!forceDownload || this.state.values.autoDownloadUpdates) {
789-
ipcRenderer.send('updates-check-and-download');
808+
ipcRenderer.send('updates-check-and-download', { explicit: !!explicit });
790809
} else {
791-
ipcRenderer.send('updates-check');
810+
ipcRenderer.send('updates-check', { explicit: !!explicit });
792811
}
793812
}
794813

newIDE/app/src/MainFrame/UpdaterTools.js

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// @flow
22
// See ElectronEventsBridge, AboutDialog and electron-app/main.js for handling the updates.
33

4-
import { Trans } from '@lingui/macro';
4+
import { Trans, t } from '@lingui/macro';
5+
import { type I18n } from '@lingui/core';
56
import React from 'react';
67

78
export type ElectronUpdateStatus = {
@@ -14,22 +15,40 @@ export type ElectronUpdateStatus = {
1415
| 'download-progress'
1516
| 'update-downloaded'
1617
| 'unknown',
18+
info?: {| version?: string |},
1719
};
1820

1921
export const getElectronUpdateNotificationTitle = (
20-
updateStatus: ElectronUpdateStatus
22+
updateStatus: ElectronUpdateStatus,
23+
i18n: I18n
2124
): string => {
2225
if (updateStatus.status === 'update-available')
23-
return 'A new update is available!';
26+
return i18n._(t`A new update is available!`);
2427

2528
return '';
2629
};
2730

2831
export const getElectronUpdateNotificationBody = (
29-
updateStatus: ElectronUpdateStatus
32+
updateStatus: ElectronUpdateStatus,
33+
i18n: I18n,
34+
autoDownloadUpdates: boolean
3035
): string => {
31-
if (updateStatus.status === 'update-available')
32-
return 'It will be downloaded and installed automatically (unless you deactivated this in preferences)';
36+
if (updateStatus.status === 'update-available') {
37+
const version = updateStatus.info && updateStatus.info.version;
38+
if (autoDownloadUpdates) {
39+
return version
40+
? i18n._(
41+
t`Version ${version} is available and will be downloaded and installed automatically.`
42+
)
43+
: i18n._(t`It will be downloaded and installed automatically.`);
44+
} else {
45+
return version
46+
? i18n._(
47+
t`Version ${version} is available. Open About to download and install it.`
48+
)
49+
: i18n._(t`Open About to download and install it.`);
50+
}
51+
}
3352

3453
return '';
3554
};

newIDE/app/src/MainFrame/index.js

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ import {
8989
import { type ResourceExternalEditor } from '../ResourcesList/ResourceExternalEditor';
9090
import { type JsExtensionsLoader } from '../JsExtensionsLoader';
9191
import EventsFunctionsExtensionsContext from '../EventsFunctionsExtensionsLoader/EventsFunctionsExtensionsContext';
92+
import optionalRequire from '../Utils/OptionalRequire';
9293
import {
9394
getElectronUpdateNotificationTitle,
9495
getElectronUpdateNotificationBody,
@@ -241,6 +242,8 @@ import StandaloneDialog from './StandAloneDialog';
241242
import { useInGameEditorSettings } from '../EmbeddedGame/InGameEditorSettings';
242243
import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope';
243244
import { useAutomatedRegularInGameEditorRestart } from '../EmbeddedGame/UseAutomatedRegularInGameEditorRestart';
245+
const electron = optionalRequire('electron');
246+
const ipcRendererForUpdates = electron ? electron.ipcRenderer : null;
244247

245248
const GD_STARTUP_TIMES = global.GD_STARTUP_TIMES || [];
246249

@@ -4472,15 +4475,35 @@ const MainFrame = (props: Props): React.MixedElement => {
44724475
const setElectronUpdateStatus = (updateStatus: ElectronUpdateStatus) => {
44734476
setState(state => ({ ...state, updateStatus }));
44744477

4475-
// TODO: use i18n to translate title and body in notification.
4476-
// Also, find a way to use preferences to know if user deactivated auto-update.
4477-
const notificationTitle = getElectronUpdateNotificationTitle(updateStatus);
4478-
const notificationBody = getElectronUpdateNotificationBody(updateStatus);
4479-
if (notificationTitle) {
4480-
const notification = new window.Notification(notificationTitle, {
4481-
body: notificationBody,
4482-
});
4483-
notification.onclick = () => openAboutDialog(true);
4478+
if (updateStatus.status === 'update-downloaded') {
4479+
// Update is ready: offer a one-click restart instead of a generic notification.
4480+
const version = updateStatus.info && updateStatus.info.version;
4481+
const restartNotification = new window.Notification(
4482+
version
4483+
? i18n._(t`GDevelop update ready (${version})`)
4484+
: i18n._(t`GDevelop update ready`),
4485+
{ body: i18n._(t`Click to restart and install the update now.`) }
4486+
);
4487+
restartNotification.onclick = () => {
4488+
if (ipcRendererForUpdates)
4489+
ipcRendererForUpdates.send('updates-install-and-quit');
4490+
};
4491+
} else {
4492+
const notificationTitle = getElectronUpdateNotificationTitle(
4493+
updateStatus,
4494+
i18n
4495+
);
4496+
const notificationBody = getElectronUpdateNotificationBody(
4497+
updateStatus,
4498+
i18n,
4499+
preferences.values.autoDownloadUpdates
4500+
);
4501+
if (notificationTitle) {
4502+
const notification = new window.Notification(notificationTitle, {
4503+
body: notificationBody,
4504+
});
4505+
notification.onclick = () => openAboutDialog(true);
4506+
}
44844507
}
44854508
};
44864509

newIDE/app/src/Utils/GlobalFetchTimeouts.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,4 @@ export const CREDITS_PACKAGES_FETCH_TIMEOUT = 8000;
4242
export const MARKETING_PLANS_FETCH_TIMEOUT = 8000;
4343

4444
export const CHECK_APP_UPDATES_TIMEOUT = 10000;
45+
export const PERIODIC_APP_UPDATES_TIMEOUT = 60 * 60 * 1000; // 1 hour

newIDE/electron-app/app/main.js

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -668,24 +668,34 @@ app.on('ready', function() {
668668
closeAllConnections(windowId);
669669
});
670670

671-
ipcMain.on('updates-check-and-download', event => {
671+
// Track whether the current update check was triggered explicitly by the user,
672+
// so that errors are only surfaced to the user for manual checks.
673+
let isExplicitUpdateCheck = false;
674+
675+
ipcMain.on('updates-check-and-download', (event, { explicit } = {}) => {
672676
// This will immediately download an update, then install when the
673677
// app quits.
678+
isExplicitUpdateCheck = !!explicit;
674679
log.info('Starting check for updates (with auto-download if any)');
675680
autoUpdater.autoDownload = true;
676681
autoUpdater.checkForUpdatesAndNotify().catch(err => {
677682
log.error('Error checking for updates:', err);
678683
});
679684
});
680685

681-
ipcMain.on('updates-check', event => {
686+
ipcMain.on('updates-check', (event, { explicit } = {}) => {
687+
isExplicitUpdateCheck = !!explicit;
682688
log.info('Starting check for updates (without auto-download)');
683689
autoUpdater.autoDownload = false;
684690
autoUpdater.checkForUpdates().catch(err => {
685691
log.error('Error checking for updates:', err);
686692
});
687693
});
688694

695+
ipcMain.on('updates-install-and-quit', () => {
696+
autoUpdater.quitAndInstall();
697+
});
698+
689699
function sendUpdateStatus(status) {
690700
log.info(status);
691701
mainWindows.forEach(window => {
@@ -704,6 +714,7 @@ app.on('ready', function() {
704714
sendUpdateStatus({
705715
message: 'Update available.',
706716
status: 'update-available',
717+
info,
707718
});
708719
});
709720
autoUpdater.on('update-not-available', info => {
@@ -713,11 +724,15 @@ app.on('ready', function() {
713724
});
714725
});
715726
autoUpdater.on('error', err => {
716-
sendUpdateStatus({
717-
message: 'Error in auto-updater. ' + err,
718-
status: 'error',
719-
err,
720-
});
727+
if (isExplicitUpdateCheck) {
728+
sendUpdateStatus({
729+
message: 'Error in auto-updater. ' + err,
730+
status: 'error',
731+
err,
732+
});
733+
} else {
734+
log.error('Background update check failed:', err);
735+
}
721736
});
722737
autoUpdater.on('download-progress', progressObj => {
723738
let logMessage = 'Download speed: ' + progressObj.bytesPerSecond;

0 commit comments

Comments
 (0)