diff --git a/newIDE/app/src/MainFrame/Preferences/PreferencesProvider.js b/newIDE/app/src/MainFrame/Preferences/PreferencesProvider.js index 2e8e57e63ac5..d827709ee8bd 100644 --- a/newIDE/app/src/MainFrame/Preferences/PreferencesProvider.js +++ b/newIDE/app/src/MainFrame/Preferences/PreferencesProvider.js @@ -28,7 +28,10 @@ import { selectLanguageOrLocale, } from '../../Utils/Language'; import { type GamesDashboardOrderBy } from '../../GameDashboard/GamesList'; -import { CHECK_APP_UPDATES_TIMEOUT } from '../../Utils/GlobalFetchTimeouts'; +import { + CHECK_APP_UPDATES_TIMEOUT, + PERIODIC_APP_UPDATES_TIMEOUT, +} from '../../Utils/GlobalFetchTimeouts'; const electron = optionalRequire('electron'); const ipcRenderer = electron ? electron.ipcRenderer : null; @@ -157,6 +160,9 @@ const getPreferences = (): PreferencesValues => { }; export default class PreferencesProvider extends React.Component { + _periodicUpdateCheckTimeout: ?TimeoutID = null; + _periodicUpdateCheckInterval: ?IntervalID = null; + // $FlowFixMe[missing-local-annot] state = { values: (getPreferences(): PreferencesValues), @@ -171,7 +177,8 @@ export default class PreferencesProvider extends React.Component { // $FlowFixMe[method-unbinding] setAutoDownloadUpdates: (this._setAutoDownloadUpdates.bind(this): any), // $FlowFixMe[method-unbinding] - checkUpdates: (this._checkUpdates.bind(this): any), + checkUpdates: ((forceDownload?: boolean) => + this._checkUpdates(forceDownload, true): any), // $FlowFixMe[method-unbinding] setAutoDisplayChangelog: (this._setAutoDisplayChangelog.bind(this): any), // $FlowFixMe[method-unbinding] @@ -401,7 +408,19 @@ export default class PreferencesProvider extends React.Component { }; componentDidMount() { - setTimeout(() => this._checkUpdates(), CHECK_APP_UPDATES_TIMEOUT); + this._periodicUpdateCheckTimeout = setTimeout( + () => this._checkUpdates(), + CHECK_APP_UPDATES_TIMEOUT + ); + this._periodicUpdateCheckInterval = setInterval( + () => this._checkUpdates(), + PERIODIC_APP_UPDATES_TIMEOUT + ); + } + + componentWillUnmount() { + clearTimeout(this._periodicUpdateCheckTimeout); + clearInterval(this._periodicUpdateCheckInterval); } _setMultipleValues(updates: ProjectSpecificPreferencesValues) { @@ -777,7 +796,7 @@ export default class PreferencesProvider extends React.Component { ); } - _checkUpdates(forceDownload?: boolean) { + _checkUpdates(forceDownload?: boolean, explicit?: boolean) { // Checking for updates is only done on Electron. // Note: This could be abstracted away later if other updates mechanisms // should be supported. @@ -785,9 +804,9 @@ export default class PreferencesProvider extends React.Component { if (!ipcRenderer || disableCheckForUpdates) return; if (!!forceDownload || this.state.values.autoDownloadUpdates) { - ipcRenderer.send('updates-check-and-download'); + ipcRenderer.send('updates-check-and-download', { explicit: !!explicit }); } else { - ipcRenderer.send('updates-check'); + ipcRenderer.send('updates-check', { explicit: !!explicit }); } } diff --git a/newIDE/app/src/MainFrame/UpdaterTools.js b/newIDE/app/src/MainFrame/UpdaterTools.js index 7c3c9c0d7f7c..ce6278b3165c 100644 --- a/newIDE/app/src/MainFrame/UpdaterTools.js +++ b/newIDE/app/src/MainFrame/UpdaterTools.js @@ -1,7 +1,8 @@ // @flow // See ElectronEventsBridge, AboutDialog and electron-app/main.js for handling the updates. -import { Trans } from '@lingui/macro'; +import { Trans, t } from '@lingui/macro'; +import { type I18n } from '@lingui/core'; import React from 'react'; export type ElectronUpdateStatus = { @@ -14,22 +15,40 @@ export type ElectronUpdateStatus = { | 'download-progress' | 'update-downloaded' | 'unknown', + info?: {| version?: string |}, }; export const getElectronUpdateNotificationTitle = ( - updateStatus: ElectronUpdateStatus + updateStatus: ElectronUpdateStatus, + i18n: I18n ): string => { if (updateStatus.status === 'update-available') - return 'A new update is available!'; + return i18n._(t`A new update is available!`); return ''; }; export const getElectronUpdateNotificationBody = ( - updateStatus: ElectronUpdateStatus + updateStatus: ElectronUpdateStatus, + i18n: I18n, + autoDownloadUpdates: boolean ): string => { - if (updateStatus.status === 'update-available') - return 'It will be downloaded and installed automatically (unless you deactivated this in preferences)'; + if (updateStatus.status === 'update-available') { + const version = updateStatus.info && updateStatus.info.version; + if (autoDownloadUpdates) { + return version + ? i18n._( + t`Version ${version} is available and will be downloaded and installed automatically.` + ) + : i18n._(t`It will be downloaded and installed automatically.`); + } else { + return version + ? i18n._( + t`Version ${version} is available. Open About to download and install it.` + ) + : i18n._(t`Open About to download and install it.`); + } + } return ''; }; diff --git a/newIDE/app/src/MainFrame/index.js b/newIDE/app/src/MainFrame/index.js index baea6ad0580f..9a4d28a738fa 100644 --- a/newIDE/app/src/MainFrame/index.js +++ b/newIDE/app/src/MainFrame/index.js @@ -82,6 +82,7 @@ import { import { type ResourceExternalEditor } from '../ResourcesList/ResourceExternalEditor'; import { type JsExtensionsLoader } from '../JsExtensionsLoader'; import EventsFunctionsExtensionsContext from '../EventsFunctionsExtensionsLoader/EventsFunctionsExtensionsContext'; +import optionalRequire from '../Utils/OptionalRequire'; import { getElectronUpdateNotificationTitle, getElectronUpdateNotificationBody, @@ -232,6 +233,8 @@ import StandaloneDialog from './StandAloneDialog'; import { useInGameEditorSettings } from '../EmbeddedGame/InGameEditorSettings'; import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope'; import { useAutomatedRegularInGameEditorRestart } from '../EmbeddedGame/UseAutomatedRegularInGameEditorRestart'; +const electron = optionalRequire('electron'); +const ipcRendererForUpdates = electron ? electron.ipcRenderer : null; const GD_STARTUP_TIMES = global.GD_STARTUP_TIMES || []; @@ -4370,15 +4373,35 @@ const MainFrame = (props: Props): React.MixedElement => { const setElectronUpdateStatus = (updateStatus: ElectronUpdateStatus) => { setState(state => ({ ...state, updateStatus })); - // TODO: use i18n to translate title and body in notification. - // Also, find a way to use preferences to know if user deactivated auto-update. - const notificationTitle = getElectronUpdateNotificationTitle(updateStatus); - const notificationBody = getElectronUpdateNotificationBody(updateStatus); - if (notificationTitle) { - const notification = new window.Notification(notificationTitle, { - body: notificationBody, - }); - notification.onclick = () => openAboutDialog(true); + if (updateStatus.status === 'update-downloaded') { + // Update is ready: offer a one-click restart instead of a generic notification. + const version = updateStatus.info && updateStatus.info.version; + const restartNotification = new window.Notification( + version + ? i18n._(t`GDevelop update ready (${version})`) + : i18n._(t`GDevelop update ready`), + { body: i18n._(t`Click to restart and install the update now.`) } + ); + restartNotification.onclick = () => { + if (ipcRendererForUpdates) + ipcRendererForUpdates.send('updates-install-and-quit'); + }; + } else { + const notificationTitle = getElectronUpdateNotificationTitle( + updateStatus, + i18n + ); + const notificationBody = getElectronUpdateNotificationBody( + updateStatus, + i18n, + preferences.values.autoDownloadUpdates + ); + if (notificationTitle) { + const notification = new window.Notification(notificationTitle, { + body: notificationBody, + }); + notification.onclick = () => openAboutDialog(true); + } } }; diff --git a/newIDE/app/src/Utils/GlobalFetchTimeouts.js b/newIDE/app/src/Utils/GlobalFetchTimeouts.js index 37382b916bda..98cbdd462ed6 100644 --- a/newIDE/app/src/Utils/GlobalFetchTimeouts.js +++ b/newIDE/app/src/Utils/GlobalFetchTimeouts.js @@ -42,3 +42,4 @@ export const CREDITS_PACKAGES_FETCH_TIMEOUT = 8000; export const MARKETING_PLANS_FETCH_TIMEOUT = 8000; export const CHECK_APP_UPDATES_TIMEOUT = 10000; +export const PERIODIC_APP_UPDATES_TIMEOUT = 60 * 60 * 1000; // 1 hour diff --git a/newIDE/electron-app/app/main.js b/newIDE/electron-app/app/main.js index 11cfe76a3ea4..cab8aea24b82 100644 --- a/newIDE/electron-app/app/main.js +++ b/newIDE/electron-app/app/main.js @@ -581,9 +581,14 @@ app.on('ready', function() { closeAllConnections(windowId); }); - ipcMain.on('updates-check-and-download', event => { + // Track whether the current update check was triggered explicitly by the user, + // so that errors are only surfaced to the user for manual checks. + let isExplicitUpdateCheck = false; + + ipcMain.on('updates-check-and-download', (event, { explicit } = {}) => { // This will immediately download an update, then install when the // app quits. + isExplicitUpdateCheck = !!explicit; log.info('Starting check for updates (with auto-download if any)'); autoUpdater.autoDownload = true; autoUpdater.checkForUpdatesAndNotify().catch(err => { @@ -591,7 +596,8 @@ app.on('ready', function() { }); }); - ipcMain.on('updates-check', event => { + ipcMain.on('updates-check', (event, { explicit } = {}) => { + isExplicitUpdateCheck = !!explicit; log.info('Starting check for updates (without auto-download)'); autoUpdater.autoDownload = false; autoUpdater.checkForUpdates().catch(err => { @@ -599,6 +605,10 @@ app.on('ready', function() { }); }); + ipcMain.on('updates-install-and-quit', () => { + autoUpdater.quitAndInstall(); + }); + function sendUpdateStatus(status) { log.info(status); mainWindows.forEach(window => { @@ -617,6 +627,7 @@ app.on('ready', function() { sendUpdateStatus({ message: 'Update available.', status: 'update-available', + info, }); }); autoUpdater.on('update-not-available', info => { @@ -626,11 +637,15 @@ app.on('ready', function() { }); }); autoUpdater.on('error', err => { - sendUpdateStatus({ - message: 'Error in auto-updater. ' + err, - status: 'error', - err, - }); + if (isExplicitUpdateCheck) { + sendUpdateStatus({ + message: 'Error in auto-updater. ' + err, + status: 'error', + err, + }); + } else { + log.error('Background update check failed:', err); + } }); autoUpdater.on('download-progress', progressObj => { let logMessage = 'Download speed: ' + progressObj.bytesPerSecond;