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/.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..fb753d5520 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,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..d4b778b91e 100644
--- a/README.md
+++ b/README.md
@@ -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).
[](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/src/electron-main.js b/electron_app/src/electron-main.js
index 3491ce0fa3..99e14b7447 100644
--- a/electron_app/src/electron-main.js
+++ b/electron_app/src/electron-main.js
@@ -29,6 +29,7 @@ const AutoLaunch = require('auto-launch');
const tray = require('./tray');
const vectorMenu = require('./vectormenu');
const webContentsHandler = require('./webcontents-handler');
+const updater = require('./updater');
const windowStateKeeper = require('electron-window-state');
@@ -46,69 +47,9 @@ try {
// Continue with the defaults (ie. an empty config)
}
-const UPDATE_POLL_INTERVAL_MS = 60 * 60 * 1000;
-const INITIAL_UPDATE_DELAY_MS = 30 * 1000;
-
let mainWindow = null;
-let appQuitting = false;
+global.appQuitting = false;
-function installUpdate() {
- // for some reason, quitAndInstall does not fire the
- // before-quit event, so we need to set the flag here.
- appQuitting = true;
- electron.autoUpdater.quitAndInstall();
-}
-
-function pollForUpdates() {
- try {
- electron.autoUpdater.checkForUpdates();
- } catch (e) {
- console.log('Couldn\'t check for update', e);
- }
-}
-
-function startAutoUpdate(updateBaseUrl) {
- if (updateBaseUrl.slice(-1) !== '/') {
- updateBaseUrl = updateBaseUrl + '/';
- }
- try {
- // For reasons best known to Squirrel, the way it checks for updates
- // is completely different between macOS and windows. On macOS, it
- // hits a URL that either gives it a 200 with some json or
- // 204 No Content. On windows it takes a base path and looks for
- // files under that path.
- if (process.platform === 'darwin') {
- // include the current version in the URL we hit. Electron doesn't add
- // it anywhere (apart from the User-Agent) so it's up to us. We could
- // (and previously did) just use the User-Agent, but this doesn't
- // rely on NSURLConnection setting the User-Agent to what we expect,
- // and also acts as a convenient cache-buster to ensure that when the
- // app updates it always gets a fresh value to avoid update-looping.
- electron.autoUpdater.setFeedURL(
- `${updateBaseUrl}macos/?localVersion=${encodeURIComponent(electron.app.getVersion())}`);
-
- } else if (process.platform === 'win32') {
- electron.autoUpdater.setFeedURL(`${updateBaseUrl}win32/${process.arch}/`);
- } else {
- // Squirrel / electron only supports auto-update on these two platforms.
- // I'm not even going to try to guess which feed style they'd use if they
- // implemented it on Linux, or if it would be different again.
- console.log('Auto update not supported on this platform');
- }
- // We check for updates ourselves rather than using 'updater' because we need to
- // do it in the main process (and we don't really need to check every 10 minutes:
- // every hour should be just fine for a desktop app)
- // However, we still let the main window listen for the update events.
- // We also wait a short time before checking for updates the first time because
- // of squirrel on windows and it taking a small amount of time to release a
- // lock file.
- setTimeout(pollForUpdates, INITIAL_UPDATE_DELAY_MS);
- setInterval(pollForUpdates, UPDATE_POLL_INTERVAL_MS);
- } catch (err) {
- // will fail if running in debug mode
- console.log('Couldn\'t enable update checking', err);
- }
-}
// handle uncaught errors otherwise it displays
// stack traces in popup dialogs, which is terrible (which
@@ -120,8 +61,6 @@ process.on('uncaughtException', function(error) {
console.log('Unhandled exception', error);
});
-electron.ipcMain.on('install_update', installUpdate);
-
let focusHandlerAttached = false;
electron.ipcMain.on('setBadgeCount', function(ev, count) {
electron.app.setBadgeCount(count);
@@ -233,7 +172,7 @@ electron.app.on('ready', () => {
if (vectorConfig.update_base_url) {
console.log(`Starting auto update with base URL: ${vectorConfig.update_base_url}`);
- startAutoUpdate(vectorConfig.update_base_url);
+ updater.start(vectorConfig.update_base_url);
} else {
console.log('No update_base_url is defined: auto update is disabled');
}
@@ -246,7 +185,7 @@ electron.app.on('ready', () => {
defaultHeight: 768,
});
- mainWindow = new electron.BrowserWindow({
+ mainWindow = global.mainWindow = new electron.BrowserWindow({
icon: iconPath,
show: false,
autoHideMenuBar: true,
@@ -264,7 +203,7 @@ electron.app.on('ready', () => {
mainWindow.hide();
// Create trayIcon icon
- tray.create(mainWindow, {
+ tray.create({
icon_path: iconPath,
brand: vectorConfig.brand || 'Riot',
});
@@ -276,10 +215,10 @@ electron.app.on('ready', () => {
}
mainWindow.on('closed', () => {
- mainWindow = null;
+ mainWindow = global.mainWindow = null;
});
mainWindow.on('close', (e) => {
- if (!appQuitting && (tray.hasTray() || process.platform === 'darwin')) {
+ if (!global.appQuitting && (tray.hasTray() || process.platform === 'darwin')) {
// On Mac, closing the window just hides it
// (this is generally how single-window Mac apps
// behave, eg. Mail.app)
@@ -302,7 +241,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..d834987e83 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -113,8 +113,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..2b1d6e82d2 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 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",
@@ -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",
@@ -119,13 +119,13 @@
"karma-cli": "^0.1.2",
"karma-junit-reporter": "^0.4.1",
"karma-mocha": "^0.2.2",
- "karma-phantomjs-launcher": "^1.0.0",
"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 +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/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/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/RoomDirectory.js b/src/components/structures/RoomDirectory.js
index 64a53d336d..ea3aa5a29c 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';
@@ -73,6 +73,7 @@ module.exports = React.createClass({
this.protocols = response;
this.setState({protocolsLoading: false});
}, (err) => {
+ console.warn(`error loading thirdparty protocols: ${err}`);
this.setState({protocolsLoading: false});
if (MatrixClientPeg.get().isGuest()) {
// Guests currently aren't allowed to use this API, so
@@ -116,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;
@@ -265,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/structures/RoomSubList.js b/src/components/structures/RoomSubList.js
index 092ed9cca0..4844f0ed35 100644
--- a/src/components/structures/RoomSubList.js
+++ b/src/components/structures/RoomSubList.js
@@ -30,6 +30,7 @@ var RoomNotifs = require('matrix-react-sdk/lib/RoomNotifs');
var FormattingUtils = require('matrix-react-sdk/lib/utils/FormattingUtils');
var AccessibleButton = require('matrix-react-sdk/lib/components/views/elements/AccessibleButton');
import Modal from 'matrix-react-sdk/lib/Modal';
+import KeyCode from 'matrix-react-sdk/lib/KeyCode';
// turn this on for drop & drag console debugging galore
var debug = false;
@@ -151,10 +152,11 @@ var RoomSubList = React.createClass({
}
},
- onRoomTileClick(roomId) {
+ onRoomTileClick(roomId, ev) {
dis.dispatch({
action: 'view_room',
room_id: roomId,
+ clear_search: (ev && (ev.keyCode == KeyCode.ENTER || ev.keyCode == KeyCode.SPACE)),
});
},
@@ -509,7 +511,7 @@ var RoomSubList = React.createClass({
if (list[i].tags[self.props.tagName] && list[i].tags[self.props.tagName].order === undefined) {
MatrixClientPeg.get().setRoomTag(list[i].roomId, self.props.tagName, {order: (order + 1.0) / 2.0}).finally(function() {
// Do any final stuff here
- }).fail(function(err) {
+ }).catch(function(err) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to add tag " + self.props.tagName + " to room" + err);
Modal.createDialog(ErrorDialog, {
diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js
index 99c4486690..7de3958a59 100644
--- a/src/components/structures/SearchBox.js
+++ b/src/components/structures/SearchBox.js
@@ -16,12 +16,13 @@ limitations under the License.
'use strict';
-var React = require('react');
+import React from 'react';
import { _t } from 'matrix-react-sdk/lib/languageHandler';
-var sdk = require('matrix-react-sdk')
-var dis = require('matrix-react-sdk/lib/dispatcher');
-var rate_limited_func = require('matrix-react-sdk/lib/ratelimitedfunc');
-var AccessibleButton = require('matrix-react-sdk/lib/components/views/elements/AccessibleButton');
+import KeyCode from 'matrix-react-sdk/lib/KeyCode';
+import sdk from 'matrix-react-sdk';
+import dis from 'matrix-react-sdk/lib/dispatcher';
+import rate_limited_func from 'matrix-react-sdk/lib/ratelimitedfunc';
+import AccessibleButton from 'matrix-react-sdk/lib/components/views/elements/AccessibleButton';
module.exports = React.createClass({
displayName: 'SearchBox',
@@ -46,18 +47,19 @@ module.exports = React.createClass({
},
onAction: function(payload) {
- // Disabling this as I find it really really annoying, and was used to the
- // previous behaviour - see https://github.com/vector-im/riot-web/issues/3348
-/*
switch (payload.action) {
- // Clear up the text field when a room is selected.
case 'view_room':
- if (this.refs.search) {
+ if (this.refs.search && payload.clear_search) {
this._clearSearch();
}
break;
+ case 'focus_room_filter':
+ if (this.refs.search) {
+ this.refs.search.focus();
+ this.refs.search.select();
+ }
+ break;
}
-*/
},
onChange: function() {
@@ -86,6 +88,15 @@ module.exports = React.createClass({
}
},
+ _onKeyDown: function(ev) {
+ switch (ev.keyCode) {
+ case KeyCode.ESCAPE:
+ this._clearSearch();
+ dis.dispatch({action: 'focus_composer'});
+ break;
+ }
+ },
+
_clearSearch: function() {
this.refs.search.value = "";
this.onChange();
@@ -135,6 +146,7 @@ module.exports = React.createClass({
className="mx_SearchBox_search"
value={ this.state.searchTerm }
onChange={ this.onChange }
+ onKeyDown={ this._onKeyDown }
placeholder={ _t('Filter room names') }
/>
];
diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js
index bc82778aae..2064cede08 100644
--- a/src/components/views/context_menus/MessageContextMenu.js
+++ b/src/components/views/context_menus/MessageContextMenu.js
@@ -67,7 +67,7 @@ module.exports = React.createClass({
onResendClick: function() {
Resend.resend(this.props.mxEvent);
- if (this.props.onFinished) this.props.onFinished();
+ this.closeMenu();
},
onViewSourceClick: function() {
@@ -75,7 +75,7 @@ module.exports = React.createClass({
Modal.createDialog(ViewSource, {
content: this.props.mxEvent.event,
}, 'mx_Dialog_viewsource');
- if (this.props.onFinished) this.props.onFinished();
+ this.closeMenu();
},
onViewClearSourceClick: function() {
@@ -84,7 +84,7 @@ module.exports = React.createClass({
// FIXME: _clearEvent is private
content: this.props.mxEvent._clearEvent,
}, 'mx_Dialog_viewsource');
- if (this.props.onFinished) this.props.onFinished();
+ this.closeMenu();
},
onRedactClick: function() {
@@ -106,12 +106,12 @@ module.exports = React.createClass({
}).done();
},
}, 'mx_Dialog_confirmredact');
- if (this.props.onFinished) this.props.onFinished();
+ this.closeMenu();
},
onCancelSendClick: function() {
Resend.removeFromQueue(this.props.mxEvent);
- if (this.props.onFinished) this.props.onFinished();
+ this.closeMenu();
},
onForwardClick: function() {
@@ -130,7 +130,7 @@ module.exports = React.createClass({
if (this.props.eventTileOps) {
this.props.eventTileOps.unhideWidget();
}
- if (this.props.onFinished) this.props.onFinished();
+ this.closeMenu();
},
onQuoteClick: function() {
@@ -139,6 +139,7 @@ module.exports = React.createClass({
action: 'quote',
event: this.props.mxEvent,
});
+ this.closeMenu();
},
render: function() {
@@ -247,7 +248,7 @@ module.exports = React.createClass({
{viewClearSourceButton}
{unhidePreviewButton}
{permalinkButton}
- {UserSettingsStore.isFeatureEnabled('rich_text_editor') ? quoteButton : null}
+ {quoteButton}
{externalURLButton}
);
diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js
index a7b19689fe..3ef0646237 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,14 +61,14 @@ 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
if (self.props.onFinished) {
self.props.onFinished();
};
- }).fail(function(err) {
+ }).catch(function(err) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: _t('Failed to remove tag %(tagName)s from room', {tagName: tagNameOff}),
@@ -85,7 +85,7 @@ module.exports = React.createClass({
if (self.props.onFinished) {
self.props.onFinished();
};
- }).fail(function(err) {
+ }).catch(function(err) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: _t('Failed to remove tag %(tagName)s from room', {tagName: tagNameOn}),
@@ -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/elements/ImageView.js b/src/components/views/elements/ImageView.js
index dd0490ac0c..f9404eacd3 100644
--- a/src/components/views/elements/ImageView.js
+++ b/src/components/views/elements/ImageView.js
@@ -158,7 +158,7 @@ module.exports = React.createClass({
var eventRedact;
if(showEventMeta) {
eventRedact = (
Includes the conditions to be matched against, the checks to be made, - * and the response to be returned. - * - * @constructor - * @param {string} method - * @param {string} path - * @param {object?} data - */ -function ExpectedRequest(method, path, data) { - this.method = method; - this.path = path; - this.data = data; - this.response = null; - this.checks = []; -} - -ExpectedRequest.prototype = { - toString: function() { - return this.method + " " + this.path - }, - - /** - * Execute a check when this request has been satisfied. - * @param {Function} fn The function to execute. - * @return {Request} for chaining calls. - */ - check: function(fn) { - this.checks.push(fn); - return this; - }, - - /** - * Respond with the given data when this request is satisfied. - * @param {Number} code The HTTP status code. - * @param {Object|Function} data The HTTP JSON body. If this is a function, - * it will be invoked when the JSON body is required (which should be returned). - */ - respond: function(code, data) { - this.response = { - response: { - statusCode: code, - headers: {}, - }, - body: data, - err: null, - }; - }, - - /** - * Fail with an Error when this request is satisfied. - * @param {Number} code The HTTP status code. - * @param {Error} err The error to throw (e.g. Network Error) - */ - fail: function(code, err) { - this.response = { - response: { - statusCode: code, - headers: {}, - }, - body: null, - err: err, - }; - }, -}; - -/** - * Represents a request made by the app. - * - * @constructor - * @param {object} opts opts passed to request() - * @param {function} callback - */ -function Request(opts, callback) { - this.opts = opts; - this.callback = callback; - - Object.defineProperty(this, 'method', { - get: function() { - return opts.method; - }, - }); - - Object.defineProperty(this, 'path', { - get: function() { - return opts.uri; - }, - }); - - Object.defineProperty(this, 'data', { - get: function() { - return opts.body; - }, - }); - - Object.defineProperty(this, 'queryParams', { - get: function() { - return opts.qs; - }, - }); - - Object.defineProperty(this, 'headers', { - get: function() { - return opts.headers || {}; - }, - }); -} - -Request.prototype = { - toString: function() { - return this.method + " " + this.path; - }, -}; - -/** - * The HttpBackend class. - */ -module.exports = HttpBackend; diff --git a/test/test-utils.js b/test/test-utils.js index a1c1cd0ad5..a5b22febfe 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,28 +28,33 @@ export function browserSupportsWebRTC() { } export function deleteIndexedDB(dbName) { - return new q.Promise((resolve, reject) => { + return new Promise((resolve, reject) => { if (!window.indexedDB) { resolve(); return; } - console.log(`Removing indexeddb instance: ${dbName}`); + const startTime = Date.now(); + console.log(`${startTime}: Removing indexeddb instance: ${dbName}`); const req = window.indexedDB.deleteDatabase(dbName); req.onblocked = () => { - console.log(`can't yet delete indexeddb because it is open elsewhere`); + console.log(`${Date.now()}: can't yet delete indexeddb ${dbName} because it is open elsewhere`); }; req.onerror = (ev) => { reject(new Error( - "unable to delete indexeddb: " + ev.target.error, + `${Date.now()}: unable to delete indexeddb ${dbName}: ${ev.target.error}`, )); }; req.onsuccess = () => { - console.log(`Removed indexeddb instance: ${dbName}`); + const now = Date.now(); + console.log(`${now}: Removed indexeddb instance: ${dbName} in ${now-startTime} ms`); resolve(); }; + }).catch((e) => { + console.error(`${Date.now()}: Error removing indexeddb instance ${dbName}: ${e}`); + throw e; }); }