diff --git a/.eslintrc.js b/.eslintrc.js
index 6cd0e1015e..74790a2964 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -64,7 +64,7 @@ module.exports = {
// to JSX.
ignorePattern: '^\\s*<',
ignoreComments: true,
- code: 90,
+ code: 120,
}],
"valid-jsdoc": ["warn"],
"new-cap": ["warn"],
diff --git a/.gitignore b/.gitignore
index 7006c02403..b99c9f1145 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,4 @@ npm-debug.log
/karma-reports
/.idea
+/src/component-index.js
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 292e60607d..97dda666de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,24 @@
+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 5c96a74f5b..444d1c5369 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
- "version": "0.8.7",
+ "version": "0.8.8",
"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": "node scripts/babelcheck.js && babel src -d lib --source-maps",
- "start": "node scripts/babelcheck.js && 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",
@@ -53,7 +55,7 @@
"draft-js-export-markdown": "^0.2.0",
"emojione": "2.2.3",
"file-saver": "^1.3.3",
- "filesize": "^3.1.2",
+ "filesize": "3.5.6",
"flux": "^2.0.3",
"fuse.js": "^2.2.0",
"glob": "^5.0.14",
@@ -67,7 +69,7 @@
"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/babelcheck.js b/scripts/babelcheck.js
deleted file mode 100644
index 14e4a28a70..0000000000
--- a/scripts/babelcheck.js
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/env node
-
-var exec = require('child_process').exec;
-
-// Makes sure the babel executable in the path is babel 6 (or greater), not
-// babel 5, which it is if you upgrade from an older version of react-sdk and
-// run 'npm install' since the package has changed to babel-cli, so 'babel'
-// remains installed and the executable in node_modules/.bin remains as babel
-// 5.
-
-exec("babel -V", function (error, stdout, stderr) {
- if ((error && error.code) || parseInt(stdout.substr(0,1), 10) < 6) {
- console.log("\033[31m\033[1m"+
- '*****************************************\n'+
- '* matrix-react-sdk has moved to babel 6 *\n'+
- '* Please "rm -rf node_modules && npm i" *\n'+
- '* then restore links as appropriate *\n'+
- '*****************************************\n'+
- "\033[91m");
- process.exit(1);
- }
-});
diff --git a/scripts/reskindex.js b/scripts/reskindex.js
index f9cbc2a711..9516614fa5 100755
--- a/scripts/reskindex.js
+++ b/scripts/reskindex.js
@@ -1,53 +1,88 @@
#!/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 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(componentIndex);
+
+ 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']) {
+ strm.write(
+ "module.exports.components = require('"+
+ packageJson['matrix-react-parent']+
+ "/lib/component-index').components;\n\n"
+ );
+ } else {
+ strm.write("module.exports.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 + " && (module.exports.components['"+moduleName+"'] = " + importName + ");");
+ strm.write('\n');
+ strm.uncork();
+ }
+
+ strm.end();
+ 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/Avatar.js b/src/Avatar.js
index 76f5e55ff0..c0127d49af 100644
--- a/src/Avatar.js
+++ b/src/Avatar.js
@@ -22,8 +22,8 @@ module.exports = {
avatarUrlForMember: function(member, width, height, resizeMethod) {
var url = member.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
- width,
- height,
+ Math.floor(width * window.devicePixelRatio),
+ Math.floor(height * window.devicePixelRatio),
resizeMethod,
false,
false
@@ -40,7 +40,9 @@ module.exports = {
avatarUrlForUser: function(user, width, height, resizeMethod) {
var url = ContentRepo.getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
- width, height, resizeMethod
+ Math.floor(width * window.devicePixelRatio),
+ Math.floor(height * window.devicePixelRatio),
+ resizeMethod
);
if (!url || url.length === 0) {
return null;
@@ -57,4 +59,3 @@ module.exports = {
return 'img/' + images[total % images.length] + '.png';
}
};
-
diff --git a/src/ConstantTimeDispatcher.js b/src/ConstantTimeDispatcher.js
deleted file mode 100644
index 265ee11fd4..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 96934d205e..4acb314c2f 100644
--- a/src/HtmlUtils.js
+++ b/src/HtmlUtils.js
@@ -111,8 +111,7 @@ var sanitizeHtmlParams = {
allowedTags: [
'font', // custom to matrix for IRC-style font coloring
'del', // for markdown
- // deliberately no h1/h2 to stop people shouting.
- 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span',
],
@@ -149,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/Lifecycle.js b/src/Lifecycle.js
index f20716cae6..f34aeae0e5 100644
--- a/src/Lifecycle.js
+++ b/src/Lifecycle.js
@@ -49,7 +49,7 @@ import sdk from './index';
* If any of steps 1-4 are successful, it will call {setLoggedIn}, which in
* turn will raise on_logged_in and will_start_client events.
*
- * It returns a promise which resolves when the above process completes.
+ * @param {object} opts
*
* @param {object} opts.realQueryParams: string->string map of the
* query-parameters extracted from the real query-string of the starting
@@ -67,6 +67,7 @@ import sdk from './index';
* @params {string} opts.guestIsUrl: homeserver URL. Only used if enableGuest is
* true; defines the IS to use.
*
+ * @returns {Promise} a promise which resolves when the above process completes.
*/
export function loadSession(opts) {
const realQueryParams = opts.realQueryParams || {};
@@ -127,7 +128,7 @@ export function loadSession(opts) {
function _loginWithToken(queryParams, defaultDeviceDisplayName) {
// create a temporary MatrixClient to do the login
- var client = Matrix.createClient({
+ const client = Matrix.createClient({
baseUrl: queryParams.homeserver,
});
@@ -159,7 +160,7 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
// Not really sure where the right home for it is.
// create a temporary MatrixClient to do the login
- var client = Matrix.createClient({
+ const client = Matrix.createClient({
baseUrl: hsUrl,
});
@@ -188,30 +189,30 @@ function _restoreFromLocalStorage() {
if (!localStorage) {
return q(false);
}
- const hs_url = localStorage.getItem("mx_hs_url");
- const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org';
- const access_token = localStorage.getItem("mx_access_token");
- const user_id = localStorage.getItem("mx_user_id");
- const device_id = localStorage.getItem("mx_device_id");
+ const hsUrl = localStorage.getItem("mx_hs_url");
+ const isUrl = localStorage.getItem("mx_is_url") || 'https://matrix.org';
+ const accessToken = localStorage.getItem("mx_access_token");
+ const userId = localStorage.getItem("mx_user_id");
+ const deviceId = localStorage.getItem("mx_device_id");
- let is_guest;
+ let isGuest;
if (localStorage.getItem("mx_is_guest") !== null) {
- is_guest = localStorage.getItem("mx_is_guest") === "true";
+ isGuest = localStorage.getItem("mx_is_guest") === "true";
} else {
// legacy key name
- is_guest = localStorage.getItem("matrix-is-guest") === "true";
+ isGuest = localStorage.getItem("matrix-is-guest") === "true";
}
- if (access_token && user_id && hs_url) {
- console.log("Restoring session for %s", user_id);
+ if (accessToken && userId && hsUrl) {
+ console.log("Restoring session for %s", userId);
try {
setLoggedIn({
- userId: user_id,
- deviceId: device_id,
- accessToken: access_token,
- homeserverUrl: hs_url,
- identityServerUrl: is_url,
- guest: is_guest,
+ userId: userId,
+ deviceId: deviceId,
+ accessToken: accessToken,
+ homeserverUrl: hsUrl,
+ identityServerUrl: isUrl,
+ guest: isGuest,
});
return q(true);
} catch (e) {
@@ -273,9 +274,13 @@ export function initRtsClient(url) {
*/
export function setLoggedIn(credentials) {
credentials.guest = Boolean(credentials.guest);
- console.log("setLoggedIn => %s (guest=%s) hs=%s",
- credentials.userId, credentials.guest,
- credentials.homeserverUrl);
+
+ console.log(
+ "setLoggedIn: mxid:", credentials.userId,
+ "deviceId:", credentials.deviceId,
+ "guest:", credentials.guest,
+ "hs:", credentials.homeserverUrl,
+ );
// This is dispatched to indicate that the user is still in the process of logging in
// because `teamPromise` may take some time to resolve, breaking the assumption that
// `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms
@@ -352,7 +357,7 @@ export function logout() {
return;
}
- return MatrixClientPeg.get().logout().then(onLoggedOut,
+ MatrixClientPeg.get().logout().then(onLoggedOut,
(err) => {
// Just throwing an error here is going to be very unhelpful
// if you're trying to log out because your server's down and
@@ -363,8 +368,8 @@ export function logout() {
// change your password).
console.log("Failed to call logout API: token will not be invalidated");
onLoggedOut();
- }
- );
+ },
+ ).done();
}
/**
@@ -420,7 +425,7 @@ export function stopMatrixClient() {
UserActivity.stop();
Presence.stop();
if (DMRoomMap.shared()) DMRoomMap.shared().stop();
- var cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.get();
if (cli) {
cli.stopClient();
cli.removeAllListeners();
diff --git a/src/Notifier.js b/src/Notifier.js
index 92770877b7..6473ab4d9c 100644
--- a/src/Notifier.js
+++ b/src/Notifier.js
@@ -15,11 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-var MatrixClientPeg = require("./MatrixClientPeg");
-var PlatformPeg = require("./PlatformPeg");
-var TextForEvent = require('./TextForEvent');
-var Avatar = require('./Avatar');
-var dis = require("./dispatcher");
+import MatrixClientPeg from './MatrixClientPeg';
+import PlatformPeg from './PlatformPeg';
+import TextForEvent from './TextForEvent';
+import Avatar from './Avatar';
+import dis from './dispatcher';
+import sdk from './index';
+import Modal from './Modal';
/*
* Dispatches:
@@ -29,7 +31,7 @@ var dis = require("./dispatcher");
* }
*/
-var Notifier = {
+const Notifier = {
notifsByRoom: {},
notificationMessageForEvent: function(ev) {
@@ -48,16 +50,16 @@ var Notifier = {
return;
}
- var msg = this.notificationMessageForEvent(ev);
+ let msg = this.notificationMessageForEvent(ev);
if (!msg) return;
- var title;
- if (!ev.sender || room.name == ev.sender.name) {
+ let title;
+ if (!ev.sender || room.name === ev.sender.name) {
title = room.name;
// notificationMessageForEvent includes sender,
// but we already have the sender here
if (ev.getContent().body) msg = ev.getContent().body;
- } else if (ev.getType() == 'm.room.member') {
+ } else if (ev.getType() === 'm.room.member') {
// context is all in the message here, we don't need
// to display sender info
title = room.name;
@@ -68,7 +70,7 @@ var Notifier = {
if (ev.getContent().body) msg = ev.getContent().body;
}
- var avatarUrl = ev.sender ? Avatar.avatarUrlForMember(
+ const avatarUrl = ev.sender ? Avatar.avatarUrlForMember(
ev.sender, 40, 40, 'crop'
) : null;
@@ -83,7 +85,7 @@ var Notifier = {
},
_playAudioNotification: function(ev, room) {
- var e = document.getElementById("messageAudio");
+ const e = document.getElementById("messageAudio");
if (e) {
e.load();
e.play();
@@ -95,7 +97,7 @@ var Notifier = {
this.boundOnSyncStateChange = this.onSyncStateChange.bind(this);
this.boundOnRoomReceipt = this.onRoomReceipt.bind(this);
MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline);
- MatrixClientPeg.get().on("Room.receipt", this.boundOnRoomReceipt);
+ MatrixClientPeg.get().on('Room.receipt', this.boundOnRoomReceipt);
MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange);
this.toolbarHidden = false;
this.isSyncing = false;
@@ -104,7 +106,7 @@ var Notifier = {
stop: function() {
if (MatrixClientPeg.get() && this.boundOnRoomTimeline) {
MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline);
- MatrixClientPeg.get().removeListener("Room.receipt", this.boundOnRoomReceipt);
+ MatrixClientPeg.get().removeListener('Room.receipt', this.boundOnRoomReceipt);
MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
}
this.isSyncing = false;
@@ -121,7 +123,7 @@ var Notifier = {
// make sure that we persist the current setting audio_enabled setting
// before changing anything
if (global.localStorage) {
- if(global.localStorage.getItem('audio_notifications_enabled') == null) {
+ if (global.localStorage.getItem('audio_notifications_enabled') === null) {
this.setAudioEnabled(this.isEnabled());
}
}
@@ -131,6 +133,16 @@ var Notifier = {
plaf.requestNotificationPermission().done((result) => {
if (result !== 'granted') {
// The permission request was dismissed or denied
+ const description = result === 'denied'
+ ? 'Riot does not have permission to send you notifications'
+ + ' - please check your browser settings'
+ : 'Riot was not given permission to send notifications'
+ + ' - please try again';
+ const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
+ Modal.createDialog(ErrorDialog, {
+ title: 'Unable to enable Notifications',
+ description,
+ });
return;
}
@@ -141,7 +153,7 @@ var Notifier = {
if (callback) callback();
dis.dispatch({
action: "notifier_enabled",
- value: true
+ value: true,
});
});
// clear the notifications_hidden flag, so that if notifications are
@@ -152,7 +164,7 @@ var Notifier = {
global.localStorage.setItem('notifications_enabled', 'false');
dis.dispatch({
action: "notifier_enabled",
- value: false
+ value: false,
});
}
},
@@ -165,7 +177,7 @@ var Notifier = {
if (!global.localStorage) return true;
- var enabled = global.localStorage.getItem('notifications_enabled');
+ const enabled = global.localStorage.getItem('notifications_enabled');
if (enabled === null) return true;
return enabled === 'true';
},
@@ -173,12 +185,12 @@ var Notifier = {
setAudioEnabled: function(enable) {
if (!global.localStorage) return;
global.localStorage.setItem('audio_notifications_enabled',
- enable ? 'true' : 'false');
+ enable ? 'true' : 'false');
},
isAudioEnabled: function(enable) {
if (!global.localStorage) return true;
- var enabled = global.localStorage.getItem(
+ const enabled = global.localStorage.getItem(
'audio_notifications_enabled');
// default to true if the popups are enabled
if (enabled === null) return this.isEnabled();
@@ -192,7 +204,7 @@ var Notifier = {
// this is nothing to do with notifier_enabled
dis.dispatch({
action: "notifier_enabled",
- value: this.isEnabled()
+ value: this.isEnabled(),
});
// update the info to localStorage for persistent settings
@@ -215,8 +227,7 @@ var Notifier = {
onSyncStateChange: function(state) {
if (state === "SYNCING") {
this.isSyncing = true;
- }
- else if (state === "STOPPED" || state === "ERROR") {
+ } else if (state === "STOPPED" || state === "ERROR") {
this.isSyncing = false;
}
},
@@ -225,10 +236,10 @@ var Notifier = {
if (toStartOfTimeline) return;
if (!room) return;
if (!this.isSyncing) return; // don't alert for any messages initially
- if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return;
+ if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return;
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
- var actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
+ const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
if (actions && actions.notify) {
if (this.isEnabled()) {
this._displayPopupNotification(ev, room);
@@ -240,7 +251,7 @@ var Notifier = {
},
onRoomReceipt: function(ev, room) {
- if (room.getUnreadNotificationCount() == 0) {
+ if (room.getUnreadNotificationCount() === 0) {
// ideally we would clear each notification when it was read,
// but we have no way, given a read receipt, to know whether
// the receipt comes before or after an event, so we can't
@@ -255,7 +266,7 @@ var Notifier = {
}
delete this.notifsByRoom[room.roomId];
}
- }
+ },
};
if (!global.mxNotifier) {
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/UserActivity.js b/src/UserActivity.js
index e7338e17e9..1ae272f5df 100644
--- a/src/UserActivity.js
+++ b/src/UserActivity.js
@@ -32,7 +32,7 @@ class UserActivity {
start() {
document.onmousedown = this._onUserActivity.bind(this);
document.onmousemove = this._onUserActivity.bind(this);
- document.onkeypress = this._onUserActivity.bind(this);
+ document.onkeydown = this._onUserActivity.bind(this);
// can't use document.scroll here because that's only the document
// itself being scrolled. Need to use addEventListener's useCapture.
// also this needs to be the wheel event, not scroll, as scroll is
@@ -50,7 +50,7 @@ class UserActivity {
stop() {
document.onmousedown = undefined;
document.onmousemove = undefined;
- document.onkeypress = undefined;
+ document.onkeydown = undefined;
window.removeEventListener('wheel', this._onUserActivity.bind(this),
{ passive: true, capture: true });
}
diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js
index 66a872958c..9de291249f 100644
--- a/src/UserSettingsStore.js
+++ b/src/UserSettingsStore.js
@@ -15,9 +15,9 @@ limitations under the License.
*/
'use strict';
-var q = require("q");
-var MatrixClientPeg = require("./MatrixClientPeg");
-var Notifier = require("./Notifier");
+import q from 'q';
+import MatrixClientPeg from './MatrixClientPeg';
+import Notifier from './Notifier';
/*
* TODO: Make things use this. This is all WIP - see UserSettings.js for usage.
@@ -33,7 +33,7 @@ module.exports = {
],
loadProfileInfo: function() {
- var cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.get();
return cli.getProfileInfo(cli.credentials.userId);
},
@@ -44,7 +44,7 @@ module.exports = {
loadThreePids: function() {
if (MatrixClientPeg.get().isGuest()) {
return q({
- threepids: []
+ threepids: [],
}); // guests can't poke 3pid endpoint
}
return MatrixClientPeg.get().getThreePids();
@@ -73,19 +73,19 @@ module.exports = {
Notifier.setAudioEnabled(enable);
},
- changePassword: function(old_password, new_password) {
- var cli = MatrixClientPeg.get();
+ changePassword: function(oldPassword, newPassword) {
+ const cli = MatrixClientPeg.get();
- var authDict = {
+ const authDict = {
type: 'm.login.password',
user: cli.credentials.userId,
- password: old_password
+ password: oldPassword,
};
- return cli.setPassword(authDict, new_password);
+ return cli.setPassword(authDict, newPassword);
},
- /**
+ /*
* Returns the email pusher (pusher of type 'email') for a given
* email address. Email pushers all have the same app ID, so since
* pushers are unique over (app ID, pushkey), there will be at most
@@ -95,8 +95,8 @@ module.exports = {
if (pushers === undefined) {
return undefined;
}
- for (var i = 0; i < pushers.length; ++i) {
- if (pushers[i].kind == 'email' && pushers[i].pushkey == address) {
+ for (let i = 0; i < pushers.length; ++i) {
+ if (pushers[i].kind === 'email' && pushers[i].pushkey === address) {
return pushers[i];
}
}
@@ -110,7 +110,7 @@ module.exports = {
addEmailPusher: function(address, data) {
return MatrixClientPeg.get().setPusher({
kind: 'email',
- app_id: "m.email",
+ app_id: 'm.email',
pushkey: address,
app_display_name: 'Email Notifications',
device_display_name: address,
@@ -121,46 +121,46 @@ module.exports = {
},
getUrlPreviewsDisabled: function() {
- var event = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls");
+ const event = MatrixClientPeg.get().getAccountData('org.matrix.preview_urls');
return (event && event.getContent().disable);
},
setUrlPreviewsDisabled: function(disabled) {
// FIXME: handle errors
- return MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", {
- disable: disabled
+ return MatrixClientPeg.get().setAccountData('org.matrix.preview_urls', {
+ disable: disabled,
});
},
getSyncedSettings: function() {
- var event = MatrixClientPeg.get().getAccountData("im.vector.web.settings");
+ const event = MatrixClientPeg.get().getAccountData('im.vector.web.settings');
return event ? event.getContent() : {};
},
getSyncedSetting: function(type, defaultValue = null) {
- var settings = this.getSyncedSettings();
- return settings.hasOwnProperty(type) ? settings[type] : null;
+ const settings = this.getSyncedSettings();
+ return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
},
setSyncedSetting: function(type, value) {
- var settings = this.getSyncedSettings();
+ const settings = this.getSyncedSettings();
settings[type] = value;
// FIXME: handle errors
- return MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings);
+ return MatrixClientPeg.get().setAccountData('im.vector.web.settings', settings);
},
getLocalSettings: function() {
- var localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
+ const localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
return JSON.parse(localSettingsString);
},
getLocalSetting: function(type, defaultValue = null) {
- var settings = this.getLocalSettings();
- return settings.hasOwnProperty(type) ? settings[type] : null;
+ const settings = this.getLocalSettings();
+ return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
},
setLocalSetting: function(type, value) {
- var settings = this.getLocalSettings();
+ const settings = this.getLocalSettings();
settings[type] = value;
// FIXME: handle errors
localStorage.setItem('mx_local_settings', JSON.stringify(settings));
@@ -171,8 +171,8 @@ module.exports = {
if (MatrixClientPeg.get().isGuest()) return false;
if (localStorage.getItem(`mx_labs_feature_${feature}`) === null) {
- for (var i = 0; i < this.LABS_FEATURES.length; i++) {
- var f = this.LABS_FEATURES[i];
+ for (let i = 0; i < this.LABS_FEATURES.length; i++) {
+ const f = this.LABS_FEATURES[i];
if (f.id === feature) {
return f.default;
}
@@ -183,5 +183,5 @@ module.exports = {
setFeatureEnabled: function(feature: string, enabled: boolean) {
localStorage.setItem(`mx_labs_feature_${feature}`, enabled);
- }
+ },
};
diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js
index a2d77f02a1..d488ac53ae 100644
--- a/src/autocomplete/EmojiProvider.js
+++ b/src/autocomplete/EmojiProvider.js
@@ -14,7 +14,7 @@ let instance = null;
export default class EmojiProvider extends AutocompleteProvider {
constructor() {
super(EMOJI_REGEX);
- this.fuse = new Fuse(EMOJI_SHORTNAMES);
+ this.fuse = new Fuse(EMOJI_SHORTNAMES, {});
}
async getCompletions(query: string, selection: SelectionRange) {
diff --git a/src/component-index.js b/src/component-index.js
deleted file mode 100644
index b9f358467e..0000000000
--- a/src/component-index.js
+++ /dev/null
@@ -1,255 +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$ForwardMessage from './components/views/rooms/ForwardMessage';
-views$rooms$ForwardMessage && (module.exports.components['views.rooms.ForwardMessage'] = views$rooms$ForwardMessage);
-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 6ac7fcb3c4..5f1aa0d32a 100644
--- a/src/components/structures/LoggedInView.js
+++ b/src/components/structures/LoggedInView.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.
@@ -106,18 +107,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) {
@@ -162,19 +151,19 @@ export default React.createClass({
},
render: function() {
- var LeftPanel = sdk.getComponent('structures.LeftPanel');
- var RightPanel = sdk.getComponent('structures.RightPanel');
- var RoomView = sdk.getComponent('structures.RoomView');
- var UserSettings = sdk.getComponent('structures.UserSettings');
- var CreateRoom = sdk.getComponent('structures.CreateRoom');
- var RoomDirectory = sdk.getComponent('structures.RoomDirectory');
- var HomePage = sdk.getComponent('structures.HomePage');
- var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
- var GuestWarningBar = sdk.getComponent('globals.GuestWarningBar');
- var NewVersionBar = sdk.getComponent('globals.NewVersionBar');
+ const LeftPanel = sdk.getComponent('structures.LeftPanel');
+ const RightPanel = sdk.getComponent('structures.RightPanel');
+ const RoomView = sdk.getComponent('structures.RoomView');
+ const UserSettings = sdk.getComponent('structures.UserSettings');
+ const CreateRoom = sdk.getComponent('structures.CreateRoom');
+ const RoomDirectory = sdk.getComponent('structures.RoomDirectory');
+ const HomePage = sdk.getComponent('structures.HomePage');
+ const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
+ const GuestWarningBar = sdk.getComponent('globals.GuestWarningBar');
+ const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
- var page_element;
- var right_panel = '';
+ let page_element;
+ let right_panel = '';
switch (this.props.page_type) {
case PageTypes.RoomView:
@@ -220,10 +209,8 @@ export default React.createClass({
case PageTypes.RoomDirectory:
page_element = ;
- if (!this.props.collapse_rhs) right_panel = ;
break;
case PageTypes.HomePage:
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index c78a395185..5b15b47cec 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -393,9 +393,10 @@ module.exports = React.createClass({
this.notifyNewScreen('forgot_password');
break;
case 'leave_room':
+ const roomToLeave = MatrixClientPeg.get().getRoom(payload.room_id);
Modal.createDialog(QuestionDialog, {
title: "Leave room",
- description: "Are you sure you want to leave the room?",
+ description: Are you sure you want to leave the room {roomToLeave.name} ? ,
onFinished: (should_leave) => {
if (should_leave) {
const d = MatrixClientPeg.get().leave(payload.room_id);
@@ -770,8 +771,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();
}
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index 8d50789eb0..d4bf147ad5 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -279,20 +279,19 @@ module.exports = React.createClass({
this.currentGhostEventId = null;
}
- var isMembershipChange = (e) =>
- e.getType() === 'm.room.member'
- && (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership);
+ var isMembershipChange = (e) => e.getType() === 'm.room.member';
for (i = 0; i < this.props.events.length; i++) {
- var mxEv = this.props.events[i];
- var wantTile = true;
- var eventId = mxEv.getId();
+ let mxEv = this.props.events[i];
+ let wantTile = true;
+ let eventId = mxEv.getId();
+ let readMarkerInMels = false;
if (!EventTile.haveTileForEvent(mxEv)) {
wantTile = false;
}
- var last = (i == lastShownEventIndex);
+ let last = (i == lastShownEventIndex);
// Wrap consecutive member events in a ListSummary, ignore if redacted
if (isMembershipChange(mxEv) &&
@@ -334,6 +333,9 @@ module.exports = React.createClass({
let eventTiles = summarisedEvents.map(
(e) => {
+ if (e.getId() === this.props.readMarkerEventId) {
+ readMarkerInMels = true;
+ }
// In order to prevent DateSeparators from appearing in the expanded form
// of MemberEventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
@@ -352,12 +354,16 @@ module.exports = React.createClass({
{eventTiles}
);
+
+ if (readMarkerInMels) {
+ ret.push(this._getReadMarkerTile(visible));
+ }
+
continue;
}
@@ -466,7 +472,7 @@ module.exports = React.createClass({
ret.push(
+ data-scroll-tokens={scrollToken}>
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;
@@ -1286,12 +1299,7 @@ module.exports = React.createClass({
return;
}
- var pos = this.refs.messagePanel.getReadMarkerPosition();
-
- // we want to show the bar if the read-marker is off the top of the
- // screen.
- var showBar = (pos < 0);
-
+ const showBar = this.refs.messagePanel.canJumpToReadMarker();
if (this.state.showTopUnreadMessagesBar != showBar) {
this.setState({showTopUnreadMessagesBar: showBar},
this.onChildResize);
@@ -1774,6 +1782,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/ScrollPanel.js b/src/components/structures/ScrollPanel.js
index d43e22e2f1..a652bcc827 100644
--- a/src/components/structures/ScrollPanel.js
+++ b/src/components/structures/ScrollPanel.js
@@ -46,9 +46,13 @@ if (DEBUG_SCROLL) {
* It also provides a hook which allows parents to provide more list elements
* when we get close to the start or end of the list.
*
- * Each child element should have a 'data-scroll-token'. This token is used to
- * serialise the scroll state, and returned as the 'trackedScrollToken'
- * attribute by getScrollState().
+ * Each child element should have a 'data-scroll-tokens'. This string of
+ * comma-separated tokens may contain a single token or many, where many indicates
+ * that the element contains elements that have scroll tokens themselves. The first
+ * token in 'data-scroll-tokens' is used to serialise the scroll state, and returned
+ * as the 'trackedScrollToken' attribute by getScrollState().
+ *
+ * IMPORTANT: INDIVIDUAL TOKENS WITHIN 'data-scroll-tokens' MUST NOT CONTAIN COMMAS.
*
* Some notes about the implementation:
*
@@ -349,8 +353,8 @@ module.exports = React.createClass({
// Subtract height of tile as if it were unpaginated
excessHeight -= tile.clientHeight;
// The tile may not have a scroll token, so guard it
- if (tile.dataset.scrollToken) {
- markerScrollToken = tile.dataset.scrollToken;
+ if (tile.dataset.scrollTokens) {
+ markerScrollToken = tile.dataset.scrollTokens.split(',')[0];
}
if (tile.clientHeight > excessHeight) {
break;
@@ -419,7 +423,8 @@ module.exports = React.createClass({
* scroll. false if we are tracking a particular child.
*
* string trackedScrollToken: undefined if stuckAtBottom is true; if it is
- * false, the data-scroll-token of the child which we are tracking.
+ * false, the first token in data-scroll-tokens of the child which we are
+ * tracking.
*
* number pixelOffset: undefined if stuckAtBottom is true; if it is false,
* the number of pixels the bottom of the tracked child is above the
@@ -551,8 +556,10 @@ module.exports = React.createClass({
var messages = this.refs.itemlist.children;
for (var i = messages.length-1; i >= 0; --i) {
var m = messages[i];
- if (!m.dataset.scrollToken) continue;
- if (m.dataset.scrollToken == scrollToken) {
+ // 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
+ // There might only be one scroll token
+ if (m.dataset.scrollTokens &&
+ m.dataset.scrollTokens.split(',').indexOf(scrollToken) !== -1) {
node = m;
break;
}
@@ -568,7 +575,7 @@ module.exports = React.createClass({
var boundingRect = node.getBoundingClientRect();
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
- debuglog("ScrollPanel: scrolling to token '" + node.dataset.scrollToken + "'+" +
+ debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
pixelOffset + " (delta: "+scrollDelta+")");
if(scrollDelta != 0) {
@@ -591,12 +598,12 @@ module.exports = React.createClass({
for (var i = messages.length-1; i >= 0; --i) {
var node = messages[i];
- if (!node.dataset.scrollToken) continue;
+ if (!node.dataset.scrollTokens) continue;
var boundingRect = node.getBoundingClientRect();
newScrollState = {
stuckAtBottom: false,
- trackedScrollToken: node.dataset.scrollToken,
+ trackedScrollToken: node.dataset.scrollTokens.split(',')[0],
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
};
// If the bottom of the panel intersects the ClientRect of node, use this node
@@ -608,7 +615,7 @@ module.exports = React.createClass({
break;
}
}
- // This is only false if there were no nodes with `node.dataset.scrollToken` set.
+ // This is only false if there were no nodes with `node.dataset.scrollTokens` set.
if (newScrollState) {
this.scrollState = newScrollState;
debuglog("ScrollPanel: saved scroll state", this.scrollState);
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index 8babdaae4a..8794713501 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -1,5 +1,6 @@
/*
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.
@@ -167,14 +168,17 @@ var TimelinePanel = React.createClass({
backPaginating: false,
forwardPaginating: false,
+
+ // cache of matrixClient.getSyncState() (but from the 'sync' event)
+ clientSyncState: MatrixClientPeg.get().getSyncState(),
};
},
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);
@@ -183,6 +187,7 @@ var TimelinePanel = React.createClass({
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
+ MatrixClientPeg.get().on("sync", this.onSync);
this._initTimeline(this.props);
},
@@ -251,6 +256,7 @@ var TimelinePanel = React.createClass({
client.removeListener("Room.receipt", this.onRoomReceipt);
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
client.removeListener("Room.accountData", this.onAccountData);
+ client.removeListener("sync", this.onSync);
}
},
@@ -487,17 +493,24 @@ var TimelinePanel = React.createClass({
}, this.props.onReadMarkerUpdated);
},
+ onSync: function(state, prevState, data) {
+ this.setState({clientSyncState: state});
+ },
+
sendReadReceipt: function() {
if (!this.refs.messagePanel) return;
if (!this.props.manageReadReceipts) return;
// This happens on user_activity_end which is delayed, and it's
// very possible have logged out within that timeframe, so check
// we still have a client.
- if (!MatrixClientPeg.get()) return;
+ const cli = MatrixClientPeg.get();
+ // 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.
//
@@ -511,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
@@ -560,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,
});
}
}
@@ -756,6 +785,19 @@ var TimelinePanel = React.createClass({
return null;
},
+ canJumpToReadMarker: function() {
+ // 1. Do not show jump bar if neither the RM nor the RR are set.
+ // 2. Only show jump bar if RR !== RM. If they are the same, there are only fully
+ // read messages and unread messages. We already have a badge count and the bottom
+ // bar to jump to "live" when we have unread messages.
+ // 3. We want to show the bar if the read-marker is off the top of the screen.
+ // 4. Also, if pos === null, the event might not be paginated - show the unread bar
+ const pos = this.getReadMarkerPosition();
+ return this.state.readMarkerEventId !== null && // 1.
+ this.state.readMarkerEventId !== this._getCurrentReadReceipt() && // 2.
+ (pos < 0 || pos === null); // 3., 4.
+ },
+
/**
* called by the parent component when PageUp/Down/etc is pressed.
*
@@ -1058,11 +1100,18 @@ var TimelinePanel = React.createClass({
// events when viewing historical messages, we get stuck in a loop
// of paginating our way through the entire history of the room.
var stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
+
+ // If the state is PREPARED, we're still waiting for the js-sdk to sync with
+ // the HS and fetch the latest events, so we are effectively forward paginating.
+ const forwardPaginating = (
+ this.state.forwardPaginating || this.state.clientSyncState == 'PREPARED'
+ );
+
return (
';
+const REACT_SDK_VERSION = 'dist' in packageJson ? packageJson.version : packageJson.gitHead || '';
// Simple method to help prettify GH Release Tags and Commit Hashes.
-const GHVersionUrl = function(repo, token) {
- const uriTail = (token.startsWith('v') && token.includes('.')) ? `releases/tag/${token}` : `commit/${token}`;
- return `https://github.com/${repo}/${uriTail}`;
-}
+const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i;
+const gHVersionLabel = function(repo, token='') {
+ const match = token.match(semVerRegex);
+ let url;
+ if (match && match[1]) { // basic semVer string possibly with commit hash
+ url = (match.length > 1 && match[2])
+ ? `https://github.com/${repo}/commit/${match[2]}`
+ : `https://github.com/${repo}/releases/tag/v${match[1]}`;
+ } else {
+ url = `https://github.com/${repo}/commit/${token.split('-')[0]}`;
+ }
+ return {token} ;
+};
// Enumerate some simple 'flip a bit' UI settings (if any).
// 'id' gives the key name in the im.vector.web.settings account data event
@@ -50,7 +59,7 @@ const SETTINGS_LABELS = [
},
{
id: 'hideReadReceipts',
- label: 'Hide read receipts'
+ label: 'Hide read receipts',
},
{
id: 'dontSendTypingNotifications',
@@ -106,7 +115,7 @@ const THEMES = [
id: 'theme',
label: 'Dark theme',
value: 'dark',
- }
+ },
];
@@ -142,10 +151,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,
};
},
@@ -180,7 +189,7 @@ module.exports = React.createClass({
});
this._refreshFromServer();
- var syncedSettings = UserSettingsStore.getSyncedSettings();
+ const syncedSettings = UserSettingsStore.getSyncedSettings();
if (!syncedSettings.theme) {
syncedSettings.theme = 'light';
}
@@ -202,16 +211,16 @@ module.exports = React.createClass({
middleOpacity: 1.0,
});
dis.unregister(this.dispatcherRef);
- let cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomMember.membership", this._onInviteStateChange);
}
},
_refreshFromServer: function() {
- var self = this;
+ const self = this;
q.all([
- UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids()
+ UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids(),
]).done(function(resps) {
self.setState({
avatarUrl: resps[0].avatar_url,
@@ -219,7 +228,7 @@ module.exports = React.createClass({
phase: "UserSettings.DISPLAY",
});
}, function(error) {
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to load user settings: " + error);
Modal.createDialog(ErrorDialog, {
title: "Can't load user settings",
@@ -236,7 +245,7 @@ module.exports = React.createClass({
onAvatarPickerClick: function(ev) {
if (MatrixClientPeg.get().isGuest()) {
- var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
+ const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Guests can't set avatars. Please register.",
@@ -250,8 +259,8 @@ module.exports = React.createClass({
},
onAvatarSelected: function(ev) {
- var self = this;
- var changeAvatar = this.refs.changeAvatar;
+ const self = this;
+ const changeAvatar = this.refs.changeAvatar;
if (!changeAvatar) {
console.error("No ChangeAvatar found to upload image to!");
return;
@@ -260,9 +269,9 @@ module.exports = React.createClass({
// dunno if the avatar changed, re-check it.
self._refreshFromServer();
}, function(err) {
- var errMsg = (typeof err === "string") ? err : (err.error || "");
+ // const errMsg = (typeof err === "string") ? err : (err.error || "");
console.error("Failed to set avatar: " + err);
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Failed to set avatar",
description: ((err && err.message) ? err.message : "Operation failed"),
@@ -271,7 +280,7 @@ module.exports = React.createClass({
},
onLogoutClicked: function(ev) {
- var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
+ const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: "Sign out?",
description:
@@ -286,7 +295,7 @@ module.exports = React.createClass({
Export E2E room keys
-
+ ,
],
onFinished: (confirmed) => {
if (confirmed) {
@@ -300,34 +309,33 @@ module.exports = React.createClass({
},
onPasswordChangeError: function(err) {
- var errMsg = err.error || "";
+ let errMsg = err.error || "";
if (err.httpStatus === 403) {
errMsg = "Failed to change password. Is your password correct?";
- }
- else if (err.httpStatus) {
+ } else if (err.httpStatus) {
errMsg += ` (HTTP status ${err.httpStatus})`;
}
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to change password: " + errMsg);
Modal.createDialog(ErrorDialog, {
title: "Error",
- description: errMsg
+ description: errMsg,
});
},
onPasswordChanged: function() {
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Success",
description: `Your password was successfully changed. You will not
receive push notifications on other devices until you
- log back in to them.`
+ log back in to them.`,
});
},
onUpgradeClicked: function() {
dis.dispatch({
- action: "start_upgrade_registration"
+ action: "start_upgrade_registration",
});
},
@@ -341,11 +349,11 @@ module.exports = React.createClass({
},
_addEmail: function() {
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
- var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
- var email_address = this.refs.add_email_input.value;
- if (!Email.looksValid(email_address)) {
+ const emailAddress = this.refs.add_email_input.value;
+ if (!Email.looksValid(emailAddress)) {
Modal.createDialog(ErrorDialog, {
title: "Invalid Email Address",
description: "This doesn't appear to be a valid email address",
@@ -355,7 +363,7 @@ module.exports = React.createClass({
this._addThreepid = new AddThreepid();
// we always bind emails when registering, so let's do the
// same here.
- this._addThreepid.addEmailAddress(email_address, true).done(() => {
+ this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
Modal.createDialog(QuestionDialog, {
title: "Verification Pending",
description: "Please check your email and click on the link it contains. Once this is done, click continue.",
@@ -364,7 +372,7 @@ module.exports = React.createClass({
});
}, (err) => {
this.setState({email_add_pending: false});
- console.error("Unable to add email address " + email_address + " " + err);
+ console.error("Unable to add email address " + emailAddress + " " + err);
Modal.createDialog(ErrorDialog, {
title: "Unable to add email address",
description: ((err && err.message) ? err.message : "Operation failed"),
@@ -418,9 +426,9 @@ module.exports = React.createClass({
this.setState({email_add_pending: false});
}, (err) => {
this.setState({email_add_pending: false});
- if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
- var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
- var message = "Unable to verify email address. ";
+ if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
+ const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
+ let message = "Unable to verify email address. ";
message += "Please check your email and click on the link it contains. Once this is done, click continue.";
Modal.createDialog(QuestionDialog, {
title: "Verification Pending",
@@ -429,7 +437,7 @@ module.exports = React.createClass({
onFinished: this.onEmailDialogFinished,
});
} else {
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to verify email address: " + err);
Modal.createDialog(ErrorDialog, {
title: "Unable to verify email address",
@@ -469,17 +477,17 @@ module.exports = React.createClass({
_onRejectAllInvitesClicked: function(rooms, ev) {
this.setState({
- rejectingInvites: true
+ rejectingInvites: true,
});
// reject the invites
- let promises = rooms.map((room) => {
+ const promises = rooms.map((room) => {
return MatrixClientPeg.get().leave(room.roomId);
});
// purposefully drop errors to the floor: we'll just have a non-zero number on the UI
// after trying to reject all the invites.
q.allSettled(promises).then(() => {
this.setState({
- rejectingInvites: false
+ rejectingInvites: false,
});
}).done();
},
@@ -492,7 +500,7 @@ module.exports = React.createClass({
}, "e2e-export");
}, {
matrixClient: MatrixClientPeg.get(),
- }
+ },
);
},
@@ -504,7 +512,7 @@ module.exports = React.createClass({
}, "e2e-export");
}, {
matrixClient: MatrixClientPeg.get(),
- }
+ },
);
},
@@ -530,8 +538,6 @@ module.exports = React.createClass({
},
_renderUserInterfaceSettings: function() {
- var client = MatrixClientPeg.get();
-
return (
User Interface
@@ -549,7 +555,7 @@ module.exports = React.createClass({
UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
+ onChange={ (e) => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
/>
Disable inline URL previews by default
@@ -562,7 +568,7 @@ module.exports = React.createClass({
UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) }
+ onChange={ (e) => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) }
/>
{ setting.label }
@@ -577,7 +583,7 @@ module.exports = React.createClass({
name={ setting.id }
value={ setting.value }
defaultChecked={ this._syncedSettings[setting.id] === setting.value }
- onChange={ e => {
+ onChange={ (e) => {
if (e.target.checked) {
UserSettingsStore.setSyncedSetting(setting.id, setting.value);
}
@@ -639,8 +645,8 @@ module.exports = React.createClass({
type="checkbox"
defaultChecked={ this._localSettings[setting.id] }
onChange={
- e => {
- UserSettingsStore.setLocalSetting(setting.id, e.target.checked)
+ (e) => {
+ UserSettingsStore.setLocalSetting(setting.id, e.target.checked);
if (setting.id === 'blacklistUnverifiedDevices') { // XXX: this is a bit ugly
client.setGlobalBlacklistUnverifiedDevices(e.target.checked);
}
@@ -654,7 +660,7 @@ module.exports = React.createClass({
},
_renderDevicesPanel: function() {
- var DevicesPanel = sdk.getComponent('settings.DevicesPanel');
+ const DevicesPanel = sdk.getComponent('settings.DevicesPanel');
return (
Devices
@@ -665,7 +671,7 @@ module.exports = React.createClass({
_renderBugReport: function() {
if (!SdkConfig.get().bug_report_endpoint_url) {
- return
+ return
;
}
return (
@@ -684,17 +690,17 @@ module.exports = React.createClass({
// default to enabled if undefined
if (this.props.enableLabs === false) return null;
- let features = UserSettingsStore.LABS_FEATURES.map(feature => (
+ const features = UserSettingsStore.LABS_FEATURES.map((feature) => (
{
+ onChange={(e) => {
if (MatrixClientPeg.get().isGuest()) {
e.target.checked = false;
- var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
+ const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Guests can't use labs features. Please register.",
@@ -746,14 +752,14 @@ module.exports = React.createClass({
},
_renderBulkOptions: function() {
- let invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => {
+ const invitedRooms = MatrixClientPeg.get().getRooms().filter((r) => {
return r.hasMembershipState(this._me, "invite");
});
if (invitedRooms.length === 0) {
return null;
}
- let Spinner = sdk.getComponent("elements.Spinner");
+ const Spinner = sdk.getComponent("elements.Spinner");
let reject =
;
if (!this.state.rejectingInvites) {
@@ -777,9 +783,7 @@ module.exports = React.createClass({
_showSpoiler: function(event) {
const target = event.target;
- const hidden = target.getAttribute('data-spoiler');
-
- target.innerHTML = hidden;
+ target.innerHTML = target.getAttribute('data-spoiler');
const range = document.createRange();
range.selectNodeContents(target);
@@ -790,12 +794,12 @@ module.exports = React.createClass({
},
nameForMedium: function(medium) {
- if (medium == 'msisdn') return 'Phone';
+ if (medium === 'msisdn') return 'Phone';
return medium[0].toUpperCase() + medium.slice(1);
},
presentableTextForThreepid: function(threepid) {
- if (threepid.medium == 'msisdn') {
+ if (threepid.medium === 'msisdn') {
return '+' + threepid.address;
} else {
return threepid.address;
@@ -803,7 +807,7 @@ module.exports = React.createClass({
},
render: function() {
- var Loader = sdk.getComponent("elements.Spinner");
+ const Loader = sdk.getComponent("elements.Spinner");
switch (this.state.phase) {
case "UserSettings.LOADING":
return (
@@ -815,18 +819,18 @@ module.exports = React.createClass({
throw new Error("Unknown state.phase => " + this.state.phase);
}
// can only get here if phase is UserSettings.DISPLAY
- var SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
- var ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName");
- var ChangePassword = sdk.getComponent("views.settings.ChangePassword");
- var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
- var Notifications = sdk.getComponent("settings.Notifications");
- var EditableText = sdk.getComponent('elements.EditableText');
+ const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
+ const ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName");
+ const ChangePassword = sdk.getComponent("views.settings.ChangePassword");
+ const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
+ const Notifications = sdk.getComponent("settings.Notifications");
+ const EditableText = sdk.getComponent('elements.EditableText');
- var avatarUrl = (
+ const avatarUrl = (
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
);
- var threepidsSection = this.state.threepids.map((val, pidIndex) => {
+ const threepidsSection = this.state.threepids.map((val, pidIndex) => {
const id = "3pid-" + val.address;
return (
@@ -851,6 +855,7 @@ module.exports = React.createClass({
addEmailSection = (
+ Email
);
- }
- else {
+ } else {
accountJsx = (
);
}
- var notification_area;
+ let notificationArea;
if (!MatrixClientPeg.get().isGuest() && this.state.threepids !== undefined) {
- notification_area = (
+ notificationArea = (
Notifications
@@ -911,7 +915,7 @@ module.exports = React.createClass({
// we are using a version old version of olm. We assume the former.
let olmVersionString = "";
if (olmVersion !== undefined) {
- olmVersionString = `v${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`;
+ olmVersionString = `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}`;
}
return (
@@ -969,7 +973,7 @@ module.exports = React.createClass({
{this._renderReferral()}
- {notification_area}
+ {notificationArea}
{this._renderUserInterfaceSettings()}
{this._renderLabs()}
@@ -985,7 +989,10 @@ module.exports = React.createClass({
Logged in as {this._me}
- Access Token: <click to reveal>
+ Access Token: <click to reveal>
Homeserver is { MatrixClientPeg.get().getHomeserverUrl() }
@@ -995,11 +1002,11 @@ module.exports = React.createClass({
matrix-react-sdk version: {(REACT_SDK_VERSION !== '
')
- ? {REACT_SDK_VERSION}
+ ? gHVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION)
: REACT_SDK_VERSION
}
- riot-web version: {(this.state.vectorVersion !== null)
- ? {this.state.vectorVersion}
+ riot-web version: {(this.state.vectorVersion !== undefined)
+ ? gHVersionLabel('vector-im/riot-web', this.state.vectorVersion)
: 'unknown'
}
olm version: {olmVersionString}
@@ -1013,5 +1020,5 @@ module.exports = React.createClass({
);
- }
+ },
});
diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js
index 315a0ea242..a3635177e2 100644
--- a/src/components/structures/login/Login.js
+++ b/src/components/structures/login/Login.js
@@ -23,6 +23,9 @@ import url from 'url';
import sdk from '../../../index';
import Login from '../../../Login';
+// For validating phone numbers without country codes
+const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
+
/**
* A wire component which glues together login UI components and Login logic
*/
@@ -125,7 +128,16 @@ module.exports = React.createClass({
},
onPhoneNumberChanged: function(phoneNumber) {
- this.setState({ phoneNumber: phoneNumber });
+ // Validate the phone number entered
+ if (!PHONE_NUMBER_REGEX.test(phoneNumber)) {
+ this.setState({ errorText: 'The phone number entered looks invalid' });
+ return;
+ }
+
+ this.setState({
+ phoneNumber: phoneNumber,
+ errorText: null,
+ });
},
onServerConfigChange: function(config) {
diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js
index 4e0d61e716..5501a39b58 100644
--- a/src/components/structures/login/Registration.js
+++ b/src/components/structures/login/Registration.js
@@ -123,18 +123,17 @@ module.exports = React.createClass({
}
},
- onHsUrlChanged: function(newHsUrl) {
- this.setState({
- hsUrl: newHsUrl,
+ onServerConfigChange: function(config) {
+ let newState = {};
+ if (config.hsUrl !== undefined) {
+ newState.hsUrl = config.hsUrl;
+ }
+ if (config.isUrl !== undefined) {
+ newState.isUrl = config.isUrl;
+ }
+ this.setState(newState, function() {
+ this._replaceClient();
});
- this._replaceClient();
- },
-
- onIsUrlChanged: function(newIsUrl) {
- this.setState({
- isUrl: newIsUrl,
- });
- this._replaceClient();
},
_replaceClient: function() {
@@ -390,8 +389,7 @@ module.exports = React.createClass({
customIsUrl={this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
- onHsUrlChanged={this.onHsUrlChanged}
- onIsUrlChanged={this.onIsUrlChanged}
+ onServerConfigChange={this.onServerConfigChange}
delayTimeMs={1000}
/>
diff --git a/src/components/views/avatars/RoomAvatar.js b/src/components/views/avatars/RoomAvatar.js
index bfa7575b0c..8041fd5cd7 100644
--- a/src/components/views/avatars/RoomAvatar.js
+++ b/src/components/views/avatars/RoomAvatar.js
@@ -59,7 +59,9 @@ module.exports = React.createClass({
ContentRepo.getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(),
props.oobData.avatarUrl,
- props.width, props.height, props.resizeMethod
+ Math.floor(props.width * window.devicePixelRatio),
+ Math.floor(props.height * window.devicePixelRatio),
+ props.resizeMethod
), // highest priority
this.getRoomAvatarUrl(props),
this.getOneToOneAvatar(props),
@@ -74,7 +76,9 @@ module.exports = React.createClass({
return props.room.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
- props.width, props.height, props.resizeMethod,
+ Math.floor(props.width * window.devicePixelRatio),
+ Math.floor(props.height * window.devicePixelRatio),
+ props.resizeMethod,
false
);
},
@@ -103,14 +107,18 @@ module.exports = React.createClass({
}
return theOtherGuy.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
- props.width, props.height, props.resizeMethod,
+ Math.floor(props.width * window.devicePixelRatio),
+ Math.floor(props.height * window.devicePixelRatio),
+ props.resizeMethod,
false
);
} else if (userIds.length == 1) {
return mlist[userIds[0]].getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
- props.width, props.height, props.resizeMethod,
- false
+ Math.floor(props.width * window.devicePixelRatio),
+ Math.floor(props.height * window.devicePixelRatio),
+ props.resizeMethod,
+ false
);
} else {
return null;
diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js
index d0f34c5fbd..0b2ca5225d 100644
--- a/src/components/views/dialogs/BaseDialog.js
+++ b/src/components/views/dialogs/BaseDialog.js
@@ -47,16 +47,6 @@ export default React.createClass({
children: React.PropTypes.node,
},
- componentWillMount: function() {
- this.priorActiveElement = document.activeElement;
- },
-
- componentWillUnmount: function() {
- if (this.priorActiveElement !== null) {
- this.priorActiveElement.focus();
- }
- },
-
_onKeyDown: function(e) {
if (e.keyCode === KeyCode.ESCAPE) {
e.stopPropagation();
@@ -77,7 +67,7 @@ export default React.createClass({
render: function() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
-
+
return (
-
+
{this.props.button}
{this.props.extraButtons}
diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js
index da9c8e8f65..4f3d4301f9 100644
--- a/src/components/views/dialogs/UnknownDeviceDialog.js
+++ b/src/components/views/dialogs/UnknownDeviceDialog.js
@@ -149,7 +149,7 @@ export default React.createClass({
>
- This room contains devices that you haven't seen before.
+ "{this.props.room.name}" contains devices that you haven't seen before.
{ warning }
Unknown devices:
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/Dropdown.js b/src/components/views/elements/Dropdown.js
index a9ecf5b669..82f8d753a9 100644
--- a/src/components/views/elements/Dropdown.js
+++ b/src/components/views/elements/Dropdown.js
@@ -114,8 +114,11 @@ export default class Dropdown extends React.Component {
}
componentWillReceiveProps(nextProps) {
+ if (!nextProps.children || nextProps.children.length === 0) {
+ return;
+ }
this._reindexChildren(nextProps.children);
- const firstChild = React.Children.toArray(nextProps.children)[0];
+ const firstChild = nextProps.children[0];
this.setState({
highlightedOption: firstChild ? firstChild.key : null,
});
@@ -149,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) {
@@ -249,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 d7f876c16e..dcf1810468 100644
--- a/src/components/views/elements/MemberEventListSummary.js
+++ b/src/components/views/elements/MemberEventListSummary.js
@@ -221,6 +221,8 @@ module.exports = React.createClass({
"banned": beConjugated + " banned",
"unbanned": beConjugated + " unbanned",
"kicked": beConjugated + " kicked",
+ "changed_name": "changed name",
+ "changed_avatar": "changed avatar",
};
if (Object.keys(map).includes(t)) {
@@ -267,7 +269,7 @@ module.exports = React.createClass({
);
});
return (
-
+
{avatars}
);
@@ -289,7 +291,24 @@ module.exports = React.createClass({
switch (e.mxEvent.getContent().membership) {
case 'invite': return 'invited';
case 'ban': return 'banned';
- case 'join': return 'joined';
+ case 'join':
+ if (e.mxEvent.getPrevContent().membership === 'join') {
+ if (e.mxEvent.getContent().displayname !==
+ e.mxEvent.getPrevContent().displayname)
+ {
+ return 'changed_name';
+ }
+ else if (e.mxEvent.getContent().avatar_url !==
+ e.mxEvent.getPrevContent().avatar_url)
+ {
+ return 'changed_avatar';
+ }
+ // console.log("MELS ignoring duplicate membership join event");
+ return null;
+ }
+ else {
+ return 'joined';
+ }
case 'leave':
if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) {
switch (e.mxEvent.getPrevContent().membership) {
@@ -350,6 +369,7 @@ module.exports = React.createClass({
render: function() {
const eventsToRender = this.props.events;
+ const eventIds = eventsToRender.map(e => e.getId()).join(',');
const fewEvents = eventsToRender.length < this.props.threshold;
const expanded = this.state.expanded || fewEvents;
@@ -360,7 +380,7 @@ module.exports = React.createClass({
if (fewEvents) {
return (
-
+
{expandedEvents}
);
@@ -418,7 +438,7 @@ module.exports = React.createClass({
);
return (
-
+
{toggleButton}
{summaryContainer}
{expanded ?
: null}
diff --git a/src/components/views/login/CountryDropdown.js b/src/components/views/login/CountryDropdown.js
index 7f6b21650d..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;
}
@@ -37,10 +41,12 @@ export default class CountryDropdown extends React.Component {
constructor(props) {
super(props);
this._onSearchChange = this._onSearchChange.bind(this);
+ this._onOptionChange = this._onOptionChange.bind(this);
+ this._getShortOption = this._getShortOption.bind(this);
this.state = {
searchQuery: '',
- }
+ };
}
componentWillMount() {
@@ -48,7 +54,7 @@ export default class CountryDropdown extends React.Component {
// If no value is given, we start with the first
// country selected, but our parent component
// doesn't know this, therefore we do this.
- this.props.onOptionChange(COUNTRIES[0].iso2);
+ this.props.onOptionChange(COUNTRIES[0]);
}
}
@@ -58,14 +64,26 @@ export default class CountryDropdown extends React.Component {
});
}
+ _onOptionChange(iso2) {
+ this.props.onOptionChange(COUNTRIES_BY_ISO2[iso2]);
+ }
+
_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() {
@@ -94,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})
;
});
@@ -102,18 +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;
- 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 568461817c..46c9598751 100644
--- a/src/components/views/login/PasswordLogin.js
+++ b/src/components/views/login/PasswordLogin.js
@@ -90,8 +90,11 @@ class PasswordLogin extends React.Component {
}
onPhoneCountryChanged(country) {
- this.setState({phoneCountry: country});
- this.props.onPhoneCountryChanged(country);
+ this.setState({
+ phoneCountry: country.iso2,
+ phonePrefix: country.prefix,
+ });
+ this.props.onPhoneCountryChanged(country.iso2);
}
onPhoneNumberChanged(ev) {
@@ -121,16 +124,17 @@ class PasswordLogin extends React.Component {
const mxidInputClasses = classNames({
"mx_Login_field": true,
"mx_Login_username": true,
+ "mx_Login_field_has_prefix": true,
"mx_Login_field_has_suffix": Boolean(this.props.hsDomain),
});
let suffix = null;
if (this.props.hsDomain) {
- suffix =
+ suffix =
:{this.props.hsDomain}
;
}
- return
-
@
+ return
+
@
= dayAfterEventTime}
/>
);
}
@@ -401,8 +390,7 @@ module.exports = WithMatrixClient(React.createClass({
var msgtype = content.msgtype;
var eventType = this.props.mxEvent.getType();
- // Info messages are basically information about commands processed on a
- // room, or emote messages
+ // Info messages are basically information about commands processed on a room
var isInfoMessage = (eventType !== 'm.room.message');
var EventTileType = sdk.getComponent(eventTileTypes[eventType]);
@@ -430,7 +418,8 @@ module.exports = WithMatrixClient(React.createClass({
menu: this.state.menu,
mx_EventTile_verified: this.state.verified == true,
mx_EventTile_unverified: this.state.verified == false,
- mx_EventTile_bad: this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted',
+ mx_EventTile_bad: msgtype === 'm.bad.encrypted',
+ mx_EventTile_emote: msgtype === 'm.emote',
mx_EventTile_redacted: isRedacted,
});
@@ -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 =
;
}
// real padlocks
else if (this.props.mxEvent.isEncrypted() || (e2eEnabled && this.props.eventSendStatus)) {
if (this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted') {
- e2e =
;
+ e2e =
;
}
else if (this.state.verified == true || (e2eEnabled && this.props.eventSendStatus)) {
- e2e =
;
+ e2e =
;
}
else {
- e2e =
;
+ e2e =
;
}
}
else if (e2eEnabled) {
- e2e =
;
+ e2e =
;
}
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({
+
{ adminTools }
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 8a3b128908..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: '',
@@ -50,7 +52,7 @@ export default class MessageComposer extends React.Component {
inputState: {
style: [],
blockType: null,
- isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true),
+ isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false),
wordCount: 0,
},
showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false),
@@ -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 417d003226..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);
@@ -94,7 +93,7 @@ export default class MessageComposerInput extends React.Component {
this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this);
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
- const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true);
+ const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false);
this.state = {
// whether we're in rich text or markdown mode
@@ -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 (
-
+
);
}
diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js
index 230efbd1ea..0911d50c3e 100644
--- a/src/components/views/rooms/ReadReceiptMarker.js
+++ b/src/components/views/rooms/ReadReceiptMarker.js
@@ -24,6 +24,8 @@ var sdk = require('../../../index');
var Velociraptor = require('../../../Velociraptor');
require('../../../VelocityBounce');
+import DateUtils from '../../../DateUtils';
+
var bounce = false;
try {
if (global.localStorage) {
@@ -63,9 +65,6 @@ module.exports = React.createClass({
// Timestamp when the receipt was read
timestamp: React.PropTypes.number,
-
- // True to show the full date/time rather than just the time
- showFullTimestamp: React.PropTypes.bool,
},
getDefaultProps: function() {
@@ -170,16 +169,8 @@ module.exports = React.createClass({
let title;
if (this.props.timestamp) {
- const prefix = "Seen by " + this.props.member.userId + " at ";
- let ts = new Date(this.props.timestamp);
- if (this.props.showFullTimestamp) {
- // "15/12/2016, 7:05:45 PM (@alice:matrix.org)"
- title = prefix + ts.toLocaleString();
- }
- else {
- // "7:05:45 PM (@alice:matrix.org)"
- title = prefix + ts.toLocaleTimeString();
- }
+ title = "Seen by " + this.props.member.userId + " at " +
+ DateUtils.formatDate(new Date(this.props.timestamp));
}
return (
diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js
index 3d2386070c..1f61ddc403 100644
--- a/src/components/views/rooms/RoomHeader.js
+++ b/src/components/views/rooms/RoomHeader.js
@@ -17,6 +17,7 @@ limitations under the License.
'use strict';
var React = require('react');
+var classNames = require('classnames');
var sdk = require('../../../index');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var Modal = require("../../../Modal");
@@ -39,6 +40,7 @@ module.exports = React.createClass({
oobData: React.PropTypes.object,
editing: React.PropTypes.bool,
saving: React.PropTypes.bool,
+ inRoom: React.PropTypes.bool,
collapsedRhs: React.PropTypes.bool,
onSettingsClick: React.PropTypes.func,
onSaveClick: React.PropTypes.func,
@@ -49,7 +51,7 @@ module.exports = React.createClass({
getDefaultProps: function() {
return {
editing: false,
- onSettingsClick: function() {},
+ inRoom: false,
onSaveClick: function() {},
};
},
@@ -228,10 +230,10 @@ module.exports = React.createClass({
roomName = this.props.room.name;
}
-
+ const emojiTextClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
name =
- {roomName}
+ { roomName }
{ searchStatus }
;
}
@@ -302,6 +304,14 @@ module.exports = React.createClass({
;
}
+ let search_button;
+ if (this.props.onSearchClick && this.props.inRoom) {
+ search_button =
+
+
+ ;
+ }
+
var rightPanel_buttons;
if (this.props.collapsedRhs) {
rightPanel_buttons =
@@ -316,9 +326,7 @@ module.exports = React.createClass({
{ settings_button }
{ forget_button }
-
-
-
+ { search_button }
{ rightPanel_buttons }
;
}
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index 96ff65498f..760b0543c6 100644
--- a/src/components/views/rooms/RoomList.js
+++ b/src/components/views/rooms/RoomList.js
@@ -21,13 +21,13 @@ var GeminiScrollbar = require('react-gemini-scrollbar');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var CallHandler = require('../../../CallHandler');
var RoomListSorter = require("../../../RoomListSorter");
+var Unread = require('../../../Unread');
var dis = require("../../../dispatcher");
var sdk = require('../../../index');
var rate_limited_func = require('../../../ratelimitedfunc');
var Rooms = require('../../../Rooms');
import DMRoomMap from '../../../utils/DMRoomMap';
var Receipt = require('../../../utils/Receipt');
-var constantTimeDispatcher = require('../../../ConstantTimeDispatcher');
var HIDE_CONFERENCE_CHANS = true;
@@ -37,19 +37,10 @@ module.exports = React.createClass({
propTypes: {
ConferenceHandler: React.PropTypes.any,
collapsed: React.PropTypes.bool.isRequired,
- selectedRoom: React.PropTypes.string,
+ currentRoom: React.PropTypes.string,
searchFilter: React.PropTypes.string,
},
- shouldComponentUpdate: function(nextProps, nextState) {
- if (nextProps.collapsed !== this.props.collapsed) return true;
- if (nextProps.searchFilter !== this.props.searchFilter) return true;
- if (nextState.lists !== this.state.lists ||
- nextState.isLoadingLeftRooms !== this.state.isLoadingLeftRooms ||
- nextState.incomingCall !== this.state.incomingCall) return true;
- return false;
- },
-
getInitialState: function() {
return {
isLoadingLeftRooms: false,
@@ -59,6 +50,8 @@ module.exports = React.createClass({
},
componentWillMount: function() {
+ this.mounted = false;
+
var cli = MatrixClientPeg.get();
cli.on("Room", this.onRoom);
cli.on("deleteRoom", this.onDeleteRoom);
@@ -66,46 +59,23 @@ module.exports = React.createClass({
cli.on("Room.name", this.onRoomName);
cli.on("Room.tags", this.onRoomTags);
cli.on("Room.receipt", this.onRoomReceipt);
- cli.on("RoomState.members", this.onRoomStateMember);
+ cli.on("RoomState.events", this.onRoomStateEvents);
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("accountData", this.onAccountData);
- // lookup for which lists a given roomId is currently in.
- this.listsForRoomId = {};
-
var s = this.getRoomLists();
this.setState(s);
-
- // order of the sublists
- //this.listOrder = [];
-
- // loop count to stop a stack overflow if the user keeps waggling the
- // mouse for >30s in a row, or if running under mocha
- this._delayedRefreshRoomListLoopCount = 0
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
// Initialise the stickyHeaders when the component is created
this._updateStickyHeaders(true);
+
+ this.mounted = true;
},
- componentWillReceiveProps: function(nextProps) {
- // short-circuit react when the room changes
- // to avoid rerendering all the sublists everywhere
- if (nextProps.selectedRoom !== this.props.selectedRoom) {
- if (this.props.selectedRoom) {
- constantTimeDispatcher.dispatch(
- "RoomTile.select", this.props.selectedRoom, {}
- );
- }
- constantTimeDispatcher.dispatch(
- "RoomTile.select", nextProps.selectedRoom, { selected: true }
- );
- }
- },
-
- componentDidUpdate: function(prevProps, prevState) {
+ componentDidUpdate: function() {
// Reinitialise the stickyHeaders when the component is updated
this._updateStickyHeaders(true);
this._repositionIncomingCallBox(undefined, false);
@@ -131,29 +101,17 @@ module.exports = React.createClass({
}
break;
case 'on_room_read':
- // poke the right RoomTile to refresh, using the constantTimeDispatcher
- // to avoid each and every RoomTile registering to the 'on_room_read' event
- // XXX: if we like the constantTimeDispatcher we might want to dispatch
- // directly from TimelinePanel rather than needlessly bouncing via here.
- constantTimeDispatcher.dispatch(
- "RoomTile.refresh", payload.room.roomId, {}
- );
-
- // also have to poke the right list(s)
- var lists = this.listsForRoomId[payload.room.roomId];
- if (lists) {
- lists.forEach(list=>{
- constantTimeDispatcher.dispatch(
- "RoomSubList.refreshHeader", list, { room: payload.room }
- );
- });
- }
-
+ // Force an update because the notif count state is too deep to cause
+ // an update. This forces the local echo of reading notifs to be
+ // reflected by the RoomTiles.
+ this.forceUpdate();
break;
}
},
componentWillUnmount: function() {
+ this.mounted = false;
+
dis.unregister(this.dispatcherRef);
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room", this.onRoom);
@@ -162,7 +120,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags);
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
- MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
+ MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
}
@@ -171,14 +129,10 @@ module.exports = React.createClass({
},
onRoom: function(room) {
- // XXX: this happens rarely; ideally we should only update the correct
- // sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
this._delayedRefreshRoomList();
},
onDeleteRoom: function(roomId) {
- // XXX: this happens rarely; ideally we should only update the correct
- // sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
this._delayedRefreshRoomList();
},
@@ -201,10 +155,6 @@ module.exports = React.createClass({
}
},
- _onMouseOver: function(ev) {
- this._lastMouseOverTs = Date.now();
- },
-
onSubListHeaderClick: function(isHidden, scrollToPosition) {
// The scroll area has expanded or contracted, so re-calculate sticky headers positions
this._updateStickyHeaders(true, scrollToPosition);
@@ -214,98 +164,41 @@ module.exports = React.createClass({
if (toStartOfTimeline) return;
if (!room) return;
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
-
- // rather than regenerate our full roomlists, which is very heavy, we poke the
- // correct sublists to just re-sort themselves. This isn't enormously reacty,
- // but is much faster than the default react reconciler, or having to do voodoo
- // with shouldComponentUpdate and a pleaseRefresh property or similar.
- var lists = this.listsForRoomId[room.roomId];
- if (lists) {
- lists.forEach(list=>{
- constantTimeDispatcher.dispatch("RoomSubList.sort", list, { room: room });
- });
- }
-
- // we have to explicitly hit the roomtile which just changed
- constantTimeDispatcher.dispatch(
- "RoomTile.refresh", room.roomId, {}
- );
+ this._delayedRefreshRoomList();
},
onRoomReceipt: function(receiptEvent, room) {
// because if we read a notification, it will affect notification count
// only bother updating if there's a receipt from us
if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) {
- var lists = this.listsForRoomId[room.roomId];
- if (lists) {
- lists.forEach(list=>{
- constantTimeDispatcher.dispatch(
- "RoomSubList.refreshHeader", list, { room: room }
- );
- });
- }
-
- // we have to explicitly hit the roomtile which just changed
- constantTimeDispatcher.dispatch(
- "RoomTile.refresh", room.roomId, {}
- );
+ this._delayedRefreshRoomList();
}
},
onRoomName: function(room) {
- constantTimeDispatcher.dispatch(
- "RoomTile.refresh", room.roomId, {}
- );
- },
-
- onRoomTags: function(event, room) {
- // XXX: this happens rarely; ideally we should only update the correct
- // sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
this._delayedRefreshRoomList();
},
- onRoomStateMember: function(ev, state, member) {
- if (ev.getStateKey() === MatrixClientPeg.get().credentials.userId &&
- ev.getPrevContent() && ev.getPrevContent().membership === "invite")
- {
- this._delayedRefreshRoomList();
- }
- else {
- constantTimeDispatcher.dispatch(
- "RoomTile.refresh", member.roomId, {}
- );
- }
+ onRoomTags: function(event, room) {
+ this._delayedRefreshRoomList();
+ },
+
+ onRoomStateEvents: function(ev, state) {
+ this._delayedRefreshRoomList();
},
onRoomMemberName: function(ev, member) {
- constantTimeDispatcher.dispatch(
- "RoomTile.refresh", member.roomId, {}
- );
+ this._delayedRefreshRoomList();
},
onAccountData: function(ev) {
if (ev.getType() == 'm.direct') {
- // XXX: this happens rarely; ideally we should only update the correct
- // sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
this._delayedRefreshRoomList();
}
- else if (ev.getType() == 'm.push_rules') {
- this._delayedRefreshRoomList();
- }
},
_delayedRefreshRoomList: new rate_limited_func(function() {
- // if the mouse has been moving over the RoomList in the last 500ms
- // then delay the refresh further to avoid bouncing around under the
- // cursor
- if (Date.now() - this._lastMouseOverTs > 500 || this._delayedRefreshRoomListLoopCount > 60) {
- this.refreshRoomList();
- this._delayedRefreshRoomListLoopCount = 0;
- }
- else {
- this._delayedRefreshRoomListLoopCount++;
- this._delayedRefreshRoomList();
- }
+ this.refreshRoomList();
}, 500),
refreshRoomList: function() {
@@ -313,12 +206,14 @@ module.exports = React.createClass({
// (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs))
// );
- // TODO: ideally we'd calculate this once at start, and then maintain
- // any changes to it incrementally, updating the appropriate sublists
- // as needed.
- // Alternatively we'd do something magical with Immutable.js or similar.
+ // TODO: rather than bluntly regenerating and re-sorting everything
+ // every time we see any kind of room change from the JS SDK
+ // we could do incremental updates on our copy of the state
+ // based on the room which has actually changed. This would stop
+ // us re-rendering all the sublists every time anything changes anywhere
+ // in the state of the client.
this.setState(this.getRoomLists());
-
+
// this._lastRefreshRoomListTs = Date.now();
},
@@ -333,26 +228,18 @@ module.exports = React.createClass({
s.lists["m.lowpriority"] = [];
s.lists["im.vector.fake.archived"] = [];
- this.listsForRoomId = {};
- var otherTagNames = {};
-
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
MatrixClientPeg.get().getRooms().forEach(function(room) {
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (!me) return;
-
+
// console.log("room = " + room.name + ", me.membership = " + me.membership +
// ", sender = " + me.events.member.getSender() +
// ", target = " + me.events.member.getStateKey() +
// ", prevMembership = " + me.events.member.getPrevContent().membership);
- if (!self.listsForRoomId[room.roomId]) {
- self.listsForRoomId[room.roomId] = [];
- }
-
if (me.membership == "invite") {
- self.listsForRoomId[room.roomId].push("im.vector.fake.invite");
s.lists["im.vector.fake.invite"].push(room);
}
else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) {
@@ -363,27 +250,23 @@ module.exports = React.createClass({
{
// Used to split rooms via tags
var tagNames = Object.keys(room.tags);
+
if (tagNames.length) {
for (var i = 0; i < tagNames.length; i++) {
var tagName = tagNames[i];
s.lists[tagName] = s.lists[tagName] || [];
- s.lists[tagName].push(room);
- self.listsForRoomId[room.roomId].push(tagName);
- otherTagNames[tagName] = 1;
+ s.lists[tagNames[i]].push(room);
}
}
else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
// "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
- self.listsForRoomId[room.roomId].push("im.vector.fake.direct");
s.lists["im.vector.fake.direct"].push(room);
}
else {
- self.listsForRoomId[room.roomId].push("im.vector.fake.recent");
s.lists["im.vector.fake.recent"].push(room);
}
}
else if (me.membership === "leave") {
- self.listsForRoomId[room.roomId].push("im.vector.fake.archived");
s.lists["im.vector.fake.archived"].push(room);
}
else {
@@ -404,10 +287,8 @@ module.exports = React.createClass({
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (me && Rooms.looksLikeDirectMessageRoom(room, me)) {
- self.listsForRoomId[room.roomId].push("im.vector.fake.direct");
s.lists["im.vector.fake.direct"].push(room);
} else {
- self.listsForRoomId[room.roomId].push("im.vector.fake.recent");
s.lists["im.vector.fake.recent"].push(room);
}
}
@@ -424,8 +305,6 @@ module.exports = React.createClass({
newMDirectEvent[otherPerson.userId] = roomList;
}
- console.warn("Resetting room DM state to be " + JSON.stringify(newMDirectEvent));
-
// if this fails, fine, we'll just do the same thing next time we get the room lists
MatrixClientPeg.get().setAccountData('m.direct', newMDirectEvent).done();
}
@@ -434,32 +313,19 @@ module.exports = React.createClass({
// we actually apply the sorting to this when receiving the prop in RoomSubLists.
- // we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down
-/*
- this.listOrder = [
- "im.vector.fake.invite",
- "m.favourite",
- "im.vector.fake.recent",
- "im.vector.fake.direct",
- Object.keys(otherTagNames).filter(tagName=>{
- return (!tagName.match(/^m\.(favourite|lowpriority)$/));
- }).sort(),
- "m.lowpriority",
- "im.vector.fake.archived"
- ];
-*/
-
return s;
},
_getScrollNode: function() {
+ if (!this.mounted) return null;
var panel = ReactDOM.findDOMNode(this);
if (!panel) return null;
- // empirically, if we have gm-prevented for some reason, the scroll node
- // is still the 3rd child (i.e. the view child). This looks to be due
- // to vdh's improved resize updater logic...?
- return panel.children[2]; // XXX: Fragile!
+ if (panel.classList.contains('gm-prevented')) {
+ return panel;
+ } else {
+ return panel.children[2]; // XXX: Fragile!
+ }
},
_whenScrolling: function(e) {
@@ -479,6 +345,7 @@ module.exports = React.createClass({
var incomingCallBox = document.getElementById("incomingCallBox");
if (incomingCallBox && incomingCallBox.parentElement) {
var scrollArea = this._getScrollNode();
+ if (!scrollArea) return;
// Use the offset of the top of the scroll area from the window
// as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
@@ -502,10 +369,11 @@ module.exports = React.createClass({
// properly through React
_initAndPositionStickyHeaders: function(initialise, scrollToPosition) {
var scrollArea = this._getScrollNode();
+ if (!scrollArea) return;
// Use the offset of the top of the scroll area from the window
// as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
- // Use the offset of the top of the component from the window
+ // Use the offset of the top of the componet from the window
// as this is used to calculate the CSS fixed top position for the stickies
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
@@ -605,16 +473,15 @@ module.exports = React.createClass({
return (
-
+ autoshow={true} onScroll={ self._whenScrolling } ref="gemscroll">
+
@@ -625,9 +492,9 @@ module.exports = React.createClass({
verb="favourite"
editable={ true }
order="manual"
+ selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed }
- selectedRoom={ self.props.selectedRoom }
searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />
@@ -638,9 +505,9 @@ module.exports = React.createClass({
verb="tag direct chat"
editable={ true }
order="recent"
+ selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed }
- selectedRoom={ self.props.selectedRoom }
alwaysShowHeader={ true }
searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick }
@@ -648,18 +515,17 @@ module.exports = React.createClass({
- { Object.keys(self.state.lists).sort().map(function(tagName) {
+ { Object.keys(self.state.lists).map(function(tagName) {
if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
return ;
@@ -684,20 +550,19 @@ module.exports = React.createClass({
verb="demote"
editable={ true }
order="recent"
+ selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed }
- selectedRoom={ self.props.selectedRoom }
searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />
-
To redact messages, you must be a
+
To redact other users' messages, you must be a
diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js
index 3b37d4608f..c123f31d19 100644
--- a/src/components/views/rooms/RoomTile.js
+++ b/src/components/views/rooms/RoomTile.js
@@ -27,8 +27,6 @@ var RoomNotifs = require('../../../RoomNotifs');
var FormattingUtils = require('../../../utils/FormattingUtils');
import AccessibleButton from '../elements/AccessibleButton';
var UserSettingsStore = require('../../../UserSettingsStore');
-var constantTimeDispatcher = require('../../../ConstantTimeDispatcher');
-var Unread = require('../../../Unread');
module.exports = React.createClass({
displayName: 'RoomTile',
@@ -38,10 +36,12 @@ module.exports = React.createClass({
connectDropTarget: React.PropTypes.func,
onClick: React.PropTypes.func,
isDragging: React.PropTypes.bool,
- selectedRoom: React.PropTypes.string,
room: React.PropTypes.object.isRequired,
collapsed: React.PropTypes.bool.isRequired,
+ selected: React.PropTypes.bool.isRequired,
+ unread: React.PropTypes.bool.isRequired,
+ highlight: React.PropTypes.bool.isRequired,
isInvite: React.PropTypes.bool.isRequired,
incomingCall: React.PropTypes.object,
},
@@ -54,11 +54,10 @@ module.exports = React.createClass({
getInitialState: function() {
return({
- hover: false,
- badgeHover: false,
+ hover : false,
+ badgeHover : false,
menuDisplayed: false,
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
- selected: this.props.room ? (this.props.selectedRoom === this.props.room.roomId) : false,
});
},
@@ -80,32 +79,23 @@ module.exports = React.createClass({
}
},
+ onAccountData: function(accountDataEvent) {
+ if (accountDataEvent.getType() == 'm.push_rules') {
+ this.setState({
+ notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
+ });
+ }
+ },
+
componentWillMount: function() {
- constantTimeDispatcher.register("RoomTile.refresh", this.props.room.roomId, this.onRefresh);
- constantTimeDispatcher.register("RoomTile.select", this.props.room.roomId, this.onSelect);
- this.onRefresh();
+ MatrixClientPeg.get().on("accountData", this.onAccountData);
},
componentWillUnmount: function() {
- constantTimeDispatcher.unregister("RoomTile.refresh", this.props.room.roomId, this.onRefresh);
- constantTimeDispatcher.unregister("RoomTile.select", this.props.room.roomId, this.onSelect);
- },
-
- componentWillReceiveProps: function(nextProps) {
- this.onRefresh();
- },
-
- onRefresh: function(params) {
- this.setState({
- unread: Unread.doesRoomHaveUnreadMessages(this.props.room),
- highlight: this.props.room.getUnreadNotificationCount('highlight') > 0 || this.props.isInvite,
- });
- },
-
- onSelect: function(params) {
- this.setState({
- selected: params.selected,
- });
+ var cli = MatrixClientPeg.get();
+ if (cli) {
+ MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
+ }
},
onClick: function(ev) {
@@ -179,13 +169,13 @@ module.exports = React.createClass({
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge();
- const mentionBadges = this.state.highlight && this._shouldShowMentionBadge();
+ const mentionBadges = this.props.highlight && this._shouldShowMentionBadge();
const badges = notifBadges || mentionBadges;
var classes = classNames({
'mx_RoomTile': true,
- 'mx_RoomTile_selected': this.state.selected,
- 'mx_RoomTile_unread': this.state.unread,
+ 'mx_RoomTile_selected': this.props.selected,
+ 'mx_RoomTile_unread': this.props.unread,
'mx_RoomTile_unreadNotify': notifBadges,
'mx_RoomTile_highlight': mentionBadges,
'mx_RoomTile_invited': (me && me.membership == 'invite'),
@@ -231,7 +221,7 @@ module.exports = React.createClass({
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed,
});
- if (this.state.selected) {
+ if (this.props.selected) {
let nameSelected =
{name} ;
label =
{ nameSelected }
;
@@ -265,8 +255,7 @@ module.exports = React.createClass({
let ret = (
{ /* Only native elements can be wrapped in a DnD object. */}
-
+
diff --git a/src/components/views/rooms/SearchResultTile.js b/src/components/views/rooms/SearchResultTile.js
index 7fac244481..1aba7c9196 100644
--- a/src/components/views/rooms/SearchResultTile.js
+++ b/src/components/views/rooms/SearchResultTile.js
@@ -60,7 +60,7 @@ module.exports = React.createClass({
}
}
return (
-
+
{ret}
);
},
diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.js
index 40995d2a72..a6f342af86 100644
--- a/src/components/views/rooms/SimpleRoomHeader.js
+++ b/src/components/views/rooms/SimpleRoomHeader.js
@@ -19,6 +19,7 @@ limitations under the License.
import React from 'react';
import dis from '../../../dispatcher';
import AccessibleButton from '../elements/AccessibleButton';
+import sdk from '../../../index';
// cancel button which is shared between room header and simple room header
export function CancelButton(props) {
@@ -45,6 +46,9 @@ export default React.createClass({
// is the RightPanel collapsed?
collapsedRhs: React.PropTypes.bool,
+
+ // `src` to a TintableSvg. Optional.
+ icon: React.PropTypes.string,
},
onShowRhsClick: function(ev) {
@@ -53,9 +57,17 @@ export default React.createClass({
render: function() {
let cancelButton;
+ let icon;
if (this.props.onCancelClick) {
cancelButton =
;
}
+ if (this.props.icon) {
+ const TintableSvg = sdk.getComponent('elements.TintableSvg');
+ icon =
;
+ }
let showRhsButton;
/* // don't bother cluttering things up with this for now.
@@ -73,6 +85,7 @@ export default React.createClass({
+ { icon }
{ this.props.title }
{ showRhsButton }
{ cancelButton }
diff --git a/src/components/views/rooms/TopUnreadMessagesBar.js b/src/components/views/rooms/TopUnreadMessagesBar.js
index 72b489a406..74c689c395 100644
--- a/src/components/views/rooms/TopUnreadMessagesBar.js
+++ b/src/components/views/rooms/TopUnreadMessagesBar.js
@@ -38,7 +38,7 @@ module.exports = React.createClass({
title="Scroll to unread messages"/>
Jump to first unread message.
-
diff --git a/src/components/views/settings/AddPhoneNumber.js b/src/components/views/settings/AddPhoneNumber.js
index 3a348393aa..6df4d8e8cc 100644
--- a/src/components/views/settings/AddPhoneNumber.js
+++ b/src/components/views/settings/AddPhoneNumber.js
@@ -50,7 +50,7 @@ export default WithMatrixClient(React.createClass({
},
_onPhoneCountryChange: function(phoneCountry) {
- this.setState({ phoneCountry: phoneCountry });
+ this.setState({ phoneCountry: phoneCountry.iso2 });
},
_onPhoneNumberChange: function(ev) {
@@ -147,12 +147,14 @@ export default WithMatrixClient(React.createClass({
return (