diff --git a/.babelrc b/.babelrc
index 8c7b6626..6ba0e0da 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/package.json b/package.json
index 83d9bd16..2b1d6e82 100644
--- a/package.json
+++ b/package.json
@@ -48,12 +48,13 @@
     "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 ChromeHeadless --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",
@@ -69,11 +70,10 @@
     "matrix-react-sdk": "0.9.7",
     "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",
@@ -120,7 +120,8 @@
     "karma-junit-reporter": "^0.4.1",
     "karma-mocha": "^0.2.2",
     "karma-webpack": "^1.7.0",
-    "matrix-mock-request": "^1.0.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",
@@ -134,7 +135,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/src/VectorConferenceHandler.js b/src/VectorConferenceHandler.js
index f34a7b73..933f5993 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/RoomDirectory.js b/src/components/structures/RoomDirectory.js
index e7d68c39..ea3aa5a2 100644
--- a/src/components/structures/RoomDirectory.js
+++ b/src/components/structures/RoomDirectory.js
@@ -28,7 +28,7 @@ var linkify = require('linkifyjs');
 var linkifyString = require('linkifyjs/string');
 var linkifyMatrix = require('matrix-react-sdk/lib/linkify-matrix');
 var sanitizeHtml = require('sanitize-html');
-var q = require('q');
+import Promise from 'bluebird';
 
 import { _t } from 'matrix-react-sdk/lib/languageHandler';
 
@@ -117,7 +117,7 @@ module.exports = React.createClass({
     },
 
     getMoreRooms: function() {
-        if (!MatrixClientPeg.get()) return q();
+        if (!MatrixClientPeg.get()) return Promise.resolve();
 
         const my_filter_string = this.state.filterString;
         const my_server = this.state.roomServer;
@@ -266,7 +266,7 @@ module.exports = React.createClass({
     },
 
     onFillRequest: function(backwards) {
-        if (backwards || !this.nextBatch) return q(false);
+        if (backwards || !this.nextBatch) return Promise.resolve(false);
 
         return this.getMoreRooms();
     },
diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js
index a7b19689..4d08e833 100644
--- a/src/components/views/context_menus/RoomTileContextMenu.js
+++ b/src/components/views/context_menus/RoomTileContextMenu.js
@@ -17,7 +17,7 @@ limitations under the License.
 
 'use strict';
 
-import q from 'q';
+import Promise from 'bluebird';
 import React from 'react';
 import classNames from 'classnames';
 import sdk from 'matrix-react-sdk';
@@ -61,7 +61,7 @@ module.exports = React.createClass({
         const roomId = this.props.room.roomId;
         var cli = MatrixClientPeg.get();
         if (!cli.isGuest()) {
-            q.delay(500).then(function() {
+            Promise.delay(500).then(function() {
                 if (tagNameOff !== null && tagNameOff !== undefined) {
                     cli.deleteRoomTag(roomId, tagNameOff).finally(function() {
                         // Close the context menu
@@ -212,7 +212,7 @@ module.exports = React.createClass({
         RoomNotifs.setRoomNotifsState(this.props.room.roomId, newState).done(() => {
             // delay slightly so that the user can see their state change
             // before closing the menu
-            return q.delay(500).then(() => {
+            return Promise.delay(500).then(() => {
                 if (this._unmounted) return;
                 // Close the context menu
                 if (this.props.onFinished) {
diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js
index 1b8de52d..451a28b8 100644
--- a/src/components/views/settings/Notifications.js
+++ b/src/components/views/settings/Notifications.js
@@ -17,7 +17,7 @@ limitations under the License.
 'use strict';
 var React = require('react');
 import { _t, _tJsx } from 'matrix-react-sdk/lib/languageHandler';
-var q = require("q");
+import Promise from 'bluebird';
 var sdk = require('matrix-react-sdk');
 var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
 var UserSettingsStore = require('matrix-react-sdk/lib/UserSettingsStore');
@@ -236,7 +236,7 @@ module.exports = React.createClass({
                 }
             }
 
-            q.all(deferreds).done(function() {
+            Promise.all(deferreds).done(function() {
                 self._refreshFromServer();
             }, function(error) {
                 var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@@ -306,7 +306,7 @@ module.exports = React.createClass({
             }
         }
 
-        q.all(deferreds).done(function(resps) {
+        Promise.all(deferreds).done(function(resps) {
             self._refreshFromServer();
         }, function(error) {
             var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@@ -361,7 +361,7 @@ module.exports = React.createClass({
         }
 
         // Then, add the new ones
-        q.all(removeDeferreds).done(function(resps) {
+        Promise.all(removeDeferreds).done(function(resps) {
             var deferreds = [];
 
             var pushRuleVectorStateKind = self.state.vectorContentRules.vectorState;
@@ -399,7 +399,7 @@ module.exports = React.createClass({
                 }
             }
 
-            q.all(deferreds).done(function(resps) {
+            Promise.all(deferreds).done(function(resps) {
                 self._refreshFromServer();
             }, onError);
         }, onError);
@@ -431,7 +431,9 @@ module.exports = React.createClass({
                             'global', kind, LEGACY_RULES[rule.rule_id], portLegacyActions(rule.actions)
                         ).then( function() {
                             return cli.deletePushRule('global', kind, rule.rule_id);
-                        })
+                        }).catch( (e) => {
+                            console.warn(`Error when porting legacy rule: ${e}`);
+                        });
                     }(kind, rule));
                 }
             }
@@ -440,7 +442,7 @@ module.exports = React.createClass({
         if (needsUpdate.length > 0) {
             // If some of the rules need to be ported then wait for the porting
             // to happen and then fetch the rules again.
-            return q.allSettled(needsUpdate).then( function() {
+            return Promise.all(needsUpdate).then( function() {
                 return cli.getPushRules();
             });
         } else {
@@ -594,7 +596,7 @@ module.exports = React.createClass({
             self.setState({pushers: resp.pushers});
         });
 
-        q.all([pushRulesPromise, pushersPromise]).then(function() {
+        Promise.all([pushRulesPromise, pushersPromise]).then(function() {
             self.setState({
                 phase: self.phases.DISPLAY
             });
diff --git a/src/skins/vector/css/_components.scss b/src/skins/vector/css/_components.scss
index a8ac1878..00c19b13 100644
--- a/src/skins/vector/css/_components.scss
+++ b/src/skins/vector/css/_components.scss
@@ -41,6 +41,7 @@
 @import "./matrix-react-sdk/views/messages/_RoomAvatarEvent.scss";
 @import "./matrix-react-sdk/views/messages/_TextualEvent.scss";
 @import "./matrix-react-sdk/views/messages/_UnknownBody.scss";
+@import "./matrix-react-sdk/views/rooms/_AppsDrawer.scss";
 @import "./matrix-react-sdk/views/rooms/_Autocomplete.scss";
 @import "./matrix-react-sdk/views/rooms/_EntityTile.scss";
 @import "./matrix-react-sdk/views/rooms/_EventTile.scss";
@@ -56,9 +57,7 @@
 @import "./matrix-react-sdk/views/rooms/_RoomSettings.scss";
 @import "./matrix-react-sdk/views/rooms/_RoomTile.scss";
 @import "./matrix-react-sdk/views/rooms/_SearchableEntityList.scss";
-@import "./matrix-react-sdk/views/rooms/_TabCompleteBar.scss";
 @import "./matrix-react-sdk/views/rooms/_TopUnreadMessagesBar.scss";
-@import "./matrix-react-sdk/views/rooms/_AppsDrawer.scss";
 @import "./matrix-react-sdk/views/settings/_DevicesPanel.scss";
 @import "./matrix-react-sdk/views/settings/_IntegrationsManager.scss";
 @import "./matrix-react-sdk/views/voip/_CallView.scss";
diff --git a/src/skins/vector/css/matrix-react-sdk/structures/_RoomStatusBar.scss b/src/skins/vector/css/matrix-react-sdk/structures/_RoomStatusBar.scss
index 1ae2a47c..d4b425ee 100644
--- a/src/skins/vector/css/matrix-react-sdk/structures/_RoomStatusBar.scss
+++ b/src/skins/vector/css/matrix-react-sdk/structures/_RoomStatusBar.scss
@@ -140,11 +140,6 @@ limitations under the License.
     cursor: pointer;
 }
 
-.mx_RoomStatusBar_tabCompleteBar {
-    padding-top: 10px;
-    color: $primary-fg-color;
-}
-
 .mx_RoomStatusBar_typingBar {
     height: 50px;
     line-height: 50px;
@@ -155,26 +150,6 @@ limitations under the License.
     display: block;
 }
 
-.mx_RoomStatusBar_tabCompleteWrapper {
-    display: flex;
-    height: 26px;
-}
-
-.mx_RoomStatusBar_tabCompleteWrapper .mx_TabCompleteBar {
-    flex: 1 1 auto;
-}
-
-.mx_RoomStatusBar_tabCompleteEol {
-    flex: 0 0 auto;
-    color: $accent-color;
-}
-
-.mx_RoomStatusBar_tabCompleteEol object {
-    vertical-align: middle;
-    margin-right: 8px;
-    margin-top: -2px;
-}
-
 .mx_MatrixChat_useCompactLayout {
     .mx_RoomStatusBar {
         min-height: 40px;
diff --git a/src/skins/vector/css/matrix-react-sdk/views/elements/_RichText.scss b/src/skins/vector/css/matrix-react-sdk/views/elements/_RichText.scss
index 2dcd8136..1392ec64 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/elements/_RichText.scss
+++ b/src/skins/vector/css/matrix-react-sdk/views/elements/_RichText.scss
@@ -3,17 +3,21 @@
 // --Matthew
 
 .mx_UserPill {
-    color: white;
+    color: $accent-fg-color;
     background-color: $accent-color;
-    padding: 2px 8px;
+    padding: 1px 5px 0px 2px;
     border-radius: 16px;
 }
 
+.mx_UserPill img, .mx_RoomPill img {
+    vertical-align: -2px;
+    margin-right: 1px
+}
+
 .mx_RoomPill {
-    background-color: white;
-    color: $accent-color;
-    border: 1px solid $accent-color;
-    padding: 2px 8px;
+    background-color: $rte-room-pill-color;
+    color: $accent-fg-color;
+    padding: 1px 5px 0px 2px;
     border-radius: 16px;
 }
 
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/_AppsDrawer.scss b/src/skins/vector/css/matrix-react-sdk/views/rooms/_AppsDrawer.scss
index 0fcabac1..7d1ac628 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/_AppsDrawer.scss
+++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_AppsDrawer.scss
@@ -76,6 +76,16 @@ limitations under the License.
 .mx_AppTileMenuBarWidget {
     // pointer-events: none;
     cursor: pointer;
+    width: 10px;
+    height: 10px;
+    padding: 1px;
+    transition-duration: 500ms;
+    border: 1px solid transparent;
+}
+
+.mx_AppTileMenuBarWidget:hover {
+    border: 1px solid $primary-hairline-color;
+    border-radius: 2px;
 }
 
 .mx_AppTileBody iframe {
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/_MessageComposer.scss b/src/skins/vector/css/matrix-react-sdk/views/rooms/_MessageComposer.scss
index 6c2216dd..43a58030 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/_MessageComposer.scss
+++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_MessageComposer.scss
@@ -100,6 +100,11 @@ limitations under the License.
     word-break: break-word;
 }
 
+.mx_MessageComposer_input .DraftEditor-root .DraftEditor-editorContainer {
+    /* Ensure mx_UserPill and mx_RoomPill (see _RichText) are not obscured from the top */
+    padding-top: 2px;
+}
+
 .mx_MessageComposer_input blockquote {
     color: $blockquote-fg-color;
     margin: 0 0 16px;
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/_TabCompleteBar.scss b/src/skins/vector/css/matrix-react-sdk/views/rooms/_TabCompleteBar.scss
deleted file mode 100644
index 5dcbd21a..00000000
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/_TabCompleteBar.scss
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
-Copyright 2015, 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.
-*/
-
-.mx_TabCompleteBar {
-    overflow: hidden;
-}
-
-.mx_TabCompleteBar_item {
-    display: inline-block;
-    margin-right: 15px;
-    margin-bottom: 2px;
-    cursor: pointer;
-}
-
-.mx_TabCompleteBar_command {
-    margin-right: 8px;
-    background-color: $accent-color;
-    padding-left: 8px;
-    padding-right: 8px;
-    padding-top: 2px;
-    padding-bottom: 2px;
-    margin-bottom: 6px;
-    border-radius: 30px;
-    position: relative;
-    top: 1px;
-}
-
-.mx_TabCompleteBar_command .mx_TabCompleteBar_text {
-    opacity: 1.0;
-    vertical-align: initial;
-    color: $accent-fg-color;
-}
-
-.mx_TabCompleteBar_item img {
-    margin-right: 8px;
-    vertical-align: middle;
-}
-
-.mx_TabCompleteBar_text {
-    color: $primary-fg-color;
-    vertical-align: middle;
-    opacity: 0.5;
-}
diff --git a/src/skins/vector/css/themes/_base.scss b/src/skins/vector/css/themes/_base.scss
index fc24af93..9701a48f 100644
--- a/src/skins/vector/css/themes/_base.scss
+++ b/src/skins/vector/css/themes/_base.scss
@@ -78,6 +78,7 @@ $voip-accept-color: #80f480;
 
 $rte-bg-color: #e9e9e9;
 $rte-code-bg-color: rgba(0, 0, 0, 0.04);
+$rte-room-pill-color: #aaa;
 
 // ********************
 
diff --git a/src/vector/index.js b/src/vector/index.js
index 81d329c0..c0cb6410 100644
--- a/src/vector/index.js
+++ b/src/vector/index.js
@@ -65,7 +65,7 @@ var sdk = require("matrix-react-sdk");
 const PlatformPeg = require("matrix-react-sdk/lib/PlatformPeg");
 sdk.loadSkin(require('../component-index'));
 var VectorConferenceHandler = require('../VectorConferenceHandler');
-var q = require('q');
+import Promise from 'bluebird';
 var request = require('browser-request');
 import * as UserSettingsStore from 'matrix-react-sdk/lib/UserSettingsStore';
 import * as languageHandler from 'matrix-react-sdk/lib/languageHandler';
@@ -187,11 +187,11 @@ var makeRegistrationUrl = function(params) {
 
 window.addEventListener('hashchange', onHashChange);
 
-function getConfig() {
-    let deferred = q.defer();
+function getConfig(configJsonFilename) {
+    let deferred = Promise.defer();
 
     request(
-        { method: "GET", url: "config.json" },
+        { method: "GET", url: configJsonFilename },
         (err, response, body) => {
             if (err || response.status < 200 || response.status >= 300) {
                 // Lack of a config isn't an error, we should
@@ -261,10 +261,20 @@ async function loadApp() {
         }
     }
 
+    // Load the config file. First try to load up a domain-specific config of the
+    // form "config.$domain.json" and if that fails, fall back to config.json.
     let configJson;
     let configError;
     try {
-        configJson = await getConfig();
+        try {
+            configJson = await getConfig(`config.${document.domain}.json`);
+            // 404s succeed with an empty json config, so check that there are keys
+            if (Object.keys(configJson).length === 0) {
+                throw new Error(); // throw to enter the catch
+            }
+        } catch (e) {
+            configJson = await getConfig("config.json");
+        }
     } catch (e) {
         configError = e;
     }
diff --git a/src/vector/platform/ElectronPlatform.js b/src/vector/platform/ElectronPlatform.js
index 4f0ffae7..8e97f238 100644
--- a/src/vector/platform/ElectronPlatform.js
+++ b/src/vector/platform/ElectronPlatform.js
@@ -20,7 +20,7 @@ limitations under the License.
 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 Promise from 'bluebird';
 import {remote, ipcRenderer} from 'electron';
 import rageshake from '../rageshake';
 
@@ -173,7 +173,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
     }
 
     getAppVersion(): Promise<string> {
-        return q(remote.app.getVersion());
+        return Promise.resolve(remote.app.getVersion());
     }
 
     startUpdateCheck() {
@@ -201,7 +201,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
     isElectron(): boolean { return true; }
 
     requestNotificationPermission(): Promise<string> {
-        return q('granted');
+        return Promise.resolve('granted');
     }
 
     reload() {
diff --git a/src/vector/platform/WebPlatform.js b/src/vector/platform/WebPlatform.js
index ae1e54b3..b88ee93f 100644
--- a/src/vector/platform/WebPlatform.js
+++ b/src/vector/platform/WebPlatform.js
@@ -21,7 +21,7 @@ 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';
-import q from 'q';
+import Promise from 'bluebird';
 
 import url from 'url';
 import UAParser from 'ua-parser-js';
@@ -68,7 +68,7 @@ export default class WebPlatform extends VectorBasePlatform {
         // annoyingly, the latest spec says this returns a
         // promise, but this is only supported in Chrome 46
         // and Firefox 47, so adapt the callback API.
-        const defer = q.defer();
+        const defer = Promise.defer();
         global.Notification.requestPermission((result) => {
             defer.resolve(result);
         });
@@ -103,7 +103,7 @@ export default class WebPlatform extends VectorBasePlatform {
     }
 
     _getVersion(): Promise<string> {
-        const deferred = q.defer();
+        const deferred = Promise.defer();
 
         // We add a cachebuster to the request to make sure that we know about
         // the most recent version on the origin server. That might not
@@ -132,7 +132,7 @@ export default class WebPlatform extends VectorBasePlatform {
 
     getAppVersion(): Promise<string> {
         if (this.runningVersion !== null) {
-            return q(this.runningVersion);
+            return Promise.resolve(this.runningVersion);
         }
         return this._getVersion();
     }
diff --git a/src/vector/rageshake.js b/src/vector/rageshake.js
index 07726f68..d0977414 100644
--- a/src/vector/rageshake.js
+++ b/src/vector/rageshake.js
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import q from "q";
+import Promise from 'bluebird';
 
 // This module contains all the code needed to log the console, persist it to
 // disk and submit bug reports. Rationale is as follows:
@@ -116,7 +116,7 @@ class IndexedDBLogStore {
      */
     connect() {
         let req = this.indexedDB.open("logs");
-        return q.Promise((resolve, reject) => {
+        return new Promise((resolve, reject) => {
             req.onsuccess = (event) => {
                 this.db = event.target.result;
                 // Periodically flush logs to local storage / indexeddb
@@ -193,7 +193,7 @@ class IndexedDBLogStore {
         }
         // there is no flush promise or there was but it has finished, so do
         // a brand new one, destroying the chain which may have been built up.
-        this.flushPromise = q.Promise((resolve, reject) => {
+        this.flushPromise = new Promise((resolve, reject) => {
             if (!this.db) {
                 // not connected yet or user rejected access for us to r/w to
                 // the db.
@@ -277,7 +277,7 @@ class IndexedDBLogStore {
         }
 
         function deleteLogs(id) {
-            return q.Promise((resolve, reject) => {
+            return new Promise((resolve, reject) => {
                 const txn = db.transaction(
                     ["logs", "logslastmod"], "readwrite"
                 );
@@ -375,7 +375,7 @@ class IndexedDBLogStore {
  */
 function selectQuery(store, keyRange, resultMapper) {
     const query = store.openCursor(keyRange);
-    return q.Promise((resolve, reject) => {
+    return new Promise((resolve, reject) => {
         let results = [];
         query.onerror = (event) => {
             reject(new Error("Query failed: " + event.target.errorCode));
diff --git a/src/vector/submit-rageshake.js b/src/vector/submit-rageshake.js
index c6c551c6..b66ec9ab 100644
--- a/src/vector/submit-rageshake.js
+++ b/src/vector/submit-rageshake.js
@@ -15,7 +15,7 @@ limitations under the License.
 */
 
 import pako from 'pako';
-import q from "q";
+import Promise from 'bluebird';
 
 import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
 import PlatformPeg from 'matrix-react-sdk/lib/PlatformPeg';
@@ -100,7 +100,7 @@ export default async function sendBugReport(bugReportEndpoint, opts) {
 }
 
 function _submitReport(endpoint, body, progressCallback) {
-    const deferred = q.defer();
+    const deferred = Promise.defer();
 
     const req = new XMLHttpRequest();
     req.open("POST", endpoint);
diff --git a/test/app-tests/joining.js b/test/app-tests/joining.js
index 11fd3d48..9e0a84c7 100644
--- a/test/app-tests/joining.js
+++ b/test/app-tests/joining.js
@@ -33,7 +33,7 @@ var React = require('react');
 var ReactDOM = require('react-dom');
 var ReactTestUtils = require('react-addons-test-utils');
 var expect = require('expect');
-var q = require('q');
+import Promise from 'bluebird';
 
 var test_utils = require('../test-utils');
 var MockHttpBackend = require('matrix-mock-request');
@@ -100,29 +100,19 @@ describe('joining a room', function () {
             // wait for /sync to happen. This may take some time, as the client
             // has to initialise indexeddb.
             console.log("waiting for /sync");
-            let syncDone = false;
             httpBackend.when('GET', '/sync')
-                .check((r) => {syncDone = true;})
                 .respond(200, {});
-            function awaitSync(attempts) {
-                if (syncDone) {
-                    return q();
-                }
-                if (!attempts) {
-                    throw new Error("Gave up waiting for /sync")
-                }
-                return httpBackend.flush().then(() => awaitSync(attempts-1));
-            }
 
-            return awaitSync(10).then(() => {
+            return httpBackend.flushAllExpected({
+                timeout: 1000,
+            }).then(() => {
                 // wait for the directory requests
                 httpBackend.when('POST', '/publicRooms').respond(200, {chunk: []});
                 httpBackend.when('GET', '/thirdparty/protocols').respond(200, {});
-                return q.all([
-                    httpBackend.flush('/thirdparty/protocols'),
-                    httpBackend.flush('/publicRooms'),
-                ]);
+                return httpBackend.flushAllExpected();
             }).then(() => {
+                console.log(`${Date.now()} App made requests for directory view; switching to a room.`);
+
                 var roomDir = ReactTestUtils.findRenderedComponentWithType(
                     matrixChat, RoomDirectory);
 
@@ -139,19 +129,17 @@ describe('joining a room', function () {
                 httpBackend.when('GET', '/rooms/'+encodeURIComponent(ROOM_ID)+"/initialSync")
                     .respond(401, {errcode: 'M_GUEST_ACCESS_FORBIDDEN'});
 
-                return q.all([
-                    httpBackend.flush('/directory/room/'+encodeURIComponent(ROOM_ALIAS), 1, 200),
-                    httpBackend.flush('/rooms/'+encodeURIComponent(ROOM_ID)+"/initialSync", 1, 200),
-                ]);
+                return httpBackend.flushAllExpected();
             }).then(() => {
-                httpBackend.verifyNoOutstandingExpectation();
+                console.log(`${Date.now()} App made room preview request`);
 
-                return q.delay(1);
-            }).then(() => {
-                // we should now have a roomview, with a preview bar
+                // we should now have a roomview
                 roomView = ReactTestUtils.findRenderedComponentWithType(
                     matrixChat, RoomView);
 
+                // the preview bar may take a tick to be displayed
+                return Promise.delay(1);
+            }).then(() => {
                 const previewBar = ReactTestUtils.findRenderedComponentWithType(
                     roomView, RoomPreviewBar);
 
@@ -164,14 +152,14 @@ describe('joining a room', function () {
                     .respond(200, {room_id: ROOM_ID});
             }).then(() => {
                 // wait for the join request to be made
-                return q.delay(1);
+                return Promise.delay(1);
             }).then(() => {
                 // and again, because the state update has to go to the store and
                 // then one dispatch within the store, then to the view
                 // XXX: This is *super flaky*: a better way would be to declare
                 // that we expect a certain state transition to happen, then wait
                 // for that transition to occur.
-                return q.delay(1);
+                return Promise.delay(1);
             }).then(() => {
                 // the roomview should now be loading
                 expect(roomView.state.room).toBe(null);
@@ -186,7 +174,7 @@ describe('joining a room', function () {
             }).then(() => {
                 httpBackend.verifyNoOutstandingExpectation();
 
-                return q.delay(1);
+                return Promise.delay(1);
             }).then(() => {
                 // We've joined, expect this to false
                 expect(roomView.state.joining).toBe(false);
diff --git a/test/app-tests/loading.js b/test/app-tests/loading.js
index d01836a3..c7151aca 100644
--- a/test/app-tests/loading.js
+++ b/test/app-tests/loading.js
@@ -22,7 +22,8 @@ import React from 'react';
 import ReactDOM from 'react-dom';
 import ReactTestUtils from 'react-addons-test-utils';
 import expect from 'expect';
-import q from 'q';
+import Promise from 'bluebird';
+import MatrixReactTestUtils from 'matrix-react-test-utils';
 
 import jssdk from 'matrix-js-sdk';
 
@@ -103,7 +104,7 @@ describe('loading:', function () {
             toString: function() { return this.search + this.hash; },
         };
 
-        let tokenLoginCompleteDefer = q.defer();
+        let tokenLoginCompleteDefer = Promise.defer();
         tokenLoginCompletePromise = tokenLoginCompleteDefer.promise;
 
         function onNewScreen(screen) {
@@ -139,7 +140,7 @@ describe('loading:', function () {
                 realQueryParams={params}
                 startingFragmentQueryParams={fragParts.params}
                 enableGuest={true}
-                onTokenLoginCompleted={tokenLoginCompleteDefer.resolve}
+                onTokenLoginCompleted={() => tokenLoginCompleteDefer.resolve()}
                 initialScreenAfterLogin={getScreenFromLocation(windowLocation)}
                 makeRegistrationUrl={() => {throw new Error('Not implemented');}}
             />, parentDiv
@@ -171,7 +172,7 @@ describe('loading:', function () {
         it('gives a login panel by default', function (done) {
             loadApp();
 
-            q.delay(1).then(() => {
+            Promise.delay(1).then(() => {
                 // at this point, we're trying to do a guest registration;
                 // we expect a spinner
                 assertAtLoadingSpinner(matrixChat);
@@ -183,11 +184,8 @@ describe('loading:', function () {
                 return httpBackend.flush();
             }).then(() => {
                 // Wait for another trip around the event loop for the UI to update
-                return q.delay(10);
+                return awaitLoginComponent(matrixChat);
             }).then(() => {
-                // we expect a single <Login> component following session load
-                ReactTestUtils.findRenderedComponentWithType(
-                    matrixChat, sdk.getComponent('structures.login.Login'));
                 expect(windowLocation.hash).toEqual("#/login");
             }).done(done, done);
         });
@@ -197,7 +195,7 @@ describe('loading:', function () {
                 uriFragment: "#/room/!room:id",
             });
 
-            q.delay(1).then(() => {
+            Promise.delay(1).then(() => {
                 // at this point, we're trying to do a guest registration;
                 // we expect a spinner
                 assertAtLoadingSpinner(matrixChat);
@@ -209,7 +207,7 @@ describe('loading:', function () {
                 return httpBackend.flush();
             }).then(() => {
                 // Wait for another trip around the event loop for the UI to update
-                return q.delay(10);
+                return Promise.delay(10);
             }).then(() => {
                 return completeLogin(matrixChat);
             }).then(() => {
@@ -232,7 +230,7 @@ describe('loading:', function () {
                 uriFragment: "#/login",
             });
 
-            return q.delay(100).then(() => {
+            return awaitLoginComponent(matrixChat).then(() => {
                 // we expect a single <Login> component
                 ReactTestUtils.findRenderedComponentWithType(
                     matrixChat, sdk.getComponent('structures.login.Login'));
@@ -339,7 +337,7 @@ describe('loading:', function () {
                 },
             });
 
-            return q.delay(1).then(() => {
+            return Promise.delay(1).then(() => {
                 // we expect a loading spinner while we log into the RTS
                 assertAtLoadingSpinner(matrixChat);
 
@@ -366,7 +364,7 @@ describe('loading:', function () {
                 });
 
                 // give the UI a chance to display
-                return q.delay(50);
+                return awaitLoginComponent(matrixChat);
             });
 
             it('shows a login view', function() {
@@ -403,7 +401,7 @@ describe('loading:', function () {
         it('shows a home page by default', function (done) {
             loadApp();
 
-            q.delay(1).then(() => {
+            Promise.delay(1).then(() => {
                 // at this point, we're trying to do a guest registration;
                 // we expect a spinner
                 assertAtLoadingSpinner(matrixChat);
@@ -436,7 +434,7 @@ describe('loading:', function () {
 
             loadApp();
 
-            q.delay(1).then(() => {
+            Promise.delay(1).then(() => {
                 // at this point, we're trying to do a guest registration;
                 // we expect a spinner
                 assertAtLoadingSpinner(matrixChat);
@@ -471,7 +469,7 @@ describe('loading:', function () {
             loadApp({
                 uriFragment: "#/room/!room:id"
             });
-            q.delay(1).then(() => {
+            Promise.delay(1).then(() => {
                 // at this point, we're trying to do a guest registration;
                 // we expect a spinner
                 assertAtLoadingSpinner(matrixChat);
@@ -530,7 +528,7 @@ describe('loading:', function () {
 
                     dis.dispatch({ action: 'start_login' });
 
-                    return q.delay(1);
+                    return awaitLoginComponent(matrixChat);
                 });
             });
 
@@ -559,7 +557,7 @@ describe('loading:', function () {
 
                 ReactTestUtils.Simulate.click(returnToApp);
 
-                return q.delay(1).then(() => {
+                return Promise.delay(1).then(() => {
                     // we should be straight back into the home page
                     ReactTestUtils.findRenderedComponentWithType(
                         matrixChat, sdk.getComponent('structures.HomePage'));
@@ -574,7 +572,7 @@ describe('loading:', function () {
                 queryString: "?loginToken=secretToken&homeserver=https%3A%2F%2Fhomeserver&identityServer=https%3A%2F%2Fidserver",
             });
 
-            q.delay(1).then(() => {
+            Promise.delay(1).then(() => {
                 // we expect a spinner while we're logging in
                 assertAtLoadingSpinner(matrixChat);
 
@@ -607,7 +605,6 @@ describe('loading:', function () {
         });
     });
 
-
     // check that we have a Login component, send a 'user:pass' login,
     // and await the HTTP requests.
     function completeLogin(matrixChat) {
@@ -629,7 +626,7 @@ describe('loading:', function () {
 
         return httpBackend.flush().then(() => {
             // Wait for another trip around the event loop for the UI to update
-            return q.delay(1);
+            return Promise.delay(1);
         }).then(() => {
             // we expect a spinner
             ReactTestUtils.findRenderedComponentWithType(
@@ -674,7 +671,7 @@ function awaitSyncingSpinner(matrixChat, retryLimit, retryCount) {
         }
         // loading can take quite a long time, because we delete the
         // indexedDB store.
-        return q.delay(5).then(() => {
+        return Promise.delay(5).then(() => {
             return awaitSyncingSpinner(matrixChat, retryLimit, retryCount + 1);
         });
     }
@@ -683,7 +680,7 @@ function awaitSyncingSpinner(matrixChat, retryLimit, retryCount) {
 
     // state looks good, check the rendered output
     assertAtSyncingSpinner(matrixChat);
-    return q();
+    return Promise.resolve();
 }
 
 function assertAtSyncingSpinner(matrixChat) {
@@ -711,7 +708,7 @@ function awaitRoomView(matrixChat, retryLimit, retryCount) {
             throw new Error("MatrixChat still not ready after " +
                             retryCount + " tries");
         }
-        return q.delay(0).then(() => {
+        return Promise.delay(0).then(() => {
             return awaitRoomView(matrixChat, retryLimit, retryCount + 1);
         });
     }
@@ -721,5 +718,11 @@ function awaitRoomView(matrixChat, retryLimit, retryCount) {
     // state looks good, check the rendered output
     ReactTestUtils.findRenderedComponentWithType(
         matrixChat, sdk.getComponent('structures.RoomView'));
-    return q();
+    return Promise.resolve();
+}
+
+function awaitLoginComponent(matrixChat, attempts) {
+    return MatrixReactTestUtils.waitForRenderedComponentWithType(
+        matrixChat, sdk.getComponent('structures.login.Login'), attempts,
+    );
 }
diff --git a/test/test-utils.js b/test/test-utils.js
index cda9a017..007883df 100644
--- a/test/test-utils.js
+++ b/test/test-utils.js
@@ -1,6 +1,6 @@
 "use strict";
 
-var q = require('q');
+import Promise from 'bluebird';
 
 /**
  * Perform common actions before each test case, e.g. printing the test case
@@ -28,7 +28,7 @@ export function browserSupportsWebRTC() {
 }
 
 export function deleteIndexedDB(dbName) {
-    return new q.Promise((resolve, reject) => {
+    return new Promise((resolve, reject) => {
         if (!window.indexedDB) {
             resolve();
             return;