diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..fabbe104 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +src/vector/modernizr.js +src/component-index.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..c181384f --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ["./node_modules/matrix-react-sdk/.eslintrc.js"], +} diff --git a/.travis.yml b/.travis.yml index 1fbc4dcc..9720d887 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,5 @@ node_js: - 6 # node v6, to match jenkins install: - npm install - - (cd node_modules/matrix-react-sdk && npm run build) + - (cd node_modules/matrix-js-sdk && npm install) + - (cd node_modules/matrix-react-sdk && npm install) diff --git a/CHANGELOG.md b/CHANGELOG.md index 265cbe80..659a3742 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,135 @@ +Changes in [0.9.7](https://github.com/vector-im/riot-web/releases/tag/v0.9.7) (2017-02-04) +========================================================================================== +[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.7-rc.3...v0.9.7) + + * Update to matrix-js-sdk 0.7.5 (no changes from 0.7.5-rc.3) + * Update to matrix-react-sdk 0.8.6 (no changes from 0.8.6-rc.3) + +Changes in [0.9.7-rc.3](https://github.com/vector-im/riot-web/releases/tag/v0.9.7-rc.3) (2017-02-03) +==================================================================================================== +[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.7-rc.2...v0.9.7-rc.3) + * Update to latest Olm to fix key import/export and use of megolm sessions + created on more recent versions + * Update to latest matrix-react-sdk and matrix-js-sdk to fix e2e device + handling + +Changes in [0.9.7-rc.2](https://github.com/vector-im/riot-web/releases/tag/v0.9.7-rc.2) (2017-02-03) +==================================================================================================== +[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.7-rc.1...v0.9.7-rc.2) + + * Update matrix-js-sdk to get new device change + notifications interface for more reliable e2e crypto + +Changes in [0.9.7-rc.1](https://github.com/vector-im/riot-web/releases/tag/v0.9.7-rc.1) (2017-02-03) +==================================================================================================== +[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.6...v0.9.7-rc.1) + + * Better user interface for screen readers and keyboard navigation + [\#2946](https://github.com/vector-im/riot-web/pull/2946) + * Allow mxc: URLs for icons in the NetworkDropdown + [\#3118](https://github.com/vector-im/riot-web/pull/3118) + * make TopRightMenu more intuitive + [\#3117](https://github.com/vector-im/riot-web/pull/3117) + * Handle icons with width > height + [\#3110](https://github.com/vector-im/riot-web/pull/3110) + * Fix jenkins build + [\#3105](https://github.com/vector-im/riot-web/pull/3105) + * Add CSS for a support box in login + [\#3081](https://github.com/vector-im/riot-web/pull/3081) + * Allow a custom login logo to be displayed on login + [\#3082](https://github.com/vector-im/riot-web/pull/3082) + * Fix the width of input fields within login/reg box + [\#3080](https://github.com/vector-im/riot-web/pull/3080) + * Set BaseAvatar_image bg colour = #fff + [\#3057](https://github.com/vector-im/riot-web/pull/3057) + * only recalculate favicon if it changes + [\#3067](https://github.com/vector-im/riot-web/pull/3067) + * CSS tweak for email address lookup + [\#3064](https://github.com/vector-im/riot-web/pull/3064) + * Glue the dialog to rageshake: honour sendLogs flag. + [\#3061](https://github.com/vector-im/riot-web/pull/3061) + * Don't use hash-named directory for dev server + [\#3049](https://github.com/vector-im/riot-web/pull/3049) + * Implement bug reporting logic + [\#3000](https://github.com/vector-im/riot-web/pull/3000) + * Add css for bug report dialog + [\#3045](https://github.com/vector-im/riot-web/pull/3045) + * Increase the max-height of the expanded status bar + [\#3043](https://github.com/vector-im/riot-web/pull/3043) + * Hopefully, fix intermittent test failure + [\#3040](https://github.com/vector-im/riot-web/pull/3040) + * CSS for 'searching known users' + [\#2971](https://github.com/vector-im/riot-web/pull/2971) + * Animate status bar max-height and margin-top + [\#2981](https://github.com/vector-im/riot-web/pull/2981) + * Add eslint config + [\#3032](https://github.com/vector-im/riot-web/pull/3032) + * Re-position typing avatars relative to "is typing" + [\#3030](https://github.com/vector-im/riot-web/pull/3030) + * CSS for avatars that appear when users are typing + [\#2998](https://github.com/vector-im/riot-web/pull/2998) + * Add StartupWMClass + [\#3001](https://github.com/vector-im/riot-web/pull/3001) + * Fix link to image for event options menu + [\#3002](https://github.com/vector-im/riot-web/pull/3002) + * Make riot desktop single instance + [\#2999](https://github.com/vector-im/riot-web/pull/2999) + * Add electron tray icon + [\#2997](https://github.com/vector-im/riot-web/pull/2997) + * Fixes to electron desktop notifs + [\#2994](https://github.com/vector-im/riot-web/pull/2994) + * Auto-hide the electron menu bar + [\#2975](https://github.com/vector-im/riot-web/pull/2975) + * A couple of tweaks to the karma config + [\#2987](https://github.com/vector-im/riot-web/pull/2987) + * Deploy script + [\#2974](https://github.com/vector-im/riot-web/pull/2974) + * Use the postcss-webpack-loader + [\#2990](https://github.com/vector-im/riot-web/pull/2990) + * Switch CSS to using postcss, and implement a dark theme. + [\#2973](https://github.com/vector-im/riot-web/pull/2973) + * Update redeploy script to keep old bundles + [\#2969](https://github.com/vector-im/riot-web/pull/2969) + * Include current version in update check explicitly + [\#2967](https://github.com/vector-im/riot-web/pull/2967) + * Add another layer of directory to webpack chunks + [\#2966](https://github.com/vector-im/riot-web/pull/2966) + * Fix links to fonts and images from CSS + [\#2965](https://github.com/vector-im/riot-web/pull/2965) + * Put parent build hash in webpack output filenames + [\#2961](https://github.com/vector-im/riot-web/pull/2961) + * update README to point to new names/locations + [\#2846](https://github.com/vector-im/riot-web/pull/2846) + +Changes in [0.9.6](https://github.com/vector-im/riot-web/releases/tag/v0.9.6) (2017-01-16) +========================================================================================== +[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.6-rc.1...v0.9.6) + + * Update to matrix-js-sdk 0.9.6 for video calling fix + +Changes in [0.9.6-rc.1](https://github.com/vector-im/riot-web/releases/tag/v0.9.6-rc.1) (2017-01-13) +==================================================================================================== +[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.5...v0.9.6-rc.1) + + * Build the js-sdk in the CI script + [\#2920](https://github.com/vector-im/riot-web/pull/2920) + * Hopefully fix Windows shortcuts + [\#2917](https://github.com/vector-im/riot-web/pull/2917) + * Update README now the js-sdk has a transpile step + [\#2921](https://github.com/vector-im/riot-web/pull/2921) + * Use the role for 'toggle dev tools' + [\#2915](https://github.com/vector-im/riot-web/pull/2915) + * Enable screen sharing easter-egg in desktop app + [\#2909](https://github.com/vector-im/riot-web/pull/2909) + * make electron send email validation URLs with a nextlink of riot.im + [\#2808](https://github.com/vector-im/riot-web/pull/2808) + * add Debian Stretch install steps to readme + [\#2809](https://github.com/vector-im/riot-web/pull/2809) + * Update desktop build instructions fixes #2792 + [\#2793](https://github.com/vector-im/riot-web/pull/2793) + * CSS for the delete threepid button + [\#2784](https://github.com/vector-im/riot-web/pull/2784) + Changes in [0.9.5](https://github.com/vector-im/riot-web/releases/tag/v0.9.5) (2016-12-24) ========================================================================================== [Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.4...v0.9.5) diff --git a/README.md b/README.md index f7754348..fd6acfd7 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ https://riot.im/develop for those who like living dangerously. To host your own copy of Riot, the quickest bet is to use a pre-built released version of Riot: -1. Download the latest version from https://github.com/vector-im/vector-web/releases +1. Download the latest version from https://github.com/vector-im/riot-web/releases 1. Untar the tarball on your web server 1. Move (or symlink) the vector-x.x.x directory to an appropriate name 1. If desired, copy `config.sample.json` to `config.json` and edit it @@ -44,7 +44,7 @@ access to Riot (or other apps) due to sharing the same domain. We have put some coarse mitigations into place to try to protect against this situation, but it's still not good practice to do it in the first place. See -https://github.com/vector-im/vector-web/issues/1977 for more details. +https://github.com/vector-im/riot-web/issues/1977 for more details. Building From Source ==================== @@ -53,13 +53,17 @@ Riot is a modular webapp built with modern ES6 and requires a npm build system to build. 1. Install or update `node.js` so that your `npm` is at least at version `2.0.0` -1. Clone the repo: `git clone https://github.com/vector-im/vector-web.git` -1. Switch to the vector-web directory: `cd vector-web` +1. Clone the repo: `git clone https://github.com/vector-im/riot-web.git` +1. Switch to the riot-web directory: `cd riot-web` 1. Install the prerequisites: `npm install` 1. If you are using the `develop` branch of vector-web, you will probably need - to rebuild one of the dependencies, due to - https://github.com/npm/npm/issues/3055: `(cd node_modules/matrix-react-sdk - && npm install)` + to rebuild some of the dependencies, due to + https://github.com/npm/npm/issues/3055: + + ``` + (cd node_modules/matrix-js-sdk && npm install) + (cd node_modules/matrix-react-sdk && npm install) + ``` 1. Configure the app by copying `config.sample.json` to `config.json` and modifying it (see below for details) 1. `npm run dist` to build a tarball to deploy. Untaring this file will give @@ -105,16 +109,18 @@ Running as a Desktop app ======================== Riot can also be run as a desktop app, wrapped in electron. You can download a -pre-built version from https://riot.im/download/desktop/ or, if you prefer, +pre-built version from https://riot.im/desktop.html or, if you prefer, built it yourself. To run as a desktop app: -``` -npm install -npm install electron -npm run build -node_modules/.bin/electron . -``` + +1. Follow the instructions in 'Building From Source' above +2. Install electron and run it: + + ``` + npm install electron + node_modules/.bin/electron . + ``` To build packages, use electron-builder. This is configured to output: * dmg + zip for macOS @@ -137,11 +143,9 @@ npm run build:electron For other packages, use electron-builder manually. For example, to build a package for 64 bit Linux: -``` -npm install -npm run build -node_modules/.bin/build -l --x64 -``` + + 1. Follow the instructions in 'Building From Source' above + 2. `node_modules/.bin/build -l --x64` All electron packages go into `electron/dist/` @@ -175,13 +179,13 @@ the `component-index.js` for the app (used in future for skinning) development on Riot forcing `matrix-react-sdk` to move fast at the expense of maintaining a clear abstraction between the two.** Hacking on Riot inevitably means hacking equally on `matrix-react-sdk`, and there are bits of -`matrix-react-sdk` behaviour incorrectly residing in the `vector-web` project +`matrix-react-sdk` behaviour incorrectly residing in the `riot-web` project (e.g. matrix-react-sdk specific CSS), and a bunch of Riot specific behaviour in the `matrix-react-sdk` (grep for `vector` / `riot`). This separation problem will be solved asap once development on Riot (and thus matrix-react-sdk) has stabilised. Until then, the two projects should basically be considered as a single unit. In particular, `matrix-react-sdk` issues are currently filed -against `vector-web` in github. +against `riot-web` in github. Please note that Riot is intended to run correctly without access to the public internet. So please don't depend on resources (JS libs, CSS, images, fonts) @@ -216,8 +220,8 @@ Then similarly with `matrix-react-sdk`: Finally, build and start Riot itself: -1. `git clone git@github.com:vector-im/vector-web.git` -1. `cd vector-web` +1. `git clone git@github.com:vector-im/riot-web.git` +1. `cd riot-web` 1. `git checkout develop` 1. `npm install` 1. `rm -r node_modules/matrix-js-sdk; ln -s ../../matrix-js-sdk node_modules/` @@ -241,10 +245,10 @@ Finally, build and start Riot itself: disables caching, so do NOT use it in production. 1. Open http://127.0.0.1:8080/ in your browser to see your newly built Riot. -When you make changes to `matrix-react-sdk`, you will need to run `npm run -build` in the relevant directory. You can do this automatically by instead -running `npm start` in the directory, to start a development builder which -will watch for changes to the files and rebuild automatically. +When you make changes to `matrix-react-sdk` or `matrix-js-sdk`, you will need +to run `npm run build` in the relevant directory. You can do this automatically +by instead running `npm start` in the directory, to start a development builder +which will watch for changes to the files and rebuild automatically. If you add or remove any components from the Riot skin, you will need to rebuild the skin's index by running, `npm run reskindex`. diff --git a/config.sample.json b/config.sample.json index e6384221..a65646ac 100644 --- a/config.sample.json +++ b/config.sample.json @@ -4,6 +4,7 @@ "brand": "Riot", "integrations_ui_url": "https://scalar.vector.im/", "integrations_rest_url": "https://scalar.vector.im/api", + "bug_report_endpoint_url": "https://vector.im/bugs", "enableLabs": true, "roomDirectory": { "servers": [ diff --git a/docs/theming.md b/docs/theming.md new file mode 100644 index 00000000..c6373e52 --- /dev/null +++ b/docs/theming.md @@ -0,0 +1,25 @@ +Theming Riot +============ + +Themes are a very basic way of providing simple alternative look & feels to the +riot-web app via CSS & custom imagery. + +They are *NOT* co be confused with 'skins', which describe apps which sit on top +of matrix-react-sdk - e.g. in theory Riot itself is a react-sdk skin. +As of Jan 2017, skins are not fully supported; riot is the only available skin. + +To define a theme for Riot: + + 1. Pick a name, e.g. `teal`. at time of writing we have `light` and `dark`. + 2. Fork `src/skins/vector/css/themes/dark.scss` to be teal.scss + 3. Fork `src/skins/vector/css/themes/_base.scss` to be _teal.scss + 4. Override variables in _teal.scss as desired. You may wish to delete ones + which don't differ from _base.scss, to make it clear which are being + overridden. If every single colour is being changed (as per _dark.scss) + then you might as well keep them all. + 5. Add the theme to the list of entrypoints in webpack.config.js + 6. Add the theme to the list of themes in matrix-react-sdk's UserSettings.js + 7. Sit back and admire your handywork. + +In future, the assets for a theme will probably be gathered together into a +single directory tree. diff --git a/electron/src/electron-main.js b/electron/src/electron-main.js index 2b135a03..33b44ce9 100644 --- a/electron/src/electron-main.js +++ b/electron/src/electron-main.js @@ -26,6 +26,8 @@ if (check_squirrel_hooks()) return; const electron = require('electron'); const url = require('url'); +const tray = require('./tray'); + const VectorMenu = require('./vectormenu'); let vectorConfig = {}; @@ -112,7 +114,16 @@ function startAutoUpdate(update_base_url) { // 204 No Content. On windows it takes a base path and looks for // files under that path. if (process.platform == 'darwin') { - electron.autoUpdater.setFeedURL(update_base_url + 'macos/'); + // 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( + update_base_url + + 'macos/?localVersion=' + encodeURIComponent(electron.app.getVersion()) + ); } else if (process.platform == 'win32') { electron.autoUpdater.setFeedURL(update_base_url + 'win32/' + process.arch + '/'); } else { @@ -148,6 +159,21 @@ process.on('uncaughtException', function (error) { electron.ipcMain.on('install_update', installUpdate); +electron.app.commandLine.appendSwitch('--enable-usermedia-screen-capturing'); + +const shouldQuit = electron.app.makeSingleInstance((commandLine, workingDirectory) => { + // Someone tried to run a second instance, we should focus our window. + if (mainWindow) { + if (!mainWindow.isVisible()) mainWindow.show(); + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); + } +}); + +if (shouldQuit) { + electron.app.quit() +} + electron.app.on('ready', () => { if (vectorConfig.update_base_url) { console.log("Starting auto update with base URL: " + vectorConfig.update_base_url); @@ -164,10 +190,17 @@ electron.app.on('ready', () => { icon: icon_path, width: 1024, height: 768, show: false, + autoHideMenuBar: true, }); mainWindow.loadURL(`file://${__dirname}/../../webapp/index.html`); electron.Menu.setApplicationMenu(VectorMenu); + // Create trayIcon icon + tray.create(mainWindow, { + icon_path: icon_path, + brand: vectorConfig.brand || 'Riot' + }); + mainWindow.once('ready-to-show', () => { mainWindow.show(); }); @@ -175,7 +208,7 @@ electron.app.on('ready', () => { mainWindow = null; }); mainWindow.on('close', (e) => { - if (process.platform == 'darwin' && !appQuitting) { + if (!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) diff --git a/electron/src/squirrelhooks.js b/electron/src/squirrelhooks.js index 295ef5cf..15ed670f 100644 --- a/electron/src/squirrelhooks.js +++ b/electron/src/squirrelhooks.js @@ -1,9 +1,30 @@ +/* +Copyright 2017 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. +*/ + const path = require('path'); const spawn = require('child_process').spawn; const app = require('electron').app; function run_update_exe(args, done) { - const updateExe = path.resolve(path.dirname(process.execPath), 'Update.exe'); + // Invokes Squirrel's Update.exe which will do things for us like create shortcuts + // Note that there's an Update.exe in the app-x.x.x directory and one in the parent + // directory: we need to run the one in the parent directory, because it discovers + // information about the app by inspecting the directory it's run from. + const updateExe = path.resolve(path.dirname(process.execPath), '..', 'Update.exe'); + console.log('Spawning `%s` with args `%s`', updateExe, args); spawn(updateExe, args, { detached: true }).on('close', done); diff --git a/electron/src/tray.js b/electron/src/tray.js new file mode 100644 index 00000000..2ccdf40c --- /dev/null +++ b/electron/src/tray.js @@ -0,0 +1,67 @@ +/* +Copyright 2017 Karl Glatz +Copyright 2017 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. +*/ + +const path = require('path'); +const electron = require('electron'); + +const app = electron.app; +const Tray = electron.Tray; +const MenuItem = electron.MenuItem; + +let trayIcon = null; + +exports.hasTray = function hasTray() { + return (trayIcon !== null); +} + +exports.create = function (win, config) { + // no trays on darwin + if (process.platform === 'darwin' || trayIcon) { + return; + } + + const toggleWin = function () { + if (win.isVisible() && !win.isMinimized()) { + win.hide(); + } else { + if (win.isMinimized()) win.restore(); + if (!win.isVisible()) win.show(); + win.focus(); + } + }; + + const contextMenu = electron.Menu.buildFromTemplate([ + { + label: 'Show/Hide ' + config.brand, + click: toggleWin + }, + { + type: 'separator' + }, + { + label: 'Quit', + click: function () { + app.quit(); + } + } + ]); + + trayIcon = new Tray(config.icon_path); + trayIcon.setToolTip(config.brand); + trayIcon.setContextMenu(contextMenu); + trayIcon.on('click', toggleWin); +}; diff --git a/electron/src/vectormenu.js b/electron/src/vectormenu.js index f4d55c15..70ed3ac3 100644 --- a/electron/src/vectormenu.js +++ b/electron/src/vectormenu.js @@ -72,11 +72,7 @@ const template = [ role: 'togglefullscreen' }, { - label: 'Toggle Developer Tools', - accelerator: process.platform == 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I', - click: function(item, focusedWindow) { - if (focusedWindow) focusedWindow.toggleDevTools(); - } + role: 'toggledevtools' } ] }, diff --git a/karma.conf.js b/karma.conf.js index 24742169..b0a48c92 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -2,13 +2,14 @@ var path = require('path'); var webpack = require('webpack'); +var webpack_config = require('./webpack.config'); /* * We use webpack to build our tests. It's a pain to have to wait for webpack * to build everything; however it's the easiest way to load our dependencies * from node_modules. * - * If you run karma in multi-run mode (with `npm run test:multi`), it will watch + * If you run karma in multi-run mode (with `npm run test-multi`), it will watch * the tests for changes, and webpack will rebuild using a cache. This is much quicker * than a clean rebuild. */ @@ -19,8 +20,41 @@ var testFile = process.env.KARMA_TEST_FILE || 'test/all-tests.js'; process.env.PHANTOMJS_BIN = 'node_modules/.bin/phantomjs'; process.env.Q_DEBUG = 1; +/* the webpack config is based on the real one, to (a) try to simulate the + * deployed environment as closely as possible, and (b) to avoid a shedload of + * cut-and-paste. + */ + +// find out if we're shipping olm, and where it is, if so. +const olm_entry = webpack_config.entry['olm']; + +// remove the default entries - karma provides its own (via the 'files' and +// 'preprocessors' config below) +delete webpack_config['entry']; + +// add ./test as a search path for js +webpack_config.module.loaders.unshift({ + test: /\.js$/, loader: "babel", + include: [path.resolve('./src'), path.resolve('./test')], +}); + +// disable parsing for sinon, because it +// tries to do voodoo with 'require' which upsets +// webpack (https://github.com/webpack/webpack/issues/304) +webpack_config.module.noParse.push(/sinon\/pkg\/sinon\.js$/); + +// ? +webpack_config.resolve.alias['sinon'] = 'sinon/pkg/sinon.js'; + +webpack_config.resolve.root = [ + path.resolve('./src'), + path.resolve('./test'), +]; + +webpack_config.devtool = 'inline-source-map'; + module.exports = function (config) { - config.set({ + const myconfig = { // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter frameworks: ['mocha'], @@ -29,19 +63,29 @@ module.exports = function (config) { files: [ 'node_modules/babel-polyfill/browser.js', testFile, - {pattern: 'vector/img/*', watched: false, included: false, served: true, nocache: false}, + + // make the images available via our httpd. They will be avaliable + // below http://localhost:[PORT]/base/. See also `proxies` which + // defines alternative URLs for them. + // + // This isn't required by any of the tests, but it stops karma + // logging warnings when it serves a 404 for them. + { + pattern: 'src/skins/vector/img/*', + watched: false, included: false, served: true, nocache: false, + }, ], - // redirect img links to the karma server proxies: { - "/img/": "/base/vector/img/", + // redirect img links to the karma server. See above. + "/img/": "/base/src/skins/vector/img/", }, // preprocess matching files before serving them to the browser // available preprocessors: // https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: { - 'test/**/*.js': ['webpack', 'sourcemap'] + '{src,test}/**/*.js': ['webpack'], }, // test results reporter to use @@ -84,53 +128,20 @@ module.exports = function (config) { outputDir: 'karma-reports', }, - webpack: { - module: { - loaders: [ - { test: /\.json$/, loader: "json" }, - { - test: /\.js$/, loader: "babel", - include: [path.resolve('./src'), - path.resolve('./test'), - ] - }, - ], - noParse: [ - // don't parse the languages within highlight.js. They - // cause stack overflows - // (https://github.com/webpack/webpack/issues/1721), and - // there is no need for webpack to parse them - they can - // just be included as-is. - /highlight\.js\/lib\/languages/, + webpack: webpack_config, - // also disable parsing for sinon, because it - // tries to do voodoo with 'require' which upsets - // webpack (https://github.com/webpack/webpack/issues/304) - /sinon\/pkg\/sinon\.js$/, - ], + webpackMiddleware: { + stats: { + // don't fill the console up with a mahoosive list of modules + chunks: false, }, - resolve: { - alias: { - // alias any requires to the react module to the one in our path, otherwise - // we tend to get the react source included twice when using npm link. - react: path.resolve('./node_modules/react'), - - // same goes for js-sdk - "matrix-js-sdk": path.resolve('./node_modules/matrix-js-sdk'), - - sinon: 'sinon/pkg/sinon.js', - }, - root: [ - path.resolve('./src'), - path.resolve('./test'), - ], - }, - plugins: [ - // olm may not be installed, so avoid webpack warnings by - // ignoring it. - new webpack.IgnorePlugin(/^olm$/), - ], - devtool: 'inline-source-map', }, - }); + }; + + // include the olm loader if we have it. + if (olm_entry) { + myconfig.files.unshift(olm_entry); + } + + config.set(myconfig); }; diff --git a/package.json b/package.json index 3f9f46e2..155523af 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "riot-web", "productName": "Riot", "main": "electron/src/electron-main.js", - "version": "0.9.5", + "version": "0.9.7", "description": "A feature-rich client for Matrix.org", "author": "Vector Creations Ltd.", "repository": { @@ -27,30 +27,26 @@ "matrix-react-parent": "matrix-react-sdk", "scripts": { "reskindex": "reskindex -h src/header", - "build:res": "cpx \"{src/skins/vector/fonts,src/skins/vector/img}/**\" webapp/ && cpx \"{res/media,res/vector-icons}/**\" webapp/", - "build:config": "cpx config.json webapp/", - "build:emojione": "cpx \"node_modules/emojione/assets/svg/*\" webapp/emojione/svg/", + "build:res": "node scripts/copy-res.js", "build:modernizr": "modernizr -c .modernizr.json -d src/vector/modernizr.js", - "build:css": "mkdirp build && catw \"src/skins/vector/css/**/*.css\" -o build/components.css --no-watch", "build:compile": "babel --source-maps -d lib src", "build:bundle": "NODE_ENV=production webpack -p --progress", "build:bundle:dev": "webpack --optimize-occurence-order --progress", "build:electron": "npm run clean && npm run build && build -wml --ia32 --x64", - "build": "node scripts/babelcheck.js && npm run build:res && npm run build:config && npm run build:emojione && npm run build:css && npm run build:bundle", - "build:dev": "node scripts/babelcheck.js && npm run build:res && npm run build:config && npm run build:emojione && npm run build:css && npm run build:bundle:dev", + "build": "node scripts/babelcheck.js && npm run build:res && npm run build:bundle", + "build:dev": "node scripts/babelcheck.js && npm run build:res && npm run build:bundle:dev", "dist": "scripts/package.sh", - "start:res": "parallelshell \"cpx -w \\\"{src/skins/vector/fonts,src/skins/vector/img}/**\\\" webapp/\" \"cpx -w \\\"{res/media,res/vector-icons}/**\\\" webapp/\"", - "start:config": "cpx -w config.json webapp/", - "start:emojione": "cpx \"node_modules/emojione/assets/svg/*\" webapp/emojione/svg/ -w", - "start:js": "webpack-dev-server -w --progress", + "start:res": "node scripts/copy-res.js -w", + "start:js": "webpack-dev-server --output-filename=bundles/_dev_/[name].js --output-chunk-file=bundles/_dev_/[name].js -w --progress", "start:js:prod": "NODE_ENV=production webpack-dev-server -w --progress", - "start:skins:css": "mkdirp build && catw \"src/skins/vector/css/**/*.css\" -o build/components.css", - "start": "node scripts/babelcheck.js && parallelshell \"npm run start:emojione\" \"npm run start:res\" \"npm run start:config\" \"npm run start:js\" \"npm run start:skins:css\"", - "start:prod": "parallelshell \"npm run start:emojione\" \"npm run start:js:prod\" \"npm run start:skins:css\"", - "clean": "rimraf build lib webapp electron/dist", + "start": "node scripts/babelcheck.js && parallelshell \"npm run start:res\" \"npm run start:js\"", + "start:prod": "parallelshell \"npm run start:res\" \"npm run start:js:prod\"", + "lint": "eslint src/", + "lintall": "eslint src/ test/", + "clean": "rimraf lib webapp electron/dist", "prepublish": "npm run build:compile", "test": "karma start --single-run=true --autoWatch=false --browsers PhantomJS --colors=false", - "test:multi": "karma start" + "test-multi": "karma start" }, "dependencies": { "babel-polyfill": "^6.5.0", @@ -66,8 +62,8 @@ "gfm.css": "^1.1.1", "highlight.js": "^9.0.0", "linkifyjs": "^2.1.3", - "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", - "matrix-react-sdk": "matrix-org/matrix-react-sdk#develop", + "matrix-js-sdk": "0.7.5", + "matrix-react-sdk": "0.8.6", "modernizr": "^3.1.0", "q": "^1.4.1", "react": "^15.4.0", @@ -80,6 +76,7 @@ "url": "^0.11.0" }, "devDependencies": { + "autoprefixer": "^6.6.0", "babel-cli": "^6.5.2", "babel-core": "^6.14.0", "babel-eslint": "^6.1.0", @@ -94,11 +91,16 @@ "babel-preset-es2017": "^6.16.0", "babel-preset-react": "^6.16.0", "babel-preset-stage-2": "^6.17.0", - "catw": "^1.0.1", + "chokidar": "^1.6.1", "cpx": "^1.3.2", "css-raw-loader": "^0.1.1", - "electron-builder": "^10.4.1", + "electron-builder": "^11.2.4", + "electron-builder-squirrel-windows": "^11.2.1", "emojione": "^2.2.3", + "eslint": "^3.14.0", + "eslint-config-google": "^0.7.1", + "eslint-plugin-flowtype": "^2.30.0", + "eslint-plugin-react": "^6.9.0", "expect": "^1.16.0", "fs-extra": "^0.30.0", "html-webpack-plugin": "^2.24.0", @@ -109,12 +111,20 @@ "karma-junit-reporter": "^0.4.1", "karma-mocha": "^0.2.2", "karma-phantomjs-launcher": "^1.0.0", - "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^1.7.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", + "postcss-mixins": "^5.4.1", + "postcss-nested": "^1.0.0", + "postcss-scss": "^0.4.0", + "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", "rimraf": "^2.4.3", @@ -123,12 +133,12 @@ "webpack-dev-server": "^1.16.2" }, "optionalDependencies": { - "olm": "https://matrix.org/packages/npm/olm/olm-2.1.0.tgz" + "olm": "https://matrix.org/packages/npm/olm/olm-2.2.1.tgz" }, "build": { "appId": "im.riot.app", "category": "Network", - "electronVersion": "1.4.11", + "electronVersion": "1.4.14", "//asar=false": "https://github.com/electron-userland/electron-builder/issues/675", "asar": false, "dereference": true, @@ -141,7 +151,10 @@ ], "linux": { "target": "deb", - "maintainer": "support@riot.im" + "maintainer": "support@riot.im", + "desktop": { + "StartupWMClass": "riot-web" + } }, "win": { "target": "squirrel" diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 00000000..5305d9ed --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,13 @@ +module.exports = { + plugins: [ + require("postcss-import")(), + require("autoprefixer")(), + require("postcss-simple-vars")(), + require("postcss-extend")(), + require("postcss-nested")(), + require("postcss-mixins")(), + require("postcss-strip-inline-comments")(), + ], + "parser": "postcss-scss", + "local-plugins": true, +}; diff --git a/scripts/copy-res.js b/scripts/copy-res.js new file mode 100755 index 00000000..826d9a96 --- /dev/null +++ b/scripts/copy-res.js @@ -0,0 +1,81 @@ +#!/usr/bin/env node + +// copies the resources into the webapp directory. +// + +// cpx includes globbed parts of the filename in the destination, but excludes +// common parents. Hence, "res/{a,b}/**": the output will be "dest/a/..." and +// "dest/b/...". +const COPY_LIST = [ + ["res/{media,vector-icons}/**", "webapp"], + ["src/skins/vector/{fonts,img}/**", "webapp"], + ["node_modules/emojione/assets/svg/*", "webapp/emojione/svg/"], + ["./config.json", "webapp", {directwatch: 1}], +]; + +const parseArgs = require('minimist'); +const Cpx = require('cpx'); +const chokidar = require('chokidar'); + +const argv = parseArgs( + process.argv.slice(2), {} +); + +var watch = argv.w; +var verbose = argv.v; + +function errCheck(err) { + if (err) { + console.error(err.message); + process.exit(1); + } +} + +function next(i, err) { + errCheck(err); + + if (i >= COPY_LIST.length) { + return; + } + + const ent = COPY_LIST[i]; + const source = ent[0]; + const dest = ent[1]; + const opts = ent[2] || {}; + + const cpx = new Cpx.Cpx(source, dest); + + if (verbose) { + cpx.on("copy", (event) => { + console.log(`Copied: ${event.srcPath} --> ${event.dstPath}`); + }); + cpx.on("remove", (event) => { + console.log(`Removed: ${event.path}`); + }); + } + + const cb = (err) => {next(i+1, err)}; + + if (watch) { + if (opts.directwatch) { + // cpx -w creates a watcher for the parent of any files specified, + // which in the case of config.json is '.', which inevitably takes + // ages to crawl. So we create our own watcher on the files + // instead. + const copy = () => {cpx.copy(errCheck)}; + chokidar.watch(source) + .on('add', copy) + .on('change', copy) + .on('ready', cb) + .on('error', errCheck); + } else { + cpx.on('watch-ready', cb); + cpx.on("watch-error", cb); + cpx.watch(); + } + } else { + cpx.copy(cb); + } +} + +next(0); diff --git a/scripts/deploy.py b/scripts/deploy.py new file mode 100755 index 00000000..c96b46e8 --- /dev/null +++ b/scripts/deploy.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python +# +# download and unpack a riot-web tarball. +# +# Allows `bundles` to be extracted to a common directory, and a link to +# config.json to be added. + +from __future__ import print_function + +import argparse +import os +import os.path +import subprocess +import sys +import tarfile + +try: + # python3 + from urllib.request import urlretrieve +except ImportError: + # python2 + from urllib import urlretrieve + +class DeployException(Exception): + pass + +def create_relative_symlink(linkname, target): + relpath = os.path.relpath(target, os.path.dirname(linkname)) + print ("Symlink %s -> %s" % (linkname, relpath)) + os.symlink(relpath, linkname) + + +def move_bundles(source, dest): + """Move the contents of the 'bundles' directory to a common dir + + We check that we will not be overwriting anything before we proceed. + + Args: + source (str): path to 'bundles' within the extracted tarball + dest (str): target common directory + """ + + if not os.path.isdir(dest): + os.mkdir(dest) + + # build a map from source to destination, checking for non-existence as we go. + renames = {} + for f in os.listdir(source): + dst = os.path.join(dest, f) + if os.path.exists(dst): + raise DeployException( + "Not deploying. The bundle includes '%s' which we have previously deployed." + % f + ) + renames[os.path.join(source, f)] = dst + + for (src, dst) in renames.iteritems(): + print ("Move %s -> %s" % (src, dst)) + os.rename(src, dst) + +class Deployer: + def __init__(self): + self.packages_path = "." + self.bundles_path = None + self.should_clean = False + self.config_location = None + self.verify_signature = True + + def deploy(self, tarball, extract_path): + """Download a tarball if necessary, and unpack it + + Returns: + (str) the path to the unpacked deployment + """ + print("Deploying %s to %s" % (tarball, extract_path)) + + name_str = os.path.basename(tarball).replace(".tar.gz", "") + extracted_dir = os.path.join(extract_path, name_str) + if os.path.exists(extracted_dir): + raise DeployException('Cannot unpack %s: %s already exists' % ( + tarball, extracted_dir)) + + downloaded = False + if tarball.startswith("http://") or tarball.startswith("https://"): + tarball = self.download_and_verify(tarball) + print("Downloaded file: %s" % tarball) + downloaded = True + + try: + with tarfile.open(tarball) as tar: + tar.extractall(extract_path) + finally: + if self.should_clean and downloaded: + os.remove(tarball) + + 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.bundles_path: + extracted_bundles = os.path.join(extracted_dir, 'bundles') + move_bundles(source=extracted_bundles, dest=self.bundles_path) + + # replace the (hopefully now empty) extracted_bundles dir with a + # symlink to the common dir. + os.rmdir(extracted_bundles) + create_relative_symlink( + target=self.bundles_path, + linkname=extracted_bundles, + ) + return extracted_dir + + def download_and_verify(self, url): + tarball = self.download_file(url) + + if self.verify_signature: + sigfile = self.download_file(url + ".asc") + subprocess.check_call(["gpg", "--verify", sigfile, tarball]) + + return tarball + + def download_file(self, url): + if not os.path.isdir(self.packages_path): + os.mkdir(self.packages_path) + local_filename = os.path.join(self.packages_path, + url.split('/')[-1]) + sys.stdout.write("Downloading %s -> %s..." % (url, local_filename)) + sys.stdout.flush() + urlretrieve(url, local_filename) + print ("Done") + return local_filename + +if __name__ == "__main__": + parser = argparse.ArgumentParser("Deploy a Riot build on a web server.") + parser.add_argument( + "-p", "--packages-dir", default="./packages", help=( + "The directory to download the tarball into. (Default: '%(default)s')" + ) + ) + parser.add_argument( + "-e", "--extract-path", default="./deploys", help=( + "The location to extract .tar.gz files to. (Default: '%(default)s')" + ) + ) + parser.add_argument( + "-b", "--bundles-dir", nargs='?', default="./bundles", help=( + "A directory to move the contents of the 'bundles' directory to. A \ + symlink to the bundles directory will also be written inside the \ + extracted tarball. Example: './bundles'. \ + (Default: '%(default)s')" + ) + ) + parser.add_argument( + "-c", "--clean", action="store_true", default=False, help=( + "Remove .tar.gz files after they have been downloaded and extracted. \ + (Default: %(default)s)" + ) + ) + parser.add_argument( + "--config", nargs='?', default='./config.json', help=( + "Write a symlink at config.json in the extracted tarball to this \ + location. (Default: '%(default)s')" + ) + ) + parser.add_argument( + "tarball", help=( + "filename of tarball, or URL to download." + ), + ) + + args = parser.parse_args() + + deployer = Deployer() + deployer.packages_path = args.packages_dir + deployer.bundles_path = args.bundles_dir + deployer.should_clean = args.clean + deployer.config_location = args.config + + deployer.deploy(args.tarball, args.extract_path) diff --git a/scripts/jenkins.sh b/scripts/jenkins.sh index be8d8dee..0f4fac25 100755 --- a/scripts/jenkins.sh +++ b/scripts/jenkins.sh @@ -19,12 +19,16 @@ tar -C olm -xz < olm/olm-*.tgz rm -r node_modules/olm cp -r olm/package node_modules/olm -# we may be using a dev branch of react-sdk, in which case we need to build it -(cd node_modules/matrix-react-sdk && npm run build) +# we may be using dev branches of js-sdk and react-sdk, in which case we need to build them +(cd node_modules/matrix-js-sdk && npm install) +(cd node_modules/matrix-react-sdk && npm install) # run the mocha tests npm run test +# run eslint +npm run lintall -- -f checkstyle -o eslint.xml || true + rm dist/vector-*.tar.gz || true # rm previous artifacts without failing if it doesn't exist # node_modules deps from 'npm install' don't have a .git dir so can't diff --git a/scripts/redeploy.py b/scripts/redeploy.py index 36585f53..598f6c52 100755 --- a/scripts/redeploy.py +++ b/scripts/redeploy.py @@ -1,26 +1,30 @@ #!/usr/bin/env python +# +# auto-deploy script for https://riot.im/develop +# +# Listens for HTTP hits. When it gets one, downloads the artifact from jenkins +# and deploys it as the new version. +# +# Requires the following python packages: +# +# - requests +# - flask +# from __future__ import print_function import json, requests, tarfile, argparse, os, errno +import time from urlparse import urljoin + from flask import Flask, jsonify, request, abort + +from deploy import Deployer, DeployException + app = Flask(__name__) -arg_jenkins_url, arg_extract_path, arg_should_clean, arg_symlink, arg_config_location = ( - None, None, None, None, None -) - -def download_file(url): - local_filename = url.split('/')[-1] - r = requests.get(url, stream=True) - with open(local_filename, 'wb') as f: - for chunk in r.iter_content(chunk_size=1024): - if chunk: # filter out keep-alive new chunks - f.write(chunk) - return local_filename - -def untar_to(tarball, dest): - with tarfile.open(tarball) as tar: - tar.extractall(dest) +arg_jenkins_url = None +deployer = None +arg_extract_path = None +arg_symlink = None def create_symlink(source, linkname): try: @@ -57,6 +61,9 @@ def on_receive_jenkins_poke(): abort(400, "Missing or bad build number") return + return fetch_jenkins_build(job_name, build_num) + +def fetch_jenkins_build(job_name, build_num): artifact_url = urljoin( arg_jenkins_url, "job/%s/%s/api/json" % (job_name, build_num) ) @@ -106,43 +113,41 @@ def on_receive_jenkins_poke(): arg_jenkins_url, "job/%s/%s/artifact/%s" % (job_name, build_num, tar_gz_path) ) - print("Retrieving .tar.gz file: %s" % tar_gz_url) - - # we rely on the fact that flask only serves one request at a time to - # ensure that we do not overwrite a tarball from a concurrent request. - filename = download_file(tar_gz_url) - print("Downloaded file: %s" % filename) - + # we extract into a directory based on the build number. This avoids the + # problem of multiple builds building the same git version and thus having + # the same tarball name. That would lead to two potential problems: + # (a) sometimes jenkins serves corrupted artifacts; we would replace + # a good deploy with a bad one + # (b) we'll be overwriting the live deployment, which means people might + # see half-written files. + build_dir = os.path.join(arg_extract_path, "%s-#%s" % (job_name, build_num)) try: - # we extract into a directory based on the build number. This avoids the - # problem of multiple builds building the same git version and thus having - # the same tarball name. That would lead to two potential problems: - # (a) sometimes jenkins serves corrupted artifacts; we would replace - # a good deploy with a bad one - # (b) we'll be overwriting the live deployment, which means people might - # see half-written files. - build_dir = os.path.join(arg_extract_path, "%s-#%s" % (job_name, build_num)) - if os.path.exists(build_dir): - abort(400, "Not deploying. We have previously deployed this build.") - return - os.mkdir(build_dir) - - untar_to(filename, build_dir) - print("Extracted to: %s" % build_dir) - finally: - if arg_should_clean: - os.remove(filename) - - name_str = filename.replace(".tar.gz", "") - extracted_dir = os.path.join(build_dir, name_str) - - if arg_config_location: - create_symlink(source=arg_config_location, linkname=os.path.join(extracted_dir, 'config.json')) + extracted_dir = deploy_tarball(tar_gz_url, build_dir) + except DeployException as e: + abort(400, e.message) create_symlink(source=extracted_dir, linkname=arg_symlink) return jsonify({}) +def deploy_tarball(tar_gz_url, build_dir): + """Download a tarball from jenkins and unpack it + + Returns: + (str) the path to the unpacked deployment + """ + if os.path.exists(build_dir): + raise DeployException( + "Not deploying. We have previously deployed this build." + ) + os.mkdir(build_dir) + + # we rely on the fact that flask only serves one request at a time to + # ensure that we do not overwrite a tarball from a concurrent request. + + return deployer.deploy(tar_gz_url, build_dir) + + if __name__ == "__main__": parser = argparse.ArgumentParser("Runs a Vector redeployment server.") parser.add_argument( @@ -161,6 +166,13 @@ if __name__ == "__main__": "The location to extract .tar.gz files to." ) ) + parser.add_argument( + "-b", "--bundles-dir", dest="bundles_dir", help=( + "A directory to move the contents of the 'bundles' directory to. A \ + symlink to the bundles directory will also be written inside the \ + extracted tarball. Example: './bundles'." + ) + ) parser.add_argument( "-c", "--clean", dest="clean", action="store_true", default=False, help=( "Remove .tar.gz files after they have been downloaded and extracted." @@ -179,18 +191,47 @@ if __name__ == "__main__": To this location." ) ) + parser.add_argument( + "--test", dest="tarball_uri", help=( + "Don't start an HTTP listener. Instead download a build from Jenkins \ + immediately." + ), + ) + args = parser.parse_args() if args.jenkins.endswith("/"): # important for urljoin arg_jenkins_url = args.jenkins else: arg_jenkins_url = args.jenkins + "/" arg_extract_path = args.extract - arg_should_clean = args.clean arg_symlink = args.symlink - arg_config_location = args.config - print( - "Listening on port %s. Extracting to %s%s. Symlinking to %s. Jenkins URL: %s. Config location: %s" % - (args.port, arg_extract_path, - " (clean after)" if arg_should_clean else "", arg_symlink, arg_jenkins_url, arg_config_location) - ) - app.run(host="0.0.0.0", port=args.port, debug=True) + + if not os.path.isdir(arg_extract_path): + os.mkdir(arg_extract_path) + + deployer = Deployer() + deployer.bundles_path = args.bundles_dir + deployer.should_clean = args.clean + deployer.config_location = args.config + + # 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 + # github not serving it compromised source). If that's not good enough for + # you, don't use riot.im/develop. + deployer.verify_signature = False + + if args.tarball_uri is not None: + build_dir = os.path.join(arg_extract_path, "test-%i" % (time.time())) + 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" % + (args.port, + arg_extract_path, + " (clean after)" if deployer.should_clean else "", + arg_symlink, + arg_jenkins_url, + deployer.config_location, + ) + ) + app.run(host="0.0.0.0", port=args.port, debug=True) diff --git a/src/component-index.js b/src/component-index.js index 3141087c..456f8176 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -27,60 +27,62 @@ limitations under the License. module.exports.components = require('matrix-react-sdk/lib/component-index').components; import structures$BottomLeftMenu from './components/structures/BottomLeftMenu'; -module.exports.components['structures.BottomLeftMenu'] = structures$BottomLeftMenu; +structures$BottomLeftMenu && (module.exports.components['structures.BottomLeftMenu'] = structures$BottomLeftMenu); import structures$CompatibilityPage from './components/structures/CompatibilityPage'; -module.exports.components['structures.CompatibilityPage'] = structures$CompatibilityPage; +structures$CompatibilityPage && (module.exports.components['structures.CompatibilityPage'] = structures$CompatibilityPage); import structures$LeftPanel from './components/structures/LeftPanel'; -module.exports.components['structures.LeftPanel'] = structures$LeftPanel; +structures$LeftPanel && (module.exports.components['structures.LeftPanel'] = structures$LeftPanel); import structures$RightPanel from './components/structures/RightPanel'; -module.exports.components['structures.RightPanel'] = structures$RightPanel; +structures$RightPanel && (module.exports.components['structures.RightPanel'] = structures$RightPanel); import structures$RoomDirectory from './components/structures/RoomDirectory'; -module.exports.components['structures.RoomDirectory'] = structures$RoomDirectory; +structures$RoomDirectory && (module.exports.components['structures.RoomDirectory'] = structures$RoomDirectory); import structures$RoomSubList from './components/structures/RoomSubList'; -module.exports.components['structures.RoomSubList'] = structures$RoomSubList; +structures$RoomSubList && (module.exports.components['structures.RoomSubList'] = structures$RoomSubList); import structures$SearchBox from './components/structures/SearchBox'; -module.exports.components['structures.SearchBox'] = structures$SearchBox; +structures$SearchBox && (module.exports.components['structures.SearchBox'] = structures$SearchBox); import structures$ViewSource from './components/structures/ViewSource'; -module.exports.components['structures.ViewSource'] = structures$ViewSource; +structures$ViewSource && (module.exports.components['structures.ViewSource'] = structures$ViewSource); import views$context_menus$MessageContextMenu from './components/views/context_menus/MessageContextMenu'; -module.exports.components['views.context_menus.MessageContextMenu'] = views$context_menus$MessageContextMenu; +views$context_menus$MessageContextMenu && (module.exports.components['views.context_menus.MessageContextMenu'] = views$context_menus$MessageContextMenu); import views$context_menus$NotificationStateContextMenu from './components/views/context_menus/NotificationStateContextMenu'; -module.exports.components['views.context_menus.NotificationStateContextMenu'] = views$context_menus$NotificationStateContextMenu; +views$context_menus$NotificationStateContextMenu && (module.exports.components['views.context_menus.NotificationStateContextMenu'] = views$context_menus$NotificationStateContextMenu); import views$context_menus$RoomTagContextMenu from './components/views/context_menus/RoomTagContextMenu'; -module.exports.components['views.context_menus.RoomTagContextMenu'] = views$context_menus$RoomTagContextMenu; +views$context_menus$RoomTagContextMenu && (module.exports.components['views.context_menus.RoomTagContextMenu'] = views$context_menus$RoomTagContextMenu); +import views$dialogs$BugReportDialog from './components/views/dialogs/BugReportDialog'; +views$dialogs$BugReportDialog && (module.exports.components['views.dialogs.BugReportDialog'] = views$dialogs$BugReportDialog); import views$dialogs$ChangelogDialog from './components/views/dialogs/ChangelogDialog'; -module.exports.components['views.dialogs.ChangelogDialog'] = views$dialogs$ChangelogDialog; +views$dialogs$ChangelogDialog && (module.exports.components['views.dialogs.ChangelogDialog'] = views$dialogs$ChangelogDialog); import views$directory$NetworkDropdown from './components/views/directory/NetworkDropdown'; -module.exports.components['views.directory.NetworkDropdown'] = views$directory$NetworkDropdown; +views$directory$NetworkDropdown && (module.exports.components['views.directory.NetworkDropdown'] = views$directory$NetworkDropdown); import views$elements$ImageView from './components/views/elements/ImageView'; -module.exports.components['views.elements.ImageView'] = views$elements$ImageView; +views$elements$ImageView && (module.exports.components['views.elements.ImageView'] = views$elements$ImageView); import views$elements$Spinner from './components/views/elements/Spinner'; -module.exports.components['views.elements.Spinner'] = views$elements$Spinner; +views$elements$Spinner && (module.exports.components['views.elements.Spinner'] = views$elements$Spinner); import views$globals$GuestWarningBar from './components/views/globals/GuestWarningBar'; -module.exports.components['views.globals.GuestWarningBar'] = views$globals$GuestWarningBar; +views$globals$GuestWarningBar && (module.exports.components['views.globals.GuestWarningBar'] = views$globals$GuestWarningBar); import views$globals$MatrixToolbar from './components/views/globals/MatrixToolbar'; -module.exports.components['views.globals.MatrixToolbar'] = views$globals$MatrixToolbar; +views$globals$MatrixToolbar && (module.exports.components['views.globals.MatrixToolbar'] = views$globals$MatrixToolbar); import views$globals$NewVersionBar from './components/views/globals/NewVersionBar'; -module.exports.components['views.globals.NewVersionBar'] = views$globals$NewVersionBar; +views$globals$NewVersionBar && (module.exports.components['views.globals.NewVersionBar'] = views$globals$NewVersionBar); import views$login$VectorCustomServerDialog from './components/views/login/VectorCustomServerDialog'; -module.exports.components['views.login.VectorCustomServerDialog'] = views$login$VectorCustomServerDialog; +views$login$VectorCustomServerDialog && (module.exports.components['views.login.VectorCustomServerDialog'] = views$login$VectorCustomServerDialog); import views$login$VectorLoginFooter from './components/views/login/VectorLoginFooter'; -module.exports.components['views.login.VectorLoginFooter'] = views$login$VectorLoginFooter; +views$login$VectorLoginFooter && (module.exports.components['views.login.VectorLoginFooter'] = views$login$VectorLoginFooter); import views$login$VectorLoginHeader from './components/views/login/VectorLoginHeader'; -module.exports.components['views.login.VectorLoginHeader'] = views$login$VectorLoginHeader; +views$login$VectorLoginHeader && (module.exports.components['views.login.VectorLoginHeader'] = views$login$VectorLoginHeader); import views$messages$DateSeparator from './components/views/messages/DateSeparator'; -module.exports.components['views.messages.DateSeparator'] = views$messages$DateSeparator; +views$messages$DateSeparator && (module.exports.components['views.messages.DateSeparator'] = views$messages$DateSeparator); import views$messages$MessageTimestamp from './components/views/messages/MessageTimestamp'; -module.exports.components['views.messages.MessageTimestamp'] = views$messages$MessageTimestamp; +views$messages$MessageTimestamp && (module.exports.components['views.messages.MessageTimestamp'] = views$messages$MessageTimestamp); import views$rooms$DNDRoomTile from './components/views/rooms/DNDRoomTile'; -module.exports.components['views.rooms.DNDRoomTile'] = views$rooms$DNDRoomTile; +views$rooms$DNDRoomTile && (module.exports.components['views.rooms.DNDRoomTile'] = views$rooms$DNDRoomTile); import views$rooms$RoomDropTarget from './components/views/rooms/RoomDropTarget'; -module.exports.components['views.rooms.RoomDropTarget'] = views$rooms$RoomDropTarget; +views$rooms$RoomDropTarget && (module.exports.components['views.rooms.RoomDropTarget'] = views$rooms$RoomDropTarget); import views$rooms$RoomTooltip from './components/views/rooms/RoomTooltip'; -module.exports.components['views.rooms.RoomTooltip'] = views$rooms$RoomTooltip; +views$rooms$RoomTooltip && (module.exports.components['views.rooms.RoomTooltip'] = views$rooms$RoomTooltip); import views$rooms$SearchBar from './components/views/rooms/SearchBar'; -module.exports.components['views.rooms.SearchBar'] = views$rooms$SearchBar; +views$rooms$SearchBar && (module.exports.components['views.rooms.SearchBar'] = views$rooms$SearchBar); import views$settings$IntegrationsManager from './components/views/settings/IntegrationsManager'; -module.exports.components['views.settings.IntegrationsManager'] = views$settings$IntegrationsManager; +views$settings$IntegrationsManager && (module.exports.components['views.settings.IntegrationsManager'] = views$settings$IntegrationsManager); import views$settings$Notifications from './components/views/settings/Notifications'; -module.exports.components['views.settings.Notifications'] = views$settings$Notifications; +views$settings$Notifications && (module.exports.components['views.settings.Notifications'] = views$settings$Notifications); diff --git a/src/components/structures/BottomLeftMenu.js b/src/components/structures/BottomLeftMenu.js index 0ea35b4e..1e352fa1 100644 --- a/src/components/structures/BottomLeftMenu.js +++ b/src/components/structures/BottomLeftMenu.js @@ -20,6 +20,7 @@ var React = require('react'); var ReactDOM = require('react-dom'); var sdk = require('matrix-react-sdk') var dis = require('matrix-react-sdk/lib/dispatcher'); +var AccessibleButton = require('matrix-react-sdk/lib/components/views/elements/AccessibleButton'); module.exports = React.createClass({ displayName: 'BottomLeftMenu', @@ -101,22 +102,22 @@ module.exports = React.createClass({ return (
-
+ { this.getLabel("Start chat", this.state.peopleHover) } -
-
+ + { this.getLabel("Room directory", this.state.directoryHover) } -
-
+ + { this.getLabel("Create new room", this.state.roomsHover) } -
-
+ + { this.getLabel("Settings", this.state.settingsHover) } -
+
); diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index d1622e5f..04a98119 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -23,6 +23,7 @@ var dis = require('matrix-react-sdk/lib/dispatcher'); var MatrixClientPeg = require("matrix-react-sdk/lib/MatrixClientPeg"); var rate_limited_func = require('matrix-react-sdk/lib/ratelimitedfunc'); var Modal = require('matrix-react-sdk/lib/Modal'); +var AccessibleButton = require('matrix-react-sdk/lib/components/views/elements/AccessibleButton'); module.exports = React.createClass({ displayName: 'RightPanel', @@ -69,45 +70,21 @@ module.exports = React.createClass({ }, onMemberListButtonClick: function() { - if (this.props.collapsed || this.state.phase !== this.Phase.MemberList) { - this.setState({ phase: this.Phase.MemberList }); - dis.dispatch({ - action: 'show_right_panel', - }); - } - else { - dis.dispatch({ - action: 'hide_right_panel', - }); - } + this.setState({ phase: this.Phase.MemberList }); }, onFileListButtonClick: function() { - if (this.props.collapsed || this.state.phase !== this.Phase.FilePanel) { - this.setState({ phase: this.Phase.FilePanel }); - dis.dispatch({ - action: 'show_right_panel', - }); - } - else { - dis.dispatch({ - action: 'hide_right_panel', - }); - } + this.setState({ phase: this.Phase.FilePanel }); }, onNotificationListButtonClick: function() { - if (this.props.collapsed || this.state.phase !== this.Phase.NotificationPanel) { - this.setState({ phase: this.Phase.NotificationPanel }); - dis.dispatch({ - action: 'show_right_panel', - }); - } - else { - dis.dispatch({ - action: 'hide_right_panel', - }); - } + this.setState({ phase: this.Phase.NotificationPanel }); + }, + + onCollapseClick: function() { + dis.dispatch({ + action: 'hide_right_panel', + }); }, onInviteButtonClick: function() { @@ -207,12 +184,12 @@ module.exports = React.createClass({ if (user_is_in_room) { inviteGroup = -
+
Invite to this room
-
; + ; } } @@ -220,20 +197,28 @@ module.exports = React.createClass({ if (this.props.roomId) { buttonGroup =
-
+
{ membersBadge ? membersBadge :  }
{ membersHighlight } -
-
+ +
 
{ filesHighlight } -
-
+ +
 
{ notificationsHighlight } +
+
+
; } @@ -276,4 +261,3 @@ module.exports = React.createClass({ ); } }); - diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index e87f3f8a..af8500df 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -26,6 +26,7 @@ var Unread = require('matrix-react-sdk/lib/Unread'); var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); 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'); // turn this on for drop & drag console debugging galore var debug = false; @@ -417,15 +418,17 @@ var RoomSubList = React.createClass({ } } + var tabindex = this.props.searchFilter === "" ? "0" : "-1"; + return (
-
+ { this.props.collapsed ? '' : this.props.label }
{ roomCount }
{ badge } { incomingCall } -
+
); }, @@ -447,11 +450,11 @@ var RoomSubList = React.createClass({ }); return ( -
+
more
-
{ content }
-
+
{ content }
+ ); }, diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index 7fc51000..12bfc6dd 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -20,6 +20,7 @@ var React = require('react'); 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'); module.exports = React.createClass({ displayName: 'SearchBox', @@ -35,6 +36,25 @@ module.exports = React.createClass({ }; }, + componentDidMount: function() { + this.dispatcherRef = dis.register(this.onAction); + }, + + componentWillUnmount: function() { + dis.unregister(this.dispatcherRef); + }, + + onAction: function(payload) { + switch (payload.action) { + // Clear up the text field when a room is selected. + case 'view_room': + if (this.refs.search) { + this._clearSearch(); + } + break; + } + }, + onChange: function() { if (!this.refs.search) return; this.setState({ searchTerm: this.refs.search.value }); @@ -61,35 +81,42 @@ module.exports = React.createClass({ } }, + _clearSearch: function() { + this.refs.search.value = ""; + this.onChange(); + }, + render: function() { var TintableSvg = sdk.getComponent('elements.TintableSvg'); + var collapseTabIndex = this.refs.search && this.refs.search.value !== "" ? "-1" : "0"; + var toggleCollapse; if (this.props.collapsed) { toggleCollapse = -
- -
+ + + } else { toggleCollapse = -
- -
+ + + } var searchControls; if (!this.props.collapsed) { searchControls = [ this.state.searchTerm.length > 0 ? -
{ this.refs.search.value = ""; this.onChange(); } }> + { this._clearSearch(); } }> -
+ :
- + All messages (loud)
- + All messages
- + Mentions only
- + Mute
diff --git a/src/components/views/dialogs/BugReportDialog.js b/src/components/views/dialogs/BugReportDialog.js new file mode 100644 index 00000000..dcc0850e --- /dev/null +++ b/src/components/views/dialogs/BugReportDialog.js @@ -0,0 +1,127 @@ +/* +Copyright 2017 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. +*/ + +import React from 'react'; +import sdk from 'matrix-react-sdk'; +import rageshake from '../../../vector/rageshake'; + +export default class BugReportDialog extends React.Component { + constructor(props, context) { + super(props, context); + this.state = { + sendLogs: true, + busy: false, + err: null, + text: "", + }; + this._onSubmit = this._onSubmit.bind(this); + this._onCancel = this._onCancel.bind(this); + this._onTextChange = this._onTextChange.bind(this); + this._onSendLogsChange = this._onSendLogsChange.bind(this); + } + + _onCancel(ev) { + this.props.onFinished(false); + } + + _onSubmit(ev) { + const sendLogs = this.state.sendLogs; + const userText = this.state.text; + if (!sendLogs && userText.trim().length === 0) { + this.setState({ + err: "Please describe the bug and/or send logs.", + }); + return; + } + this.setState({ busy: true, err: null }); + rageshake.sendBugReport(userText, sendLogs).then(() => { + this.setState({ busy: false }); + this.props.onFinished(false); + }, (err) => { + this.setState({ busy: false, err: `Failed: ${err.message}` }); + }); + } + + _onTextChange(ev) { + this.setState({ text: ev.target.value }); + } + + _onSendLogsChange(ev) { + this.setState({ sendLogs: ev.target.checked }); + } + + render() { + const Loader = sdk.getComponent("elements.Spinner"); + + let error = null; + if (this.state.err) { + error =
+ {this.state.err} +
; + } + + const okLabel = this.state.busy ? : 'Send'; + + let cancelButton = null; + if (!this.state.busy) { + cancelButton = ; + } + + return ( +
+
+ Report a bug +
+
+

Please describe the bug. What did you do? + What did you expect to happen? + What actually happened?

+