From c02f3a22305d9af55a68d8f8368f3a4fb88f7eb6 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Tue, 25 Oct 2016 16:41:20 +0100
Subject: [PATCH] Make autoupdate work on electron.

Changes the update process on web a bit to pull more out to the
platform classes.
---
 electron/src/electron-main.js                 | 49 ++++++++++++++++-
 src/components/views/globals/NewVersionBar.js | 52 +++++++++++++------
 src/skins/vector/css/common.css               |  4 ++
 src/vector/index.js                           |  7 +--
 src/vector/platform/ElectronPlatform.js       | 24 +++++++++
 src/vector/platform/WebPlatform.js            | 43 +++++++++++++++
 src/vector/updater.js                         | 48 ++++-------------
 7 files changed, 165 insertions(+), 62 deletions(-)

diff --git a/electron/src/electron-main.js b/electron/src/electron-main.js
index bcc235d36b..963323044f 100644
--- a/electron/src/electron-main.js
+++ b/electron/src/electron-main.js
@@ -27,6 +27,8 @@ const PERMITTED_URL_SCHEMES = [
     'mailto:',
 ];
 
+const UPDATE_POLL_INTERVAL_MS = 60 * 60 * 1000;
+
 let mainWindow = null;
 let appQuitting = false;
 
@@ -66,14 +68,57 @@ function onLinkContextMenu(ev, params) {
     ev.preventDefault();
 }
 
+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);
+    }
+}
+
+electron.ipcMain.on('install_update', installUpdate);
+
 electron.app.on('ready', () => {
-    // Enable auto-update once we can codesign on OS X
-    //electron.autoUpdater.setFeedURL("http://localhost:8888/");
+    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') {
+            electron.autoUpdater.setFeedURL("https://riot.im/autoupdate/desktop/");
+        } else if (process.platform == 'win32') {
+            electron.autoUpdater.setFeedURL("https://riot.im/download/desktop/win32/");
+        } 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.
+        pollForUpdates();
+        setInterval(pollForUpdates, UPDATE_POLL_INTERVAL_MS);
+    } catch (err) {
+        // will fail if running in debug mode
+        console.log("Couldn't enable update checking", err);
+    }
 
     mainWindow = new electron.BrowserWindow({
         icon: `${__dirname}/../../vector/img/logo.png`,
         width: 1024, height: 768,
     });
+    // A useful one to uncomment for debugging
+    //mainWindow.webContents.openDevTools();
     mainWindow.loadURL(`file://${__dirname}/../../vector/index.html`);
     electron.Menu.setApplicationMenu(VectorMenu);
 
diff --git a/src/components/views/globals/NewVersionBar.js b/src/components/views/globals/NewVersionBar.js
index 90b30f727b..3ae9580411 100644
--- a/src/components/views/globals/NewVersionBar.js
+++ b/src/components/views/globals/NewVersionBar.js
@@ -19,6 +19,7 @@ limitations under the License.
 var React = require('react');
 var sdk = require('matrix-react-sdk');
 import Modal from 'matrix-react-sdk/lib/Modal';
+import PlatformPeg from 'matrix-react-sdk/lib/PlatformPeg';
 
 /**
  * Check a version string is compatible with the Changelog
@@ -31,30 +32,50 @@ function checkVersion(ver) {
 
 export default function NewVersionBar(props) {
     const onChangelogClicked = () => {
-        const ChangelogDialog = sdk.getComponent('dialogs.ChangelogDialog');
-
-        Modal.createDialog(ChangelogDialog, {
-            version: props.version,
-            newVersion: props.newVersion,
-            onFinished: (update) => {
-                if(update) {
-                    window.location.reload();
+        if (props.releaseNotes) {
+            const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
+            Modal.createDialog(QuestionDialog, {
+                title: "What's New",
+                description: <pre className="changelog_text">{props.releaseNotes}</pre>,
+                button: "Update",
+                onFinished: (update) => {
+                    if(update && PlatformPeg.get()) {
+                        PlatformPeg.get().installUpdate();
+                    }
                 }
-            }
-        });
+            });
+        } else {
+            const ChangelogDialog = sdk.getComponent('dialogs.ChangelogDialog');
+            Modal.createDialog(ChangelogDialog, {
+                version: props.version,
+                newVersion: props.newVersion,
+                releaseNotes: releaseNotes,
+                onFinished: (update) => {
+                    if(update && PlatformPeg.get()) {
+                        PlatformPeg.get().installUpdate();
+                    }
+                }
+            });
+        }
     };
 
-    let changelog_button;
-    if (checkVersion(props.version) && checkVersion(props.newVersion)) {
-        changelog_button = <button className="mx_MatrixToolbar_action" onClick={onChangelogClicked}>Changelog</button>;
+    const onUpdateClicked = () => {
+        PlatformPeg.get().installUpdate();
+    };
+
+    let action_button;
+    if (props.releaseNotes || (checkVersion(props.version) && checkVersion(props.newVersion))) {
+        action_button = <button className="mx_MatrixToolbar_action" onClick={onChangelogClicked}>What's new?</button>;
+    } else if (PlatformPeg.get()) {
+        action_button = <button className="mx_MatrixToolbar_action" onClick={onUpdateClicked}>Update</button>;
     }
     return (
         <div className="mx_MatrixToolbar">
             <img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="/!\"/>
             <div className="mx_MatrixToolbar_content">
-                A new version of Riot is available. Refresh your browser.
+                A new version of Riot is available.
             </div>
-            {changelog_button}
+            {action_button}
         </div>
     );
 }
@@ -62,4 +83,5 @@ export default function NewVersionBar(props) {
 NewVersionBar.propTypes = {
     version: React.PropTypes.string.isRequired,
     newVersion: React.PropTypes.string.isRequired,
+    releaseNotes: React.PropTypes.string,
 };
diff --git a/src/skins/vector/css/common.css b/src/skins/vector/css/common.css
index 5165e9c57c..bb00bbd8c0 100644
--- a/src/skins/vector/css/common.css
+++ b/src/skins/vector/css/common.css
@@ -288,3 +288,7 @@ textarea {
     cursor: pointer;
     display: inline;
 }
+
+.changelog_text {
+    font-family: 'Open Sans', Arial, Helvetica, Sans-Serif;
+}
diff --git a/src/vector/index.js b/src/vector/index.js
index 4c965ef317..bf764a499d 100644
--- a/src/vector/index.js
+++ b/src/vector/index.js
@@ -112,10 +112,6 @@ function onHashChange(ev) {
     routeUrl(window.location);
 }
 
-function onVersion(current, latest) {
-    window.matrixChat.onVersion(current, latest);
-}
-
 var loaded = false;
 var lastLoadedScreen = null;
 
@@ -164,8 +160,7 @@ window.onload = function() {
     if (!validBrowser) {
         return;
     }
-    UpdateChecker.setVersionListener(onVersion);
-    UpdateChecker.run();
+    UpdateChecker.start();
     routeUrl(window.location);
     loaded = true;
     if (lastLoadedScreen) {
diff --git a/src/vector/platform/ElectronPlatform.js b/src/vector/platform/ElectronPlatform.js
index da901b923a..cb0e343afb 100644
--- a/src/vector/platform/ElectronPlatform.js
+++ b/src/vector/platform/ElectronPlatform.js
@@ -17,11 +17,22 @@ limitations under the License.
 */
 
 import BasePlatform from './BasePlatform';
+import dis from 'matrix-react-sdk/lib/dispatcher';
+
+function onUpdateDownloaded(ev, releaseNotes, ver, date, updateURL) {
+    dis.dispatch({
+        action: 'new_version',
+        currentVersion: electron.remote.app.getVersion(),
+        newVersion: ver,
+        releaseNotes: releaseNotes,
+    });
+}
 
 // index.js imports us unconditionally, so we need this check here as well
 let electron = null, remote = null;
 if (window && window.process && window.process && window.process.type === 'renderer') {
     electron = require('electron');
+    electron.remote.autoUpdater.on('update-downloaded', onUpdateDownloaded);
     remote = electron.remote;
 }
 
@@ -56,4 +67,17 @@ export default class ElectronPlatform extends BasePlatform {
     clearNotification(notif: Notification) {
         notif.close();
     }
+
+    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.
+    }
+
+    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');
+    }
 }
diff --git a/src/vector/platform/WebPlatform.js b/src/vector/platform/WebPlatform.js
index d1ae01d630..8a57a62ced 100644
--- a/src/vector/platform/WebPlatform.js
+++ b/src/vector/platform/WebPlatform.js
@@ -18,10 +18,14 @@ limitations under the License.
 
 import BasePlatform from './BasePlatform';
 import Favico from 'favico.js';
+import request from 'browser-request';
+import dis from 'matrix-react-sdk/lib/dispatcher.js';
+import q from 'q';
 
 export default class WebPlatform extends BasePlatform {
     constructor() {
         super();
+        this.runningVersion = null;
         // The 'animations' are really low framerate and look terrible.
         // Also it re-starts the animationb every time you set the badge,
         // and we set the state each time, even if the value hasn't changed,
@@ -85,4 +89,43 @@ export default class WebPlatform extends BasePlatform {
             notification.close();
         }, 5 * 1000);
     }
+
+    _getVersion() {
+        const deferred = q.defer();
+        request(
+            { method: "GET", url: "version" },
+            (err, response, body) => {
+                if (err || response.status < 200 || response.status >= 300) {
+                    if (err == null) err = { status: response.status };
+                    deferred.reject(err);
+                    return;
+                }
+
+                const ver = body.trim();
+                deferred.resolve(ver);
+            }
+        );
+        return deferred.promise;
+    }
+
+    pollForUpdate() {
+        this._getVersion().done((ver) => {
+            if (this.runningVersion == null) {
+                this.runningVersion = ver;
+            } else if (this.runningVersion != ver) {
+                dis.dispatch({
+                    action: 'new_version',
+                    currentVersion: this.runningVersion,
+                    newVersion: ver,
+                    releaseNotes: "Theses are some release notes innit\n * Do a thing\n * Do some more things",
+                });
+            }
+        }, (err) => {
+            console.error("Failed to poll for update", err);
+        });
+    }
+
+    installUpdate() {
+        window.location.reload();
+    }
 }
diff --git a/src/vector/updater.js b/src/vector/updater.js
index e8d6830d02..fc556d9f89 100644
--- a/src/vector/updater.js
+++ b/src/vector/updater.js
@@ -13,48 +13,18 @@ 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.
 */
-var POKE_RATE_MS = 10 * 60 * 1000; // 10 min
-var currentVersion = null;
-var latestVersion = null;
-var listener = function(){}; // NOP
+
+import PlatformPeg from 'matrix-react-sdk/lib/PlatformPeg';
+
+var POKE_RATE_MS = 1 * 6 * 1000; // 10 min
 
 module.exports = {
-    setVersionListener: function(fn) { // invoked with fn(currentVer, newVer)
-        listener = fn;
+    start: function() {
+        module.exports.poll();
+        setInterval(module.exports.poll, POKE_RATE_MS);
     },
 
-    run: function() {
-        var req = new XMLHttpRequest();
-        req.addEventListener("load", function() {
-            if (!req.responseText) {
-                return;
-            }
-            var ver = req.responseText.trim();
-            if (!currentVersion) {
-                currentVersion = ver;
-                listener(currentVersion, currentVersion);
-            }
-
-            if (ver !== latestVersion) {
-                latestVersion = ver;
-                if (module.exports.hasNewVersion()) {
-                    console.log("Current=%s Latest=%s", currentVersion, latestVersion);
-                    listener(currentVersion, latestVersion);
-                }
-            }
-        });
-        var cacheBuster = "?ts=" + new Date().getTime();
-        req.open("GET", "version" + cacheBuster);
-        req.send(); // can't suppress 404s from being logged.
-
-        setTimeout(module.exports.run, POKE_RATE_MS);
-    },
-
-    getCurrentVersion: function() {
-        return currentVersion;
-    },
-
-    hasNewVersion: function() {
-        return currentVersion && latestVersion && (currentVersion !== latestVersion);
+    poll: function() {
+        PlatformPeg.get().pollForUpdate();
     }
 };