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/.travis.yml b/.travis.yml index e020ba7d..94ed745c 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 cdba0551..fb753d55 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 89f2148f..d4b778b9 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 3491ce0f..99e14b74 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 039e7133..bd07d7d4 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 00000000..49fa4e04 --- /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 1e043663..d834987e 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 f5129006..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 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 c96b46e8..cc350e4c 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 598f6c52..e10a48c0 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 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/HomePage.js b/src/components/structures/HomePage.js index 2311cc1f..bdba55eb 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 64a53d33..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'; @@ -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 092ed9cc..4844f0ed 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 99c44866..7de3958a 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 bc82778a..2064cede 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 a7b19689..3ef06462 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 dd0490ac..f9404eac 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 a1c1cd0a..a5b22feb 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; }); }