diff --git a/electron_app/src/electron-main.js b/electron_app/src/electron-main.js index 3491ce0fa3..c8dd6266a3 100644 --- a/electron_app/src/electron-main.js +++ b/electron_app/src/electron-main.js @@ -29,6 +29,7 @@ const AutoLaunch = require('auto-launch'); const tray = require('./tray'); const vectorMenu = require('./vectormenu'); const webContentsHandler = require('./webcontents-handler'); +const updater = require('./updater'); const windowStateKeeper = require('electron-window-state'); @@ -46,69 +47,9 @@ try { // Continue with the defaults (ie. an empty config) } -const UPDATE_POLL_INTERVAL_MS = 60 * 60 * 1000; -const INITIAL_UPDATE_DELAY_MS = 30 * 1000; - let mainWindow = null; -let appQuitting = false; +global.appQuitting = false; -function installUpdate() { - // for some reason, quitAndInstall does not fire the - // before-quit event, so we need to set the flag here. - appQuitting = true; - electron.autoUpdater.quitAndInstall(); -} - -function pollForUpdates() { - try { - electron.autoUpdater.checkForUpdates(); - } catch (e) { - console.log('Couldn\'t check for update', e); - } -} - -function startAutoUpdate(updateBaseUrl) { - if (updateBaseUrl.slice(-1) !== '/') { - updateBaseUrl = updateBaseUrl + '/'; - } - try { - // For reasons best known to Squirrel, the way it checks for updates - // is completely different between macOS and windows. On macOS, it - // hits a URL that either gives it a 200 with some json or - // 204 No Content. On windows it takes a base path and looks for - // files under that path. - if (process.platform === 'darwin') { - // include the current version in the URL we hit. Electron doesn't add - // it anywhere (apart from the User-Agent) so it's up to us. We could - // (and previously did) just use the User-Agent, but this doesn't - // rely on NSURLConnection setting the User-Agent to what we expect, - // and also acts as a convenient cache-buster to ensure that when the - // app updates it always gets a fresh value to avoid update-looping. - electron.autoUpdater.setFeedURL( - `${updateBaseUrl}macos/?localVersion=${encodeURIComponent(electron.app.getVersion())}`); - - } else if (process.platform === 'win32') { - electron.autoUpdater.setFeedURL(`${updateBaseUrl}win32/${process.arch}/`); - } else { - // Squirrel / electron only supports auto-update on these two platforms. - // I'm not even going to try to guess which feed style they'd use if they - // implemented it on Linux, or if it would be different again. - console.log('Auto update not supported on this platform'); - } - // We check for updates ourselves rather than using 'updater' because we need to - // do it in the main process (and we don't really need to check every 10 minutes: - // every hour should be just fine for a desktop app) - // However, we still let the main window listen for the update events. - // We also wait a short time before checking for updates the first time because - // of squirrel on windows and it taking a small amount of time to release a - // lock file. - setTimeout(pollForUpdates, INITIAL_UPDATE_DELAY_MS); - setInterval(pollForUpdates, UPDATE_POLL_INTERVAL_MS); - } catch (err) { - // will fail if running in debug mode - console.log('Couldn\'t enable update checking', err); - } -} // handle uncaught errors otherwise it displays // stack traces in popup dialogs, which is terrible (which @@ -120,8 +61,6 @@ process.on('uncaughtException', function(error) { console.log('Unhandled exception', error); }); -electron.ipcMain.on('install_update', installUpdate); - let focusHandlerAttached = false; electron.ipcMain.on('setBadgeCount', function(ev, count) { electron.app.setBadgeCount(count); @@ -233,7 +172,7 @@ electron.app.on('ready', () => { if (vectorConfig.update_base_url) { console.log(`Starting auto update with base URL: ${vectorConfig.update_base_url}`); - startAutoUpdate(vectorConfig.update_base_url); + updater.start(vectorConfig.update_base_url); } else { console.log('No update_base_url is defined: auto update is disabled'); } @@ -246,7 +185,7 @@ electron.app.on('ready', () => { defaultHeight: 768, }); - mainWindow = new electron.BrowserWindow({ + mainWindow = global.mainWindow = new electron.BrowserWindow({ icon: iconPath, show: false, autoHideMenuBar: true, @@ -264,7 +203,7 @@ electron.app.on('ready', () => { mainWindow.hide(); // Create trayIcon icon - tray.create(mainWindow, { + tray.create({ icon_path: iconPath, brand: vectorConfig.brand || 'Riot', }); @@ -276,10 +215,10 @@ electron.app.on('ready', () => { } mainWindow.on('closed', () => { - mainWindow = null; + mainWindow = global.mainWindow = null; }); mainWindow.on('close', (e) => { - if (!appQuitting && (tray.hasTray() || process.platform === 'darwin')) { + if (!global.appQuitting && (tray.hasTray() || process.platform === 'darwin')) { // On Mac, closing the window just hides it // (this is generally how single-window Mac apps // behave, eg. Mail.app) @@ -302,7 +241,7 @@ electron.app.on('activate', () => { }); electron.app.on('before-quit', () => { - appQuitting = true; + global.appQuitting = true; }); // Set the App User Model ID to match what the squirrel diff --git a/electron_app/src/tray.js b/electron_app/src/tray.js index c2fa50c96b..bd07d7d433 100644 --- a/electron_app/src/tray.js +++ b/electron_app/src/tray.js @@ -26,17 +26,17 @@ exports.hasTray = function hasTray() { return (trayIcon !== null); }; -exports.create = function(win, config) { +exports.create = function(config) { // no trays on darwin if (process.platform === 'darwin' || trayIcon) return; const toggleWin = function() { - if (win.isVisible() && !win.isMinimized()) { - win.hide(); + if (global.mainWindow.isVisible() && !global.mainWindow.isMinimized()) { + global.mainWindow.hide(); } else { - if (win.isMinimized()) win.restore(); - if (!win.isVisible()) win.show(); - win.focus(); + if (global.mainWindow.isMinimized()) global.mainWindow.restore(); + if (!global.mainWindow.isVisible()) global.mainWindow.show(); + global.mainWindow.focus(); } }; @@ -62,7 +62,7 @@ exports.create = function(win, config) { trayIcon.on('click', toggleWin); let lastFavicon = null; - win.webContents.on('page-favicon-updated', async function(ev, favicons) { + global.mainWindow.webContents.on('page-favicon-updated', async function(ev, favicons) { if (!favicons || favicons.length <= 0 || !favicons[0].startsWith('data:')) { if (lastFavicon !== null) { win.setIcon(defaultIcon); @@ -90,10 +90,10 @@ exports.create = function(win, config) { } trayIcon.setImage(newFavicon); - win.setIcon(newFavicon); + global.mainWindow.setIcon(newFavicon); }); - win.webContents.on('page-title-updated', function(ev, title) { + global.mainWindow.webContents.on('page-title-updated', function(ev, title) { trayIcon.setToolTip(title); }); }; diff --git a/electron_app/src/updater.js b/electron_app/src/updater.js new file mode 100644 index 0000000000..49fa4e0419 --- /dev/null +++ b/electron_app/src/updater.js @@ -0,0 +1,84 @@ +const { app, autoUpdater, ipcMain } = require('electron'); + +const UPDATE_POLL_INTERVAL_MS = 60 * 60 * 1000; +const INITIAL_UPDATE_DELAY_MS = 30 * 1000; + +function installUpdate() { + // for some reason, quitAndInstall does not fire the + // before-quit event, so we need to set the flag here. + global.appQuitting = true; + autoUpdater.quitAndInstall(); +} + +function pollForUpdates() { + try { + autoUpdater.checkForUpdates(); + } catch (e) { + console.log('Couldn\'t check for update', e); + } +} + +module.exports = {}; +module.exports.start = function startAutoUpdate(updateBaseUrl) { + if (updateBaseUrl.slice(-1) !== '/') { + updateBaseUrl = updateBaseUrl + '/'; + } + try { + let url; + // For reasons best known to Squirrel, the way it checks for updates + // is completely different between macOS and windows. On macOS, it + // hits a URL that either gives it a 200 with some json or + // 204 No Content. On windows it takes a base path and looks for + // files under that path. + if (process.platform === 'darwin') { + // include the current version in the URL we hit. Electron doesn't add + // it anywhere (apart from the User-Agent) so it's up to us. We could + // (and previously did) just use the User-Agent, but this doesn't + // rely on NSURLConnection setting the User-Agent to what we expect, + // and also acts as a convenient cache-buster to ensure that when the + // app updates it always gets a fresh value to avoid update-looping. + url = `${updateBaseUrl}macos/?localVersion=${encodeURIComponent(app.getVersion())}`; + + } else if (process.platform === 'win32') { + url = `${updateBaseUrl}win32/${process.arch}/`; + } else { + // Squirrel / electron only supports auto-update on these two platforms. + // I'm not even going to try to guess which feed style they'd use if they + // implemented it on Linux, or if it would be different again. + console.log('Auto update not supported on this platform'); + } + + if (url) { + autoUpdater.setFeedURL(url); + // We check for updates ourselves rather than using 'updater' because we need to + // do it in the main process (and we don't really need to check every 10 minutes: + // every hour should be just fine for a desktop app) + // However, we still let the main window listen for the update events. + // We also wait a short time before checking for updates the first time because + // of squirrel on windows and it taking a small amount of time to release a + // lock file. + setTimeout(pollForUpdates, INITIAL_UPDATE_DELAY_MS); + setInterval(pollForUpdates, UPDATE_POLL_INTERVAL_MS); + } + } catch (err) { + // will fail if running in debug mode + console.log('Couldn\'t enable update checking', err); + } +} + +ipcMain.on('install_update', installUpdate); +ipcMain.on('check_updates', pollForUpdates); + +function ipcChannelSendUpdateStatus(status) { + if (global.mainWindow) { + global.mainWindow.webContents.send('check_updates', status); + } +} + +autoUpdater.on('update-available', function() { + ipcChannelSendUpdateStatus(true); +}).on('update-not-available', function() { + ipcChannelSendUpdateStatus(false); +}).on('error', function(error) { + ipcChannelSendUpdateStatus(error.message); +}); diff --git a/src/components/views/globals/MatrixToolbar.js b/src/components/views/globals/MatrixToolbar.js index 488b5def47..06bfa36e9e 100644 --- a/src/components/views/globals/MatrixToolbar.js +++ b/src/components/views/globals/MatrixToolbar.js @@ -35,7 +35,7 @@ module.exports = React.createClass({ render: function() { return (
- /!\ + Warning
{ _t('You are not receiving desktop notifications') } { _t('Enable them now') }
diff --git a/src/components/views/globals/NewVersionBar.js b/src/components/views/globals/NewVersionBar.js index 219ef02a9a..d25fa3a6f2 100644 --- a/src/components/views/globals/NewVersionBar.js +++ b/src/components/views/globals/NewVersionBar.js @@ -96,7 +96,7 @@ export default React.createClass({ } return (
- /!\ + Warning
{_t("A new version of Riot is available.")}
diff --git a/src/components/views/globals/UpdateCheckBar.js b/src/components/views/globals/UpdateCheckBar.js new file mode 100644 index 0000000000..926ccbcccf --- /dev/null +++ b/src/components/views/globals/UpdateCheckBar.js @@ -0,0 +1,85 @@ +/* +Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +'use strict'; + +import React from 'react'; +import { _t } from 'matrix-react-sdk/lib/languageHandler'; +import PlatformPeg from 'matrix-react-sdk/lib/PlatformPeg'; +import {updateCheckStatusEnum} from '../../../vector/platform/VectorBasePlatform'; +import AccessibleButton from 'matrix-react-sdk/lib/components/views/elements/AccessibleButton'; + +const doneStatuses = [ + updateCheckStatusEnum.ERROR, + updateCheckStatusEnum.NOTAVAILABLE, +]; + +export default React.createClass({ + propTypes: { + status: React.PropTypes.oneOf(Object.values(updateCheckStatusEnum)).isRequired, + // Currently for error detail but will be usable for download progress + // once that is a thing that squirrel passes through electron. + detail: React.PropTypes.string, + }, + + getDefaultProps: function() { + return { + detail: '', + } + }, + + getStatusText: function() { + switch(this.props.status) { + case updateCheckStatusEnum.ERROR: + return _t('Error encountered (%(errorDetail)s).', { errorDetail: this.props.detail }); + case updateCheckStatusEnum.CHECKING: + return _t('Checking for an update...'); + case updateCheckStatusEnum.NOTAVAILABLE: + return _t('No update available.'); + case updateCheckStatusEnum.DOWNLOADING: + return _t('Downloading update...'); + } + } + , + + hideToolbar: function() { + PlatformPeg.get().stopUpdateCheck(); + }, + + render: function() { + const message = this.getStatusText(); + const warning = _t('Warning'); + + let image; + if (doneStatuses.includes(this.props.status)) { + image = {warning}/; + } else { + image = {message}/; + } + + return ( +
+ {image} +
+ {message} +
+ + + +
+ ); + } +}); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8674e536ce..10c21a62d7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -159,6 +159,11 @@ "Today": "Today", "Yesterday": "Yesterday", "OK": "OK", + "Warning": "Warning", + "Checking for an update...": "Checking for an update...", + "Error encountered (%(errorDetail)s).": "Error encountered (%(errorDetail)s).", + "No update available.": "No update available.", + "Downloading update...": "Downloading update...", "You need to be using HTTPS to place a screen-sharing call.": "You need to be using HTTPS to place a screen-sharing call.", "Welcome page": "Welcome page", "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!", diff --git a/src/vector/index.js b/src/vector/index.js index da03327dab..9ad86d0ec2 100644 --- a/src/vector/index.js +++ b/src/vector/index.js @@ -59,7 +59,6 @@ var sdk = require("matrix-react-sdk"); const PlatformPeg = require("matrix-react-sdk/lib/PlatformPeg"); sdk.loadSkin(require('../component-index')); var VectorConferenceHandler = require('../VectorConferenceHandler'); -var UpdateChecker = require("./updater"); var q = require('q'); var request = require('browser-request'); import * as UserSettingsStore from 'matrix-react-sdk/lib/UserSettingsStore'; @@ -275,7 +274,9 @@ async function loadApp() { Unable to load config file: please refresh the page to try again.
, document.getElementById('matrixchat')); } else if (validBrowser) { - UpdateChecker.start(); + const platform = PlatformPeg.get(); + platform.startUpdater(); + const MatrixChat = sdk.getComponent('structures.MatrixChat'); window.matrixChat = ReactDOM.render( , document.getElementById('matrixchat') ); diff --git a/src/vector/platform/ElectronPlatform.js b/src/vector/platform/ElectronPlatform.js index fa0f999cb8..7a1bf58292 100644 --- a/src/vector/platform/ElectronPlatform.js +++ b/src/vector/platform/ElectronPlatform.js @@ -17,11 +17,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import VectorBasePlatform from './VectorBasePlatform'; +import VectorBasePlatform, {updateCheckStatusEnum} from './VectorBasePlatform'; import dis from 'matrix-react-sdk/lib/dispatcher'; import { _t } from 'matrix-react-sdk/lib/languageHandler'; import q from 'q'; -import electron, {remote, ipcRenderer} from 'electron'; +import {remote, ipcRenderer} from 'electron'; remote.autoUpdater.on('update-downloaded', onUpdateDownloaded); @@ -62,10 +62,42 @@ function _onAction(payload: Object) { } } +function getUpdateCheckStatus(status) { + if (status === true) { + return { status: updateCheckStatusEnum.DOWNLOADING }; + } else if (status === false) { + return { status: updateCheckStatusEnum.NOTAVAILABLE }; + } else { + return { + status: updateCheckStatusEnum.ERROR, + detail: status, + }; + } +} + export default class ElectronPlatform extends VectorBasePlatform { constructor() { super(); dis.register(_onAction); + this.updatable = Boolean(remote.autoUpdater.getFeedURL()); + + /* + IPC Call `check_updates` returns: + true if there is an update available + false if there is not + or the error if one is encountered + */ + ipcRenderer.on('check_updates', (event, status) => { + if (!this.showUpdateCheck) return; + dis.dispatch({ + action: 'check_updates', + value: getUpdateCheckStatus(status), + }); + this.showUpdateCheck = false; + }); + + this.startUpdateCheck = this.startUpdateCheck.bind(this); + this.stopUpdateCheck = this.stopUpdateCheck.bind(this); } getHumanReadableName(): string { @@ -137,17 +169,18 @@ export default class ElectronPlatform extends VectorBasePlatform { return q(remote.app.getVersion()); } - pollForUpdate() { - // In electron we control the update process ourselves, since - // it needs to run in the main process, so we just run the timer - // loop in the main electron process instead. + startUpdateCheck() { + if (this.showUpdateCheck) return; + super.startUpdateCheck(); + + ipcRenderer.send('check_updates'); } installUpdate() { // IPC to the main process to install the update, since quitAndInstall // doesn't fire the before-quit event so the main process needs to know // it should exit. - electron.ipcRenderer.send('install_update'); + ipcRenderer.send('install_update'); } getDefaultDeviceDisplayName(): string { diff --git a/src/vector/platform/VectorBasePlatform.js b/src/vector/platform/VectorBasePlatform.js index 76707d1d58..c3df04f5b7 100644 --- a/src/vector/platform/VectorBasePlatform.js +++ b/src/vector/platform/VectorBasePlatform.js @@ -19,9 +19,18 @@ limitations under the License. import BasePlatform from 'matrix-react-sdk/lib/BasePlatform'; import { _t } from 'matrix-react-sdk/lib/languageHandler'; +import dis from 'matrix-react-sdk/lib/dispatcher'; import Favico from 'favico.js'; +export const updateCheckStatusEnum = { + CHECKING: 'CHECKING', + ERROR: 'ERROR', + NOTAVAILABLE: 'NOTAVAILABLE', + DOWNLOADING: 'DOWNLOADING', + READY: 'READY', +}; + /** * Vector-specific extensions to the BasePlatform template */ @@ -34,7 +43,12 @@ export default class VectorBasePlatform extends BasePlatform { // and we set the state each time, even if the value hasn't changed, // so we'd need to fix that if enabling the animation. this.favicon = new Favico({animation: 'none'}); + this.showUpdateCheck = false; this._updateFavicon(); + this.updatable = true; + + this.startUpdateCheck = this.startUpdateCheck.bind(this); + this.stopUpdateCheck = this.stopUpdateCheck.bind(this); } getHumanReadableName(): string { @@ -75,12 +89,32 @@ export default class VectorBasePlatform extends BasePlatform { } /** - * Check for the availability of an update to the version of the - * app that's currently running. - * If an update is available, this function should dispatch the - * 'new_version' action. + * Begin update polling, if applicable */ - pollForUpdate() { + startUpdater() { + } + + /** + * Whether we can call checkForUpdate on this platform build + */ + canSelfUpdate(): boolean { + return this.updatable; + } + + startUpdateCheck() { + this.showUpdateCheck = true; + dis.dispatch({ + action: 'check_updates', + value: { status: updateCheckStatusEnum.CHECKING }, + }); + } + + stopUpdateCheck() { + this.showUpdateCheck = false; + dis.dispatch({ + action: 'check_updates', + value: false, + }) } /** diff --git a/src/vector/platform/WebPlatform.js b/src/vector/platform/WebPlatform.js index b9e941e63c..ae1e54b307 100644 --- a/src/vector/platform/WebPlatform.js +++ b/src/vector/platform/WebPlatform.js @@ -17,7 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import VectorBasePlatform from './VectorBasePlatform'; +import VectorBasePlatform, {updateCheckStatusEnum} from './VectorBasePlatform'; import request from 'browser-request'; import dis from 'matrix-react-sdk/lib/dispatcher.js'; import { _t } from 'matrix-react-sdk/lib/languageHandler'; @@ -26,10 +26,15 @@ import q from 'q'; import url from 'url'; import UAParser from 'ua-parser-js'; +var POKE_RATE_MS = 10 * 60 * 1000; // 10 min + export default class WebPlatform extends VectorBasePlatform { constructor() { super(); this.runningVersion = null; + + this.startUpdateCheck = this.startUpdateCheck.bind(this); + this.stopUpdateCheck = this.stopUpdateCheck.bind(this); } getHumanReadableName(): string { @@ -132,8 +137,13 @@ export default class WebPlatform extends VectorBasePlatform { return this._getVersion(); } + startUpdater() { + this.pollForUpdate(); + setInterval(this.pollForUpdate.bind(this), POKE_RATE_MS); + } + pollForUpdate() { - this._getVersion().done((ver) => { + return this._getVersion().then((ver) => { if (this.runningVersion === null) { this.runningVersion = ver; } else if (this.runningVersion !== ver) { @@ -142,9 +152,29 @@ export default class WebPlatform extends VectorBasePlatform { currentVersion: this.runningVersion, newVersion: ver, }); + // Return to skip a MatrixChat state update + return; } + return { status: updateCheckStatusEnum.NOTAVAILABLE }; }, (err) => { console.error("Failed to poll for update", err); + return { + status: updateCheckStatusEnum.ERROR, + detail: err.message || err.status ? err.status.toString() : 'Unknown Error', + }; + }); + } + + startUpdateCheck() { + if (this.showUpdateCheck) return; + super.startUpdateCheck(); + this.pollForUpdate().then((updateState) => { + if (!this.showUpdateCheck) return; + if (!updateState) return; + dis.dispatch({ + action: 'check_updates', + value: updateState, + }); }); } diff --git a/src/vector/updater.js b/src/vector/updater.js deleted file mode 100644 index 19d40b4f38..0000000000 --- a/src/vector/updater.js +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import PlatformPeg from 'matrix-react-sdk/lib/PlatformPeg'; - -var POKE_RATE_MS = 10 * 60 * 1000; // 10 min - -module.exports = { - start: function() { - module.exports.poll(); - setInterval(module.exports.poll, POKE_RATE_MS); - }, - - poll: function() { - PlatformPeg.get().pollForUpdate(); - } -};