diff --git a/.gitignore b/.gitignore index 5139d614ad..dcfe1c355d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ npm-debug.log # test reports created by karma /karma-reports + +# ignore auto-generated component index +/src/component-index.js diff --git a/.travis-test-riot.sh b/.travis-test-riot.sh index c280044246..4296c72e6c 100755 --- a/.travis-test-riot.sh +++ b/.travis-test-riot.sh @@ -9,11 +9,16 @@ set -ev RIOT_WEB_DIR=riot-web REACT_SDK_DIR=`pwd` -git clone --depth=1 --branch develop https://github.com/vector-im/riot-web.git \ +curbranch="${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH}" +echo "Determined branch to be $curbranch" + +git clone https://github.com/vector-im/riot-web.git \ "$RIOT_WEB_DIR" cd "$RIOT_WEB_DIR" +git checkout "$curbranch" || git checkout develop + mkdir node_modules npm install diff --git a/CHANGELOG.md b/CHANGELOG.md index 292e60607d..3b9ecdb325 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,175 @@ +Changes in [0.8.9](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.9) (2017-05-22) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.9-rc.1...v0.8.9) + + * No changes + + +Changes in [0.8.9-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.9-rc.1) (2017-05-19) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8...v0.8.9-rc.1) + + * Prevent an exception getting scroll node + [\#902](https://github.com/matrix-org/matrix-react-sdk/pull/902) + * Fix a few remaining snags with country dd + [\#901](https://github.com/matrix-org/matrix-react-sdk/pull/901) + * Add left_aligned class to CountryDropdown + [\#900](https://github.com/matrix-org/matrix-react-sdk/pull/900) + * Swap to new flag files (which are stored as GB.png) + [\#899](https://github.com/matrix-org/matrix-react-sdk/pull/899) + * Improve phone number country dropdown for registration and login (Act. 2, + Return of the Prefix) + [\#897](https://github.com/matrix-org/matrix-react-sdk/pull/897) + * Support for pasting files into normal composer + [\#892](https://github.com/matrix-org/matrix-react-sdk/pull/892) + * tell guests they can't use filepanel until they register + [\#887](https://github.com/matrix-org/matrix-react-sdk/pull/887) + * Prevent reskindex -w from running when file names have not changed + [\#888](https://github.com/matrix-org/matrix-react-sdk/pull/888) + * I broke UserSettings for webpack-dev-server + [\#884](https://github.com/matrix-org/matrix-react-sdk/pull/884) + * various fixes to RoomHeader + [\#880](https://github.com/matrix-org/matrix-react-sdk/pull/880) + * remove /me whether or not it has a space after it + [\#885](https://github.com/matrix-org/matrix-react-sdk/pull/885) + * show error if we can't set a filter because no room + [\#883](https://github.com/matrix-org/matrix-react-sdk/pull/883) + * Fix RM not updating if RR event unpaginated + [\#874](https://github.com/matrix-org/matrix-react-sdk/pull/874) + * change roomsettings wording + [\#878](https://github.com/matrix-org/matrix-react-sdk/pull/878) + * make reskindex windows friendly + [\#875](https://github.com/matrix-org/matrix-react-sdk/pull/875) + * Fixes 2 issues with Dialog closing + [\#867](https://github.com/matrix-org/matrix-react-sdk/pull/867) + * Automatic Reskindex + [\#871](https://github.com/matrix-org/matrix-react-sdk/pull/871) + * Put room name in 'leave room' confirmation dialog + [\#873](https://github.com/matrix-org/matrix-react-sdk/pull/873) + * Fix this/self fail in LeftPanel + [\#872](https://github.com/matrix-org/matrix-react-sdk/pull/872) + * Don't show null URL previews + [\#870](https://github.com/matrix-org/matrix-react-sdk/pull/870) + * Fix keys for AddressSelector + [\#869](https://github.com/matrix-org/matrix-react-sdk/pull/869) + * Make left panel better for new users (mk II) + [\#859](https://github.com/matrix-org/matrix-react-sdk/pull/859) + * Explicitly save composer content onUnload + [\#866](https://github.com/matrix-org/matrix-react-sdk/pull/866) + * Warn on unload + [\#851](https://github.com/matrix-org/matrix-react-sdk/pull/851) + * Log deviceid at login + [\#862](https://github.com/matrix-org/matrix-react-sdk/pull/862) + * Guests can't send RR so no point trying + [\#860](https://github.com/matrix-org/matrix-react-sdk/pull/860) + * Remove babelcheck + [\#861](https://github.com/matrix-org/matrix-react-sdk/pull/861) + * T3chguy/settings versions improvements + [\#857](https://github.com/matrix-org/matrix-react-sdk/pull/857) + * Change max-len 90->120 + [\#852](https://github.com/matrix-org/matrix-react-sdk/pull/852) + * Remove DM-guessing code + [\#829](https://github.com/matrix-org/matrix-react-sdk/pull/829) + * Fix jumping to an unread event when in MELS + [\#855](https://github.com/matrix-org/matrix-react-sdk/pull/855) + * Validate phone number on login + [\#856](https://github.com/matrix-org/matrix-react-sdk/pull/856) + * Failed to enable HTML5 Notifications Error Dialogs + [\#827](https://github.com/matrix-org/matrix-react-sdk/pull/827) + * Pin filesize ver to fix break upstream + [\#854](https://github.com/matrix-org/matrix-react-sdk/pull/854) + * Improve RoomDirectory Look & Feel + [\#848](https://github.com/matrix-org/matrix-react-sdk/pull/848) + * Only show jumpToReadMarker bar when RM !== RR + [\#845](https://github.com/matrix-org/matrix-react-sdk/pull/845) + * Allow MELS to have its own RM + [\#846](https://github.com/matrix-org/matrix-react-sdk/pull/846) + * Use document.onkeydown instead of onkeypress + [\#844](https://github.com/matrix-org/matrix-react-sdk/pull/844) + * (Room)?Avatar: Request 96x96 avatars on high DPI screens + [\#808](https://github.com/matrix-org/matrix-react-sdk/pull/808) + * Add mx_EventTile_emote class + [\#842](https://github.com/matrix-org/matrix-react-sdk/pull/842) + * Fix dialog reappearing after hitting Enter + [\#841](https://github.com/matrix-org/matrix-react-sdk/pull/841) + * Fix spinner that shows until the first sync + [\#840](https://github.com/matrix-org/matrix-react-sdk/pull/840) + * Show spinner until first sync has completed + [\#839](https://github.com/matrix-org/matrix-react-sdk/pull/839) + * Style fixes for LoggedInView + [\#838](https://github.com/matrix-org/matrix-react-sdk/pull/838) + * Fix specifying custom server for registration + [\#834](https://github.com/matrix-org/matrix-react-sdk/pull/834) + * Improve country dropdown UX and expose +prefix + [\#833](https://github.com/matrix-org/matrix-react-sdk/pull/833) + * Fix user settings store + [\#836](https://github.com/matrix-org/matrix-react-sdk/pull/836) + * show the room name in the UDE Dialog + [\#832](https://github.com/matrix-org/matrix-react-sdk/pull/832) + * summarise profile changes in MELS + [\#826](https://github.com/matrix-org/matrix-react-sdk/pull/826) + * Transform h1 and h2 tags to h3 tags + [\#820](https://github.com/matrix-org/matrix-react-sdk/pull/820) + * limit our keyboard shortcut modifiers correctly + [\#825](https://github.com/matrix-org/matrix-react-sdk/pull/825) + * Specify cross platform regexes and add olm to noParse + [\#823](https://github.com/matrix-org/matrix-react-sdk/pull/823) + * Remember element that was in focus before rendering dialog + [\#822](https://github.com/matrix-org/matrix-react-sdk/pull/822) + * move user settings outward and use built in read receipts disabling + [\#824](https://github.com/matrix-org/matrix-react-sdk/pull/824) + * File Download Consistency + [\#802](https://github.com/matrix-org/matrix-react-sdk/pull/802) + * Show Access Token under Advanced in Settings + [\#806](https://github.com/matrix-org/matrix-react-sdk/pull/806) + * Link tags/commit hashes in the UserSettings version section + [\#810](https://github.com/matrix-org/matrix-react-sdk/pull/810) + * On return to RoomView from auxPanel, send focus back to Composer + [\#813](https://github.com/matrix-org/matrix-react-sdk/pull/813) + * Change presence status labels to 'for' instead of 'ago' + [\#817](https://github.com/matrix-org/matrix-react-sdk/pull/817) + * Disable Scalar Integrations if urls passed to it are falsey + [\#816](https://github.com/matrix-org/matrix-react-sdk/pull/816) + * Add option to hide other people's read receipts. + [\#818](https://github.com/matrix-org/matrix-react-sdk/pull/818) + * Add option to not send typing notifications + [\#819](https://github.com/matrix-org/matrix-react-sdk/pull/819) + * Sync RM across instances of Riot + [\#805](https://github.com/matrix-org/matrix-react-sdk/pull/805) + * First iteration on improving login UI + [\#811](https://github.com/matrix-org/matrix-react-sdk/pull/811) + * focus on composer after jumping to bottom + [\#809](https://github.com/matrix-org/matrix-react-sdk/pull/809) + * Improve RoomList performance via side-stepping React + [\#807](https://github.com/matrix-org/matrix-react-sdk/pull/807) + * Don't show link preview when link is inside of a quote + [\#762](https://github.com/matrix-org/matrix-react-sdk/pull/762) + * Escape closes UserSettings + [\#765](https://github.com/matrix-org/matrix-react-sdk/pull/765) + * Implement user power-level changes in timeline + [\#794](https://github.com/matrix-org/matrix-react-sdk/pull/794) + +Changes in [0.8.8](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8) (2017-04-25) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8-rc.2...v0.8.8) + + * No changes + + +Changes in [0.8.8-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8-rc.2) (2017-04-24) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8-rc.1...v0.8.8-rc.2) + + * Fix bug where links to Riot would fail to open. + + +Changes in [0.8.8-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8-rc.1) (2017-04-21) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7...v0.8.8-rc.1) + + * Update js-sdk to fix registration without a captcha (https://github.com/vector-im/riot-web/issues/3621) + + Changes in [0.8.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.7) (2017-04-12) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.7-rc.4...v0.8.7) diff --git a/code_style.md b/code_style.md index f0eca75ffc..2cac303e54 100644 --- a/code_style.md +++ b/code_style.md @@ -69,25 +69,41 @@ General Style console.log("I am a fish"); // Bad } ``` +- No new line before else, catch, finally, etc: + + ```javascript + if (x) { + console.log("I am a fish"); + } else { + console.log("I am a chimp"); // Good + } + + if (x) { + console.log("I am a fish"); + } + else { + console.log("I am a chimp"); // Bad + } + ``` - Declare one variable per var statement (consistent with Node). Unless they are simple and closely related. If you put the next declaration on a new line, treat yourself to another `var`: ```javascript - var key = "foo", + const key = "foo", comparator = function(x, y) { return x - y; }; // Bad - var key = "foo"; - var comparator = function(x, y) { + const key = "foo"; + const comparator = function(x, y) { return x - y; }; // Good - var x = 0, y = 0; // Fine + let x = 0, y = 0; // Fine - var x = 0; - var y = 0; // Also fine + let x = 0; + let y = 0; // Also fine ``` - A single line `if` is fine, all others have braces. This prevents errors when adding to the code.: diff --git a/header b/header index 060709b82e..beee1ebe89 100644 --- a/header +++ b/header @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/package.json b/package.json index 836f7fd353..059fdd390f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.8.7", + "version": "0.8.9", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -31,9 +31,11 @@ "reskindex": "scripts/reskindex.js" }, "scripts": { - "reskindex": "scripts/reskindex.js -h header", - "build": "babel src -d lib --source-maps", - "start": "babel src -w -d lib --source-maps", + "reskindex": "node scripts/reskindex.js -h header", + "reskindex:watch": "node scripts/reskindex.js -h header -w", + "build": "npm run reskindex && babel src -d lib --source-maps", + "build:watch": "babel src -w -d lib --source-maps", + "start": "parallelshell \"npm run build:watch\" \"npm run reskindex:watch\"", "lint": "eslint src/", "lintall": "eslint src/ test/", "clean": "rimraf lib", @@ -61,13 +63,13 @@ "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.3", "lodash": "^4.13.1", - "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "0.7.8", "optimist": "^0.6.1", "q": "^1.4.1", "react": "^15.4.0", "react-addons-css-transition-group": "15.3.2", "react-dom": "^15.4.0", - "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#39d858c", + "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", "sanitize-html": "^1.11.1", "text-encoding-utf-8": "^1.0.1", "velocity-vector": "vector-im/velocity#059e3b2", @@ -88,6 +90,7 @@ "babel-preset-es2016": "^6.11.3", "babel-preset-es2017": "^6.14.0", "babel-preset-react": "^6.11.1", + "chokidar": "^1.6.1", "eslint": "^3.13.1", "eslint-config-google": "^0.7.1", "eslint-plugin-babel": "^4.0.1", @@ -104,6 +107,7 @@ "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^1.7.0", "mocha": "^2.4.5", + "parallelshell": "^1.2.0", "phantomjs-prebuilt": "^2.1.7", "react-addons-test-utils": "^15.4.0", "require-json": "0.0.1", diff --git a/scripts/reskindex.js b/scripts/reskindex.js index f9cbc2a711..833151a298 100755 --- a/scripts/reskindex.js +++ b/scripts/reskindex.js @@ -1,53 +1,99 @@ #!/usr/bin/env node - var fs = require('fs'); var path = require('path'); var glob = require('glob'); - var args = require('optimist').argv; - -var header = args.h || args.header; - -var componentsDir = path.join('src', 'components'); +var chokidar = require('chokidar'); var componentIndex = path.join('src', 'component-index.js'); +var componentIndexTmp = componentIndex+".tmp"; +var componentsDir = path.join('src', 'components'); +var componentGlob = '**/*.js'; +var prevFiles = []; -var packageJson = JSON.parse(fs.readFileSync('./package.json')); +function reskindex() { + var files = glob.sync(componentGlob, {cwd: componentsDir}).sort(); + if (!filesHaveChanged(files, prevFiles)) { + return; + } + prevFiles = files; -var strm = fs.createWriteStream(componentIndex); + var header = args.h || args.header; + var packageJson = JSON.parse(fs.readFileSync('./package.json')); -if (header) { - strm.write(fs.readFileSync(header)); - strm.write('\n'); + var strm = fs.createWriteStream(componentIndexTmp); + + if (header) { + strm.write(fs.readFileSync(header)); + strm.write('\n'); + } + + strm.write("/*\n"); + strm.write(" * THIS FILE IS AUTO-GENERATED\n"); + strm.write(" * You can edit it you like, but your changes will be overwritten,\n"); + strm.write(" * so you'd just be trying to swim upstream like a salmon.\n"); + strm.write(" * You are not a salmon.\n"); + strm.write(" */\n\n"); + + if (packageJson['matrix-react-parent']) { + const parentIndex = packageJson['matrix-react-parent'] + + '/lib/component-index'; + strm.write( +`let components = require('${parentIndex}').components; +if (!components) { + throw new Error("'${parentIndex}' didn't export components"); +} +`); + } else { + strm.write("let components = {};\n"); + } + + for (var i = 0; i < files.length; ++i) { + var file = files[i].replace('.js', ''); + + var moduleName = (file.replace(/\//g, '.')); + var importName = moduleName.replace(/\./g, "$"); + + strm.write("import " + importName + " from './components/" + file + "';\n"); + strm.write(importName + " && (components['"+moduleName+"'] = " + importName + ");"); + strm.write('\n'); + strm.uncork(); + } + + strm.write("export {components};\n"); + strm.end(); + fs.rename(componentIndexTmp, componentIndex, function(err) { + if(err) { + console.error("Error moving new index into place: " + err); + } else { + console.log('Reskindex: completed'); + } + }); } -strm.write("/*\n"); -strm.write(" * THIS FILE IS AUTO-GENERATED\n"); -strm.write(" * You can edit it you like, but your changes will be overwritten,\n"); -strm.write(" * so you'd just be trying to swim upstream like a salmon.\n"); -strm.write(" * You are not a salmon.\n"); -strm.write(" *\n"); -strm.write(" * To update it, run:\n"); -strm.write(" * ./reskindex.js -h header\n"); -strm.write(" */\n\n"); - -if (packageJson['matrix-react-parent']) { - strm.write("module.exports.components = require('"+packageJson['matrix-react-parent']+"/lib/component-index').components;\n\n"); -} else { - strm.write("module.exports.components = {};\n"); +// Expects both arrays of file names to be sorted +function filesHaveChanged(files, prevFiles) { + if (files.length !== prevFiles.length) { + return true; + } + // Check for name changes + for (var i = 0; i < files.length; i++) { + if (prevFiles[i] !== files[i]) { + return true; + } + } + return false; } -var files = glob.sync('**/*.js', {cwd: componentsDir}).sort(); -for (var i = 0; i < files.length; ++i) { - var file = files[i].replace('.js', ''); - - var moduleName = (file.replace(/\//g, '.')); - var importName = moduleName.replace(/\./g, "$"); - - strm.write("import " + importName + " from './components/" + file + "';\n"); - strm.write(importName + " && (module.exports.components['"+moduleName+"'] = " + importName + ");"); - strm.write('\n'); - strm.uncork(); +// -w indicates watch mode where any FS events will trigger reskindex +if (!args.w) { + reskindex(); + return; } -strm.end(); +var watchDebouncer = null; +chokidar.watch(path.join(componentsDir, componentGlob)).on('all', (event, path) => { + if (path === componentIndex) return; + if (watchDebouncer) clearTimeout(watchDebouncer); + watchDebouncer = setTimeout(reskindex, 1000); +}); diff --git a/src/ConstantTimeDispatcher.js b/src/ConstantTimeDispatcher.js deleted file mode 100644 index 6c2c3266aa..0000000000 --- a/src/ConstantTimeDispatcher.js +++ /dev/null @@ -1,62 +0,0 @@ -/* -Copyright 2017 Vector Creations 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. -*/ - -// singleton which dispatches invocations of a given type & argument -// rather than just a type (as per EventEmitter and Flux's dispatcher etc) -// -// This means you can have a single point which listens for an EventEmitter event -// and then dispatches out to one of thousands of RoomTiles (for instance) rather than -// having each RoomTile register for the EventEmitter event and having to -// iterate over all of them. -class ConstantTimeDispatcher { - constructor() { - // type -> arg -> [ listener(arg, params) ] - this.listeners = {}; - } - - register(type, arg, listener) { - if (!this.listeners[type]) this.listeners[type] = {}; - if (!this.listeners[type][arg]) this.listeners[type][arg] = []; - this.listeners[type][arg].push(listener); - } - - unregister(type, arg, listener) { - if (this.listeners[type] && this.listeners[type][arg]) { - var i = this.listeners[type][arg].indexOf(listener); - if (i > -1) { - this.listeners[type][arg].splice(i, 1); - } - } - else { - console.warn("Unregistering unrecognised listener (type=" + type + ", arg=" + arg + ")"); - } - } - - dispatch(type, arg, params) { - if (!this.listeners[type] || !this.listeners[type][arg]) { - //console.warn("No registered listeners for dispatch (type=" + type + ", arg=" + arg + ")"); - return; - } - this.listeners[type][arg].forEach(listener=>{ - listener.call(arg, params); - }); - } -} - -if (!global.constantTimeDispatcher) { - global.constantTimeDispatcher = new ConstantTimeDispatcher(); -} -module.exports = global.constantTimeDispatcher; diff --git a/src/DateUtils.js b/src/DateUtils.js index 07bab4ae7b..c58c09d4de 100644 --- a/src/DateUtils.js +++ b/src/DateUtils.js @@ -19,13 +19,14 @@ limitations under the License. var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; +function pad(n) { + return (n < 10 ? '0' : '') + n; +} + module.exports = { formatDate: function(date) { // date.toLocaleTimeString is completely system dependent. // just go 24h for now - function pad(n) { - return (n < 10 ? '0' : '') + n; - } var now = new Date(); if (date.toDateString() === now.toDateString()) { @@ -34,19 +35,20 @@ module.exports = { else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { return days[date.getDay()] + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); } - else /* if (now.getFullYear() === date.getFullYear()) */ { + else if (now.getFullYear() === date.getFullYear()) { return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); } - /* else { - return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + date.getFullYear() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); + return this.formatFullDate(date); } - */ + }, + + formatFullDate: function(date) { + return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + date.getFullYear() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); }, formatTime: function(date) { - //return pad(date.getHours()) + ':' + pad(date.getMinutes()); - return ('00' + date.getHours()).slice(-2) + ':' + ('00' + date.getMinutes()).slice(-2); + return pad(date.getHours()) + ':' + pad(date.getMinutes()); } }; diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index a31601790f..4acb314c2f 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -148,17 +148,18 @@ var sanitizeHtmlParams = { attribs.href = m[1]; delete attribs.target; } - - m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN); - if (m) { - var entity = m[1]; - if (entity[0] === '@') { - attribs.href = '#/user/' + entity; + else { + m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN); + if (m) { + var entity = m[1]; + if (entity[0] === '@') { + attribs.href = '#/user/' + entity; + } + else if (entity[0] === '#' || entity[0] === '!') { + attribs.href = '#/room/' + entity; + } + delete attribs.target; } - else if (entity[0] === '#' || entity[0] === '!') { - attribs.href = '#/room/' + entity; - } - delete attribs.target; } } attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/ diff --git a/src/KeyCode.js b/src/KeyCode.js index f164dbc15c..c9cac01239 100644 --- a/src/KeyCode.js +++ b/src/KeyCode.js @@ -32,5 +32,4 @@ module.exports = { DELETE: 46, KEY_D: 68, KEY_E: 69, - KEY_K: 75, }; diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 1ddcf4832d..bd68f1a6fe 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -var MatrixClientPeg = require("./MatrixClientPeg"); -var dis = require("./dispatcher"); -var Tinter = require("./Tinter"); +import MatrixClientPeg from "./MatrixClientPeg"; +import dis from "./dispatcher"; +import Tinter from "./Tinter"; import sdk from './index'; import Modal from './Modal'; @@ -45,19 +45,25 @@ class Command { } } -var reject = function(msg) { +function reject(msg) { return { - error: msg + error: msg, }; -}; +} -var success = function(promise) { +function success(promise) { return { - promise: promise + promise: promise, }; -}; +} -var commands = { +/* Disable the "unexpected this" error for these commands - all of the run + * functions are called with `this` bound to the Command instance. + */ + +/* eslint-disable babel/no-invalid-this */ + +const commands = { ddg: new Command("ddg", "", function(roomId, args) { const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); // TODO Don't explain this away, actually show a search UI here. @@ -69,30 +75,30 @@ var commands = { }), // Change your nickname - nick: new Command("nick", "", function(room_id, args) { + nick: new Command("nick", "", function(roomId, args) { if (args) { return success( - MatrixClientPeg.get().setDisplayName(args) + MatrixClientPeg.get().setDisplayName(args), ); } return reject(this.getUsage()); }), // Changes the colorscheme of your current room - tint: new Command("tint", " []", function(room_id, args) { + tint: new Command("tint", " []", function(roomId, args) { if (args) { - var matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/); + const matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/); if (matches) { Tinter.tint(matches[1], matches[4]); - var colorScheme = {}; + const colorScheme = {}; colorScheme.primary_color = matches[1]; if (matches[4]) { colorScheme.secondary_color = matches[4]; } return success( MatrixClientPeg.get().setRoomAccountData( - room_id, "org.matrix.room.color_scheme", colorScheme - ) + roomId, "org.matrix.room.color_scheme", colorScheme, + ), ); } } @@ -100,22 +106,22 @@ var commands = { }), // Change the room topic - topic: new Command("topic", "", function(room_id, args) { + topic: new Command("topic", "", function(roomId, args) { if (args) { return success( - MatrixClientPeg.get().setRoomTopic(room_id, args) + MatrixClientPeg.get().setRoomTopic(roomId, args), ); } return reject(this.getUsage()); }), // Invite a user - invite: new Command("invite", "", function(room_id, args) { + invite: new Command("invite", "", function(roomId, args) { if (args) { - var matches = args.match(/^(\S+)$/); + const matches = args.match(/^(\S+)$/); if (matches) { return success( - MatrixClientPeg.get().invite(room_id, matches[1]) + MatrixClientPeg.get().invite(roomId, matches[1]), ); } } @@ -123,21 +129,21 @@ var commands = { }), // Join a room - join: new Command("join", "#alias:domain", function(room_id, args) { + join: new Command("join", "#alias:domain", function(roomId, args) { if (args) { - var matches = args.match(/^(\S+)$/); + const matches = args.match(/^(\S+)$/); if (matches) { - var room_alias = matches[1]; - if (room_alias[0] !== '#') { + let roomAlias = matches[1]; + if (roomAlias[0] !== '#') { return reject(this.getUsage()); } - if (!room_alias.match(/:/)) { - room_alias += ':' + MatrixClientPeg.get().getDomain(); + if (!roomAlias.match(/:/)) { + roomAlias += ':' + MatrixClientPeg.get().getDomain(); } dis.dispatch({ action: 'view_room', - room_alias: room_alias, + roomAlias: roomAlias, auto_join: true, }); @@ -147,29 +153,29 @@ var commands = { return reject(this.getUsage()); }), - part: new Command("part", "[#alias:domain]", function(room_id, args) { - var targetRoomId; + part: new Command("part", "[#alias:domain]", function(roomId, args) { + let targetRoomId; if (args) { - var matches = args.match(/^(\S+)$/); + const matches = args.match(/^(\S+)$/); if (matches) { - var room_alias = matches[1]; - if (room_alias[0] !== '#') { + let roomAlias = matches[1]; + if (roomAlias[0] !== '#') { return reject(this.getUsage()); } - if (!room_alias.match(/:/)) { - room_alias += ':' + MatrixClientPeg.get().getDomain(); + if (!roomAlias.match(/:/)) { + roomAlias += ':' + MatrixClientPeg.get().getDomain(); } // Try to find a room with this alias - var rooms = MatrixClientPeg.get().getRooms(); - for (var i = 0; i < rooms.length; i++) { - var aliasEvents = rooms[i].currentState.getStateEvents( - "m.room.aliases" + const rooms = MatrixClientPeg.get().getRooms(); + for (let i = 0; i < rooms.length; i++) { + const aliasEvents = rooms[i].currentState.getStateEvents( + "m.room.aliases", ); - for (var j = 0; j < aliasEvents.length; j++) { - var aliases = aliasEvents[j].getContent().aliases || []; - for (var k = 0; k < aliases.length; k++) { - if (aliases[k] === room_alias) { + for (let j = 0; j < aliasEvents.length; j++) { + const aliases = aliasEvents[j].getContent().aliases || []; + for (let k = 0; k < aliases.length; k++) { + if (aliases[k] === roomAlias) { targetRoomId = rooms[i].roomId; break; } @@ -178,27 +184,28 @@ var commands = { } if (targetRoomId) { break; } } - } - if (!targetRoomId) { - return reject("Unrecognised room alias: " + room_alias); + if (!targetRoomId) { + return reject("Unrecognised room alias: " + roomAlias); + } } } - if (!targetRoomId) targetRoomId = room_id; + if (!targetRoomId) targetRoomId = roomId; return success( MatrixClientPeg.get().leave(targetRoomId).then( - function() { - dis.dispatch({action: 'view_next_room'}); - }) + function() { + dis.dispatch({action: 'view_next_room'}); + }, + ), ); }), // Kick a user from the room with an optional reason - kick: new Command("kick", " []", function(room_id, args) { + kick: new Command("kick", " []", function(roomId, args) { if (args) { - var matches = args.match(/^(\S+?)( +(.*))?$/); + const matches = args.match(/^(\S+?)( +(.*))?$/); if (matches) { return success( - MatrixClientPeg.get().kick(room_id, matches[1], matches[3]) + MatrixClientPeg.get().kick(roomId, matches[1], matches[3]), ); } } @@ -206,12 +213,12 @@ var commands = { }), // Ban a user from the room with an optional reason - ban: new Command("ban", " []", function(room_id, args) { + ban: new Command("ban", " []", function(roomId, args) { if (args) { - var matches = args.match(/^(\S+?)( +(.*))?$/); + const matches = args.match(/^(\S+?)( +(.*))?$/); if (matches) { return success( - MatrixClientPeg.get().ban(room_id, matches[1], matches[3]) + MatrixClientPeg.get().ban(roomId, matches[1], matches[3]), ); } } @@ -219,13 +226,13 @@ var commands = { }), // Unban a user from the room - unban: new Command("unban", "", function(room_id, args) { + unban: new Command("unban", "", function(roomId, args) { if (args) { - var matches = args.match(/^(\S+)$/); + const matches = args.match(/^(\S+)$/); if (matches) { // Reset the user membership to "leave" to unban him return success( - MatrixClientPeg.get().unban(room_id, matches[1]) + MatrixClientPeg.get().unban(roomId, matches[1]), ); } } @@ -233,27 +240,27 @@ var commands = { }), // Define the power level of a user - op: new Command("op", " []", function(room_id, args) { + op: new Command("op", " []", function(roomId, args) { if (args) { - var matches = args.match(/^(\S+?)( +(\d+))?$/); - var powerLevel = 50; // default power level for op + const matches = args.match(/^(\S+?)( +(\d+))?$/); + let powerLevel = 50; // default power level for op if (matches) { - var user_id = matches[1]; + const userId = matches[1]; if (matches.length === 4 && undefined !== matches[3]) { powerLevel = parseInt(matches[3]); } - if (powerLevel !== NaN) { - var room = MatrixClientPeg.get().getRoom(room_id); + if (!isNaN(powerLevel)) { + const room = MatrixClientPeg.get().getRoom(roomId); if (!room) { - return reject("Bad room ID: " + room_id); + return reject("Bad room ID: " + roomId); } - var powerLevelEvent = room.currentState.getStateEvents( - "m.room.power_levels", "" + const powerLevelEvent = room.currentState.getStateEvents( + "m.room.power_levels", "", ); return success( MatrixClientPeg.get().setPowerLevel( - room_id, user_id, powerLevel, powerLevelEvent - ) + roomId, userId, powerLevel, powerLevelEvent, + ), ); } } @@ -262,32 +269,87 @@ var commands = { }), // Reset the power level of a user - deop: new Command("deop", "", function(room_id, args) { + deop: new Command("deop", "", function(roomId, args) { if (args) { - var matches = args.match(/^(\S+)$/); + const matches = args.match(/^(\S+)$/); if (matches) { - var room = MatrixClientPeg.get().getRoom(room_id); + const room = MatrixClientPeg.get().getRoom(roomId); if (!room) { - return reject("Bad room ID: " + room_id); + return reject("Bad room ID: " + roomId); } - var powerLevelEvent = room.currentState.getStateEvents( - "m.room.power_levels", "" + const powerLevelEvent = room.currentState.getStateEvents( + "m.room.power_levels", "", ); return success( MatrixClientPeg.get().setPowerLevel( - room_id, args, undefined, powerLevelEvent - ) + roomId, args, undefined, powerLevelEvent, + ), ); } } return reject(this.getUsage()); - }) + }), + + // Verify a user, device, and pubkey tuple + verify: new Command("verify", " ", function(roomId, args) { + if (args) { + const matches = args.match(/^(\S+) +(\S+) +(\S+)$/); + if (matches) { + const userId = matches[1]; + const deviceId = matches[2]; + const fingerprint = matches[3]; + + const device = MatrixClientPeg.get().getStoredDevice(userId, deviceId); + if (!device) { + return reject(`Unknown (user, device) pair: (${userId}, ${deviceId})`); + } + + if (device.isVerified()) { + if (device.getFingerprint() === fingerprint) { + return reject(`Device already verified!`); + } else { + return reject(`WARNING: Device already verified, but keys do NOT MATCH!`); + } + } + + if (device.getFingerprint() === fingerprint) { + MatrixClientPeg.get().setDeviceVerified( + userId, deviceId, true, + ); + + // Tell the user we verified everything! + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + Modal.createDialog(QuestionDialog, { + title: "Verified key", + description: ( +
+

+ The signing key you provided matches the signing key you received + from { userId }'s device { deviceId }. Device marked as verified. +

+
+ ), + hasCancelButton: false, + }); + + return success(); + } else { + return reject(`WARNING: KEY VERIFICATION FAILED! The signing key for ${userId} and device + ${deviceId} is "${device.getFingerprint()}" which does not match the provided key + "${fingerprint}". This could mean your communications are being intercepted!`); + } + } + } + return reject(this.getUsage()); + }), }; +/* eslint-enable babel/no-invalid-this */ + // helpful aliases -var aliases = { - j: "join" +const aliases = { + j: "join", }; module.exports = { @@ -304,13 +366,13 @@ module.exports = { // IRC-style commands input = input.replace(/\s+$/, ""); if (input[0] === "/" && input[1] !== "/") { - var bits = input.match(/^(\S+?)( +((.|\n)*))?$/); - var cmd, args; + const bits = input.match(/^(\S+?)( +((.|\n)*))?$/); + let cmd; + let args; if (bits) { cmd = bits[1].substring(1).toLowerCase(); args = bits[3]; - } - else { + } else { cmd = input; } if (cmd === "me") return null; @@ -319,8 +381,7 @@ module.exports = { } if (commands[cmd]) { return commands[cmd].run(roomId, args); - } - else { + } else { return reject("Unrecognised command: " + input); } } @@ -329,12 +390,12 @@ module.exports = { getCommandList: function() { // Return all the commands plus /me and /markdown which aren't handled like normal commands - var cmds = Object.keys(commands).sort().map(function(cmdKey) { + const cmds = Object.keys(commands).sort().map(function(cmdKey) { return commands[cmdKey]; }); cmds.push(new Command("me", "", function() {})); cmds.push(new Command("markdown", "", function() {})); return cmds; - } + }, }; diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 40d6a49998..3f200a089d 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -65,8 +65,8 @@ function textForMemberEvent(ev) { } else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) { return senderName + " set a profile picture"; } else { - // hacky hack for https://github.com/vector-im/vector-web/issues/2020 - return senderName + " rejoined the room."; + // suppress null rejoins + return ''; } } else { if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key); diff --git a/src/Unread.js b/src/Unread.js index d7490c8632..67166dc24f 100644 --- a/src/Unread.js +++ b/src/Unread.js @@ -25,7 +25,9 @@ module.exports = { eventTriggersUnreadCount: function(ev) { if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) { return false; - } else if (ev.getType() == "m.room.member") { + } else if (ev.getType() == 'm.room.member') { + return false; + } else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') { return false; } else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') { return false; diff --git a/src/component-index.js b/src/component-index.js deleted file mode 100644 index d6873c6dfd..0000000000 --- a/src/component-index.js +++ /dev/null @@ -1,253 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -/* - * THIS FILE IS AUTO-GENERATED - * You can edit it you like, but your changes will be overwritten, - * so you'd just be trying to swim upstream like a salmon. - * You are not a salmon. - * - * To update it, run: - * ./reskindex.js -h header - */ - -module.exports.components = {}; -import structures$ContextualMenu from './components/structures/ContextualMenu'; -structures$ContextualMenu && (module.exports.components['structures.ContextualMenu'] = structures$ContextualMenu); -import structures$CreateRoom from './components/structures/CreateRoom'; -structures$CreateRoom && (module.exports.components['structures.CreateRoom'] = structures$CreateRoom); -import structures$FilePanel from './components/structures/FilePanel'; -structures$FilePanel && (module.exports.components['structures.FilePanel'] = structures$FilePanel); -import structures$InteractiveAuth from './components/structures/InteractiveAuth'; -structures$InteractiveAuth && (module.exports.components['structures.InteractiveAuth'] = structures$InteractiveAuth); -import structures$LoggedInView from './components/structures/LoggedInView'; -structures$LoggedInView && (module.exports.components['structures.LoggedInView'] = structures$LoggedInView); -import structures$MatrixChat from './components/structures/MatrixChat'; -structures$MatrixChat && (module.exports.components['structures.MatrixChat'] = structures$MatrixChat); -import structures$MessagePanel from './components/structures/MessagePanel'; -structures$MessagePanel && (module.exports.components['structures.MessagePanel'] = structures$MessagePanel); -import structures$NotificationPanel from './components/structures/NotificationPanel'; -structures$NotificationPanel && (module.exports.components['structures.NotificationPanel'] = structures$NotificationPanel); -import structures$RoomStatusBar from './components/structures/RoomStatusBar'; -structures$RoomStatusBar && (module.exports.components['structures.RoomStatusBar'] = structures$RoomStatusBar); -import structures$RoomView from './components/structures/RoomView'; -structures$RoomView && (module.exports.components['structures.RoomView'] = structures$RoomView); -import structures$ScrollPanel from './components/structures/ScrollPanel'; -structures$ScrollPanel && (module.exports.components['structures.ScrollPanel'] = structures$ScrollPanel); -import structures$TimelinePanel from './components/structures/TimelinePanel'; -structures$TimelinePanel && (module.exports.components['structures.TimelinePanel'] = structures$TimelinePanel); -import structures$UploadBar from './components/structures/UploadBar'; -structures$UploadBar && (module.exports.components['structures.UploadBar'] = structures$UploadBar); -import structures$UserSettings from './components/structures/UserSettings'; -structures$UserSettings && (module.exports.components['structures.UserSettings'] = structures$UserSettings); -import structures$login$ForgotPassword from './components/structures/login/ForgotPassword'; -structures$login$ForgotPassword && (module.exports.components['structures.login.ForgotPassword'] = structures$login$ForgotPassword); -import structures$login$Login from './components/structures/login/Login'; -structures$login$Login && (module.exports.components['structures.login.Login'] = structures$login$Login); -import structures$login$PostRegistration from './components/structures/login/PostRegistration'; -structures$login$PostRegistration && (module.exports.components['structures.login.PostRegistration'] = structures$login$PostRegistration); -import structures$login$Registration from './components/structures/login/Registration'; -structures$login$Registration && (module.exports.components['structures.login.Registration'] = structures$login$Registration); -import views$avatars$BaseAvatar from './components/views/avatars/BaseAvatar'; -views$avatars$BaseAvatar && (module.exports.components['views.avatars.BaseAvatar'] = views$avatars$BaseAvatar); -import views$avatars$MemberAvatar from './components/views/avatars/MemberAvatar'; -views$avatars$MemberAvatar && (module.exports.components['views.avatars.MemberAvatar'] = views$avatars$MemberAvatar); -import views$avatars$RoomAvatar from './components/views/avatars/RoomAvatar'; -views$avatars$RoomAvatar && (module.exports.components['views.avatars.RoomAvatar'] = views$avatars$RoomAvatar); -import views$create_room$CreateRoomButton from './components/views/create_room/CreateRoomButton'; -views$create_room$CreateRoomButton && (module.exports.components['views.create_room.CreateRoomButton'] = views$create_room$CreateRoomButton); -import views$create_room$Presets from './components/views/create_room/Presets'; -views$create_room$Presets && (module.exports.components['views.create_room.Presets'] = views$create_room$Presets); -import views$create_room$RoomAlias from './components/views/create_room/RoomAlias'; -views$create_room$RoomAlias && (module.exports.components['views.create_room.RoomAlias'] = views$create_room$RoomAlias); -import views$dialogs$BaseDialog from './components/views/dialogs/BaseDialog'; -views$dialogs$BaseDialog && (module.exports.components['views.dialogs.BaseDialog'] = views$dialogs$BaseDialog); -import views$dialogs$ChatCreateOrReuseDialog from './components/views/dialogs/ChatCreateOrReuseDialog'; -views$dialogs$ChatCreateOrReuseDialog && (module.exports.components['views.dialogs.ChatCreateOrReuseDialog'] = views$dialogs$ChatCreateOrReuseDialog); -import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog'; -views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog); -import views$dialogs$ConfirmRedactDialog from './components/views/dialogs/ConfirmRedactDialog'; -views$dialogs$ConfirmRedactDialog && (module.exports.components['views.dialogs.ConfirmRedactDialog'] = views$dialogs$ConfirmRedactDialog); -import views$dialogs$ConfirmUserActionDialog from './components/views/dialogs/ConfirmUserActionDialog'; -views$dialogs$ConfirmUserActionDialog && (module.exports.components['views.dialogs.ConfirmUserActionDialog'] = views$dialogs$ConfirmUserActionDialog); -import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog'; -views$dialogs$DeactivateAccountDialog && (module.exports.components['views.dialogs.DeactivateAccountDialog'] = views$dialogs$DeactivateAccountDialog); -import views$dialogs$ErrorDialog from './components/views/dialogs/ErrorDialog'; -views$dialogs$ErrorDialog && (module.exports.components['views.dialogs.ErrorDialog'] = views$dialogs$ErrorDialog); -import views$dialogs$InteractiveAuthDialog from './components/views/dialogs/InteractiveAuthDialog'; -views$dialogs$InteractiveAuthDialog && (module.exports.components['views.dialogs.InteractiveAuthDialog'] = views$dialogs$InteractiveAuthDialog); -import views$dialogs$NeedToRegisterDialog from './components/views/dialogs/NeedToRegisterDialog'; -views$dialogs$NeedToRegisterDialog && (module.exports.components['views.dialogs.NeedToRegisterDialog'] = views$dialogs$NeedToRegisterDialog); -import views$dialogs$QuestionDialog from './components/views/dialogs/QuestionDialog'; -views$dialogs$QuestionDialog && (module.exports.components['views.dialogs.QuestionDialog'] = views$dialogs$QuestionDialog); -import views$dialogs$SessionRestoreErrorDialog from './components/views/dialogs/SessionRestoreErrorDialog'; -views$dialogs$SessionRestoreErrorDialog && (module.exports.components['views.dialogs.SessionRestoreErrorDialog'] = views$dialogs$SessionRestoreErrorDialog); -import views$dialogs$SetDisplayNameDialog from './components/views/dialogs/SetDisplayNameDialog'; -views$dialogs$SetDisplayNameDialog && (module.exports.components['views.dialogs.SetDisplayNameDialog'] = views$dialogs$SetDisplayNameDialog); -import views$dialogs$TextInputDialog from './components/views/dialogs/TextInputDialog'; -views$dialogs$TextInputDialog && (module.exports.components['views.dialogs.TextInputDialog'] = views$dialogs$TextInputDialog); -import views$dialogs$UnknownDeviceDialog from './components/views/dialogs/UnknownDeviceDialog'; -views$dialogs$UnknownDeviceDialog && (module.exports.components['views.dialogs.UnknownDeviceDialog'] = views$dialogs$UnknownDeviceDialog); -import views$elements$AccessibleButton from './components/views/elements/AccessibleButton'; -views$elements$AccessibleButton && (module.exports.components['views.elements.AccessibleButton'] = views$elements$AccessibleButton); -import views$elements$AddressSelector from './components/views/elements/AddressSelector'; -views$elements$AddressSelector && (module.exports.components['views.elements.AddressSelector'] = views$elements$AddressSelector); -import views$elements$AddressTile from './components/views/elements/AddressTile'; -views$elements$AddressTile && (module.exports.components['views.elements.AddressTile'] = views$elements$AddressTile); -import views$elements$DeviceVerifyButtons from './components/views/elements/DeviceVerifyButtons'; -views$elements$DeviceVerifyButtons && (module.exports.components['views.elements.DeviceVerifyButtons'] = views$elements$DeviceVerifyButtons); -import views$elements$DirectorySearchBox from './components/views/elements/DirectorySearchBox'; -views$elements$DirectorySearchBox && (module.exports.components['views.elements.DirectorySearchBox'] = views$elements$DirectorySearchBox); -import views$elements$Dropdown from './components/views/elements/Dropdown'; -views$elements$Dropdown && (module.exports.components['views.elements.Dropdown'] = views$elements$Dropdown); -import views$elements$EditableText from './components/views/elements/EditableText'; -views$elements$EditableText && (module.exports.components['views.elements.EditableText'] = views$elements$EditableText); -import views$elements$EditableTextContainer from './components/views/elements/EditableTextContainer'; -views$elements$EditableTextContainer && (module.exports.components['views.elements.EditableTextContainer'] = views$elements$EditableTextContainer); -import views$elements$EmojiText from './components/views/elements/EmojiText'; -views$elements$EmojiText && (module.exports.components['views.elements.EmojiText'] = views$elements$EmojiText); -import views$elements$MemberEventListSummary from './components/views/elements/MemberEventListSummary'; -views$elements$MemberEventListSummary && (module.exports.components['views.elements.MemberEventListSummary'] = views$elements$MemberEventListSummary); -import views$elements$PowerSelector from './components/views/elements/PowerSelector'; -views$elements$PowerSelector && (module.exports.components['views.elements.PowerSelector'] = views$elements$PowerSelector); -import views$elements$ProgressBar from './components/views/elements/ProgressBar'; -views$elements$ProgressBar && (module.exports.components['views.elements.ProgressBar'] = views$elements$ProgressBar); -import views$elements$TintableSvg from './components/views/elements/TintableSvg'; -views$elements$TintableSvg && (module.exports.components['views.elements.TintableSvg'] = views$elements$TintableSvg); -import views$elements$TruncatedList from './components/views/elements/TruncatedList'; -views$elements$TruncatedList && (module.exports.components['views.elements.TruncatedList'] = views$elements$TruncatedList); -import views$elements$UserSelector from './components/views/elements/UserSelector'; -views$elements$UserSelector && (module.exports.components['views.elements.UserSelector'] = views$elements$UserSelector); -import views$login$CaptchaForm from './components/views/login/CaptchaForm'; -views$login$CaptchaForm && (module.exports.components['views.login.CaptchaForm'] = views$login$CaptchaForm); -import views$login$CasLogin from './components/views/login/CasLogin'; -views$login$CasLogin && (module.exports.components['views.login.CasLogin'] = views$login$CasLogin); -import views$login$CountryDropdown from './components/views/login/CountryDropdown'; -views$login$CountryDropdown && (module.exports.components['views.login.CountryDropdown'] = views$login$CountryDropdown); -import views$login$CustomServerDialog from './components/views/login/CustomServerDialog'; -views$login$CustomServerDialog && (module.exports.components['views.login.CustomServerDialog'] = views$login$CustomServerDialog); -import views$login$InteractiveAuthEntryComponents from './components/views/login/InteractiveAuthEntryComponents'; -views$login$InteractiveAuthEntryComponents && (module.exports.components['views.login.InteractiveAuthEntryComponents'] = views$login$InteractiveAuthEntryComponents); -import views$login$LoginFooter from './components/views/login/LoginFooter'; -views$login$LoginFooter && (module.exports.components['views.login.LoginFooter'] = views$login$LoginFooter); -import views$login$LoginHeader from './components/views/login/LoginHeader'; -views$login$LoginHeader && (module.exports.components['views.login.LoginHeader'] = views$login$LoginHeader); -import views$login$PasswordLogin from './components/views/login/PasswordLogin'; -views$login$PasswordLogin && (module.exports.components['views.login.PasswordLogin'] = views$login$PasswordLogin); -import views$login$RegistrationForm from './components/views/login/RegistrationForm'; -views$login$RegistrationForm && (module.exports.components['views.login.RegistrationForm'] = views$login$RegistrationForm); -import views$login$ServerConfig from './components/views/login/ServerConfig'; -views$login$ServerConfig && (module.exports.components['views.login.ServerConfig'] = views$login$ServerConfig); -import views$messages$MAudioBody from './components/views/messages/MAudioBody'; -views$messages$MAudioBody && (module.exports.components['views.messages.MAudioBody'] = views$messages$MAudioBody); -import views$messages$MFileBody from './components/views/messages/MFileBody'; -views$messages$MFileBody && (module.exports.components['views.messages.MFileBody'] = views$messages$MFileBody); -import views$messages$MImageBody from './components/views/messages/MImageBody'; -views$messages$MImageBody && (module.exports.components['views.messages.MImageBody'] = views$messages$MImageBody); -import views$messages$MVideoBody from './components/views/messages/MVideoBody'; -views$messages$MVideoBody && (module.exports.components['views.messages.MVideoBody'] = views$messages$MVideoBody); -import views$messages$MessageEvent from './components/views/messages/MessageEvent'; -views$messages$MessageEvent && (module.exports.components['views.messages.MessageEvent'] = views$messages$MessageEvent); -import views$messages$SenderProfile from './components/views/messages/SenderProfile'; -views$messages$SenderProfile && (module.exports.components['views.messages.SenderProfile'] = views$messages$SenderProfile); -import views$messages$TextualBody from './components/views/messages/TextualBody'; -views$messages$TextualBody && (module.exports.components['views.messages.TextualBody'] = views$messages$TextualBody); -import views$messages$TextualEvent from './components/views/messages/TextualEvent'; -views$messages$TextualEvent && (module.exports.components['views.messages.TextualEvent'] = views$messages$TextualEvent); -import views$messages$UnknownBody from './components/views/messages/UnknownBody'; -views$messages$UnknownBody && (module.exports.components['views.messages.UnknownBody'] = views$messages$UnknownBody); -import views$room_settings$AliasSettings from './components/views/room_settings/AliasSettings'; -views$room_settings$AliasSettings && (module.exports.components['views.room_settings.AliasSettings'] = views$room_settings$AliasSettings); -import views$room_settings$ColorSettings from './components/views/room_settings/ColorSettings'; -views$room_settings$ColorSettings && (module.exports.components['views.room_settings.ColorSettings'] = views$room_settings$ColorSettings); -import views$room_settings$UrlPreviewSettings from './components/views/room_settings/UrlPreviewSettings'; -views$room_settings$UrlPreviewSettings && (module.exports.components['views.room_settings.UrlPreviewSettings'] = views$room_settings$UrlPreviewSettings); -import views$rooms$Autocomplete from './components/views/rooms/Autocomplete'; -views$rooms$Autocomplete && (module.exports.components['views.rooms.Autocomplete'] = views$rooms$Autocomplete); -import views$rooms$AuxPanel from './components/views/rooms/AuxPanel'; -views$rooms$AuxPanel && (module.exports.components['views.rooms.AuxPanel'] = views$rooms$AuxPanel); -import views$rooms$EntityTile from './components/views/rooms/EntityTile'; -views$rooms$EntityTile && (module.exports.components['views.rooms.EntityTile'] = views$rooms$EntityTile); -import views$rooms$EventTile from './components/views/rooms/EventTile'; -views$rooms$EventTile && (module.exports.components['views.rooms.EventTile'] = views$rooms$EventTile); -import views$rooms$LinkPreviewWidget from './components/views/rooms/LinkPreviewWidget'; -views$rooms$LinkPreviewWidget && (module.exports.components['views.rooms.LinkPreviewWidget'] = views$rooms$LinkPreviewWidget); -import views$rooms$MemberDeviceInfo from './components/views/rooms/MemberDeviceInfo'; -views$rooms$MemberDeviceInfo && (module.exports.components['views.rooms.MemberDeviceInfo'] = views$rooms$MemberDeviceInfo); -import views$rooms$MemberInfo from './components/views/rooms/MemberInfo'; -views$rooms$MemberInfo && (module.exports.components['views.rooms.MemberInfo'] = views$rooms$MemberInfo); -import views$rooms$MemberList from './components/views/rooms/MemberList'; -views$rooms$MemberList && (module.exports.components['views.rooms.MemberList'] = views$rooms$MemberList); -import views$rooms$MemberTile from './components/views/rooms/MemberTile'; -views$rooms$MemberTile && (module.exports.components['views.rooms.MemberTile'] = views$rooms$MemberTile); -import views$rooms$MessageComposer from './components/views/rooms/MessageComposer'; -views$rooms$MessageComposer && (module.exports.components['views.rooms.MessageComposer'] = views$rooms$MessageComposer); -import views$rooms$MessageComposerInput from './components/views/rooms/MessageComposerInput'; -views$rooms$MessageComposerInput && (module.exports.components['views.rooms.MessageComposerInput'] = views$rooms$MessageComposerInput); -import views$rooms$MessageComposerInputOld from './components/views/rooms/MessageComposerInputOld'; -views$rooms$MessageComposerInputOld && (module.exports.components['views.rooms.MessageComposerInputOld'] = views$rooms$MessageComposerInputOld); -import views$rooms$PresenceLabel from './components/views/rooms/PresenceLabel'; -views$rooms$PresenceLabel && (module.exports.components['views.rooms.PresenceLabel'] = views$rooms$PresenceLabel); -import views$rooms$ReadReceiptMarker from './components/views/rooms/ReadReceiptMarker'; -views$rooms$ReadReceiptMarker && (module.exports.components['views.rooms.ReadReceiptMarker'] = views$rooms$ReadReceiptMarker); -import views$rooms$RoomHeader from './components/views/rooms/RoomHeader'; -views$rooms$RoomHeader && (module.exports.components['views.rooms.RoomHeader'] = views$rooms$RoomHeader); -import views$rooms$RoomList from './components/views/rooms/RoomList'; -views$rooms$RoomList && (module.exports.components['views.rooms.RoomList'] = views$rooms$RoomList); -import views$rooms$RoomNameEditor from './components/views/rooms/RoomNameEditor'; -views$rooms$RoomNameEditor && (module.exports.components['views.rooms.RoomNameEditor'] = views$rooms$RoomNameEditor); -import views$rooms$RoomPreviewBar from './components/views/rooms/RoomPreviewBar'; -views$rooms$RoomPreviewBar && (module.exports.components['views.rooms.RoomPreviewBar'] = views$rooms$RoomPreviewBar); -import views$rooms$RoomSettings from './components/views/rooms/RoomSettings'; -views$rooms$RoomSettings && (module.exports.components['views.rooms.RoomSettings'] = views$rooms$RoomSettings); -import views$rooms$RoomTile from './components/views/rooms/RoomTile'; -views$rooms$RoomTile && (module.exports.components['views.rooms.RoomTile'] = views$rooms$RoomTile); -import views$rooms$RoomTopicEditor from './components/views/rooms/RoomTopicEditor'; -views$rooms$RoomTopicEditor && (module.exports.components['views.rooms.RoomTopicEditor'] = views$rooms$RoomTopicEditor); -import views$rooms$SearchResultTile from './components/views/rooms/SearchResultTile'; -views$rooms$SearchResultTile && (module.exports.components['views.rooms.SearchResultTile'] = views$rooms$SearchResultTile); -import views$rooms$SearchableEntityList from './components/views/rooms/SearchableEntityList'; -views$rooms$SearchableEntityList && (module.exports.components['views.rooms.SearchableEntityList'] = views$rooms$SearchableEntityList); -import views$rooms$SimpleRoomHeader from './components/views/rooms/SimpleRoomHeader'; -views$rooms$SimpleRoomHeader && (module.exports.components['views.rooms.SimpleRoomHeader'] = views$rooms$SimpleRoomHeader); -import views$rooms$TabCompleteBar from './components/views/rooms/TabCompleteBar'; -views$rooms$TabCompleteBar && (module.exports.components['views.rooms.TabCompleteBar'] = views$rooms$TabCompleteBar); -import views$rooms$TopUnreadMessagesBar from './components/views/rooms/TopUnreadMessagesBar'; -views$rooms$TopUnreadMessagesBar && (module.exports.components['views.rooms.TopUnreadMessagesBar'] = views$rooms$TopUnreadMessagesBar); -import views$rooms$UserTile from './components/views/rooms/UserTile'; -views$rooms$UserTile && (module.exports.components['views.rooms.UserTile'] = views$rooms$UserTile); -import views$settings$AddPhoneNumber from './components/views/settings/AddPhoneNumber'; -views$settings$AddPhoneNumber && (module.exports.components['views.settings.AddPhoneNumber'] = views$settings$AddPhoneNumber); -import views$settings$ChangeAvatar from './components/views/settings/ChangeAvatar'; -views$settings$ChangeAvatar && (module.exports.components['views.settings.ChangeAvatar'] = views$settings$ChangeAvatar); -import views$settings$ChangeDisplayName from './components/views/settings/ChangeDisplayName'; -views$settings$ChangeDisplayName && (module.exports.components['views.settings.ChangeDisplayName'] = views$settings$ChangeDisplayName); -import views$settings$ChangePassword from './components/views/settings/ChangePassword'; -views$settings$ChangePassword && (module.exports.components['views.settings.ChangePassword'] = views$settings$ChangePassword); -import views$settings$DevicesPanel from './components/views/settings/DevicesPanel'; -views$settings$DevicesPanel && (module.exports.components['views.settings.DevicesPanel'] = views$settings$DevicesPanel); -import views$settings$DevicesPanelEntry from './components/views/settings/DevicesPanelEntry'; -views$settings$DevicesPanelEntry && (module.exports.components['views.settings.DevicesPanelEntry'] = views$settings$DevicesPanelEntry); -import views$settings$EnableNotificationsButton from './components/views/settings/EnableNotificationsButton'; -views$settings$EnableNotificationsButton && (module.exports.components['views.settings.EnableNotificationsButton'] = views$settings$EnableNotificationsButton); -import views$voip$CallView from './components/views/voip/CallView'; -views$voip$CallView && (module.exports.components['views.voip.CallView'] = views$voip$CallView); -import views$voip$IncomingCallBox from './components/views/voip/IncomingCallBox'; -views$voip$IncomingCallBox && (module.exports.components['views.voip.IncomingCallBox'] = views$voip$IncomingCallBox); -import views$voip$VideoFeed from './components/views/voip/VideoFeed'; -views$voip$VideoFeed && (module.exports.components['views.voip.VideoFeed'] = views$voip$VideoFeed); -import views$voip$VideoView from './components/views/voip/VideoView'; -views$voip$VideoView && (module.exports.components['views.voip.VideoView'] = views$voip$VideoView); diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index fc4cbd9423..d83b6b5564 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -59,6 +59,8 @@ var FilePanel = React.createClass({ var client = MatrixClientPeg.get(); var room = client.getRoom(roomId); + this.noRoom = !room; + if (room) { var filter = new Matrix.Filter(client.credentials.userId); filter.setDefinition( @@ -82,13 +84,22 @@ var FilePanel = React.createClass({ console.error("Failed to get or create file panel filter", error); } ); - } - else { + } else { console.error("Failed to add filtered timelineSet for FilePanel as no room!"); } }, render: function() { + if (MatrixClientPeg.get().isGuest()) { + return
+
You must register to use this functionality
+
; + } else if (this.noRoom) { + return
+
You must join the room to see its files
+
; + } + // wrap a TimelinePanel with the jump-to-event bits turned off. var TimelinePanel = sdk.getComponent("structures.TimelinePanel"); var Loader = sdk.getComponent("elements.Spinner"); diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index fd6599dd00..a84661bcd2 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -112,18 +112,6 @@ export default React.createClass({ var handled = false; switch (ev.keyCode) { - case KeyCode.ESCAPE: - - // Implemented this way so possible handling for other pages is neater - switch (this.props.page_type) { - case PageTypes.UserSettings: - this.props.onUserSettingsClose(); - handled = true; - break; - } - - break; - case KeyCode.UP: case KeyCode.DOWN: if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 9b8aa3426a..0de38ab226 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -17,27 +17,24 @@ limitations under the License. import q from 'q'; -var React = require('react'); -var Matrix = require("matrix-js-sdk"); +import React from 'react'; +import Matrix from "matrix-js-sdk"; -var MatrixClientPeg = require("../../MatrixClientPeg"); -var PlatformPeg = require("../../PlatformPeg"); -var SdkConfig = require("../../SdkConfig"); -var ContextualMenu = require("./ContextualMenu"); -var RoomListSorter = require("../../RoomListSorter"); -var UserActivity = require("../../UserActivity"); -var Presence = require("../../Presence"); -var dis = require("../../dispatcher"); +import MatrixClientPeg from "../../MatrixClientPeg"; +import PlatformPeg from "../../PlatformPeg"; +import SdkConfig from "../../SdkConfig"; +import * as RoomListSorter from "../../RoomListSorter"; +import dis from "../../dispatcher"; -var Modal = require("../../Modal"); -var Tinter = require("../../Tinter"); -var sdk = require('../../index'); -var Rooms = require('../../Rooms'); -var linkifyMatrix = require("../../linkify-matrix"); -var Lifecycle = require('../../Lifecycle'); -var PageTypes = require('../../PageTypes'); +import Modal from "../../Modal"; +import Tinter from "../../Tinter"; +import sdk from '../../index'; +import * as Rooms from '../../Rooms'; +import linkifyMatrix from "../../linkify-matrix"; +import * as Lifecycle from '../../Lifecycle'; +import PageTypes from '../../PageTypes'; -var createRoom = require("../../createRoom"); +import createRoom from "../../createRoom"; import * as UDEHandler from '../../UnknownDeviceErrorHandler'; module.exports = React.createClass({ @@ -89,7 +86,7 @@ module.exports = React.createClass({ }, getInitialState: function() { - var s = { + const s = { loading: true, screen: undefined, screenAfterLogin: this.props.initialScreenAfterLogin, @@ -156,11 +153,9 @@ module.exports = React.createClass({ return this.state.register_hs_url; } else if (MatrixClientPeg.get()) { return MatrixClientPeg.get().getHomeserverUrl(); - } - else if (window.localStorage && window.localStorage.getItem("mx_hs_url")) { + } else if (window.localStorage && window.localStorage.getItem("mx_hs_url")) { return window.localStorage.getItem("mx_hs_url"); - } - else { + } else { return this.getDefaultHsUrl(); } }, @@ -178,11 +173,9 @@ module.exports = React.createClass({ return this.state.register_is_url; } else if (MatrixClientPeg.get()) { return MatrixClientPeg.get().getIdentityServerUrl(); - } - else if (window.localStorage && window.localStorage.getItem("mx_is_url")) { + } else if (window.localStorage && window.localStorage.getItem("mx_is_url")) { return window.localStorage.getItem("mx_is_url"); - } - else { + } else { return this.getDefaultIsUrl(); } }, @@ -324,28 +317,14 @@ module.exports = React.createClass({ onAction: function(payload) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - var roomIndexDelta = 1; + const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); - var self = this; switch (payload.action) { case 'logout': Lifecycle.logout(); break; case 'start_registration': - const params = payload.params || {}; - this.setStateForNewScreen({ - screen: 'register', - // these params may be undefined, but if they are, - // unset them from our state: we don't want to - // resume a previous registration session if the - // user just clicked 'register' - register_client_secret: params.client_secret, - register_session_id: params.session_id, - register_hs_url: params.hs_url, - register_is_url: params.is_url, - register_id_sid: params.sid, - }); - this.notifyNewScreen('register'); + this._startRegistration(payload.params || {}); break; case 'start_login': if (MatrixClientPeg.get() && @@ -362,7 +341,7 @@ module.exports = React.createClass({ break; case 'start_post_registration': this.setState({ // don't clobber loggedIn status - screen: 'post_registration' + screen: 'post_registration', }); break; case 'start_upgrade_registration': @@ -392,33 +371,7 @@ module.exports = React.createClass({ this.notifyNewScreen('forgot_password'); break; case 'leave_room': - Modal.createDialog(QuestionDialog, { - title: "Leave room", - description: "Are you sure you want to leave the room?", - onFinished: (should_leave) => { - if (should_leave) { - const d = MatrixClientPeg.get().leave(payload.room_id); - - // FIXME: controller shouldn't be loading a view :( - const Loader = sdk.getComponent("elements.Spinner"); - const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); - - d.then(() => { - modal.close(); - if (this.currentRoomId === payload.room_id) { - dis.dispatch({action: 'view_next_room'}); - } - }, (err) => { - modal.close(); - console.error("Failed to leave room " + payload.room_id + " " + err); - Modal.createDialog(ErrorDialog, { - title: "Failed to leave room", - description: (err && err.message ? err.message : "Server may be unavailable, overloaded, or you hit a bug."), - }); - }); - } - } - }); + this._leaveRoom(payload.room_id); break; case 'reject_invite': Modal.createDialog(QuestionDialog, { @@ -439,11 +392,11 @@ module.exports = React.createClass({ modal.close(); Modal.createDialog(ErrorDialog, { title: "Failed to reject invitation", - description: err.toString() + description: err.toString(), }); }); } - } + }, }); break; case 'view_user': @@ -468,30 +421,13 @@ module.exports = React.createClass({ this._viewRoom(payload); break; case 'view_prev_room': - roomIndexDelta = -1; + this._viewNextRoom(-1); + break; case 'view_next_room': - var allRooms = RoomListSorter.mostRecentActivityFirst( - MatrixClientPeg.get().getRooms() - ); - var roomIndex = -1; - for (var i = 0; i < allRooms.length; ++i) { - if (allRooms[i].roomId == this.state.currentRoomId) { - roomIndex = i; - break; - } - } - roomIndex = (roomIndex + roomIndexDelta) % allRooms.length; - if (roomIndex < 0) roomIndex = allRooms.length - 1; - this._viewRoom({ room_id: allRooms[roomIndex].roomId }); + this._viewNextRoom(1); break; case 'view_indexed_room': - var allRooms = RoomListSorter.mostRecentActivityFirst( - MatrixClientPeg.get().getRooms() - ); - var roomIndex = payload.roomIndex; - if (allRooms[roomIndex]) { - this._viewRoom({ room_id: allRooms[roomIndex].roomId }); - } + this._viewIndexedRoom(payload.roomIndex); break; case 'view_user_settings': this._setPage(PageTypes.UserSettings); @@ -500,19 +436,17 @@ module.exports = React.createClass({ case 'view_create_room': //this._setPage(PageTypes.CreateRoom); //this.notifyNewScreen('new'); - - var TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); Modal.createDialog(TextInputDialog, { title: "Create Room", description: "Room name (optional)", button: "Create Room", - onFinished: (should_create, name) => { - if (should_create) { + onFinished: (shouldCreate, name) => { + if (shouldCreate) { const createOpts = {}; if (name) createOpts.name = name; createRoom({createOpts}).done(); } - } + }, }); break; case 'view_room_directory': @@ -583,7 +517,7 @@ module.exports = React.createClass({ case 'new_version': this.onVersion( payload.currentVersion, payload.newVersion, - payload.releaseNotes + payload.releaseNotes, ); break; } @@ -595,6 +529,47 @@ module.exports = React.createClass({ }); }, + _startRegistration: function(params) { + this.setStateForNewScreen({ + screen: 'register', + // these params may be undefined, but if they are, + // unset them from our state: we don't want to + // resume a previous registration session if the + // user just clicked 'register' + register_client_secret: params.client_secret, + register_session_id: params.session_id, + register_hs_url: params.hs_url, + register_is_url: params.is_url, + register_id_sid: params.sid, + }); + this.notifyNewScreen('register'); + }, + + _viewNextRoom: function(roomIndexDelta) { + const allRooms = RoomListSorter.mostRecentActivityFirst( + MatrixClientPeg.get().getRooms(), + ); + let roomIndex = -1; + for (let i = 0; i < allRooms.length; ++i) { + if (allRooms[i].roomId == this.state.currentRoomId) { + roomIndex = i; + break; + } + } + roomIndex = (roomIndex + roomIndexDelta) % allRooms.length; + if (roomIndex < 0) roomIndex = allRooms.length - 1; + this._viewRoom({ room_id: allRooms[roomIndex].roomId }); + }, + + _viewIndexedRoom: function(roomIndex) { + const allRooms = RoomListSorter.mostRecentActivityFirst( + MatrixClientPeg.get().getRooms(), + ); + if (allRooms[roomIndex]) { + this._viewRoom({ room_id: allRooms[roomIndex].roomId }); + } + }, + // switch view to the given room // // @param {Object} room_info Object containing data about the room to be joined @@ -614,7 +589,7 @@ module.exports = React.createClass({ _viewRoom: function(room_info) { this.focusComposer = true; - var newState = { + const newState = { initialEventId: room_info.event_id, highlightedEventId: room_info.event_id, initialEventPixelOffset: undefined, @@ -634,7 +609,7 @@ module.exports = React.createClass({ // // TODO: do this in RoomView rather than here if (!room_info.event_id && this.refs.loggedInView) { - var scrollState = this.refs.loggedInView.getScrollStateForRoom(room_info.room_id); + const scrollState = this.refs.loggedInView.getScrollStateForRoom(room_info.room_id); if (scrollState) { newState.initialEventId = scrollState.focussedEvent; newState.initialEventPixelOffset = scrollState.pixelOffset; @@ -676,14 +651,14 @@ module.exports = React.createClass({ }, _createChat: function() { - var ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog"); + const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog"); Modal.createDialog(ChatInviteDialog, { title: "Start a new chat", }); }, _invite: function(roomId) { - var ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog"); + const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog"); Modal.createDialog(ChatInviteDialog, { title: "Invite new room members", button: "Send Invites", @@ -692,6 +667,41 @@ module.exports = React.createClass({ }); }, + _leaveRoom: function(roomId) { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + + const roomToLeave = MatrixClientPeg.get().getRoom(roomId); + Modal.createDialog(QuestionDialog, { + title: "Leave room", + description: Are you sure you want to leave the room {roomToLeave.name}?, + onFinished: (shouldLeave) => { + if (shouldLeave) { + const d = MatrixClientPeg.get().leave(roomId); + + // FIXME: controller shouldn't be loading a view :( + const Loader = sdk.getComponent("elements.Spinner"); + const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); + + d.then(() => { + modal.close(); + if (this.currentRoomId === roomId) { + dis.dispatch({action: 'view_next_room'}); + } + }, (err) => { + modal.close(); + console.error("Failed to leave room " + roomId + " " + err); + Modal.createDialog(ErrorDialog, { + title: "Failed to leave room", + description: (err && err.message ? err.message : + "Server may be unavailable, overloaded, or you hit a bug."), + }); + }); + } + }, + }); + }, + /** * Called when the sessionloader has finished */ @@ -710,6 +720,8 @@ module.exports = React.createClass({ /** * Called whenever someone changes the theme + * + * @param {string} theme new theme */ _onSetTheme: function(theme) { if (!theme) { @@ -718,12 +730,12 @@ module.exports = React.createClass({ // look for the stylesheet elements. // styleElements is a map from style name to HTMLLinkElement. - var styleElements = Object.create(null); - var i, a; - for (i = 0; (a = document.getElementsByTagName("link")[i]); i++) { - var href = a.getAttribute("href"); + const styleElements = Object.create(null); + let a; + for (let i = 0; (a = document.getElementsByTagName("link")[i]); i++) { + const href = a.getAttribute("href"); // shouldn't we be using the 'title' tag rather than the href? - var match = href.match(/^bundles\/.*\/theme-(.*)\.css$/); + const match = href.match(/^bundles\/.*\/theme-(.*)\.css$/); if (match) { styleElements[match[1]] = a; } @@ -746,14 +758,15 @@ module.exports = React.createClass({ // abuse the tinter to change all the SVG's #fff to #2d2d2d // XXX: obviously this shouldn't be hardcoded here. Tinter.tintSvgWhite('#2d2d2d'); - } - else { + } else { Tinter.tintSvgWhite('#ffffff'); } }, /** * Called when a new logged in session has started + * + * @param {string} teamToken */ _onLoggedIn: function(teamToken) { this.setState({ @@ -767,8 +780,12 @@ module.exports = React.createClass({ this._teamToken = teamToken; dis.dispatch({action: 'view_home_page'}); } else if (this._is_registered) { + if (this.props.config.welcomeUserId) { + createRoom({dmUserId: this.props.config.welcomeUserId}); + return; + } // The user has just logged in after registering - dis.dispatch({action: 'view_user_settings'}); + dis.dispatch({action: 'view_room_directory'}); } else { this._showScreenAfterLogin(); } @@ -780,7 +797,7 @@ module.exports = React.createClass({ if (this.state.screenAfterLogin && this.state.screenAfterLogin.screen) { this.showScreen( this.state.screenAfterLogin.screen, - this.state.screenAfterLogin.params + this.state.screenAfterLogin.params, ); this.notifyNewScreen(this.state.screenAfterLogin.screen); this.setState({screenAfterLogin: null}); @@ -821,8 +838,8 @@ module.exports = React.createClass({ * (useful for setting listeners) */ _onWillStartClient() { - var self = this; - var cli = MatrixClientPeg.get(); + const self = this; + const cli = MatrixClientPeg.get(); // Allow the JS SDK to reap timeline events. This reduces the amount of // memory consumed as the JS SDK stores multiple distinct copies of room @@ -863,17 +880,17 @@ module.exports = React.createClass({ cli.on('Call.incoming', function(call) { dis.dispatch({ action: 'incoming_call', - call: call + call: call, }); }); cli.on('Session.logged_out', function(call) { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Signed Out", - description: "For security, this session has been signed out. Please sign in again." + description: "For security, this session has been signed out. Please sign in again.", }); dis.dispatch({ - action: 'logout' + action: 'logout', }); }); cli.on("accountData", function(ev) { @@ -896,17 +913,17 @@ module.exports = React.createClass({ if (screen == 'register') { dis.dispatch({ action: 'start_registration', - params: params + params: params, }); } else if (screen == 'login') { dis.dispatch({ action: 'start_login', - params: params + params: params, }); } else if (screen == 'forgot_password') { dis.dispatch({ action: 'start_password_recovery', - params: params + params: params, }); } else if (screen == 'new') { dis.dispatch({ @@ -929,26 +946,26 @@ module.exports = React.createClass({ action: 'start_post_registration', }); } else if (screen.indexOf('room/') == 0) { - var segments = screen.substring(5).split('/'); - var roomString = segments[0]; - var eventId = segments[1]; // undefined if no event id given + const segments = screen.substring(5).split('/'); + const roomString = segments[0]; + const eventId = segments[1]; // undefined if no event id given // FIXME: sort_out caseConsistency - var third_party_invite = { + const thirdPartyInvite = { inviteSignUrl: params.signurl, invitedEmail: params.email, }; - var oob_data = { + const oobData = { name: params.room_name, avatarUrl: params.room_avatar_url, inviterName: params.inviter_name, }; - var payload = { + const payload = { action: 'view_room', event_id: eventId, - third_party_invite: third_party_invite, - oob_data: oob_data, + third_party_invite: thirdPartyInvite, + oob_data: oobData, }; if (roomString[0] == '#') { payload.room_alias = roomString; @@ -962,19 +979,18 @@ module.exports = React.createClass({ dis.dispatch(payload); } } else if (screen.indexOf('user/') == 0) { - var userId = screen.substring(5); + const userId = screen.substring(5); this.setState({ viewUserId: userId }); this._setPage(PageTypes.UserView); this.notifyNewScreen('user/' + userId); - var member = new Matrix.RoomMember(null, userId); + const member = new Matrix.RoomMember(null, userId); if (member) { dis.dispatch({ action: 'view_user', member: member, }); } - } - else { + } else { console.info("Ignoring showScreen for '%s'", screen); } }, @@ -993,7 +1009,7 @@ module.exports = React.createClass({ onUserClick: function(event, userId) { event.preventDefault(); - var member = new Matrix.RoomMember(null, userId); + const member = new Matrix.RoomMember(null, userId); if (!member) { return; } dis.dispatch({ action: 'view_user', @@ -1003,17 +1019,17 @@ module.exports = React.createClass({ onLogoutClick: function(event) { dis.dispatch({ - action: 'logout' + action: 'logout', }); event.stopPropagation(); event.preventDefault(); }, handleResize: function(e) { - var hideLhsThreshold = 1000; - var showLhsThreshold = 1000; - var hideRhsThreshold = 820; - var showRhsThreshold = 820; + const hideLhsThreshold = 1000; + const showLhsThreshold = 1000; + const hideRhsThreshold = 820; + const showRhsThreshold = 820; if (this.state.width > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) { dis.dispatch({ action: 'hide_left_panel' }); @@ -1031,10 +1047,10 @@ module.exports = React.createClass({ this.setState({width: window.innerWidth}); }, - onRoomCreated: function(room_id) { + onRoomCreated: function(roomId) { dis.dispatch({ action: "view_room", - room_id: room_id, + room_id: roomId, }); }, @@ -1068,7 +1084,7 @@ module.exports = React.createClass({ onFinishPostRegistration: function() { // Don't confuse this with "PageType" which is the middle window to show this.setState({ - screen: undefined + screen: undefined, }); this.showScreen("settings"); }, @@ -1083,10 +1099,10 @@ module.exports = React.createClass({ }, updateStatusIndicator: function(state, prevState) { - var notifCount = 0; + let notifCount = 0; - var rooms = MatrixClientPeg.get().getRooms(); - for (var i = 0; i < rooms.length; ++i) { + const rooms = MatrixClientPeg.get().getRooms(); + for (let i = 0; i < rooms.length; ++i) { if (rooms[i].hasMembershipState(MatrixClientPeg.get().credentials.userId, 'invite')) { notifCount++; } else if (rooms[i].getUnreadNotificationCount()) { @@ -1113,19 +1129,18 @@ module.exports = React.createClass({ action: 'view_room', room_id: this.state.currentRoomId, }); - } - else { + } else { dis.dispatch({ action: 'view_room_directory', }); } }, - onRoomIdResolved: function(room_id) { + onRoomIdResolved: function(roomId) { // It's the RoomView's resposibility to look up room aliases, but we need the // ID to pass into things like the Member List, so the Room View tells us when // its done that resolution so we can display things that take a room ID. - this.setState({currentRoomId: room_id}); + this.setState({currentRoomId: roomId}); }, _makeRegistrationUrl: function(params) { @@ -1148,14 +1163,20 @@ module.exports = React.createClass({ ); } + // needs to be before normal PageTypes as you are logged in technically - else if (this.state.screen == 'post_registration') { + if (this.state.screen == 'post_registration') { const PostRegistration = sdk.getComponent('structures.login.PostRegistration'); return ( ); - } else if (this.state.loggedIn && this.state.ready) { + } + + // `ready` and `loggedIn` may be set before `page_type` (because the + // latter is set via the dispatcher). If we don't yet have a `page_type`, + // keep showing the spinner for now. + if (this.state.loggedIn && this.state.ready && this.state.page_type) { /* for now, we stuff the entirety of our props and state into the LoggedInView. * we should go through and figure out what we actually need to pass down, as well * as using something like redux to avoid having a billion bits of state kicking around. @@ -1237,5 +1258,5 @@ module.exports = React.createClass({ /> ); } - } + }, }); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index a0c36374b6..af0c595ea9 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -271,6 +271,7 @@ module.exports = React.createClass({ this._updateConfCallNotification(); + window.addEventListener('beforeunload', this.onPageUnload); window.addEventListener('resize', this.onResize); this.onResize(); @@ -353,6 +354,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().removeListener("accountData", this.onAccountData); } + window.removeEventListener('beforeunload', this.onPageUnload); window.removeEventListener('resize', this.onResize); document.removeEventListener("keydown", this.onKeyDown); @@ -365,6 +367,17 @@ module.exports = React.createClass({ // Tinter.tint(); // reset colourscheme }, + onPageUnload(event) { + if (ContentMessages.getCurrentUploads().length > 0) { + return event.returnValue = + 'You seem to be uploading files, are you sure you want to quit?'; + } else if (this._getCallForRoom() && this.state.callState !== 'ended') { + return event.returnValue = + 'You seem to be in a call, are you sure you want to quit?'; + } + }, + + onKeyDown: function(ev) { let handled = false; const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; @@ -1759,6 +1772,7 @@ module.exports = React.createClass({ oobData={this.props.oobData} editing={this.state.editingRoomSettings} saving={this.state.uploadingRoomSettings} + inRoom={myMember && myMember.membership === 'join'} collapsedRhs={ this.props.collapsedRhs } onSearchClick={this.onSearchClick} onSettingsClick={this.onSettingsClick} diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 7c89694a29..8794713501 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -177,8 +177,8 @@ var TimelinePanel = React.createClass({ componentWillMount: function() { debuglog("TimelinePanel: mounting"); - this.last_rr_sent_event_id = undefined; - this.last_rm_sent_event_id = undefined; + this.lastRRSentEventId = undefined; + this.lastRMSentEventId = undefined; this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); @@ -504,12 +504,13 @@ var TimelinePanel = React.createClass({ // very possible have logged out within that timeframe, so check // we still have a client. const cli = MatrixClientPeg.get(); - // if no client or client is guest don't send RR + // if no client or client is guest don't send RR or RM if (!cli || cli.isGuest()) return; - var currentReadUpToEventId = this._getCurrentReadReceipt(true); - var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId); + let shouldSendRR = true; + const currentRREventId = this._getCurrentReadReceipt(true); + const currentRREventIndex = this._indexForEventId(currentRREventId); // We want to avoid sending out read receipts when we are looking at // events in the past which are before the latest RR. // @@ -523,43 +524,60 @@ var TimelinePanel = React.createClass({ // RRs) - but that is a bit of a niche case. It will sort itself out when // the user eventually hits the live timeline. // - if (currentReadUpToEventId && currentReadUpToEventIndex === null && + if (currentRREventId && currentRREventIndex === null && this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) { - return; + shouldSendRR = false; } - var lastReadEventIndex = this._getLastDisplayedEventIndex({ - ignoreOwn: true + const lastReadEventIndex = this._getLastDisplayedEventIndex({ + ignoreOwn: true, }); - if (lastReadEventIndex === null) return; + if (lastReadEventIndex === null) { + shouldSendRR = false; + } + let lastReadEvent = this.state.events[lastReadEventIndex]; + shouldSendRR = shouldSendRR && + // Only send a RR if the last read event is ahead in the timeline relative to + // the current RR event. + lastReadEventIndex > currentRREventIndex && + // Only send a RR if the last RR set != the one we would send + this.lastRRSentEventId != lastReadEvent.getId(); - var lastReadEvent = this.state.events[lastReadEventIndex]; + // Only send a RM if the last RM sent != the one we would send + const shouldSendRM = + this.lastRMSentEventId != this.state.readMarkerEventId; // we also remember the last read receipt we sent to avoid spamming the // same one at the server repeatedly - if ((lastReadEventIndex > currentReadUpToEventIndex && - this.last_rr_sent_event_id != lastReadEvent.getId()) || - this.last_rm_sent_event_id != this.state.readMarkerEventId) { - - this.last_rr_sent_event_id = lastReadEvent.getId(); - this.last_rm_sent_event_id = this.state.readMarkerEventId; + if (shouldSendRR || shouldSendRM) { + if (shouldSendRR) { + this.lastRRSentEventId = lastReadEvent.getId(); + } else { + lastReadEvent = null; + } + this.lastRMSentEventId = this.state.readMarkerEventId; + debuglog('TimelinePanel: Sending Read Markers for ', + this.props.timelineSet.room.roomId, + 'rm', this.state.readMarkerEventId, + lastReadEvent ? 'rr ' + lastReadEvent.getId() : '', + ); MatrixClientPeg.get().setRoomReadMarkers( this.props.timelineSet.room.roomId, this.state.readMarkerEventId, - lastReadEvent + lastReadEvent, // Could be null, in which case no RR is sent ).catch((e) => { // /read_markers API is not implemented on this HS, fallback to just RR - if (e.errcode === 'M_UNRECOGNIZED') { + if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) { return MatrixClientPeg.get().sendReadReceipt( - lastReadEvent + lastReadEvent, ).catch(() => { - this.last_rr_sent_event_id = undefined; + this.lastRRSentEventId = undefined; }); } // it failed, so allow retries next time the user is active - this.last_rr_sent_event_id = undefined; - this.last_rm_sent_event_id = undefined; + this.lastRRSentEventId = undefined; + this.lastRMSentEventId = undefined; }); // do a quick-reset of our unreadNotificationCount to avoid having @@ -572,7 +590,6 @@ var TimelinePanel = React.createClass({ this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0); dis.dispatch({ action: 'on_room_read', - room: this.props.timelineSet.room, }); } } diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 9d53bfda31..de88566300 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -30,6 +30,7 @@ const Email = require('../../email'); const AddThreepid = require('../../AddThreepid'); const SdkConfig = require('../../SdkConfig'); import AccessibleButton from '../views/elements/AccessibleButton'; +import * as FormattingUtils from '../../utils/FormattingUtils'; // if this looks like a release, use the 'version' from package.json; else use // the git sha. Prepend version with v, to look like riot-web version @@ -37,7 +38,7 @@ const REACT_SDK_VERSION = 'dist' in packageJson ? packageJson.version : packageJ // Simple method to help prettify GH Release Tags and Commit Hashes. const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i; -const gHVersionLabel = function(repo, token) { +const gHVersionLabel = function(repo, token='') { const match = token.match(semVerRegex); let url; if (match && match[1]) { // basic semVer string possibly with commit hash @@ -47,7 +48,7 @@ const gHVersionLabel = function(repo, token) { } else { url = `https://github.com/${repo}/commit/${token.split('-')[0]}`; } - return {token}; + return {token}; }; // Enumerate some simple 'flip a bit' UI settings (if any). @@ -151,10 +152,10 @@ module.exports = React.createClass({ getInitialState: function() { return { avatarUrl: null, - threePids: [], + threepids: [], phase: "UserSettings.LOADING", // LOADING, DISPLAY email_add_pending: false, - vectorVersion: null, + vectorVersion: undefined, rejectingInvites: false, mediaDevices: null, }; @@ -618,7 +619,12 @@ module.exports = React.createClass({ _renderCryptoInfo: function() { const client = MatrixClientPeg.get(); const deviceId = client.deviceId; - const identityKey = client.getDeviceEd25519Key() || ""; + let identityKey = client.getDeviceEd25519Key(); + if (!identityKey) { + identityKey = ""; + } else { + identityKey = FormattingUtils.formatCryptoKey(identityKey); + } let importExportButtons = null; @@ -930,6 +936,7 @@ module.exports = React.createClass({ addEmailSection = (
+
- riot-web version: {(this.state.vectorVersion !== null) + riot-web version: {(this.state.vectorVersion !== undefined) ? gHVersionLabel('vector-im/riot-web', this.state.vectorVersion) : 'unknown' }
diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 279dedbd43..0b2ca5225d 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -47,19 +47,7 @@ export default React.createClass({ children: React.PropTypes.node, }, - componentWillMount: function() { - this.priorActiveElement = document.activeElement; - }, - - componentWillUnmount: function() { - if (this.priorActiveElement !== null) { - this.priorActiveElement.focus(); - } - }, - - // Must be when the key is released (and not pressed) otherwise componentWillUnmount - // will focus another element which will receive future key events - _onKeyUp: function(e) { + _onKeyDown: function(e) { if (e.keyCode === KeyCode.ESCAPE) { e.stopPropagation(); e.preventDefault(); @@ -79,9 +67,9 @@ export default React.createClass({ render: function() { const TintableSvg = sdk.getComponent("elements.TintableSvg"); - + return ( -
+
diff --git a/src/components/views/dialogs/DeviceVerifyDialog.js b/src/components/views/dialogs/DeviceVerifyDialog.js new file mode 100644 index 0000000000..f9feb718b0 --- /dev/null +++ b/src/components/views/dialogs/DeviceVerifyDialog.js @@ -0,0 +1,76 @@ +/* +Copyright 2016 OpenMarket Ltd +Copyright 2017 Vector Creations 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 MatrixClientPeg from '../../../MatrixClientPeg'; +import sdk from '../../../index'; +import * as FormattingUtils from '../../../utils/FormattingUtils'; + +export default function DeviceVerifyDialog(props) { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + + const key = FormattingUtils.formatCryptoKey(props.device.getFingerprint()); + const body = ( +
+

+ To verify that this device can be trusted, please contact its + owner using some other means (e.g. in person or a phone call) + and ask them whether the key they see in their User Settings + for this device matches the key below: +

+
+
    +
  • { props.device.getDisplayName() }
  • +
  • { props.device.deviceId}
  • +
  • { key }
  • +
+
+

+ If it matches, press the verify button below. + If it doesnt, then someone else is intercepting this device + and you probably want to press the blacklist button instead. +

+

+ In future this verification process will be more sophisticated. +

+
+ ); + + function onFinished(confirm) { + if (confirm) { + MatrixClientPeg.get().setDeviceVerified( + props.userId, props.device.deviceId, true, + ); + } + props.onFinished(confirm); + } + + return ( + + ); +} + +DeviceVerifyDialog.propTypes = { + userId: React.PropTypes.string.isRequired, + device: React.PropTypes.object.isRequired, + onFinished: React.PropTypes.func.isRequired, +}; diff --git a/src/components/views/dialogs/QuestionDialog.js b/src/components/views/dialogs/QuestionDialog.js index 8e20b0d2bc..6012541b94 100644 --- a/src/components/views/dialogs/QuestionDialog.js +++ b/src/components/views/dialogs/QuestionDialog.js @@ -47,12 +47,6 @@ export default React.createClass({ this.props.onFinished(false); }, - componentDidMount: function() { - if (this.props.focus) { - this.refs.button.focus(); - } - }, - render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const cancelButton = this.props.hasCancelButton ? ( @@ -69,7 +63,7 @@ export default React.createClass({ {this.props.description}
- {this.props.extraButtons} diff --git a/src/components/views/elements/AddressSelector.js b/src/components/views/elements/AddressSelector.js index 6bad15f7d0..5329994037 100644 --- a/src/components/views/elements/AddressSelector.js +++ b/src/components/views/elements/AddressSelector.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -138,7 +139,7 @@ export default React.createClass({ onClick={this.onClick.bind(this, i)} onMouseEnter={this.onMouseEnter.bind(this, i)} onMouseLeave={this.onMouseLeave} - key={this.props.addressList[i].userId} + key={this.props.addressList[i].addressType + "/" + this.props.addressList[i].address} ref={(ref) => { this.addressListElement = ref; }} > diff --git a/src/components/views/elements/DeviceVerifyButtons.js b/src/components/views/elements/DeviceVerifyButtons.js index fdd34e6ad2..28a36c429e 100644 --- a/src/components/views/elements/DeviceVerifyButtons.js +++ b/src/components/views/elements/DeviceVerifyButtons.js @@ -50,42 +50,10 @@ export default React.createClass({ }, onVerifyClick: function() { - var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createDialog(QuestionDialog, { - title: "Verify device", - description: ( -
-

- To verify that this device can be trusted, please contact its - owner using some other means (e.g. in person or a phone call) - and ask them whether the key they see in their User Settings - for this device matches the key below: -

-
-
    -
  • { this.state.device.getDisplayName() }
  • -
  • { this.state.device.deviceId}
  • -
  • { this.state.device.getFingerprint() }
  • -
-
-

- If it matches, press the verify button below. - If it doesnt, then someone else is intercepting this device - and you probably want to press the blacklist button instead. -

-

- In future this verification process will be more sophisticated. -

-
- ), - button: "I verify that the keys match", - onFinished: confirm=>{ - if (confirm) { - MatrixClientPeg.get().setDeviceVerified( - this.props.userId, this.state.device.deviceId, true - ); - } - }, + const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog'); + Modal.createDialog(DeviceVerifyDialog, { + userId: this.props.userId, + device: this.state.device, }); }, diff --git a/src/components/views/elements/DirectorySearchBox.js b/src/components/views/elements/DirectorySearchBox.js index 467878caad..eaa6ee34ba 100644 --- a/src/components/views/elements/DirectorySearchBox.js +++ b/src/components/views/elements/DirectorySearchBox.js @@ -93,7 +93,7 @@ export default class DirectorySearchBox extends React.Component { className="mx_DirectorySearchBox_input" ref={this._collectInput} onChange={this._onChange} onKeyUp={this._onKeyUp} - placeholder={this.props.placeholder} + placeholder={this.props.placeholder} autoFocus /> {join_button} diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js index b4d2545e04..82f8d753a9 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.js @@ -152,10 +152,12 @@ export default class Dropdown extends React.Component { } _onInputClick(ev) { - this.setState({ - expanded: !this.state.expanded, - }); - ev.preventDefault(); + if (!this.state.expanded) { + this.setState({ + expanded: true, + }); + ev.preventDefault(); + } } _onMenuOptionClick(dropdownKey) { @@ -252,7 +254,7 @@ export default class Dropdown extends React.Component { ); }); if (options.length === 0) { - return [
+ return [
No results
]; } diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index ae8678894d..dcf1810468 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -269,7 +269,7 @@ module.exports = React.createClass({ ); }); return ( - + {avatars} ); diff --git a/src/components/views/login/CountryDropdown.js b/src/components/views/login/CountryDropdown.js index 6323b3f558..7024db339c 100644 --- a/src/components/views/login/CountryDropdown.js +++ b/src/components/views/login/CountryDropdown.js @@ -19,7 +19,6 @@ import React from 'react'; import sdk from '../../../index'; import { COUNTRIES } from '../../../phonenumber'; -import { charactersToImageNode } from '../../../HtmlUtils'; const COUNTRIES_BY_ISO2 = new Object(null); for (const c of COUNTRIES) { @@ -27,9 +26,14 @@ for (const c of COUNTRIES) { } function countryMatchesSearchQuery(query, country) { + // Remove '+' if present (when searching for a prefix) + if (query[0] === '+') { + query = query.slice(1); + } + if (country.name.toUpperCase().indexOf(query.toUpperCase()) == 0) return true; if (country.iso2 == query.toUpperCase()) return true; - if (country.prefix == query) return true; + if (country.prefix.indexOf(query) !== -1) return true; return false; } @@ -38,10 +42,11 @@ export default class CountryDropdown extends React.Component { super(props); this._onSearchChange = this._onSearchChange.bind(this); this._onOptionChange = this._onOptionChange.bind(this); + this._getShortOption = this._getShortOption.bind(this); this.state = { searchQuery: '', - } + }; } componentWillMount() { @@ -64,13 +69,21 @@ export default class CountryDropdown extends React.Component { } _flagImgForIso2(iso2) { - // Unicode Regional Indicator Symbol letter 'A' - const RIS_A = 0x1F1E6; - const ASCII_A = 65; - return charactersToImageNode(iso2, true, - RIS_A + (iso2.charCodeAt(0) - ASCII_A), - RIS_A + (iso2.charCodeAt(1) - ASCII_A), - ); + return ; + } + + _getShortOption(iso2) { + if (!this.props.isSmall) { + return undefined; + } + let countryPrefix; + if (this.props.showPrefix) { + countryPrefix = '+' + COUNTRIES_BY_ISO2[iso2].prefix; + } + return + { this._flagImgForIso2(iso2) } + { countryPrefix } + ; } render() { @@ -99,7 +112,7 @@ export default class CountryDropdown extends React.Component { const options = displayedCountries.map((country) => { return
{this._flagImgForIso2(country.iso2)} - {country.name} + {country.name} (+{country.prefix})
; }); @@ -107,21 +120,21 @@ export default class CountryDropdown extends React.Component { // values between mounting and the initial value propgating const value = this.props.value || COUNTRIES[0].iso2; - const getShortOption = this.props.isSmall ? this._flagImgForIso2 : undefined; - - return {options} - + ; } } CountryDropdown.propTypes = { className: React.PropTypes.string, isSmall: React.PropTypes.bool, + // if isSmall, show +44 in the selected value + showPrefix: React.PropTypes.bool, onOptionChange: React.PropTypes.func.isRequired, value: React.PropTypes.string, }; diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 349dd0d139..46c9598751 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -149,28 +149,26 @@ class PasswordLogin extends React.Component {
; case PasswordLogin.LOGIN_FIELD_PHONE: const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); - const prefix = this.state.phonePrefix; return
+ -
-
+{prefix}
- -
; } } diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 2bc2b8946a..e55a224531 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -314,24 +314,23 @@ module.exports = React.createClass({ const phoneSection = (
+ -
-
+{this.state.phonePrefix}
- -
); diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index d1486d22c9..44c4051995 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -295,16 +295,6 @@ module.exports = WithMatrixClient(React.createClass({ const receiptOffset = 15; let left = 0; - // It's possible that the receipt was sent several days AFTER the event. - // If it is, we want to display the complete date along with the HH:MM:SS, - // rather than just HH:MM:SS. - let dayAfterEvent = new Date(this.props.mxEvent.getTs()); - dayAfterEvent.setDate(dayAfterEvent.getDate() + 1); - dayAfterEvent.setHours(0); - dayAfterEvent.setMinutes(0); - dayAfterEvent.setSeconds(0); - let dayAfterEventTime = dayAfterEvent.getTime(); - var receipts = this.props.readReceipts || []; for (var i = 0; i < receipts.length; ++i) { var receipt = receipts[i]; @@ -340,7 +330,6 @@ module.exports = WithMatrixClient(React.createClass({ suppressAnimation={this._suppressReadReceiptAnimation} onClick={this.toggleAllReadAvatars} timestamp={receipt.ts} - showFullTimestamp={receipt.ts >= dayAfterEventTime} /> ); } @@ -492,22 +481,22 @@ module.exports = WithMatrixClient(React.createClass({ var e2e; // cosmetic padlocks: if ((e2eEnabled && this.props.eventSendStatus) || this.props.mxEvent.getType() === 'm.room.encryption') { - e2e = ; + e2e = Encrypted by verified device; } // real padlocks else if (this.props.mxEvent.isEncrypted() || (e2eEnabled && this.props.eventSendStatus)) { if (this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted') { - e2e = ; + e2e = Undecryptable; } else if (this.state.verified == true || (e2eEnabled && this.props.eventSendStatus)) { - e2e = ; + e2e = Encrypted by verified device; } else { - e2e = ; + e2e = Encrypted by unverified device; } } else if (e2eEnabled) { - e2e = ; + e2e = Unencrypted message; } const timestamp = this.props.mxEvent.getTs() ? : null; diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index ef8fb29cbc..35e6d28b1f 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -100,7 +100,9 @@ module.exports = React.createClass({ render: function() { var p = this.state.preview; - if (!p) return
; + if (!p || Object.keys(p).length === 0) { + return
; + } // FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing? var image = p["og:image"]; diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 1a9a8d5e0f..839405c922 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -717,8 +717,16 @@ module.exports = WithMatrixClient(React.createClass({ const memberName = this.props.member.name; + if (this.props.member.user) { + var presenceState = this.props.member.user.presence; + var presenceLastActiveAgo = this.props.member.user.lastActiveAgo; + var presenceLastTs = this.props.member.user.lastPresenceTs; + var presenceCurrentlyActive = this.props.member.user.currentlyActive; + } + var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); var PowerSelector = sdk.getComponent('elements.PowerSelector'); + var PresenceLabel = sdk.getComponent('rooms.PresenceLabel'); const EmojiText = sdk.getComponent('elements.EmojiText'); return (
@@ -736,6 +744,11 @@ module.exports = WithMatrixClient(React.createClass({
Level:
+
+ +
{ adminTools } diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 88230062fe..830d3f38ff 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -33,6 +33,7 @@ export default class MessageComposer extends React.Component { this.onHangupClick = this.onHangupClick.bind(this); this.onUploadClick = this.onUploadClick.bind(this); this.onUploadFileSelected = this.onUploadFileSelected.bind(this); + this.uploadFiles = this.uploadFiles.bind(this); this.onVoiceCallClick = this.onVoiceCallClick.bind(this); this.onInputContentChanged = this.onInputContentChanged.bind(this); this.onUpArrow = this.onUpArrow.bind(this); @@ -43,6 +44,7 @@ export default class MessageComposer extends React.Component { this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this); this.onInputStateChanged = this.onInputStateChanged.bind(this); this.onEvent = this.onEvent.bind(this); + this.onPageUnload = this.onPageUnload.bind(this); this.state = { autocompleteQuery: '', @@ -64,12 +66,21 @@ export default class MessageComposer extends React.Component { // marked as encrypted. // XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something. MatrixClientPeg.get().on("event", this.onEvent); + + window.addEventListener('beforeunload', this.onPageUnload); } componentWillUnmount() { if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("event", this.onEvent); } + window.removeEventListener('beforeunload', this.onPageUnload); + } + + onPageUnload(event) { + if (this.messageComposerInput) { + this.messageComposerInput.sentHistory.saveLastTextEntry(); + } } onEvent(event) { @@ -91,10 +102,11 @@ export default class MessageComposer extends React.Component { this.refs.uploadInput.click(); } - onUploadFileSelected(files, isPasted) { - if (!isPasted) - files = files.target.files; + onUploadFileSelected(files) { + this.uploadFiles(files.target.files); + } + uploadFiles(files) { let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); let TintableSvg = sdk.getComponent("elements.TintableSvg"); @@ -300,7 +312,7 @@ export default class MessageComposer extends React.Component { tryComplete={this._tryComplete} onUpArrow={this.onUpArrow} onDownArrow={this.onDownArrow} - onUploadFileSelected={this.onUploadFileSelected} + onFilesPasted={this.uploadFiles} tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete onContentChanged={this.onInputContentChanged} onInputStateChanged={this.onInputStateChanged} />, diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 8efd2fa579..af361db235 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -84,7 +84,6 @@ export default class MessageComposerInput extends React.Component { this.onAction = this.onAction.bind(this); this.handleReturn = this.handleReturn.bind(this); this.handleKeyCommand = this.handleKeyCommand.bind(this); - this.handlePastedFiles = this.handlePastedFiles.bind(this); this.onEditorContentChanged = this.onEditorContentChanged.bind(this); this.setEditorState = this.setEditorState.bind(this); this.onUpArrow = this.onUpArrow.bind(this); @@ -477,10 +476,6 @@ export default class MessageComposerInput extends React.Component { return false; } - handlePastedFiles(files) { - this.props.onUploadFileSelected(files, true); - } - handleReturn(ev) { if (ev.shiftKey) { this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState)); @@ -542,9 +537,9 @@ export default class MessageComposerInput extends React.Component { let sendTextFn = this.client.sendTextMessage; if (contentText.startsWith('/me')) { - contentText = contentText.replace('/me ', ''); + contentText = contentText.substring(4); // bit of a hack, but the alternative would be quite complicated - if (contentHTML) contentHTML = contentHTML.replace('/me ', ''); + if (contentHTML) contentHTML = contentHTML.replace(/\/me ?/, ''); sendHtmlFn = this.client.sendHtmlEmote; sendTextFn = this.client.sendEmoteMessage; } @@ -734,7 +729,7 @@ export default class MessageComposerInput extends React.Component { keyBindingFn={MessageComposerInput.getKeyBinding} handleKeyCommand={this.handleKeyCommand} handleReturn={this.handleReturn} - handlePastedFiles={this.handlePastedFiles} + handlePastedFiles={this.props.onFilesPasted} stripPastedStyles={!this.state.isRichtextEnabled} onTab={this.onTab} onUpArrow={this.onUpArrow} @@ -764,7 +759,7 @@ MessageComposerInput.propTypes = { onDownArrow: React.PropTypes.func, - onUploadFileSelected: React.PropTypes.func, + onFilesPasted: React.PropTypes.func, // attempts to confirm currently selected completion, returns whether actually confirmed tryComplete: React.PropTypes.func, diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index 378644478c..adc6bc2c91 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -69,6 +69,9 @@ export default React.createClass({ // The text to use a placeholder in the input box placeholder: React.PropTypes.string.isRequired, + + // callback to handle files pasted into the composer + onFilesPasted: React.PropTypes.func, }, componentWillMount: function() { @@ -439,10 +442,27 @@ export default React.createClass({ this.refs.textarea.focus(); }, + _onPaste: function(ev) { + const items = ev.clipboardData.items; + const files = []; + for (const item of items) { + if (item.kind === 'file') { + files.push(item.getAsFile()); + } + } + if (files.length && this.props.onFilesPasted) { + this.props.onFilesPasted(files); + return true; + } + return false; + }, + render: function() { return (
-