diff --git a/.babelrc b/.babelrc index 8c7b66269d..6ba0e0dae0 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,4 @@ { "presets": ["react", "es2015", "es2016"], - "plugins": ["transform-class-properties", "transform-object-rest-spread", "transform-async-to-generator", "transform-runtime", "add-module-exports"] + "plugins": ["transform-class-properties", "transform-object-rest-spread", "transform-async-to-bluebird", "transform-runtime", "add-module-exports"] } diff --git a/.gitignore b/.gitignore index 060ca6e934..2ad05012a0 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,6 @@ npm-debug.log electron/dist electron/pub -/.idea +**/.idea /config.json /src/component-index.js diff --git a/.travis.yml b/.travis.yml index e020ba7d15..94ed745cd8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,10 @@ +# we need trusty for the chrome addon +dist: trusty + +# we don't need sudo, so can run in a container, which makes startup much +# quicker. +sudo: false + language: node_js node_js: # make sure we work with a range of node versions. @@ -5,8 +12,9 @@ node_js: # - 4.x is still in LTS (until April 2018), but some of our deps (notably # extract-zip) don't work with it # - 5.x has been EOLed for nearly a year. - # - 6.x is the current 'LTS' version - # - 7.x is the current 'current' version (until October 2017) + # - 6.x is the active 'LTS' version + # - 7.x is no longer supported + # - 8.x is the current 'current' version (until October 2017) # # see: https://github.com/nodejs/LTS/ # @@ -16,6 +24,8 @@ node_js: - 6.3 - 6 - 7 +addons: + chrome: stable install: # clone the deps with depth 1: we know we will only ever need that one # commit. diff --git a/CHANGELOG.md b/CHANGELOG.md index cdba055144..a78d26e0bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,168 @@ +Changes in [0.12.1](https://github.com/vector-im/riot-web/releases/tag/v0.12.1) (2017-08-23) +============================================================================================ +[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.12.1-rc.1...v0.12.1) + + * [No changes] + +Changes in [0.12.1-rc.1](https://github.com/vector-im/riot-web/releases/tag/v0.12.1-rc.1) (2017-08-22) +====================================================================================================== +[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.12.0-rc.2...v0.12.1-rc.1) + + * Update from Weblate. + [\#4832](https://github.com/vector-im/riot-web/pull/4832) + * Misc styling fixes. + [\#4826](https://github.com/vector-im/riot-web/pull/4826) + * Show / Hide apps icons + [\#4774](https://github.com/vector-im/riot-web/pull/4774) + +Changes in [0.12.0-rc.1](https://github.com/vector-im/riot-web/releases/tag/v0.12.0-rc.1) (2017-08-16) +====================================================================================================== +[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.11.4...v0.12.0-rc.1) + + * Update from Weblate. + [\#4797](https://github.com/vector-im/riot-web/pull/4797) + * move focus-via-up/down cursors to LeftPanel + [\#4777](https://github.com/vector-im/riot-web/pull/4777) + * Remove userId property on RightPanel + [\#4775](https://github.com/vector-im/riot-web/pull/4775) + * Make member device info buttons fluid and stackable with flexbox + [\#4776](https://github.com/vector-im/riot-web/pull/4776) + * un-i18n Modal Analytics + [\#4688](https://github.com/vector-im/riot-web/pull/4688) + * Quote using innerText + [\#4773](https://github.com/vector-im/riot-web/pull/4773) + * Karma tweaks for riot-web + [\#4765](https://github.com/vector-im/riot-web/pull/4765) + * Fix typo with scripts/fetch-develop-deps.sh in Building From Source + [\#4764](https://github.com/vector-im/riot-web/pull/4764) + * Adjust CSS for optional avatars in pills + [\#4757](https://github.com/vector-im/riot-web/pull/4757) + * Fix crypto on develop + [\#4754](https://github.com/vector-im/riot-web/pull/4754) + * Fix signing key url in readme + [\#4464](https://github.com/vector-im/riot-web/pull/4464) + * update gitignore to allow .idea directory to exist in subdirs + [\#4749](https://github.com/vector-im/riot-web/pull/4749) + * tweak compact theme + [\#4665](https://github.com/vector-im/riot-web/pull/4665) + * Update draft-js from 0.10.1 to 0.11.0-alpha + [\#4740](https://github.com/vector-im/riot-web/pull/4740) + * electron support for mouse forward/back buttons in Windows + [\#4739](https://github.com/vector-im/riot-web/pull/4739) + * Update draft-js from 0.8.1 to 0.10.1 + [\#4730](https://github.com/vector-im/riot-web/pull/4730) + * Make pills, emoji translucent when sending + [\#4693](https://github.com/vector-im/riot-web/pull/4693) + * Widget permissions styling and icon + [\#4690](https://github.com/vector-im/riot-web/pull/4690) + * CSS required for composer autoscroll + [\#4682](https://github.com/vector-im/riot-web/pull/4682) + * CSS for group edit UI + [\#4608](https://github.com/vector-im/riot-web/pull/4608) + * Fix a couple of minor errors in the room list + [\#4671](https://github.com/vector-im/riot-web/pull/4671) + * Styling for beta testing icon. + [\#4584](https://github.com/vector-im/riot-web/pull/4584) + * Increase the timeout for clearing indexeddbs + [\#4650](https://github.com/vector-im/riot-web/pull/4650) + * Make some adjustments to mx_UserPill and mx_RoomPill + [\#4597](https://github.com/vector-im/riot-web/pull/4597) + * Apply CSS to
 tags to distinguish them from each other
+   [\#4639](https://github.com/vector-im/riot-web/pull/4639)
+ * Use `catch` instead of `fail` to handle room tag error
+   [\#4643](https://github.com/vector-im/riot-web/pull/4643)
+ * CSS for decorated matrix.to links in the composer
+   [\#4583](https://github.com/vector-im/riot-web/pull/4583)
+ * Deflake the joining test
+   [\#4579](https://github.com/vector-im/riot-web/pull/4579)
+ * Bump react to 15.6 to fix build problems
+   [\#4577](https://github.com/vector-im/riot-web/pull/4577)
+ * Improve AppTile menu bar button styling.
+   [\#4567](https://github.com/vector-im/riot-web/pull/4567)
+ * Transform `async` functions to bluebird promises
+   [\#4572](https://github.com/vector-im/riot-web/pull/4572)
+ * use flushAllExpected in joining test
+   [\#4570](https://github.com/vector-im/riot-web/pull/4570)
+ * Switch riot-web to bluebird
+   [\#4565](https://github.com/vector-im/riot-web/pull/4565)
+ * loading tests: wait for login component
+   [\#4564](https://github.com/vector-im/riot-web/pull/4564)
+ * Remove CSS for the MessageComposerInputOld
+   [\#4568](https://github.com/vector-im/riot-web/pull/4568)
+ * Implement the focus_room_filter action
+   [\#4560](https://github.com/vector-im/riot-web/pull/4560)
+ * CSS for Rooms in Group View
+   [\#4530](https://github.com/vector-im/riot-web/pull/4530)
+ * more HomePage tweaks
+   [\#4557](https://github.com/vector-im/riot-web/pull/4557)
+ * Give HomePage an unmounted guard
+   [\#4556](https://github.com/vector-im/riot-web/pull/4556)
+ * Take RTE out of labs
+   [\#4500](https://github.com/vector-im/riot-web/pull/4500)
+ * CSS for Groups page
+   [\#4468](https://github.com/vector-im/riot-web/pull/4468)
+ * CSS for GroupView
+   [\#4442](https://github.com/vector-im/riot-web/pull/4442)
+ * remove unused class
+   [\#4525](https://github.com/vector-im/riot-web/pull/4525)
+ * Fix long words causing MessageComposer to widen
+   [\#4466](https://github.com/vector-im/riot-web/pull/4466)
+ * Add visual bell animation for RTE
+   [\#4516](https://github.com/vector-im/riot-web/pull/4516)
+ * Truncate auto-complete pills properly
+   [\#4502](https://github.com/vector-im/riot-web/pull/4502)
+ * Use chrome headless instead of phantomjs
+   [\#4512](https://github.com/vector-im/riot-web/pull/4512)
+ * Use external mock-request
+   [\#4489](https://github.com/vector-im/riot-web/pull/4489)
+ * fix Quote not closing contextual menu
+   [\#4443](https://github.com/vector-im/riot-web/pull/4443)
+ * Apply white-space: pre-wrap to mx_MEmoteBody
+   [\#4470](https://github.com/vector-im/riot-web/pull/4470)
+ * Add some style improvements to autocompletions
+   [\#4456](https://github.com/vector-im/riot-web/pull/4456)
+ * Styling for apps / widgets
+   [\#4447](https://github.com/vector-im/riot-web/pull/4447)
+ * Attempt to flush the rageshake logs on close
+   [\#4400](https://github.com/vector-im/riot-web/pull/4400)
+ * Update from Weblate.
+   [\#4401](https://github.com/vector-im/riot-web/pull/4401)
+ * improve update polling electron and provide a manual check for updates
+   button
+   [\#4176](https://github.com/vector-im/riot-web/pull/4176)
+ * Fix load failure in firefox when indexedDB is disabled
+   [\#4395](https://github.com/vector-im/riot-web/pull/4395)
+ * Change missed 'Redact' to 'Remove' in ImageView.
+   [\#4362](https://github.com/vector-im/riot-web/pull/4362)
+ * explicit convert to nativeImage to stabilise trayIcon on Windows [Electron]
+   [\#4355](https://github.com/vector-im/riot-web/pull/4355)
+ * Use _tJsx for PasswordNagBar (because it has )
+   [\#4373](https://github.com/vector-im/riot-web/pull/4373)
+ * Clean up some log outputs from the integ tests
+   [\#4376](https://github.com/vector-im/riot-web/pull/4376)
+ * CSS for redeisng of password warning
+   [\#4367](https://github.com/vector-im/riot-web/pull/4367)
+ * Give _t to PasswordNagBar, add CSS for UserSettings password warning
+   [\#4366](https://github.com/vector-im/riot-web/pull/4366)
+ * Update from Weblate.
+   [\#4361](https://github.com/vector-im/riot-web/pull/4361)
+ * Update from Weblate.
+   [\#4360](https://github.com/vector-im/riot-web/pull/4360)
+ * Test 'return-to-app' functionality
+   [\#4352](https://github.com/vector-im/riot-web/pull/4352)
+ * Update from Weblate.
+   [\#4354](https://github.com/vector-im/riot-web/pull/4354)
+ * onLoadCompleted is now onTokenLoginCompleted
+   [\#4335](https://github.com/vector-im/riot-web/pull/4335)
+ * Tweak tests to match updates to matrixchat
+   [\#4325](https://github.com/vector-im/riot-web/pull/4325)
+ * Update from Weblate.
+   [\#4346](https://github.com/vector-im/riot-web/pull/4346)
+ * change dispatcher forward_event signature
+   [\#4337](https://github.com/vector-im/riot-web/pull/4337)
+ * Add border on hover for code blocks
+   [\#4259](https://github.com/vector-im/riot-web/pull/4259)
+
 Changes in [0.11.4](https://github.com/vector-im/riot-web/releases/tag/v0.11.4) (2017-06-22)
 ============================================================================================
 [Full Changelog](https://github.com/vector-im/riot-web/compare/v0.11.3...v0.11.4)
@@ -5,7 +170,7 @@ Changes in [0.11.4](https://github.com/vector-im/riot-web/releases/tag/v0.11.4)
  * Update matrix-js-sdk and react-sdk to fix a regression where the
    background indexedb worker was disabled, failures to open indexeddb
    causing the app to fail to start, a race when starting that could break
-   switching to rooms, and the inability to to invite user with mixed case
+   switching to rooms, and the inability to invite users with mixed case
    usernames.
 
 Changes in [0.11.3](https://github.com/vector-im/riot-web/releases/tag/v0.11.3) (2017-06-20)
diff --git a/README.md b/README.md
index 89f2148f5e..271382030e 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ released version of Riot:
 1. Enter the URL into your browser and log into Riot!
 
 Releases are signed by PGP, and can be checked against the public key
-at https://riot.im/packages/keys/riot-master.asc
+at https://riot.im/packages/keys/riot.asc
 
 Note that Chrome does not allow microphone or webcam access for sites served
 over http (except localhost), so for working VoIP you will need to serve Riot
@@ -62,7 +62,7 @@ to build.
 1. If you're using the `develop` branch, install the develop versions of the
    dependencies, as the released ones will be too old:
    ```
-   scripts/fetch-develop-deps.sh
+   scripts/fetch-develop.deps.sh
    ```
    Whenever you git pull on riot-web you will also probably need to force an update
    to these dependencies - the simplest way is to re-run the script, but you can also
@@ -81,7 +81,7 @@ to build.
    npm run build
    ```
    However, we recommend setting up a proper development environment (see "Setting
-   up a development environment" below) if you want to run your own copy of the
+   up a dev environment" below) if you want to run your own copy of the
    `develop` branch, as it makes it much easier to keep these dependencies
    up-to-date.  Or just use https://riot.im/develop - the continuous integration
    release of the develop branch.
@@ -253,7 +253,6 @@ Finally, build and start Riot itself:
 1. `rm -r node_modules/matrix-react-sdk; ln -s ../../matrix-react-sdk node_modules/`
 1. `npm start`
 1. Wait a few seconds for the initial build to finish; you should see something like:
-
     ```
     Hash: b0af76309dd56d7275c8
     Version: webpack 1.12.14
@@ -282,19 +281,34 @@ If any of these steps error with, `file table overflow`, you are probably on a m
 which has a very low limit on max open files. Run `ulimit -Sn 1024` and try again.
 You'll need to do this in each new terminal you open before building Riot.
 
-How to add a new translation?
-=============================
+Running the tests
+-----------------
+
+There are a number of application-level tests in the `tests` directory; these
+are designed to run in a browser instance under the control of
+[karma](https://karma-runner.github.io). To run them:
+
+* Make sure you have Chrome installed (a recent version, like 59)
+* Make sure you have `matrix-js-sdk` and `matrix-react-sdk` installed and
+  built, as above
+* `npm run test`
+
+The above will run the tests under Chrome in a `headless` mode.
+
+You can also tell karma to run the tests in a loop (every time the source
+changes), in an instance of Chrome on your desktop, with `npm run
+test-multi`. This also gives you the option of running the tests in 'debug'
+mode, which is useful for stepping through the tests in the developer tools.
+
+Translations
+============
+
+To add a new translation, head to the [translating doc](docs/translating.md).
+
+For a developer guide, see the [translating dev doc](docs/translating-dev.md).
 
 [translationsstatus](https://translate.riot.im/engage/riot-web/?utm_source=widget)
 
-
-Head to the [translating doc](docs/translating.md)
-
-Adding Strings to the translations (Developer Guide)
-====================================================
-
-Head to the [translating dev doc](docs/translating-dev.md)
-
 Triaging issues
 ===============
 
diff --git a/electron_app/package.json b/electron_app/package.json
index 2c6e62f2f5..0a3b092e77 100644
--- a/electron_app/package.json
+++ b/electron_app/package.json
@@ -2,7 +2,7 @@
   "name": "riot-web",
   "productName": "Riot",
   "main": "src/electron-main.js",
-  "version": "0.11.4",
+  "version": "0.12.1",
   "description": "A feature-rich client for Matrix.org",
   "author": "Vector Creations Ltd.",
   "dependencies": {
diff --git a/electron_app/src/electron-main.js b/electron_app/src/electron-main.js
index 3491ce0fa3..ce5ac38413 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)
@@ -289,6 +228,17 @@ electron.app.on('ready', () => {
         }
     });
 
+    if (process.platform === 'win32') {
+        // Handle forward/backward mouse buttons in Windows
+        mainWindow.on('app-command', (e, cmd) => {
+            if (cmd === 'browser-backward' && mainWindow.webContents.canGoBack()) {
+                mainWindow.webContents.goBack();
+            } else if (cmd === 'browser-forward' && mainWindow.webContents.canGoForward()) {
+                mainWindow.webContents.goForward();
+            }
+        });
+    }
+
     webContentsHandler(mainWindow.webContents);
     mainWindowState.manage(mainWindow);
 });
@@ -302,7 +252,10 @@ electron.app.on('activate', () => {
 });
 
 electron.app.on('before-quit', () => {
-    appQuitting = true;
+    global.appQuitting = true;
+    if (mainWindow) {
+        mainWindow.webContents.send('before-quit');
+    }
 });
 
 // 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 039e7133fa..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();
         }
     };
 
@@ -54,41 +54,46 @@ exports.create = function(win, config) {
         },
     ]);
 
-    trayIcon = new Tray(config.icon_path);
+    const defaultIcon = nativeImage.createFromPath(config.icon_path);
+
+    trayIcon = new Tray(defaultIcon);
     trayIcon.setToolTip(config.brand);
     trayIcon.setContextMenu(contextMenu);
     trayIcon.on('click', toggleWin);
 
     let lastFavicon = null;
-    win.webContents.on('page-favicon-updated', async function(ev, favicons) {
-        let newFavicon = config.icon_path;
-        if (favicons && favicons.length > 0 && favicons[0].startsWith('data:')) {
-            newFavicon = favicons[0];
+    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);
+                trayIcon.setImage(defaultIcon);
+                lastFavicon = null;
+            }
+            return;
         }
 
         // No need to change, shortcut
-        if (newFavicon === lastFavicon) return;
-        lastFavicon = newFavicon;
+        if (favicons[0] === lastFavicon) return;
+        lastFavicon = favicons[0];
 
-        // if its not default we have to construct into nativeImage
-        if (newFavicon !== config.icon_path) {
-            newFavicon = nativeImage.createFromDataURL(favicons[0]);
+        let newFavicon = nativeImage.createFromDataURL(favicons[0]);
 
-            if (process.platform === 'win32') {
-                try {
-                    const icoPath = path.join(app.getPath('temp'), 'win32_riot_icon.ico')
-                    const icoBuf = await pngToIco(newFavicon.toPNG());
-                    fs.writeFileSync(icoPath, icoBuf);
-                    newFavicon = icoPath;
-                } catch (e) {console.error(e);}
+        // Windows likes ico's too much.
+        if (process.platform === 'win32') {
+            try {
+                const icoPath = path.join(app.getPath('temp'), 'win32_riot_icon.ico');
+                fs.writeFileSync(icoPath, await pngToIco(newFavicon.toPNG()));
+                newFavicon = nativeImage.createFromPath(icoPath);
+            } catch (e) {
+                console.error("Failed to make win32 ico", e);
             }
         }
 
         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/karma.conf.js b/karma.conf.js
index 1e04366313..3b415b1ae6 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -84,13 +84,23 @@ module.exports = function (config) {
         // available preprocessors:
         // https://npmjs.org/browse/keyword/karma-preprocessor
         preprocessors: {
-            '{src,test}/**/*.js': ['webpack'],
+            '{src,test}/**/*.js': ['webpack', 'sourcemap'],
         },
 
         // test results reporter to use
-        // possible values: 'dots', 'progress'
         // available reporters: https://npmjs.org/browse/keyword/karma-reporter
-        reporters: ['progress', 'junit'],
+        reporters: ['logcapture', 'spec', 'junit', 'summary'],
+
+        specReporter: {
+            suppressErrorSummary: false, // do print error summary
+            suppressFailed: false, // do print information about failed tests
+            suppressPassed: false, // do print information about passed tests
+            showSpecTiming: true, // print the time elapsed for each spec
+        },
+
+        client: {
+            captureLogs: true,
+        },
 
         // web server port
         port: 9876,
@@ -113,8 +123,23 @@ module.exports = function (config) {
         browsers: [
             'Chrome',
             //'PhantomJS',
+            //'ChromeHeadless'
         ],
 
+        customLaunchers: {
+            'ChromeHeadless': {
+                base: 'Chrome',
+                flags: [
+                    // '--no-sandbox',
+                    // See https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md
+                    '--headless',
+                    '--disable-gpu',
+                    // Without a remote debugging port, Google Chrome exits immediately.
+                    '--remote-debugging-port=9222',
+                ],
+            }
+        },
+
         // Continuous Integration mode
         // if true, Karma captures browsers, runs the tests and exits
         // singleRun: false,
diff --git a/package.json b/package.json
index f51290061d..44b1dc5948 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
   "name": "riot-web",
   "productName": "Riot",
   "main": "electron_app/src/electron-main.js",
-  "version": "0.11.4",
+  "version": "0.12.1",
   "description": "A feature-rich client for Matrix.org",
   "author": "Vector Creations Ltd.",
   "repository": {
@@ -31,8 +31,8 @@
     "build:res": "node scripts/copy-res.js",
     "build:modernizr": "modernizr -c .modernizr.json -d src/vector/modernizr.js",
     "build:compile": "npm run reskindex && babel --source-maps -d lib src",
-    "build:bundle": "cross-env NODE_ENV=production webpack -p --progress",
-    "build:bundle:dev": "webpack --optimize-occurence-order --progress",
+    "build:bundle": "cross-env NODE_ENV=production webpack -p --progress --bail",
+    "build:bundle:dev": "webpack --optimize-occurence-order --progress --bail",
     "build:electron": "npm run clean && npm run build && npm run install:electron && build -wml --ia32 --x64",
     "build": "npm run reskindex && npm run build:res && npm run build:bundle",
     "build:dev": "npm run reskindex && npm run build:res && npm run build:bundle:dev",
@@ -48,15 +48,16 @@
     "lintall": "eslint src/ test/",
     "clean": "rimraf lib webapp electron_app/dist",
     "prepublish": "npm run build:compile",
-    "test": "karma start --single-run=true --autoWatch=false --browsers PhantomJS --colors=false",
+    "test": "karma start --single-run=true --autoWatch=false --browsers ChromeHeadless",
     "test-multi": "karma start"
   },
   "dependencies": {
     "babel-polyfill": "^6.5.0",
     "babel-runtime": "^6.11.6",
+    "bluebird": "^3.5.0",
     "browser-request": "^0.3.3",
     "classnames": "^2.1.2",
-    "draft-js": "^0.8.1",
+    "draft-js": "^0.11.0-alpha",
     "extract-text-webpack-plugin": "^0.9.1",
     "favico.js": "^0.3.10",
     "filesize": "3.5.6",
@@ -65,15 +66,14 @@
     "gfm.css": "^1.1.1",
     "highlight.js": "^9.0.0",
     "linkifyjs": "^2.1.3",
-    "matrix-js-sdk": "0.7.13",
-    "matrix-react-sdk": "0.9.7",
+    "matrix-js-sdk": "0.8.1",
+    "matrix-react-sdk": "0.10.1",
     "modernizr": "^3.1.0",
     "pako": "^1.0.5",
-    "q": "^1.4.1",
-    "react": "^15.4.0",
+    "react": "^15.6.0",
     "react-dnd": "^2.1.4",
     "react-dnd-html5-backend": "^2.1.2",
-    "react-dom": "^15.4.0",
+    "react-dom": "^15.6.0",
     "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
     "sanitize-html": "^1.11.1",
     "text-encoding-utf-8": "^1.0.1",
@@ -88,7 +88,7 @@
     "babel-eslint": "^6.1.0",
     "babel-loader": "^6.2.5",
     "babel-plugin-add-module-exports": "^0.2.1",
-    "babel-plugin-transform-async-to-generator": "^6.16.0",
+    "babel-plugin-transform-async-to-bluebird": "^1.1.1",
     "babel-plugin-transform-class-properties": "^6.16.0",
     "babel-plugin-transform-object-rest-spread": "^6.16.0",
     "babel-plugin-transform-runtime": "^6.15.0",
@@ -114,18 +114,22 @@
     "fs-extra": "^0.30.0",
     "html-webpack-plugin": "^2.24.0",
     "json-loader": "^0.5.3",
-    "karma": "^0.13.22",
+    "karma": "^1.7.0",
     "karma-chrome-launcher": "^0.2.3",
     "karma-cli": "^0.1.2",
     "karma-junit-reporter": "^0.4.1",
+    "karma-logcapture-reporter": "0.0.1",
     "karma-mocha": "^0.2.2",
-    "karma-phantomjs-launcher": "^1.0.0",
+    "karma-sourcemap-loader": "^0.3.7",
+    "karma-spec-reporter": "0.0.31",
+    "karma-summary-reporter": "^1.3.3",
     "karma-webpack": "^1.7.0",
+    "matrix-mock-request": "^1.2.0",
+    "matrix-react-test-utils": "^0.2.0",
     "minimist": "^1.2.0",
     "mkdirp": "^0.5.1",
     "mocha": "^2.4.5",
     "parallelshell": "^1.2.0",
-    "phantomjs-prebuilt": "^2.1.7",
     "postcss-extend": "^1.0.5",
     "postcss-import": "^9.0.0",
     "postcss-loader": "^1.2.2",
@@ -135,7 +139,7 @@
     "postcss-simple-vars": "^3.0.0",
     "postcss-strip-inline-comments": "^0.1.5",
     "react-addons-perf": "^15.4.0",
-    "react-addons-test-utils": "^15.4.0",
+    "react-addons-test-utils": "^15.6.0",
     "rimraf": "^2.4.3",
     "source-map-loader": "^0.1.5",
     "webpack": "^1.12.14",
diff --git a/release.sh b/release.sh
index 8ae307f7e2..136750181e 100755
--- a/release.sh
+++ b/release.sh
@@ -11,7 +11,7 @@ cd `dirname $0`
 
 for i in matrix-js-sdk matrix-react-sdk
 do
-    depver=`cat package.json | jq -r .dependencies.\"$i\"`
+    depver=`cat package.json | jq -r .dependencies[\"$i\"]`
     latestver=`npm show $i version`
     if [ "$depver" != "$latestver" ]
     then
diff --git a/scripts/copy-res.js b/scripts/copy-res.js
index e8f6684d21..fa52492e00 100755
--- a/scripts/copy-res.js
+++ b/scripts/copy-res.js
@@ -9,24 +9,27 @@
 // This could readily be automated, but it's nice to explicitly
 // control when we languages are available.
 const INCLUDE_LANGS = [
+    {'value': 'da', 'label': 'Dansk'},
+    {'value': 'de_DE', 'label': 'Deutsch'},
     {'value': 'en_EN', 'label': 'English'},
     {'value': 'en_US', 'label': 'English (US)'},
-    {'value': 'da', 'label': 'Dansk'},
     {'value': 'el', 'label': 'Ελληνικά'},
     {'value': 'eo', 'label': 'Esperanto'},
-    {'value': 'nl', 'label': 'Nederlands'},
-    {'value': 'de_DE', 'label': 'Deutsch'},
+    {'value': 'es', 'label': 'Español'},
+    {'value': 'eu', 'label': 'Euskal'},
     {'value': 'fr', 'label': 'Français'},
     {'value': 'hu', 'label': 'Magyar'},
     {'value': 'ko', 'label': '한국어'},
+    {'value': 'lv', 'label': 'Latviešu'},
     {'value': 'nb_NO', 'label': 'Norwegian Bokmål'},
+    {'value': 'nl', 'label': 'Nederlands'},
     {'value': 'pl', 'label': 'Polski'},
     {'value': 'pt', 'label': 'Português'},
     {'value': 'pt_BR', 'label': 'Português do Brasil'},
     {'value': 'ru', 'label': 'Русский'},
     {'value': 'sv', 'label': 'Svenska'},
-    {'value': 'es', 'label': 'Español'},
     {'value': 'th', 'label': 'ไทย'},
+    {'value': 'te', 'label': 'తెలుగు'},
     {'value': 'tr', 'label': 'Türk'},
     {'value': 'zh_Hans', 'label': '简体中文'}, // simplified chinese
     {'value': 'zh_Hant', 'label': '繁體中文'}, // traditional chinese
diff --git a/scripts/deploy.py b/scripts/deploy.py
index c96b46e81f..cc350e4c9a 100755
--- a/scripts/deploy.py
+++ b/scripts/deploy.py
@@ -63,7 +63,8 @@ class Deployer:
         self.packages_path = "."
         self.bundles_path = None
         self.should_clean = False
-        self.config_location = None
+        # filename -> symlink path e.g 'config.localhost.json' => '../localhost/config.json'
+        self.config_locations = {}
         self.verify_signature = True
 
     def deploy(self, tarball, extract_path):
@@ -95,11 +96,12 @@ class Deployer:
 
         print ("Extracted into: %s" % extracted_dir)
 
-        if self.config_location:
-            create_relative_symlink(
-                target=self.config_location,
-                linkname=os.path.join(extracted_dir, 'config.json')
-            )
+        if self.config_locations:
+            for config_filename, config_loc in self.config_locations.iteritems():
+                create_relative_symlink(
+                    target=config_loc,
+                    linkname=os.path.join(extracted_dir, config_filename)
+                )
 
         if self.bundles_path:
             extracted_bundles = os.path.join(extracted_dir, 'bundles')
@@ -178,6 +180,8 @@ if __name__ == "__main__":
     deployer.packages_path = args.packages_dir
     deployer.bundles_path = args.bundles_dir
     deployer.should_clean = args.clean
-    deployer.config_location = args.config
+    deployer.config_locations = {
+        "config.json": args.config,
+    }
 
     deployer.deploy(args.tarball, args.extract_path)
diff --git a/scripts/fetch-develop.deps.sh b/scripts/fetch-develop.deps.sh
index 4fa1a4a22c..e2d40341a0 100755
--- a/scripts/fetch-develop.deps.sh
+++ b/scripts/fetch-develop.deps.sh
@@ -49,42 +49,47 @@ function dodep() {
         [ "$curbranch" != 'develop' ] && clone $org $repo develop
     } || return $?
 
-    (
-        cd $repo
-        echo "$repo set to branch "`git rev-parse --abbrev-ref HEAD`
-    )
+    echo "$repo set to branch "`git -C "$repo" rev-parse --abbrev-ref HEAD`
 
     mkdir -p node_modules
     rm -r "node_modules/$repo" 2>/dev/null || true
     ln -sv "../$repo" node_modules/
+
+    (
+        cd $repo
+        npm install
+    )
 }
 
+##############################
+
 echo -en 'travis_fold:start:matrix-js-sdk\r'
 echo 'Setting up matrix-js-sdk'
 
 dodep matrix-org matrix-js-sdk
-(
-    cd node_modules/matrix-js-sdk
-    npm install
-)
 
 echo -en 'travis_fold:end:matrix-js-sdk\r'
 
+##############################
+
 echo -en 'travis_fold:start:matrix-react-sdk\r'
 echo 'Setting up matrix-react-sdk'
 
 dodep matrix-org matrix-react-sdk
 
-mkdir -p node_modules/matrix-react-sdk/node_modules
+# replace the version of js-sdk that got pulled into react-sdk with a symlink
+# to our version. Make sure to do this *after* doing 'npm i' in react-sdk,
+# otherwise npm helpfully moves another-json from matrix-js-sdk/node_modules
+# into matrix-react-sdk/node_modules.
+#
+# (note this matches the instructions in the README.)
+rm -r node_modules/matrix-react-sdk/node_modules/matrix-js-sdk
 ln -s ../../matrix-js-sdk node_modules/matrix-react-sdk/node_modules/
 
-(
-    cd node_modules/matrix-react-sdk
-    npm install
-)
-
 echo -en 'travis_fold:end:matrix-react-sdk\r'
 
+##############################
+
 # Link the reskindex binary in place: if we used npm link,
 # npm would do this for us, but we don't because we'd have
 # to define the npm prefix somewhere so it could put the
diff --git a/scripts/jenkins.sh b/scripts/jenkins.sh
index 4f2e940564..7b5b4c8e2e 100755
--- a/scripts/jenkins.sh
+++ b/scripts/jenkins.sh
@@ -8,8 +8,11 @@ nvm use 6
 
 set -x
 
-# check out corresponding branches of dependencies
-`dirname $0`/fetch-develop.deps.sh
+# check out corresponding branches of dependencies.
+#
+# clone the deps with depth 1: we know we will only ever need that one
+# commit.
+`dirname $0`/fetch-develop.deps.sh --depth 1
 
 npm install
 
diff --git a/scripts/redeploy.py b/scripts/redeploy.py
index 598f6c5265..e10a48c008 100755
--- a/scripts/redeploy.py
+++ b/scripts/redeploy.py
@@ -13,6 +13,7 @@
 from __future__ import print_function
 import json, requests, tarfile, argparse, os, errno
 import time
+import traceback
 from urlparse import urljoin
 
 from flask import Flask, jsonify, request, abort
@@ -124,6 +125,7 @@ def fetch_jenkins_build(job_name, build_num):
     try:
         extracted_dir = deploy_tarball(tar_gz_url, build_dir)
     except DeployException as e:
+        traceback.print_exc()
         abort(400, e.message)
 
     create_symlink(source=extracted_dir, linkname=arg_symlink)
@@ -185,10 +187,16 @@ if __name__ == "__main__":
             to the /vector directory INSIDE the tarball."
         )
     )
+
+    def _raise(ex):
+        raise ex
+
+    # --config config.json=../../config.json --config config.localhost.json=./localhost.json
     parser.add_argument(
-        "--config", dest="config", help=(
-            "Write a symlink to config.json in the extracted tarball. \
-            To this location."
+        "--config", action="append", dest="configs",
+        type=lambda kv: kv.split("=", 1) if "=" in kv else _raise(Exception("Missing =")), help=(
+            "A list of configs to symlink into the extracted tarball. \
+            For example, --config config.json=../config.json config2.json=../test/config.json"
         )
     )
     parser.add_argument(
@@ -212,7 +220,8 @@ if __name__ == "__main__":
     deployer = Deployer()
     deployer.bundles_path = args.bundles_dir
     deployer.should_clean = args.clean
-    deployer.config_location = args.config
+    deployer.config_locations = dict(args.configs) if args.configs else {}
+
 
     # we don't pgp-sign jenkins artifacts; instead we rely on HTTPS access to
     # the jenkins server (and the jenkins server not being compromised and/or
@@ -225,13 +234,13 @@ if __name__ == "__main__":
         deploy_tarball(args.tarball_uri, build_dir)
     else:
         print(
-            "Listening on port %s. Extracting to %s%s. Symlinking to %s. Jenkins URL: %s. Config location: %s" %
+            "Listening on port %s. Extracting to %s%s. Symlinking to %s. Jenkins URL: %s. Config locations: %s" %
             (args.port,
              arg_extract_path,
              " (clean after)" if deployer.should_clean else "",
              arg_symlink,
              arg_jenkins_url,
-             deployer.config_location,
+             deployer.config_locations,
             )
         )
         app.run(host="0.0.0.0", port=args.port, debug=True)
diff --git a/src/VectorConferenceHandler.js b/src/VectorConferenceHandler.js
index f34a7b732b..933f59937e 100644
--- a/src/VectorConferenceHandler.js
+++ b/src/VectorConferenceHandler.js
@@ -16,7 +16,7 @@ limitations under the License.
 
 "use strict";
 
-var q = require("q");
+import Promise from 'bluebird';
 var Matrix = require("matrix-js-sdk");
 var Room = Matrix.Room;
 var CallHandler = require('matrix-react-sdk/lib/CallHandler');
@@ -53,11 +53,11 @@ ConferenceCall.prototype._joinConferenceUser = function() {
     // Make sure the conference user is in the group chat room
     var groupRoom = this.client.getRoom(this.groupRoomId);
     if (!groupRoom) {
-        return q.reject("Bad group room ID");
+        return Promise.reject("Bad group room ID");
     }
     var member = groupRoom.getMember(this.confUserId);
     if (member && member.membership === "join") {
-        return q();
+        return Promise.resolve();
     }
     return this.client.invite(this.groupRoomId, this.confUserId);
 };
@@ -75,7 +75,7 @@ ConferenceCall.prototype._getConferenceUserRoom = function() {
         }
     }
     if (confRoom) {
-        return q(confRoom);
+        return Promise.resolve(confRoom);
     }
     return this.client.createRoom({
         preset: "private_chat",
diff --git a/src/components/structures/HomePage.js b/src/components/structures/HomePage.js
index 2311cc1f30..bdba55eb0e 100644
--- a/src/components/structures/HomePage.js
+++ b/src/components/structures/HomePage.js
@@ -52,6 +52,8 @@ module.exports = React.createClass({
     },
 
     componentWillMount: function() {
+        this._unmounted = false;
+
         if (this.props.teamToken && this.props.teamServerUrl) {
             this.setState({
                 iframeSrc: `${this.props.teamServerUrl}/static/${this.props.teamToken}/home.html`
@@ -67,9 +69,14 @@ module.exports = React.createClass({
             request(
                 { method: "GET", url: src },
                 (err, response, body) => {
+                    if (this._unmounted) {
+                        return;
+                    }
+
                     if (err || response.status < 200 || response.status >= 300) {
-                        console.log(err);
-                        this.setState({ page: "Couldn't load home page" });
+                        console.warn(`Error loading home page: ${err}`);
+                        this.setState({ page: _t("Couldn't load home page") });
+                        return;
                     }
 
                     body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1));
@@ -79,6 +86,10 @@ module.exports = React.createClass({
         }
     },
 
+    componentWillUnmount: function() {
+        this._unmounted = true;
+    },
+
     render: function() {
         if (this.state.iframeSrc) {
             return (
diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js
index 77338404fa..4539df1ffa 100644
--- a/src/components/structures/LeftPanel.js
+++ b/src/components/structures/LeftPanel.js
@@ -16,17 +16,16 @@ limitations under the License.
 
 'use strict';
 
-var React = require('react');
-var DragDropContext = require('react-dnd').DragDropContext;
-var HTML5Backend = require('react-dnd-html5-backend');
-var sdk = require('matrix-react-sdk')
-var dis = require('matrix-react-sdk/lib/dispatcher');
+import React from 'react';
+import { DragDropContext } from 'react-dnd';
+import HTML5Backend from 'react-dnd-html5-backend';
+import KeyCode from 'matrix-react-sdk/lib/KeyCode';
+import sdk from 'matrix-react-sdk';
+import dis from 'matrix-react-sdk/lib/dispatcher';
 import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
-
-var VectorConferenceHandler = require('../../VectorConferenceHandler');
-var CallHandler = require("matrix-react-sdk/lib/CallHandler");
-
+import CallHandler from 'matrix-react-sdk/lib/CallHandler';
 import AccessibleButton from 'matrix-react-sdk/lib/components/views/elements/AccessibleButton';
+import VectorConferenceHandler from '../../VectorConferenceHandler';
 
 var LeftPanel = React.createClass({
     displayName: 'LeftPanel',
@@ -42,6 +41,10 @@ var LeftPanel = React.createClass({
         };
     },
 
+    componentWillMount: function() {
+        this.focusedElement = null;
+    },
+
     componentDidMount: function() {
         this.dispatcherRef = dis.register(this.onAction);
     },
@@ -64,6 +67,91 @@ var LeftPanel = React.createClass({
         }
     },
 
+    _onFocus: function(ev) {
+        this.focusedElement = ev.target;
+    },
+
+    _onBlur: function(ev) {
+        this.focusedElement = null;
+    },
+
+    _onKeyDown: function(ev) {
+        if (!this.focusedElement) return;
+        let handled = false;
+
+        switch (ev.keyCode) {
+            case KeyCode.UP:
+                this._onMoveFocus(true);
+                handled = true;
+                break;
+            case KeyCode.DOWN:
+                this._onMoveFocus(false);
+                handled = true;
+                break;
+        }
+
+        if (handled) {
+            ev.stopPropagation();
+            ev.preventDefault();
+        }
+    },
+
+    _onMoveFocus: function(up) {
+        var element = this.focusedElement;
+
+        // unclear why this isn't needed
+        // var descending = (up == this.focusDirection) ? this.focusDescending : !this.focusDescending;
+        // this.focusDirection = up;
+
+        var descending = false; // are we currently descending or ascending through the DOM tree?
+        var classes;
+
+        do {
+            var child = up ? element.lastElementChild : element.firstElementChild;
+            var sibling = up ? element.previousElementSibling : element.nextElementSibling;
+
+            if (descending) {
+                if (child) {
+                    element = child;
+                }
+                else if (sibling) {
+                    element = sibling;
+                }
+                else {
+                    descending = false;
+                    element = element.parentElement;
+                }
+            }
+            else {
+                if (sibling) {
+                    element = sibling;
+                    descending = true;
+                }
+                else {
+                    element = element.parentElement;
+                }
+            }
+
+            if (element) {
+                classes = element.classList;
+                if (classes.contains("mx_LeftPanel")) { // we hit the top
+                    element = up ? element.lastElementChild : element.firstElementChild;
+                    descending = true;
+                }
+            }
+
+        } while(element && !(
+            classes.contains("mx_RoomTile") ||
+            classes.contains("mx_SearchBox_search") ||
+            classes.contains("mx_RoomSubList_ellipsis")));
+
+        if (element) {
+            element.focus();
+            this.focusedElement = element;
+            this.focusedDescending = descending;
+        }
+    },
+
     _recheckCallElement: function(selectedRoomId) {
         // if we aren't viewing a room with an ongoing call, but there is an
         // active call, show the call element - we need to do this to make
@@ -126,7 +214,8 @@ var LeftPanel = React.createClass({
         }
 
         return (
-