From 1d5d44d63d2c07ac51f9886eb426c8ee8f9a22ab Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 12 Jan 2017 11:45:47 +0000 Subject: [PATCH 01/57] TextEncoder polyfill Apparently Safari doesn't sport a TextEncoder, so here's a polyfill for it. --- src/utils/TextDecoderPolyfill.js | 131 +++++++++++++++++++++++++ src/utils/TextEncoderPolyfill.js | 78 +++++++++++++++ test/utils/TextDecoderPolyfill-test.js | 85 ++++++++++++++++ test/utils/TextEncoderPolyfill-test.js | 39 ++++++++ 4 files changed, 333 insertions(+) create mode 100644 src/utils/TextDecoderPolyfill.js create mode 100644 src/utils/TextEncoderPolyfill.js create mode 100644 test/utils/TextDecoderPolyfill-test.js create mode 100644 test/utils/TextEncoderPolyfill-test.js diff --git a/src/utils/TextDecoderPolyfill.js b/src/utils/TextDecoderPolyfill.js new file mode 100644 index 0000000000..e203676bb7 --- /dev/null +++ b/src/utils/TextDecoderPolyfill.js @@ -0,0 +1,131 @@ +/* +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. +*/ + +"use strict"; + +// Polyfill for TextDecoder. + +const REPLACEMENT_CHAR = '\uFFFD'; + +export default class TextDecoder { + /** + * Decode a UTF-8 byte array as a javascript string + * + * @param {Uint8Array} u8Array UTF-8-encoded onput + * @return {str} + */ + decode(u8Array) { + let u0, u1, u2, u3; + + let str = ''; + let idx = 0; + while (idx < u8Array.length) { + u0 = u8Array[idx++]; + if (!(u0 & 0x80)) { + str += String.fromCharCode(u0); + continue; + } + + if ((u0 & 0xC0) != 0xC0) { + // continuation byte where we expect a leading byte + str += REPLACEMENT_CHAR; + continue; + } + + if (u0 > 0xF4) { + // this would imply a 5-byte or longer encoding, which is + // invalid and unsupported here. + str += REPLACEMENT_CHAR; + continue; + } + + u1 = u8Array[idx++]; + if (u1 === undefined) { + str += REPLACEMENT_CHAR; + continue; + } + + if ((u1 & 0xC0) != 0x80) { + // leading byte where we expect a continuation byte + str += REPLACEMENT_CHAR.repeat(2); + continue; + } + u1 &= 0x3F; + if (!(u0 & 0x20)) { + const u = ((u0 & 0x1F) << 6) | u1; + if (u < 0x80) { + // over-long + str += REPLACEMENT_CHAR.repeat(2); + } else { + str += String.fromCharCode(u); + } + continue; + } + + u2 = u8Array[idx++]; + if (u2 === undefined) { + str += REPLACEMENT_CHAR.repeat(2); + continue; + } + if ((u2 & 0xC0) != 0x80) { + // leading byte where we expect a continuation byte + str += REPLACEMENT_CHAR.repeat(3); + continue; + } + u2 &= 0x3F; + if (!(u0 & 0x10)) { + const u = ((u0 & 0x0F) << 12) | (u1 << 6) | u2; + if (u < 0x800) { + // over-long + str += REPLACEMENT_CHAR.repeat(3); + } else if (u == 0xFEFF && idx == 3) { + // byte-order mark: do not add to output + } else { + str += String.fromCharCode(u); + } + continue; + } + + u3 = u8Array[idx++]; + if (u3 === undefined) { + str += REPLACEMENT_CHAR.repeat(3); + continue; + } + if ((u3 & 0xC0) != 0x80) { + // leading byte where we expect a continuation byte + str += REPLACEMENT_CHAR.repeat(4); + continue; + } + u3 &= 0x3F; + const u = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | u3; + if (u < 0x10000) { + // over-long + str += REPLACEMENT_CHAR.repeat(4); + continue; + } + if (u > 0x1FFFF) { + // unicode stops here. + str += REPLACEMENT_CHAR.repeat(4); + continue; + } + + // encode as utf-16 + const v = u - 0x10000; + str += String.fromCharCode(0xD800 | (v >> 10), 0xDC00 | (v & 0x3FF)); + } + return str; + } +} diff --git a/src/utils/TextEncoderPolyfill.js b/src/utils/TextEncoderPolyfill.js new file mode 100644 index 0000000000..2da09710f2 --- /dev/null +++ b/src/utils/TextEncoderPolyfill.js @@ -0,0 +1,78 @@ +/* +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. +*/ + +"use strict"; + +// Polyfill for TextEncoder. Based on emscripten's stringToUTF8Array. + +function utf8len(str) { + var len = 0; + for (var i = 0; i < str.length; ++i) { + var u = str.charCodeAt(i); + if (u >= 0xD800 && u <= 0xDFFF && i < str.length-1) { + // lead surrogate - combine with next surrogate + u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF); + } + + if (u <= 0x7F) { + ++len; + } else if (u <= 0x7FF) { + len += 2; + } else if (u <= 0xFFFF) { + len += 3; + } else { + len += 4; + } + } + return len; +} + +export default class TextEncoder { + /** + * Encode a javascript string as utf-8 + * + * @param {String} str String to encode + * @return {Uint8Array} UTF-8-encoded output + */ + encode(str) { + const outU8Array = new Uint8Array(utf8len(str)); + var outIdx = 0; + for (var i = 0; i < str.length; ++i) { + var u = str.charCodeAt(i); + if (u >= 0xD800 && u <= 0xDFFF && i < str.length-1) { + // lead surrogate - combine with next surrogate + u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF); + } + + if (u <= 0x7F) { + outU8Array[outIdx++] = u; + } else if (u <= 0x7FF) { + outU8Array[outIdx++] = 0xC0 | (u >> 6); + outU8Array[outIdx++] = 0x80 | (u & 63); + } else if (u <= 0xFFFF) { + outU8Array[outIdx++] = 0xE0 | (u >> 12); + outU8Array[outIdx++] = 0x80 | ((u >> 6) & 63); + outU8Array[outIdx++] = 0x80 | (u & 63); + } else { + outU8Array[outIdx++] = 0xF0 | (u >> 18); + outU8Array[outIdx++] = 0x80 | ((u >> 12) & 63); + outU8Array[outIdx++] = 0x80 | ((u >> 6) & 63); + outU8Array[outIdx++] = 0x80 | (u & 63); + } + } + return outU8Array; + } +} diff --git a/test/utils/TextDecoderPolyfill-test.js b/test/utils/TextDecoderPolyfill-test.js new file mode 100644 index 0000000000..84f5edf187 --- /dev/null +++ b/test/utils/TextDecoderPolyfill-test.js @@ -0,0 +1,85 @@ +/* +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. +*/ + +"use strict"; + +import TextDecoderPolyfill from 'utils/TextDecoderPolyfill'; + +import * as testUtils from '../test-utils'; +import expect from 'expect'; + +describe('textDecoderPolyfill', function() { + beforeEach(function() { + testUtils.beforeEach(this); + }); + + it('should correctly decode a range of strings', function() { + const decoder = new TextDecoderPolyfill(); + + expect(decoder.decode(Uint8Array.of(65, 66, 67))).toEqual('ABC'); + expect(decoder.decode(Uint8Array.of(0xC3, 0xA6))).toEqual('æ'); + expect(decoder.decode(Uint8Array.of(0xE2, 0x82, 0xAC))).toEqual('€'); + expect(decoder.decode(Uint8Array.of(0xF0, 0x9F, 0x92, 0xA9))).toEqual('\uD83D\uDCA9'); + }); + + it('should ignore byte-order marks', function() { + const decoder = new TextDecoderPolyfill(); + expect(decoder.decode(Uint8Array.of(0xEF, 0xBB, 0xBF, 65))) + .toEqual('A'); + }); + + it('should not ignore byte-order marks in the middle of the array', function() { + const decoder = new TextDecoderPolyfill(); + expect(decoder.decode(Uint8Array.of(65, 0xEF, 0xBB, 0xBF, 66))) + .toEqual('A\uFEFFB'); + }); + + it('should reject overlong encodings', function() { + const decoder = new TextDecoderPolyfill(); + + // euro, as 4 bytes + expect(decoder.decode(Uint8Array.of(65, 0xF0, 0x82, 0x82, 0xAC, 67))) + .toEqual('A\uFFFD\uFFFD\uFFFD\uFFFDC'); + }); + + it('should reject 5 and 6-byte encodings', function() { + const decoder = new TextDecoderPolyfill(); + + expect(decoder.decode(Uint8Array.of(65, 0xF8, 0x82, 0x82, 0x82, 0x82, 67))) + .toEqual('A\uFFFD\uFFFD\uFFFD\uFFFD\uFFFDC'); + }); + + it('should reject code points beyond 0x10000', function() { + const decoder = new TextDecoderPolyfill(); + + expect(decoder.decode(Uint8Array.of(0xF4, 0xA0, 0x80, 0x80))) + .toEqual('\uFFFD\uFFFD\uFFFD\uFFFD'); + }); + + it('should cope with end-of-string', function() { + const decoder = new TextDecoderPolyfill(); + + expect(decoder.decode(Uint8Array.of(65, 0xC3))) + .toEqual('A\uFFFD'); + + expect(decoder.decode(Uint8Array.of(65, 0xE2, 0x82))) + .toEqual('A\uFFFD\uFFFD'); + + expect(decoder.decode(Uint8Array.of(65, 0xF0, 0x9F, 0x92))) + .toEqual('A\uFFFD\uFFFD\uFFFD'); + }); + +}); diff --git a/test/utils/TextEncoderPolyfill-test.js b/test/utils/TextEncoderPolyfill-test.js new file mode 100644 index 0000000000..4f422ec375 --- /dev/null +++ b/test/utils/TextEncoderPolyfill-test.js @@ -0,0 +1,39 @@ +/* +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. +*/ + +"use strict"; + +import TextEncoderPolyfill from 'utils/TextEncoderPolyfill'; + +import * as testUtils from '../test-utils'; +import expect from 'expect'; + +describe('textEncoderPolyfill', function() { + beforeEach(function() { + testUtils.beforeEach(this); + }); + + it('should correctly encode a range of strings', function() { + const encoder = new TextEncoderPolyfill(); + + expect(encoder.encode('ABC')).toEqual(Uint8Array.of(65, 66, 67)); + expect(encoder.encode('æ')).toEqual(Uint8Array.of(0xC3, 0xA6)); + expect(encoder.encode('€')).toEqual(Uint8Array.of(0xE2, 0x82, 0xAC)); + + // PILE OF POO (💩) + expect(encoder.encode('\uD83D\uDCA9')).toEqual(Uint8Array.of(0xF0, 0x9F, 0x92, 0xA9)); + }); +}); From f8e56778ea5b0577d0d594f4416305b79ee6d281 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 11 Jan 2017 22:22:11 +0000 Subject: [PATCH 02/57] Encryption and decryption for megolm backups --- src/utils/MegolmExportEncryption.js | 312 +++++++++++++++++++++ test/utils/MegolmExportEncryption-test.js | 115 ++++++++ test/utils/generate-megolm-test-vectors.py | 117 ++++++++ 3 files changed, 544 insertions(+) create mode 100644 src/utils/MegolmExportEncryption.js create mode 100644 test/utils/MegolmExportEncryption-test.js create mode 100755 test/utils/generate-megolm-test-vectors.py diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js new file mode 100644 index 0000000000..5b2e16ef29 --- /dev/null +++ b/src/utils/MegolmExportEncryption.js @@ -0,0 +1,312 @@ +/* +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. +*/ + +"use strict"; + +// polyfill textencoder if necessary +let TextEncoder = window.TextEncoder; +if (!TextEncoder) { + TextEncoder = require('./TextEncoderPolyfill'); +} +let TextDecoder = window.TextDecoder; +if (TextDecoder) { + TextDecoder = require('./TextDecoderPolyfill'); +} + +const subtleCrypto = window.crypto.subtle || window.crypto.webkitSubtle; + +/** + * Decrypt a megolm key file + * + * @param {ArrayBuffer} file + * @param {String} password + * @return {Promise} promise for decrypted output + */ +export function decryptMegolmKeyFile(data, password) { + const body = unpackMegolmKeyFile(data); + + // check we have a version byte + if (body.length < 1) { + throw new Error('Invalid file: too short'); + } + + const version = body[0]; + if (version !== 1) { + throw new Error('Unsupported version'); + } + + const ciphertextLength = body.length-(1+16+16+4+32); + if (body.length < 0) { + throw new Error('Invalid file: too short'); + } + + const salt = body.subarray(1, 1+16); + const iv = body.subarray(17, 17+16); + const iterations = body[33] << 24 | body[34] << 16 | body[35] << 8 | body[36]; + const ciphertext = body.subarray(37, 37+ciphertextLength); + const hmac = body.subarray(-32); + + return deriveKeys(salt, iterations, password).then((keys) => { + const [aes_key, sha_key] = keys; + + const toVerify = body.subarray(0, -32); + return subtleCrypto.verify( + {name: 'HMAC'}, + sha_key, + hmac, + toVerify, + ).then((isValid) => { + if (!isValid) { + throw new Error('Authentication check failed: incorrect password?') + } + + return subtleCrypto.decrypt( + { + name: "AES-CTR", + counter: iv, + length: 64, + }, + aes_key, + ciphertext, + ); + }); + }).then((plaintext) => { + return new TextDecoder().decode(new Uint8Array(plaintext)); + }); +} + + +/** + * Encrypt a megolm key file + * + * @param {String} data + * @param {String} password + * @param {Object=} options + * @param {Nunber=} options.kdf_rounds Number of iterations to perform of the + * key-derivation function. + * @return {Promise} promise for encrypted output + */ +export function encryptMegolmKeyFile(data, password, options) { + options = options || {}; + const kdf_rounds = options.kdf_rounds || 100000; + + const salt = new Uint8Array(16); + window.crypto.getRandomValues(salt); + const iv = new Uint8Array(16); + window.crypto.getRandomValues(iv); + + return deriveKeys(salt, kdf_rounds, password).then((keys) => { + const [aes_key, sha_key] = keys; + + return subtleCrypto.encrypt( + { + name: "AES-CTR", + counter: iv, + length: 64, + }, + aes_key, + new TextEncoder().encode(data), + ).then((ciphertext) => { + const cipherArray = new Uint8Array(ciphertext); + const bodyLength = (1+salt.length+iv.length+4+cipherArray.length+32); + const resultBuffer = new Uint8Array(bodyLength); + let idx = 0; + resultBuffer[idx++] = 1; // version + resultBuffer.set(salt, idx); idx += salt.length; + resultBuffer.set(iv, idx); idx += iv.length; + resultBuffer[idx++] = kdf_rounds >> 24; + resultBuffer[idx++] = (kdf_rounds >> 16) & 0xff; + resultBuffer[idx++] = (kdf_rounds >> 8) & 0xff; + resultBuffer[idx++] = kdf_rounds & 0xff; + resultBuffer.set(cipherArray, idx); idx += cipherArray.length; + + const toSign = resultBuffer.subarray(0, idx); + + return subtleCrypto.sign( + {name: 'HMAC'}, + sha_key, + toSign, + ).then((hmac) => { + hmac = new Uint8Array(hmac); + resultBuffer.set(hmac, idx); + return packMegolmKeyFile(resultBuffer); + }); + }); + }); +} + +/** + * Derive the AES and SHA keys for the file + * + * @param {Unit8Array} salt salt for pbkdf + * @param {Number} iterations number of pbkdf iterations + * @param {String} password password + * @return {Promise<[CryptoKey, CryptoKey]>} promise for [aes key, sha key] + */ +function deriveKeys(salt, iterations, password) { + return subtleCrypto.importKey( + 'raw', + new TextEncoder().encode(password), + {name: 'PBKDF2'}, + false, + ['deriveBits'] + ).then((key) => { + return subtleCrypto.deriveBits( + { + name: 'PBKDF2', + salt: salt, + iterations: iterations, + hash: 'SHA-512', + }, + key, + 512 + ); + }).then((keybits) => { + const aes_key = keybits.slice(0, 32); + const sha_key = keybits.slice(32); + + const aes_prom = subtleCrypto.importKey( + 'raw', + aes_key, + {name: 'AES-CTR'}, + false, + ['encrypt', 'decrypt'] + ); + const sha_prom = subtleCrypto.importKey( + 'raw', + sha_key, + { + name: 'HMAC', + hash: {name: 'SHA-256'}, + }, + false, + ['sign', 'verify'] + ); + return Promise.all([aes_prom, sha_prom]); + }); +} + +const HEADER_LINE = '-----BEGIN MEGOLM SESSION DATA-----'; +const TRAILER_LINE = '-----END MEGOLM SESSION DATA-----'; + +/** + * Unbase64 an ascii-armoured megolm key file + * + * Strips the header and trailer lines, and unbase64s the content + * + * @param {ArrayBuffer} data input file + * @return {Uint8Array} unbase64ed content + */ +function unpackMegolmKeyFile(data) { + // parse the file as a great big String. This should be safe, because there + // should be no non-ASCII characters, and it means that we can do string + // comparisons to find the header and footer, and feed it into window.atob. + const fileStr = new TextDecoder().decode(new Uint8Array(data)); + + // look for the start line + let lineStart = 0; + while (1) { + const lineEnd = fileStr.indexOf('\n', lineStart); + if (lineEnd < 0) { + throw new Error('Header line not found'); + } + const line = fileStr.slice(lineStart, lineEnd).trim(); + + // start the next line after the newline + lineStart = lineEnd+1; + + if (line === HEADER_LINE) { + break; + } + } + + const dataStart = lineStart; + + // look for the end line + while (1) { + const lineEnd = fileStr.indexOf('\n', lineStart); + const line = fileStr.slice(lineStart, lineEnd < 0 ? undefined : lineEnd) + .trim(); + if (line === TRAILER_LINE) { + break; + } + + if (lineEnd < 0) { + throw new Error('Trailer line not found'); + } + + // start the next line after the newline + lineStart = lineEnd+1; + } + + const dataEnd = lineStart; + return decodeBase64(fileStr.slice(dataStart, dataEnd)); +} + +/** + * ascii-armour a megolm key file + * + * base64s the content, and adds header and trailer lines + * + * @param {Uint8Array} data raw data + * @return {ArrayBuffer} formatted file + */ +function packMegolmKeyFile(data) { + // we split into lines before base64ing, because encodeBase64 doesn't deal + // terribly well with large arrays. + const LINE_LENGTH = (72 * 4 / 3); + const nLines = Math.ceil(data.length / LINE_LENGTH); + const lines = new Array(nLines + 3); + lines[0] = HEADER_LINE; + let o = 0; + let i; + for (i = 1; i <= nLines; i++) { + lines[i] = encodeBase64(data.subarray(o, o+LINE_LENGTH)); + o += LINE_LENGTH; + } + lines[i++] = TRAILER_LINE; + lines[i] = ''; + return (new TextEncoder().encode(lines.join('\n'))).buffer; +} + +/** + * Encode a typed array of uint8 as base64. + * @param {Uint8Array} uint8Array The data to encode. + * @return {string} The base64. + */ +function encodeBase64(uint8Array) { + // Misinterpt the Uint8Array as Latin-1. + // window.btoa expects a unicode string with codepoints in the range 0-255. + var latin1String = String.fromCharCode.apply(null, uint8Array); + // Use the builtin base64 encoder. + return window.btoa(latin1String); +} + +/** + * Decode a base64 string to a typed array of uint8. + * @param {string} base64 The base64 to decode. + * @return {Uint8Array} The decoded data. + */ +function decodeBase64(base64) { + // window.atob returns a unicode string with codepoints in the range 0-255. + var latin1String = window.atob(base64); + // Encode the string as a Uint8Array + var uint8Array = new Uint8Array(latin1String.length); + for (var i = 0; i < latin1String.length; i++) { + uint8Array[i] = latin1String.charCodeAt(i); + } + return uint8Array; +} diff --git a/test/utils/MegolmExportEncryption-test.js b/test/utils/MegolmExportEncryption-test.js new file mode 100644 index 0000000000..fa51d83c6d --- /dev/null +++ b/test/utils/MegolmExportEncryption-test.js @@ -0,0 +1,115 @@ +/* +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. +*/ + +"use strict"; + +import * as MegolmExportEncryption from 'utils/MegolmExportEncryption'; + +import * as testUtils from '../test-utils'; +import expect from 'expect'; + +// polyfill textencoder if necessary +let TextEncoder = window.TextEncoder; +if (!TextEncoder) { + TextEncoder = require('utils/TextEncoderPolyfill'); +} + +const TEST_VECTORS=[ + [ + "plain", + "password", + "-----BEGIN MEGOLM SESSION DATA-----\nAXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx\ncissyYBxjsfsAndErh065A8=\n-----END MEGOLM SESSION DATA-----" + ], + [ + "Hello, World", + "betterpassword", + "-----BEGIN MEGOLM SESSION DATA-----\nAW1vcmVzYWx0bW9yZXNhbHT//////////wAAAAAAAAAAAAAD6KyBpe1Niv5M5NPm4ZATsJo5nghk\nKYu63a0YQ5DRhUWEKk7CcMkrKnAUiZny\n-----END MEGOLM SESSION DATA-----" + ], + [ + "alphanumericallyalphanumericallyalphanumericallyalphanumerically", + "SWORDFISH", + "-----BEGIN MEGOLM SESSION DATA-----\nAXllc3NhbHR5Z29vZG5lc3P//////////wAAAAAAAAAAAAAD6OIW+Je7gwvjd4kYrb+49gKCfExw\nMgJBMD4mrhLkmgAngwR1pHjbWXaoGybtiAYr0moQ93GrBQsCzPbvl82rZhaXO3iH5uHo/RCEpOqp\nPgg29363BGR+/Ripq/VCLKGNbw==\n-----END MEGOLM SESSION DATA-----" + ], + [ + "alphanumericallyalphanumericallyalphanumericallyalphanumerically", + "passwordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpassword", + "-----BEGIN MEGOLM SESSION DATA-----\nAf//////////////////////////////////////////AAAD6IAZJy7IQ7Y0idqSw/bmpngEEVVh\ngsH+8ptgqxw6ZVWQnohr8JsuwH9SwGtiebZuBu5smPCO+RFVWH2cQYslZijXv/BEH/txvhUrrtCd\nbWnSXS9oymiqwUIGs08sXI33ZA==\n-----END MEGOLM SESSION DATA-----" + ] +] +; + +function stringToArray(s) { + return new TextEncoder().encode(s).buffer; +} + +describe('MegolmExportEncryption', function() { + beforeEach(function() { + testUtils.beforeEach(this); + }); + + describe('decrypt', function() { + it('should handle missing header', function() { + const input=stringToArray(`-----`); + expect(()=>{MegolmExportEncryption.decryptMegolmKeyFile(input, '')}) + .toThrow('Header line not found'); + }); + + it('should handle missing trailer', function() { + const input=stringToArray(`-----BEGIN MEGOLM SESSION DATA----- +-----`); + expect(()=>{MegolmExportEncryption.decryptMegolmKeyFile(input, '')}) + .toThrow('Trailer line not found'); + }); + + it('should decrypt a range of inputs', function(done) { + function next(i) { + if (i >= TEST_VECTORS.length) { + done(); + return; + } + + const [plain, password, input] = TEST_VECTORS[i]; + return MegolmExportEncryption.decryptMegolmKeyFile( + stringToArray(input), password + ).then((decrypted) => { + expect(decrypted).toEqual(plain); + return next(i+1); + }) + }; + return next(0).catch(done); + }); + }); + + describe('encrypt', function() { + it('should round-trip', function(done) { + const input = + 'words words many words in plain text here'.repeat(100); + + const password = 'my super secret passphrase'; + + return MegolmExportEncryption.encryptMegolmKeyFile( + input, password, {kdf_rounds: 1000}, + ).then((ciphertext) => { + return MegolmExportEncryption.decryptMegolmKeyFile( + ciphertext, password + ); + }).then((plaintext) => { + expect(plaintext).toEqual(input); + done(); + }).catch(done); + }); + }); +}); diff --git a/test/utils/generate-megolm-test-vectors.py b/test/utils/generate-megolm-test-vectors.py new file mode 100755 index 0000000000..0ce5f5e4b3 --- /dev/null +++ b/test/utils/generate-megolm-test-vectors.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python + +from __future__ import print_function + +import base64 +import json +import struct + +from cryptography.hazmat import backends +from cryptography.hazmat.primitives import ciphers, hashes, hmac +from cryptography.hazmat.primitives.kdf import pbkdf2 +from cryptography.hazmat.primitives.ciphers import algorithms, modes + +backend = backends.default_backend() + +def parse_u128(s): + a, b = struct.unpack(">QQ", s) + return (a << 64) | b + +def encrypt_ctr(key, iv, plaintext, counter_bits=64): + alg = algorithms.AES(key) + + # Some AES-CTR implementations treat some parts of the IV as a nonce (which + # remains constant throughought encryption), and some as a counter (which + # increments every block, ie 16 bytes, and wraps after a while). Different + # implmententations use different amounts of the IV for each part. + # + # The python cryptography library uses the whole IV as a counter; to make + # it match other implementations with a given counter size, we manually + # implement wrapping the counter. + + # number of AES blocks between each counter wrap + limit = 1 << counter_bits + + # parse IV as a 128-bit int + parsed_iv = parse_u128(iv) + + # split IV into counter and nonce + counter = parsed_iv & (limit - 1) + nonce = parsed_iv & ~(limit - 1) + + # encrypt up to the first counter wraparound + size = 16 * (limit - counter) + encryptor = ciphers.Cipher( + alg, + modes.CTR(iv), + backend=backend + ).encryptor() + input = plaintext[:size] + result = encryptor.update(input) + encryptor.finalize() + offset = size + + # do remaining data starting with a counter of zero + iv = struct.pack(">QQ", nonce >> 64, nonce & ((1 << 64) - 1)) + size = 16 * limit + + while offset < len(plaintext): + encryptor = ciphers.Cipher( + alg, + modes.CTR(iv), + backend=backend + ).encryptor() + input = plaintext[offset:offset+size] + result += encryptor.update(input) + encryptor.finalize() + offset += size + + return result + +def hmac_sha256(key, message): + h = hmac.HMAC(key, hashes.SHA256(), backend=backend) + h.update(message) + return h.finalize() + +def encrypt(key, iv, salt, plaintext, iterations=1000): + """ + Returns: + (bytes) ciphertext + """ + if len(salt) != 16: + raise Exception("Expected 128 bits of salt - got %i bits" % len((salt) * 8)) + if len(iv) != 16: + raise Exception("Expected 128 bits of IV - got %i bits" % (len(iv) * 8)) + + sha = hashes.SHA512() + kdf = pbkdf2.PBKDF2HMAC(sha, 64, salt, iterations, backend) + k = kdf.derive(key) + + aes_key = k[0:32] + sha_key = k[32:] + + packed_file = ( + b"\x01" # version + + salt + + iv + + struct.pack(">L", iterations) + + encrypt_ctr(aes_key, iv, plaintext) + ) + packed_file += hmac_sha256(sha_key, packed_file) + + return ( + b"-----BEGIN MEGOLM SESSION DATA-----\n" + + base64.encodestring(packed_file) + + b"-----END MEGOLM SESSION DATA-----" + ) + +def gen(password, iv, salt, plaintext, iterations=1000): + ciphertext = encrypt( + password.encode('utf-8'), iv, salt, plaintext.encode('utf-8'), iterations + ) + return (plaintext, password, ciphertext.decode('utf-8')) + +print (json.dumps([ + gen("password", b"\x88"*16, b"saltsaltsaltsalt", "plain", 10), + gen("betterpassword", b"\xFF"*8 + b"\x00"*8, b"moresaltmoresalt", "Hello, World"), + gen("SWORDFISH", b"\xFF"*8 + b"\x00"*8, b"yessaltygoodness", "alphanumerically" * 4), + gen("password"*32, b"\xFF"*16, b"\xFF"*16, "alphanumerically" * 4), +], indent=4)) From d63f7e83599937a8b9e5dbff638b0a93efa7fafa Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Sat, 14 Jan 2017 01:21:26 +0000 Subject: [PATCH 03/57] Expose megolm import/export via the devtools --- src/index.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/index.js b/src/index.js index 4b920d95d4..5d4145a39b 100644 --- a/src/index.js +++ b/src/index.js @@ -28,3 +28,27 @@ module.exports.getComponent = function(componentName) { return Skinner.getComponent(componentName); }; + +/* hacky functions for megolm import/export until we give it a UI */ +import * as MegolmExportEncryption from './utils/MegolmExportEncryption'; +import MatrixClientPeg from './MatrixClientPeg'; + +window.exportKeys = function(password) { + return MatrixClientPeg.get().exportRoomKeys().then((k) => { + return MegolmExportEncryption.encryptMegolmKeyFile( + JSON.stringify(k), password + ); + }).then((f) => { + console.log(new TextDecoder().decode(new Uint8Array(f))); + }).done(); +}; + +window.importKeys = function(password, data) { + const arrayBuffer = new TextEncoder().encode(data).buffer; + return MegolmExportEncryption.decryptMegolmKeyFile( + arrayBuffer, password + ).then((j) => { + const k = JSON.parse(j); + return MatrixClientPeg.get().importRoomKeys(k); + }); +}; From e37bf6b7be824caade5019da1bc1a4b92c9d6114 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Sat, 14 Jan 2017 01:41:48 +0000 Subject: [PATCH 04/57] Skip crypto tests on PhantomJS --- test/utils/MegolmExportEncryption-test.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/utils/MegolmExportEncryption-test.js b/test/utils/MegolmExportEncryption-test.js index fa51d83c6d..db38a931ed 100644 --- a/test/utils/MegolmExportEncryption-test.js +++ b/test/utils/MegolmExportEncryption-test.js @@ -56,6 +56,13 @@ function stringToArray(s) { } describe('MegolmExportEncryption', function() { + before(function() { + // if we don't have subtlecrypto, go home now + if (!window.crypto.subtle && !window.crypto.webkitSubtle) { + this.skip(); + } + }) + beforeEach(function() { testUtils.beforeEach(this); }); From ac22803ba004b519d88a16a37487efb833575ce9 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 16 Jan 2017 17:01:26 +0000 Subject: [PATCH 05/57] Allow Modal to be used with async-loaded components Add Modal.createDialogAsync, which can be used to display asynchronously-loaded React components. Also make EncryptedEventDialog use it as a handy demonstration. --- src/Modal.js | 73 ++++++++++++++++++- .../views/dialogs/EncryptedEventDialog.js | 0 src/component-index.js | 2 - src/components/views/rooms/EventTile.js | 5 +- 4 files changed, 74 insertions(+), 6 deletions(-) rename src/{components => async-components}/views/dialogs/EncryptedEventDialog.js (100%) diff --git a/src/Modal.js b/src/Modal.js index 44072b9278..c2ce04c4e8 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -19,6 +19,53 @@ limitations under the License. var React = require('react'); var ReactDOM = require('react-dom'); +import sdk from './index'; + +/** + * Wrap an asynchronous loader function with a react component which shows a + * spinner until the real component loads. + */ +const AsyncWrapper = React.createClass({ + propTypes: { + /** A function which takes a 'callback' argument which it will call + * with the real component once it loads. + */ + loader: React.PropTypes.func.isRequired, + }, + + getInitialState: function() { + return { + component: null, + } + }, + + componentWillMount: function() { + this._unmounted = false; + this.props.loader((e) => { + if (this._unmounted) { + return; + } + this.setState({component: e}); + }); + }, + + componentWillUnmount: function() { + this._unmounted = true; + }, + + render: function() { + const {loader, ...otherProps} = this.props; + + if (this.state.component) { + const Component = this.state.component; + return ; + } else { + // show a spinner until the component is loaded. + const Spinner = sdk.getComponent("elements.Spinner"); + return ; + } + }, +}); module.exports = { DialogContainerId: "mx_Dialog_Container", @@ -36,8 +83,30 @@ module.exports = { }, createDialog: function (Element, props, className) { - var self = this; + return this.createDialogAsync((cb) => {cb(Element)}, props, className); + }, + /** + * Open a modal view. + * + * This can be used to display a react component which is loaded as an asynchronous + * webpack component. To do this, set 'loader' as: + * + * (cb) => { + * require([''], cb); + * } + * + * @param {Function} loader a function which takes a 'callback' argument, + * which it should call with a React component which will be displayed as + * the modal view. + * + * @param {Object} props properties to pass to the displayed + * component. (We will also pass an 'onFinished' property.) + * + * @param {String} className CSS class to apply to the modal wrapper + */ + createDialogAsync: function (loader, props, className) { + var self = this; // never call this via modal.close() from onFinished() otherwise it will loop var closeDialog = function() { if (props && props.onFinished) props.onFinished.apply(null, arguments); @@ -49,7 +118,7 @@ module.exports = { var dialog = (
- +
diff --git a/src/components/views/dialogs/EncryptedEventDialog.js b/src/async-components/views/dialogs/EncryptedEventDialog.js similarity index 100% rename from src/components/views/dialogs/EncryptedEventDialog.js rename to src/async-components/views/dialogs/EncryptedEventDialog.js diff --git a/src/component-index.js b/src/component-index.js index bc3d698cac..e83de8739d 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -75,8 +75,6 @@ import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInvit views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog); import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog'; views$dialogs$DeactivateAccountDialog && (module.exports.components['views.dialogs.DeactivateAccountDialog'] = views$dialogs$DeactivateAccountDialog); -import views$dialogs$EncryptedEventDialog from './components/views/dialogs/EncryptedEventDialog'; -views$dialogs$EncryptedEventDialog && (module.exports.components['views.dialogs.EncryptedEventDialog'] = views$dialogs$EncryptedEventDialog); 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'; diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 42dbe78630..d5a2acfdd6 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -366,10 +366,11 @@ module.exports = WithMatrixClient(React.createClass({ }, onCryptoClicked: function(e) { - var EncryptedEventDialog = sdk.getComponent("dialogs.EncryptedEventDialog"); var event = this.props.mxEvent; - Modal.createDialog(EncryptedEventDialog, { + Modal.createDialogAsync((cb) => { + require(['../../../async-components/views/dialogs/EncryptedEventDialog'], cb) + }, { event: event, }); }, From 09ce74cc767a5f98021f5980ce7f4e2b2aece7c4 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 16 Jan 2017 18:44:46 +0000 Subject: [PATCH 06/57] Fix a couple of minor review comments --- src/utils/MegolmExportEncryption.js | 20 ++++++++++---------- src/utils/TextEncoderPolyfill.js | 12 ++++++------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js index 5b2e16ef29..351f58aaa6 100644 --- a/src/utils/MegolmExportEncryption.js +++ b/src/utils/MegolmExportEncryption.js @@ -60,12 +60,12 @@ export function decryptMegolmKeyFile(data, password) { const hmac = body.subarray(-32); return deriveKeys(salt, iterations, password).then((keys) => { - const [aes_key, sha_key] = keys; + const [aes_key, hmac_key] = keys; const toVerify = body.subarray(0, -32); return subtleCrypto.verify( {name: 'HMAC'}, - sha_key, + hmac_key, hmac, toVerify, ).then((isValid) => { @@ -109,7 +109,7 @@ export function encryptMegolmKeyFile(data, password, options) { window.crypto.getRandomValues(iv); return deriveKeys(salt, kdf_rounds, password).then((keys) => { - const [aes_key, sha_key] = keys; + const [aes_key, hmac_key] = keys; return subtleCrypto.encrypt( { @@ -137,7 +137,7 @@ export function encryptMegolmKeyFile(data, password, options) { return subtleCrypto.sign( {name: 'HMAC'}, - sha_key, + hmac_key, toSign, ).then((hmac) => { hmac = new Uint8Array(hmac); @@ -149,12 +149,12 @@ export function encryptMegolmKeyFile(data, password, options) { } /** - * Derive the AES and SHA keys for the file + * Derive the AES and HMAC-SHA-256 keys for the file * * @param {Unit8Array} salt salt for pbkdf * @param {Number} iterations number of pbkdf iterations * @param {String} password password - * @return {Promise<[CryptoKey, CryptoKey]>} promise for [aes key, sha key] + * @return {Promise<[CryptoKey, CryptoKey]>} promise for [aes key, hmac key] */ function deriveKeys(salt, iterations, password) { return subtleCrypto.importKey( @@ -176,7 +176,7 @@ function deriveKeys(salt, iterations, password) { ); }).then((keybits) => { const aes_key = keybits.slice(0, 32); - const sha_key = keybits.slice(32); + const hmac_key = keybits.slice(32); const aes_prom = subtleCrypto.importKey( 'raw', @@ -185,9 +185,9 @@ function deriveKeys(salt, iterations, password) { false, ['encrypt', 'decrypt'] ); - const sha_prom = subtleCrypto.importKey( + const hmac_prom = subtleCrypto.importKey( 'raw', - sha_key, + hmac_key, { name: 'HMAC', hash: {name: 'SHA-256'}, @@ -195,7 +195,7 @@ function deriveKeys(salt, iterations, password) { false, ['sign', 'verify'] ); - return Promise.all([aes_prom, sha_prom]); + return Promise.all([aes_prom, hmac_prom]); }); } diff --git a/src/utils/TextEncoderPolyfill.js b/src/utils/TextEncoderPolyfill.js index 2da09710f2..41ee4782a9 100644 --- a/src/utils/TextEncoderPolyfill.js +++ b/src/utils/TextEncoderPolyfill.js @@ -61,16 +61,16 @@ export default class TextEncoder { outU8Array[outIdx++] = u; } else if (u <= 0x7FF) { outU8Array[outIdx++] = 0xC0 | (u >> 6); - outU8Array[outIdx++] = 0x80 | (u & 63); + outU8Array[outIdx++] = 0x80 | (u & 0x3F); } else if (u <= 0xFFFF) { outU8Array[outIdx++] = 0xE0 | (u >> 12); - outU8Array[outIdx++] = 0x80 | ((u >> 6) & 63); - outU8Array[outIdx++] = 0x80 | (u & 63); + outU8Array[outIdx++] = 0x80 | ((u >> 6) & 0x3F); + outU8Array[outIdx++] = 0x80 | (u & 0x3F); } else { outU8Array[outIdx++] = 0xF0 | (u >> 18); - outU8Array[outIdx++] = 0x80 | ((u >> 12) & 63); - outU8Array[outIdx++] = 0x80 | ((u >> 6) & 63); - outU8Array[outIdx++] = 0x80 | (u & 63); + outU8Array[outIdx++] = 0x80 | ((u >> 12) & 0x3F); + outU8Array[outIdx++] = 0x80 | ((u >> 6) & 0x3F); + outU8Array[outIdx++] = 0x80 | (u & 0x3F); } } return outU8Array; From 203172c886b326782e66bf53d8dbd8d00104ff3c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 17 Jan 2017 14:11:01 +0000 Subject: [PATCH 07/57] typos --- src/components/structures/ContextualMenu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index fecb2a1841..da419897dc 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -67,7 +67,7 @@ module.exports = { chevronOffset.top = props.chevronOffset; } - // To overide the deafult chevron colour, if it's been set + // To override the default chevron colour, if it's been set var chevronCSS = ""; if (props.menuColour) { chevronCSS = ` From a18d94099e76232454ed3feefa837cfe8da532ab Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 17 Jan 2017 14:11:15 +0000 Subject: [PATCH 08/57] switch to using for edit button --- src/components/views/rooms/EventTile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 42dbe78630..ef578d47db 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -263,7 +263,7 @@ module.exports = WithMatrixClient(React.createClass({ // The window X and Y offsets are to adjust position when zoomed in to page var x = buttonRect.right + window.pageXOffset; - var y = (buttonRect.top + (e.target.height / 2) + window.pageYOffset) - 19; + var y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19; var self = this; ContextualMenu.createMenu(MessageContextMenu, { chevronOffset: 10, @@ -465,7 +465,7 @@ module.exports = WithMatrixClient(React.createClass({ } var editButton = ( - Options + ); var e2e; From 0b67fd5b4ef834355dfaf9f8f6c4ac2dc62a5a01 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 17 Jan 2017 14:48:50 +0000 Subject: [PATCH 09/57] Add 'searching known users' to the user picker So it's more obvious it's only finding people you've already seen Fixes https://github.com/vector-im/riot-web/issues/2931 --- src/components/views/dialogs/ChatInviteDialog.js | 7 ++++++- src/components/views/elements/AddressSelector.js | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index e9a041357f..7fee741e47 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -396,11 +396,16 @@ module.exports = React.createClass({ if (this.state.error) { error =
You have entered an invalid contact. Try using their Matrix ID or email address.
} else { + const addressSelectorHeader =
+ Searching known users +
; addressSelector = ( {this.addressSelector = ref}} addressList={ this.state.queryList } onSelected={ this.onSelected } - truncateAt={ TRUNCATE_QUERY_LIST } /> + truncateAt={ TRUNCATE_QUERY_LIST } + header={ addressSelectorHeader } + /> ); } diff --git a/src/components/views/elements/AddressSelector.js b/src/components/views/elements/AddressSelector.js index 2c2d7e2d61..8b2855e99d 100644 --- a/src/components/views/elements/AddressSelector.js +++ b/src/components/views/elements/AddressSelector.js @@ -28,6 +28,9 @@ module.exports = React.createClass({ addressList: React.PropTypes.array.isRequired, truncateAt: React.PropTypes.number.isRequired, selected: React.PropTypes.number, + + // Element to put as a header on top of the list + header: React.PropTypes.node, }, getInitialState: function() { @@ -147,6 +150,7 @@ module.exports = React.createClass({ return (
{this.scrollElement = ref}}> + { this.props.header } { this.createAddressListTiles() }
); From 893a5c971fd43186637f50cb3725eefa09b6d365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Fri, 2 Dec 2016 19:58:35 +0100 Subject: [PATCH 10/57] Fix escaping markdown by rendering plaintext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We still need to parse "plaintext" messages through the markdown renderer so that escappes are rendered properly. Fixes vector-im/riot-web#2870. Signed-off-by: Johannes Löthberg --- src/Markdown.js | 32 ++++++++++++------- .../views/rooms/MessageComposerInput.js | 8 +++-- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/Markdown.js b/src/Markdown.js index 18c888b541..2eb84b9041 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -56,23 +56,31 @@ export default class Markdown { return is_plain; } - toHTML() { + render(html) { const parser = new commonmark.Parser(); const renderer = new commonmark.HtmlRenderer({safe: true}); const real_paragraph = renderer.paragraph; - renderer.paragraph = function(node, entering) { - // If there is only one top level node, just return the - // bare text: it's a single line of text and so should be - // 'inline', rather than unnecessarily wrapped in its own - // p tag. If, however, we have multiple nodes, each gets - // its own p tag to keep them as separate paragraphs. - var par = node; - while (par.parent) { - par = par.parent + if (html) { + renderer.paragraph = function(node, entering) { + // If there is only one top level node, just return the + // bare text: it's a single line of text and so should be + // 'inline', rather than unnecessarily wrapped in its own + // p tag. If, however, we have multiple nodes, each gets + // its own p tag to keep them as separate paragraphs. + var par = node; + while (par.parent) { + par = par.parent + } + if (par.firstChild != par.lastChild) { + real_paragraph.call(this, node, entering); + } } - if (par.firstChild != par.lastChild) { - real_paragraph.call(this, node, entering); + } else { + renderer.paragraph = function(node, entering) { + if (entering) { + this.lit('\n\n'); + } } } diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 37d937d6f5..5e8df592da 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -401,7 +401,7 @@ export default class MessageComposerInput extends React.Component { let contentState = null; if (enabled) { const md = new Markdown(this.state.editorState.getCurrentContent().getPlainText()); - contentState = RichText.HTMLtoContentState(md.toHTML()); + contentState = RichText.HTMLtoContentState(md.render(true)); } else { let markdown = stateToMarkdown(this.state.editorState.getCurrentContent()); if (markdown[markdown.length - 1] === '\n') { @@ -523,8 +523,10 @@ export default class MessageComposerInput extends React.Component { ); } else { const md = new Markdown(contentText); - if (!md.isPlainText()) { - contentHTML = md.toHTML(); + if (md.isPlainText()) { + contentText = md.render(false); + } else { + contentHTML = md.render(true); } } From a87e7d66170bcae5c28075aef6aae3de696703ae Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 17 Jan 2017 18:17:51 +0000 Subject: [PATCH 11/57] Make user search do a bit better on word boundary --- .../views/dialogs/ChatInviteDialog.js | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index e9a041357f..59e95d1538 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -27,6 +27,10 @@ var Modal = require('../../../Modal'); const TRUNCATE_QUERY_LIST = 40; +function escapeRegExp(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); +} + module.exports = React.createClass({ displayName: "ChatInviteDialog", propTypes: { @@ -315,13 +319,18 @@ module.exports = React.createClass({ return true; } - // split spaces in name and try matching constituent parts - var parts = name.split(" "); - for (var i = 0; i < parts.length; i++) { - if (parts[i].indexOf(query) === 0) { - return true; - } + // Try to find the query following a "word boundary", except that + // this does avoids using \b because it only considers letters from + // the roman alphabet to be word characters. + // Instead, we look for the query following either: + // * The start of the string + // * Whitespace, or + // * A fixed number of punctuation characters + let expr = new RegExp("(?:^|[\\s\\('\",\.-])" + escapeRegExp(query)); + if (expr.test(name)) { + return true; } + return false; }, From c2cdb626bdf3b8156168a6594ba1c8462d482c2f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 17 Jan 2017 19:13:04 +0000 Subject: [PATCH 12/57] let the tinter handle 'white' highlights of SVGs too --- src/Tinter.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Tinter.js b/src/Tinter.js index 534a1d810b..c18d3068a7 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -42,6 +42,7 @@ var keyHex = [ "#76CFA6", // Vector Green "#EAF5F0", // Vector Light Green "#D3EFE1", // BottomLeftMenu overlay (20% Vector Green overlaid on Vector Light Green) + "#FFFFFF", // white highlights of the SVGs (for switching to dark theme) ]; // cache of our replacement colours @@ -50,6 +51,7 @@ var colors = [ keyHex[0], keyHex[1], keyHex[2], + keyHex[3], ]; var cssFixups = [ @@ -172,7 +174,7 @@ module.exports = { tintables.push(tintable); }, - tint: function(primaryColor, secondaryColor, tertiaryColor) { + tint: function(primaryColor, secondaryColor, tertiaryColor, whiteColor) { if (!cached) { calcCssFixups(); @@ -203,14 +205,19 @@ module.exports = { tertiaryColor = rgbToHex(rgb1); } + if (!whiteColor) { + whiteColor = colors[3]; + } + if (colors[0] === primaryColor && colors[1] === secondaryColor && - colors[2] === tertiaryColor) + colors[2] === tertiaryColor && + colors[3] === whiteColor) { return; } - colors = [primaryColor, secondaryColor, tertiaryColor]; + colors = [primaryColor, secondaryColor, tertiaryColor, whiteColor]; if (DEBUG) console.log("Tinter.tint"); From 8288eb730c0ddd7239dbb542031dd6b56de4eeac Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 17 Jan 2017 19:13:23 +0000 Subject: [PATCH 13/57] a rather hacky implementation of theme switching --- src/components/structures/MatrixChat.js | 42 +++++++++++++++ src/components/structures/UserSettings.js | 63 ++++++++++++++++++----- 2 files changed, 93 insertions(+), 12 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index c47109db94..79e93bb990 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -25,6 +25,7 @@ var SdkConfig = require("../../SdkConfig"); var ContextualMenu = require("./ContextualMenu"); var RoomListSorter = require("../../RoomListSorter"); var UserActivity = require("../../UserActivity"); +var UserSettingsStore = require('../../UserSettingsStore'); var Presence = require("../../Presence"); var dis = require("../../dispatcher"); @@ -456,6 +457,9 @@ module.exports = React.createClass({ middleOpacity: payload.middleOpacity, }); break; + case 'set_theme': + this._onSetTheme(payload.value); + break; case 'on_logged_in': this._onLoggedIn(); break; @@ -584,6 +588,44 @@ module.exports = React.createClass({ _onLoadCompleted: function() { this.props.onLoadCompleted(); this.setState({loading: false}); + + // set up the right theme. + // XXX: this will temporarily flicker the wrong CSS. + dis.dispatch({ + action: 'set_theme', + value: UserSettingsStore.getSyncedSetting('theme') + }); + }, + + /** + * Called whenever someone changes the theme + */ + _onSetTheme: function(theme) { + if (!theme) { + theme = 'light'; + } + + var i, a; + for (i = 0; (a = document.getElementsByTagName("link")[i]); i++) { + var href = a.getAttribute("href"); + if (href.startsWith("theme-")) { + if (href.startsWith("theme-" + theme + ".")) { + a.disabled = false; + } + else { + a.disabled = true; + } + } + } + + if (theme === 'dark') { + // abuse the tinter to change all the SVG's #fff to #2d2d2d + // XXX: obviously this shouldn't be hardcoded here. + Tinter.tint(undefined, undefined, undefined, '#2d2d2d'); + } + else { + Tinter.tint(undefined, undefined, undefined, '#ffffff'); + } }, /** diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index a41eab3a76..ebfd6dc0a7 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -363,7 +363,23 @@ module.exports = React.createClass({ */ ]; + var themes = [ + { + id: 'theme', + label: 'Light theme', + value: 'light', + }, + { + id: 'theme', + label: 'Dark theme', + value: 'dark', + } + ]; + var syncedSettings = UserSettingsStore.getSyncedSettings(); + if (!syncedSettings.theme) { + syncedSettings.theme = 'light'; + } return (
@@ -379,19 +395,42 @@ module.exports = React.createClass({ Disable inline URL previews by default
+ { settingsLabels.map( setting => { + return
+ UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) } + /> + +
+ })} + { themes.map( setting => { + return
+ { + if (e.target.checked) { + UserSettingsStore.setSyncedSetting(setting.id, setting.value) + } + dis.dispatch({ + action: 'set_theme', + value: setting.value, + }); + } + } + /> + +
+ })} - { settingsLabels.forEach( setting => { -
- UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) } - /> - -
- })} ); }, From 35d70f0b35aeefa4af43379078b63922f0f70afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Tue, 17 Jan 2017 20:32:06 +0100 Subject: [PATCH 14/57] markdown: Only add \n\n on multiple paragraphs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johannes Löthberg --- src/Markdown.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Markdown.js b/src/Markdown.js index 2eb84b9041..17723e42f8 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -78,8 +78,20 @@ export default class Markdown { } } else { renderer.paragraph = function(node, entering) { - if (entering) { - this.lit('\n\n'); + // If there is only one top level node, just return the + // bare text: it's a single line of text and so should be + // 'inline', rather than unnecessarily wrapped in its own + // p tag. If, however, we have multiple nodes, each gets + // its own p tag to keep them as separate paragraphs. + var par = node; + while (par.parent) { + node = par; + par = par.parent; + } + if (node != par.lastChild) { + if (!entering) { + this.lit('\n\n'); + } } } } From c819b433a247ca502daf600c3bc763f138262f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Tue, 17 Jan 2017 20:37:27 +0100 Subject: [PATCH 15/57] Make old message composer use new markdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johannes Löthberg --- src/components/views/rooms/MessageComposerInputOld.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index 28e3186c50..3b0100278b 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -325,12 +325,13 @@ module.exports = React.createClass({ } if (send_markdown) { - const htmlText = mdown.toHTML(); + const htmlText = mdown.render(true); sendMessagePromise = isEmote ? MatrixClientPeg.get().sendHtmlEmote(this.props.room.roomId, contentText, htmlText) : MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText); } else { + const contentText = mdown.render(false); sendMessagePromise = isEmote ? MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) : MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText); From 49d60ff879aa677e5befb10c831b0d89e9952d0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Tue, 17 Jan 2017 21:04:12 +0100 Subject: [PATCH 16/57] Markdown: softbreak is not HTML MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johannes Löthberg --- src/Markdown.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Markdown.js b/src/Markdown.js index 17723e42f8..ad7ec5ef0c 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -48,6 +48,7 @@ export default class Markdown { } // text and paragraph are just text dummy_renderer.text = function(t) { return t; } + dummy_renderer.softbreak = function(t) { return t; } dummy_renderer.paragraph = function(t) { return t; } const dummy_parser = new commonmark.Parser(); From 2e3bdcf5c697009b0d7b6b82ab5f902ec49115de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Tue, 17 Jan 2017 22:20:05 +0100 Subject: [PATCH 17/57] Markdown: Don't XML escape the output when not HTML MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johannes Löthberg --- src/Markdown.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Markdown.js b/src/Markdown.js index ad7ec5ef0c..e6f5f59f01 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -78,6 +78,10 @@ export default class Markdown { } } } else { + renderer.out = function(s) { + this.lit(s); + } + renderer.paragraph = function(node, entering) { // If there is only one top level node, just return the // bare text: it's a single line of text and so should be From 31df78f946ef64f73007c6183d17e1c4dc326448 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 18 Jan 2017 11:39:44 +0000 Subject: [PATCH 18/57] Use text-encoding-utf-8 as a TextEncoder polyfill Somebody else seems to have done a good job of polyfilling TextEncoder, so let's use that. --- package.json | 1 + .../views/dialogs/ExportE2eKeysDialog.js | 84 +++++++++++ src/utils/MegolmExportEncryption.js | 7 +- src/utils/TextDecoderPolyfill.js | 131 ------------------ src/utils/TextEncoderPolyfill.js | 78 ----------- test/utils/TextDecoderPolyfill-test.js | 85 ------------ test/utils/TextEncoderPolyfill-test.js | 39 ------ 7 files changed, 89 insertions(+), 336 deletions(-) create mode 100644 src/async-components/views/dialogs/ExportE2eKeysDialog.js delete mode 100644 src/utils/TextDecoderPolyfill.js delete mode 100644 src/utils/TextEncoderPolyfill.js delete mode 100644 test/utils/TextDecoderPolyfill-test.js delete mode 100644 test/utils/TextEncoderPolyfill-test.js diff --git a/package.json b/package.json index 1eaee39c41..e0cfb72148 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "react-dom": "^15.4.0", "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", "whatwg-fetch": "^1.0.0" }, diff --git a/src/async-components/views/dialogs/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/ExportE2eKeysDialog.js new file mode 100644 index 0000000000..284d299f4b --- /dev/null +++ b/src/async-components/views/dialogs/ExportE2eKeysDialog.js @@ -0,0 +1,84 @@ +/* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import sdk from '../../../index'; + +import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption'; + +export default React.createClass({ + displayName: 'ExportE2eKeysDialog', + + getInitialState: function() { + return { + collectedPassword: false, + }; + }, + + _onPassphraseFormSubmit: function(ev) { + ev.preventDefault(); + console.log(this.refs.passphrase1.value); + return false; + }, + + render: function() { + let content; + if (!this.state.collectedPassword) { + content = ( +
+

+ This process will allow you to export the keys for messages + you have received in encrypted rooms to a local file. You + will then be able to import the file into another Matrix + client in the future, so that client will also be able to + decrypt these messages. +

+

+ The exported file will allow anyone who can read it to decrypt + any encrypted messages that you can see, so you should be + careful to keep it secure. To help with this, you should enter + a passphrase below, which will be used to encrypt the exported + data. It will only be possible to import the data by using the + same passphrase. +

+
+
+ +
+
+ +
+
+ +
+
+
+ ); + } + + return ( +
+
+ Export room keys +
+ {content} +
+ ); + }, +}); diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js index 351f58aaa6..e3ca7e68f2 100644 --- a/src/utils/MegolmExportEncryption.js +++ b/src/utils/MegolmExportEncryption.js @@ -17,13 +17,14 @@ limitations under the License. "use strict"; // polyfill textencoder if necessary +import * as TextEncodingUtf8 from 'text-encoding-utf-8'; let TextEncoder = window.TextEncoder; if (!TextEncoder) { - TextEncoder = require('./TextEncoderPolyfill'); + TextEncoder = TextEncodingUtf8.TextEncoder; } let TextDecoder = window.TextDecoder; -if (TextDecoder) { - TextDecoder = require('./TextDecoderPolyfill'); +if (!TextDecoder) { + TextDecoder = TextEncodingUtf8.TextDecoder; } const subtleCrypto = window.crypto.subtle || window.crypto.webkitSubtle; diff --git a/src/utils/TextDecoderPolyfill.js b/src/utils/TextDecoderPolyfill.js deleted file mode 100644 index e203676bb7..0000000000 --- a/src/utils/TextDecoderPolyfill.js +++ /dev/null @@ -1,131 +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. -*/ - -"use strict"; - -// Polyfill for TextDecoder. - -const REPLACEMENT_CHAR = '\uFFFD'; - -export default class TextDecoder { - /** - * Decode a UTF-8 byte array as a javascript string - * - * @param {Uint8Array} u8Array UTF-8-encoded onput - * @return {str} - */ - decode(u8Array) { - let u0, u1, u2, u3; - - let str = ''; - let idx = 0; - while (idx < u8Array.length) { - u0 = u8Array[idx++]; - if (!(u0 & 0x80)) { - str += String.fromCharCode(u0); - continue; - } - - if ((u0 & 0xC0) != 0xC0) { - // continuation byte where we expect a leading byte - str += REPLACEMENT_CHAR; - continue; - } - - if (u0 > 0xF4) { - // this would imply a 5-byte or longer encoding, which is - // invalid and unsupported here. - str += REPLACEMENT_CHAR; - continue; - } - - u1 = u8Array[idx++]; - if (u1 === undefined) { - str += REPLACEMENT_CHAR; - continue; - } - - if ((u1 & 0xC0) != 0x80) { - // leading byte where we expect a continuation byte - str += REPLACEMENT_CHAR.repeat(2); - continue; - } - u1 &= 0x3F; - if (!(u0 & 0x20)) { - const u = ((u0 & 0x1F) << 6) | u1; - if (u < 0x80) { - // over-long - str += REPLACEMENT_CHAR.repeat(2); - } else { - str += String.fromCharCode(u); - } - continue; - } - - u2 = u8Array[idx++]; - if (u2 === undefined) { - str += REPLACEMENT_CHAR.repeat(2); - continue; - } - if ((u2 & 0xC0) != 0x80) { - // leading byte where we expect a continuation byte - str += REPLACEMENT_CHAR.repeat(3); - continue; - } - u2 &= 0x3F; - if (!(u0 & 0x10)) { - const u = ((u0 & 0x0F) << 12) | (u1 << 6) | u2; - if (u < 0x800) { - // over-long - str += REPLACEMENT_CHAR.repeat(3); - } else if (u == 0xFEFF && idx == 3) { - // byte-order mark: do not add to output - } else { - str += String.fromCharCode(u); - } - continue; - } - - u3 = u8Array[idx++]; - if (u3 === undefined) { - str += REPLACEMENT_CHAR.repeat(3); - continue; - } - if ((u3 & 0xC0) != 0x80) { - // leading byte where we expect a continuation byte - str += REPLACEMENT_CHAR.repeat(4); - continue; - } - u3 &= 0x3F; - const u = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | u3; - if (u < 0x10000) { - // over-long - str += REPLACEMENT_CHAR.repeat(4); - continue; - } - if (u > 0x1FFFF) { - // unicode stops here. - str += REPLACEMENT_CHAR.repeat(4); - continue; - } - - // encode as utf-16 - const v = u - 0x10000; - str += String.fromCharCode(0xD800 | (v >> 10), 0xDC00 | (v & 0x3FF)); - } - return str; - } -} diff --git a/src/utils/TextEncoderPolyfill.js b/src/utils/TextEncoderPolyfill.js deleted file mode 100644 index 41ee4782a9..0000000000 --- a/src/utils/TextEncoderPolyfill.js +++ /dev/null @@ -1,78 +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. -*/ - -"use strict"; - -// Polyfill for TextEncoder. Based on emscripten's stringToUTF8Array. - -function utf8len(str) { - var len = 0; - for (var i = 0; i < str.length; ++i) { - var u = str.charCodeAt(i); - if (u >= 0xD800 && u <= 0xDFFF && i < str.length-1) { - // lead surrogate - combine with next surrogate - u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF); - } - - if (u <= 0x7F) { - ++len; - } else if (u <= 0x7FF) { - len += 2; - } else if (u <= 0xFFFF) { - len += 3; - } else { - len += 4; - } - } - return len; -} - -export default class TextEncoder { - /** - * Encode a javascript string as utf-8 - * - * @param {String} str String to encode - * @return {Uint8Array} UTF-8-encoded output - */ - encode(str) { - const outU8Array = new Uint8Array(utf8len(str)); - var outIdx = 0; - for (var i = 0; i < str.length; ++i) { - var u = str.charCodeAt(i); - if (u >= 0xD800 && u <= 0xDFFF && i < str.length-1) { - // lead surrogate - combine with next surrogate - u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF); - } - - if (u <= 0x7F) { - outU8Array[outIdx++] = u; - } else if (u <= 0x7FF) { - outU8Array[outIdx++] = 0xC0 | (u >> 6); - outU8Array[outIdx++] = 0x80 | (u & 0x3F); - } else if (u <= 0xFFFF) { - outU8Array[outIdx++] = 0xE0 | (u >> 12); - outU8Array[outIdx++] = 0x80 | ((u >> 6) & 0x3F); - outU8Array[outIdx++] = 0x80 | (u & 0x3F); - } else { - outU8Array[outIdx++] = 0xF0 | (u >> 18); - outU8Array[outIdx++] = 0x80 | ((u >> 12) & 0x3F); - outU8Array[outIdx++] = 0x80 | ((u >> 6) & 0x3F); - outU8Array[outIdx++] = 0x80 | (u & 0x3F); - } - } - return outU8Array; - } -} diff --git a/test/utils/TextDecoderPolyfill-test.js b/test/utils/TextDecoderPolyfill-test.js deleted file mode 100644 index 84f5edf187..0000000000 --- a/test/utils/TextDecoderPolyfill-test.js +++ /dev/null @@ -1,85 +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. -*/ - -"use strict"; - -import TextDecoderPolyfill from 'utils/TextDecoderPolyfill'; - -import * as testUtils from '../test-utils'; -import expect from 'expect'; - -describe('textDecoderPolyfill', function() { - beforeEach(function() { - testUtils.beforeEach(this); - }); - - it('should correctly decode a range of strings', function() { - const decoder = new TextDecoderPolyfill(); - - expect(decoder.decode(Uint8Array.of(65, 66, 67))).toEqual('ABC'); - expect(decoder.decode(Uint8Array.of(0xC3, 0xA6))).toEqual('æ'); - expect(decoder.decode(Uint8Array.of(0xE2, 0x82, 0xAC))).toEqual('€'); - expect(decoder.decode(Uint8Array.of(0xF0, 0x9F, 0x92, 0xA9))).toEqual('\uD83D\uDCA9'); - }); - - it('should ignore byte-order marks', function() { - const decoder = new TextDecoderPolyfill(); - expect(decoder.decode(Uint8Array.of(0xEF, 0xBB, 0xBF, 65))) - .toEqual('A'); - }); - - it('should not ignore byte-order marks in the middle of the array', function() { - const decoder = new TextDecoderPolyfill(); - expect(decoder.decode(Uint8Array.of(65, 0xEF, 0xBB, 0xBF, 66))) - .toEqual('A\uFEFFB'); - }); - - it('should reject overlong encodings', function() { - const decoder = new TextDecoderPolyfill(); - - // euro, as 4 bytes - expect(decoder.decode(Uint8Array.of(65, 0xF0, 0x82, 0x82, 0xAC, 67))) - .toEqual('A\uFFFD\uFFFD\uFFFD\uFFFDC'); - }); - - it('should reject 5 and 6-byte encodings', function() { - const decoder = new TextDecoderPolyfill(); - - expect(decoder.decode(Uint8Array.of(65, 0xF8, 0x82, 0x82, 0x82, 0x82, 67))) - .toEqual('A\uFFFD\uFFFD\uFFFD\uFFFD\uFFFDC'); - }); - - it('should reject code points beyond 0x10000', function() { - const decoder = new TextDecoderPolyfill(); - - expect(decoder.decode(Uint8Array.of(0xF4, 0xA0, 0x80, 0x80))) - .toEqual('\uFFFD\uFFFD\uFFFD\uFFFD'); - }); - - it('should cope with end-of-string', function() { - const decoder = new TextDecoderPolyfill(); - - expect(decoder.decode(Uint8Array.of(65, 0xC3))) - .toEqual('A\uFFFD'); - - expect(decoder.decode(Uint8Array.of(65, 0xE2, 0x82))) - .toEqual('A\uFFFD\uFFFD'); - - expect(decoder.decode(Uint8Array.of(65, 0xF0, 0x9F, 0x92))) - .toEqual('A\uFFFD\uFFFD\uFFFD'); - }); - -}); diff --git a/test/utils/TextEncoderPolyfill-test.js b/test/utils/TextEncoderPolyfill-test.js deleted file mode 100644 index 4f422ec375..0000000000 --- a/test/utils/TextEncoderPolyfill-test.js +++ /dev/null @@ -1,39 +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. -*/ - -"use strict"; - -import TextEncoderPolyfill from 'utils/TextEncoderPolyfill'; - -import * as testUtils from '../test-utils'; -import expect from 'expect'; - -describe('textEncoderPolyfill', function() { - beforeEach(function() { - testUtils.beforeEach(this); - }); - - it('should correctly encode a range of strings', function() { - const encoder = new TextEncoderPolyfill(); - - expect(encoder.encode('ABC')).toEqual(Uint8Array.of(65, 66, 67)); - expect(encoder.encode('æ')).toEqual(Uint8Array.of(0xC3, 0xA6)); - expect(encoder.encode('€')).toEqual(Uint8Array.of(0xE2, 0x82, 0xAC)); - - // PILE OF POO (💩) - expect(encoder.encode('\uD83D\uDCA9')).toEqual(Uint8Array.of(0xF0, 0x9F, 0x92, 0xA9)); - }); -}); From 5ef5204c8c974b8ada599e573515031e5dd2fc47 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 18 Jan 2017 12:48:28 +0100 Subject: [PATCH 19/57] Implement simple team-based registration (#620) * Implement simple team-based registration Config required goes in the `teams` top-level property in config.json. This consists of an array of team objects: ```json { "name": "University of Bath", "emailSuffix": "bath.ac.uk" } ``` These can be selected on registration and require a user to have a certain email address in order to register as part of a team. This is for vector-im/riot-web#2940. The next step would be sending users with emails matching the emailSuffix of a team to the correct welcome page as in vector-im/riot-web#2430. --- src/components/structures/MatrixChat.js | 1 + .../structures/login/Registration.js | 11 ++ .../views/login/RegistrationForm.js | 108 ++++++++++++++++-- 3 files changed, 108 insertions(+), 12 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index c47109db94..4d98e3f09e 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1003,6 +1003,7 @@ module.exports = React.createClass({ defaultHsUrl={this.getDefaultHsUrl()} defaultIsUrl={this.getDefaultIsUrl()} brand={this.props.config.brand} + teamsConfig={this.props.config.teamsConfig} customHsUrl={this.getCurrentHsUrl()} customIsUrl={this.getCurrentIsUrl()} registrationUrl={this.props.registrationUrl} diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 269aabed9b..fb24b61504 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -49,6 +49,16 @@ module.exports = React.createClass({ email: React.PropTypes.string, username: React.PropTypes.string, guestAccessToken: React.PropTypes.string, + teamsConfig: React.PropTypes.shape({ + // Email address to request new teams + supportEmail: React.PropTypes.string, + teams: React.PropTypes.arrayOf(React.PropTypes.shape({ + // The displayed name of the team + "name": React.PropTypes.string, + // The suffix with which every team email address ends + "emailSuffix": React.PropTypes.string, + })).required, + }), defaultDeviceDisplayName: React.PropTypes.string, @@ -254,6 +264,7 @@ module.exports = React.createClass({ defaultUsername={this.state.formVals.username} defaultEmail={this.state.formVals.email} defaultPassword={this.state.formVals.password} + teamsConfig={this.props.teamsConfig} guestUsername={this.props.username} minPasswordLength={MIN_PASSWORD_LENGTH} onError={this.onFormValidationFailed} diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 33809fbfd6..3e07302a91 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -38,6 +38,16 @@ module.exports = React.createClass({ defaultEmail: React.PropTypes.string, defaultUsername: React.PropTypes.string, defaultPassword: React.PropTypes.string, + teamsConfig: React.PropTypes.shape({ + // Email address to request new teams + supportEmail: React.PropTypes.string, + teams: React.PropTypes.arrayOf(React.PropTypes.shape({ + // The displayed name of the team + "name": React.PropTypes.string, + // The suffix with which every team email address ends + "emailSuffix": React.PropTypes.string, + })).required, + }), // A username that will be used if no username is entered. // Specifying this param will also warn the user that entering @@ -62,7 +72,8 @@ module.exports = React.createClass({ getInitialState: function() { return { - fieldValid: {} + fieldValid: {}, + selectedTeam: null, }; }, @@ -119,6 +130,25 @@ module.exports = React.createClass({ } }, + onSelectTeam: function(teamIndex) { + let team = this._getSelectedTeam(teamIndex); + if (team) { + this.refs.email.value = this.refs.email.value.split("@")[0]; + } + this.setState({ + selectedTeam: team, + showSupportEmail: teamIndex === "other", + }); + }, + + _getSelectedTeam: function(teamIndex) { + if (this.props.teamsConfig && + this.props.teamsConfig.teams[teamIndex]) { + return this.props.teamsConfig.teams[teamIndex]; + } + return null; + }, + /** * Returns true if all fields were valid last time * they were validated. @@ -139,11 +169,15 @@ module.exports = React.createClass({ switch (field_id) { case FIELD_EMAIL: - this.markFieldValid( - field_id, - this.refs.email.value == '' || Email.looksValid(this.refs.email.value), - "RegistrationForm.ERR_EMAIL_INVALID" - ); + let email = this.refs.email.value; + if (this.props.teamsConfig) { + let team = this.state.selectedTeam; + if (team) { + email = email + "@" + team.emailSuffix; + } + } + let valid = email === '' || Email.looksValid(email); + this.markFieldValid(field_id, valid, "RegistrationForm.ERR_EMAIL_INVALID"); break; case FIELD_USERNAME: // XXX: SPEC-1 @@ -222,17 +256,64 @@ module.exports = React.createClass({ return cls; }, + _renderEmailInputSuffix: function() { + let suffix = null; + if (!this.state.selectedTeam) { + return suffix; + } + let team = this.state.selectedTeam; + if (team) { + suffix = "@" + team.emailSuffix; + } + return suffix; + }, + render: function() { var self = this; - var emailSection, registerButton; + var emailSection, teamSection, teamAdditionSupport, registerButton; if (this.props.showEmail) { + let emailSuffix = this._renderEmailInputSuffix(); emailSection = ( - +
+ + {emailSuffix ? : null } +
); + if (this.props.teamsConfig) { + teamSection = ( + + ); + if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) { + teamAdditionSupport = ( + + If your team is not listed, email  + + {this.props.teamsConfig.supportEmail} + + + ); + } + } } if (this.props.onRegisterClick) { registerButton = ( @@ -248,6 +329,9 @@ module.exports = React.createClass({ return (
+ {teamSection} + {teamAdditionSupport} +
{emailSection}
Date: Wed, 18 Jan 2017 14:25:11 +0100 Subject: [PATCH 20/57] Markdown: Add comment about out function override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johannes Löthberg --- src/Markdown.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Markdown.js b/src/Markdown.js index e6f5f59f01..35ae42f770 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -78,6 +78,9 @@ export default class Markdown { } } } else { + // The default `out` function only sends the input through an XML + // escaping function, which causes messages to be entity encoded, + // which we don't want in this case. renderer.out = function(s) { this.lit(s); } From 3d30553b7fa21f5884abd26c27212626f38d2ce2 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 18 Jan 2017 14:06:47 +0000 Subject: [PATCH 21/57] review fixes, plus unbreak to work with new webpack layout --- src/Tinter.js | 26 ++- src/components/structures/MatrixChat.js | 9 +- src/components/structures/UserSettings.js | 184 ++++++++++++---------- 3 files changed, 123 insertions(+), 96 deletions(-) diff --git a/src/Tinter.js b/src/Tinter.js index c18d3068a7..4a5e4e453c 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -174,7 +174,7 @@ module.exports = { tintables.push(tintable); }, - tint: function(primaryColor, secondaryColor, tertiaryColor, whiteColor) { + tint: function(primaryColor, secondaryColor, tertiaryColor) { if (!cached) { calcCssFixups(); @@ -205,19 +205,16 @@ module.exports = { tertiaryColor = rgbToHex(rgb1); } - if (!whiteColor) { - whiteColor = colors[3]; - } - if (colors[0] === primaryColor && colors[1] === secondaryColor && - colors[2] === tertiaryColor && - colors[3] === whiteColor) + colors[2] === tertiaryColor) { return; } - colors = [primaryColor, secondaryColor, tertiaryColor, whiteColor]; + colors[0] = primaryColor; + colors[1] = secondaryColor; + colors[2] = tertiaryColor; if (DEBUG) console.log("Tinter.tint"); @@ -231,6 +228,19 @@ module.exports = { }); }, + tintSvgWhite: function(whiteColor) { + if (!whiteColor) { + whiteColor = colors[3]; + } + if (colors[3] === whiteColor) { + return; + } + colors[3] = whiteColor; + tintables.forEach(function(tintable) { + tintable(); + }); + }, + // XXX: we could just move this all into TintableSvg, but as it's so similar // to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg) // keeping it here for now. diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 7d74e2ee02..8917f0535e 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -608,8 +608,9 @@ module.exports = React.createClass({ var i, a; for (i = 0; (a = document.getElementsByTagName("link")[i]); i++) { var href = a.getAttribute("href"); - if (href.startsWith("theme-")) { - if (href.startsWith("theme-" + theme + ".")) { + var match = href.match(/^bundles\/.*\/theme-(.*)\.css$/); + if (match) { + if (match[1] === theme) { a.disabled = false; } else { @@ -621,10 +622,10 @@ module.exports = React.createClass({ if (theme === 'dark') { // abuse the tinter to change all the SVG's #fff to #2d2d2d // XXX: obviously this shouldn't be hardcoded here. - Tinter.tint(undefined, undefined, undefined, '#2d2d2d'); + Tinter.tintSvgWhite('#2d2d2d'); } else { - Tinter.tint(undefined, undefined, undefined, '#ffffff'); + Tinter.tintSvgWhite('#ffffff'); } }, diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index ebfd6dc0a7..a120d365d1 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -32,6 +32,47 @@ var AddThreepid = require('../../AddThreepid'); const REACT_SDK_VERSION = 'dist' in package_json ? package_json.version : package_json.gitHead || ""; + +// Enumerate some simple 'flip a bit' UI settings (if any) +const SETTINGS_LABELS = [ +/* + { + id: 'alwaysShowTimestamps', + label: 'Always show message timestamps', + }, + { + id: 'showTwelveHourTimestamps', + label: 'Show timestamps in 12 hour format (e.g. 2:30pm)', + }, + { + id: 'useCompactLayout', + label: 'Use compact timeline layout', + }, + { + id: 'useFixedWidthFont', + label: 'Use fixed width font', + }, +*/ +]; + +// Enumerate the available themes, with a nice human text label. +// XXX: Ideally we would have a theme manifest or something and they'd be nicely +// packaged up in a single directory, and/or located at the application layer. +// But for now for expedience we just hardcode them here. +const THEMES = [ + { + id: 'theme', + label: 'Light theme', + value: 'light', + }, + { + id: 'theme', + label: 'Dark theme', + value: 'dark', + } +]; + + module.exports = React.createClass({ displayName: 'UserSettings', @@ -93,6 +134,12 @@ module.exports = React.createClass({ middleOpacity: 0.3, }); this._refreshFromServer(); + + var syncedSettings = UserSettingsStore.getSyncedSettings(); + if (!syncedSettings.theme) { + syncedSettings.theme = 'light'; + } + this._syncedSettings = syncedSettings; }, componentDidMount: function() { @@ -342,99 +389,68 @@ module.exports = React.createClass({ _renderUserInterfaceSettings: function() { var client = MatrixClientPeg.get(); - var settingsLabels = [ - /* - { - id: 'alwaysShowTimestamps', - label: 'Always show message timestamps', - }, - { - id: 'showTwelveHourTimestamps', - label: 'Show timestamps in 12 hour format (e.g. 2:30pm)', - }, - { - id: 'useCompactLayout', - label: 'Use compact timeline layout', - }, - { - id: 'useFixedWidthFont', - label: 'Use fixed width font', - }, - */ - ]; - - var themes = [ - { - id: 'theme', - label: 'Light theme', - value: 'light', - }, - { - id: 'theme', - label: 'Dark theme', - value: 'dark', - } - ]; - - var syncedSettings = UserSettingsStore.getSyncedSettings(); - if (!syncedSettings.theme) { - syncedSettings.theme = 'light'; - } - return (

User Interface

-
- UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) } - /> - -
- { settingsLabels.map( setting => { - return
- UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) } - /> - -
- })} - { themes.map( setting => { - return
- { - if (e.target.checked) { - UserSettingsStore.setSyncedSetting(setting.id, setting.value) - } - dis.dispatch({ - action: 'set_theme', - value: setting.value, - }); - } - } - /> - -
- })} + { this._renderUrlPreviewSelector() } + { SETTINGS_LABELS.map( this._renderSyncedSetting ) } + { THEMES.map( this._renderThemeSelector ) }
); }, + _renderUrlPreviewSelector: function() { + return
+ UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) } + /> + +
+ }, + + _renderSyncedSetting: function(setting) { + return
+ UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) } + /> + +
+ }, + + _renderThemeSelector: function(setting) { + return
+ { + if (e.target.checked) { + UserSettingsStore.setSyncedSetting(setting.id, setting.value) + } + dis.dispatch({ + action: 'set_theme', + value: setting.value, + }); + } + } + /> + +
+ }, + _renderCryptoInfo: function() { const client = MatrixClientPeg.get(); const deviceId = client.deviceId; From fc630672378bb292dcde1188255deb830a486596 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 18 Jan 2017 16:36:27 +0000 Subject: [PATCH 22/57] improve commentary --- src/components/structures/UserSettings.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index a120d365d1..5ce9ab1a15 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -33,7 +33,9 @@ const REACT_SDK_VERSION = 'dist' in package_json ? package_json.version : package_json.gitHead || ""; -// Enumerate some simple 'flip a bit' UI settings (if any) +// Enumerate some simple 'flip a bit' UI settings (if any). +// 'id' gives the key name in the im.vector.web.settings account data event +// 'label' is how we describe it in the UI. const SETTINGS_LABELS = [ /* { @@ -56,6 +58,10 @@ const SETTINGS_LABELS = [ ]; // Enumerate the available themes, with a nice human text label. +// 'id' gives the key name in the im.vector.web.settings account data event +// 'value' is the value for that key in the event +// 'label' is how we describe it in the UI. +// // XXX: Ideally we would have a theme manifest or something and they'd be nicely // packaged up in a single directory, and/or located at the application layer. // But for now for expedience we just hardcode them here. From f105ec279451cf8866fc1a202a546458fb5cde50 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 18 Jan 2017 17:51:39 +0000 Subject: [PATCH 23/57] Attempt to sanitize ChatInviteDialog a bit * Use binds rather than onFoo functions which aren't actually handler functions themselves but return them * Rename onKeyUp to moveSelectionDown etc,, reserving onKeyUp for "a key has been released" rather than, "the up arrow key has been pressed" --- .../views/dialogs/ChatInviteDialog.js | 6 ++--- .../views/elements/AddressSelector.js | 24 +++++++------------ 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index e9a041357f..9ca3ff635d 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -119,15 +119,15 @@ module.exports = React.createClass({ } else if (e.keyCode === 38) { // up arrow e.stopPropagation(); e.preventDefault(); - this.addressSelector.onKeyUp(); + this.addressSelector.moveSelectionUp(); } else if (e.keyCode === 40) { // down arrow e.stopPropagation(); e.preventDefault(); - this.addressSelector.onKeyDown(); + this.addressSelector.moveSelectionDown(); } else if (this.state.queryList.length > 0 && (e.keyCode === 188, e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab e.stopPropagation(); e.preventDefault(); - this.addressSelector.onKeySelect(); + this.addressSelector.chooseSelection(); } else if (this.refs.textinput.value.length === 0 && this.state.inviteList.length && e.keyCode === 8) { // backspace e.stopPropagation(); e.preventDefault(); diff --git a/src/components/views/elements/AddressSelector.js b/src/components/views/elements/AddressSelector.js index 2c2d7e2d61..c477b8e7eb 100644 --- a/src/components/views/elements/AddressSelector.js +++ b/src/components/views/elements/AddressSelector.js @@ -55,7 +55,7 @@ module.exports = React.createClass({ } }, - onKeyUp: function() { + moveSelectionUp: function() { if (this.state.selected > 0) { this.setState({ selected: this.state.selected - 1, @@ -64,7 +64,7 @@ module.exports = React.createClass({ } }, - onKeyDown: function() { + moveSelectionDown: function() { if (this.state.selected < this._maxSelected(this.props.addressList)) { this.setState({ selected: this.state.selected + 1, @@ -73,25 +73,19 @@ module.exports = React.createClass({ } }, - onKeySelect: function() { + chooseSelection: function() { this.selectAddress(this.state.selected); }, onClick: function(index) { - var self = this; - return function() { - self.selectAddress(index); - }; + this.selectAddress(index); }, onMouseEnter: function(index) { - var self = this; - return function() { - self.setState({ - selected: index, - hover: true, - }); - }; + this.setState({ + selected: index, + hover: true, + }); }, onMouseLeave: function() { @@ -124,7 +118,7 @@ module.exports = React.createClass({ // Saving the addressListElement so we can use it to work out, in the componentDidUpdate // method, how far to scroll when using the arrow keys addressList.push( -
{ this.addressListElement = ref; }} > +
{ this.addressListElement = ref; }} >
); From 30bd01cdf2a2548cf536c7edc72a8950d1238175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Wed, 18 Jan 2017 19:29:11 +0100 Subject: [PATCH 24/57] Markdown: Split up render function into toHTML/toPlaintext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johannes Löthberg --- src/Markdown.js | 98 +++++++++++-------- .../views/rooms/MessageComposerInput.js | 6 +- .../views/rooms/MessageComposerInputOld.js | 4 +- 3 files changed, 61 insertions(+), 47 deletions(-) diff --git a/src/Markdown.js b/src/Markdown.js index 35ae42f770..80d1aa4335 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -23,7 +23,9 @@ import commonmark from 'commonmark'; */ export default class Markdown { constructor(input) { - this.input = input + this.input = input; + this.parser = new commonmark.Parser(); + this.renderer = new commonmark.HtmlRenderer({safe: false}); } isPlainText() { @@ -57,54 +59,66 @@ export default class Markdown { return is_plain; } - render(html) { - const parser = new commonmark.Parser(); + toHTML(html) { + const real_paragraph = this.renderer.paragraph; - const renderer = new commonmark.HtmlRenderer({safe: true}); - const real_paragraph = renderer.paragraph; - if (html) { - renderer.paragraph = function(node, entering) { - // If there is only one top level node, just return the - // bare text: it's a single line of text and so should be - // 'inline', rather than unnecessarily wrapped in its own - // p tag. If, however, we have multiple nodes, each gets - // its own p tag to keep them as separate paragraphs. - var par = node; - while (par.parent) { - par = par.parent - } - if (par.firstChild != par.lastChild) { - real_paragraph.call(this, node, entering); - } + this.renderer.paragraph = function(node, entering) { + // If there is only one top level node, just return the + // bare text: it's a single line of text and so should be + // 'inline', rather than unnecessarily wrapped in its own + // p tag. If, however, we have multiple nodes, each gets + // its own p tag to keep them as separate paragraphs. + var par = node; + while (par.parent) { + par = par.parent } - } else { - // The default `out` function only sends the input through an XML - // escaping function, which causes messages to be entity encoded, - // which we don't want in this case. - renderer.out = function(s) { - this.lit(s); + if (par.firstChild != par.lastChild) { + real_paragraph.call(this, node, entering); } + } - renderer.paragraph = function(node, entering) { - // If there is only one top level node, just return the - // bare text: it's a single line of text and so should be - // 'inline', rather than unnecessarily wrapped in its own - // p tag. If, however, we have multiple nodes, each gets - // its own p tag to keep them as separate paragraphs. - var par = node; - while (par.parent) { - node = par; - par = par.parent; - } - if (node != par.lastChild) { - if (!entering) { - this.lit('\n\n'); - } + var parsed = this.parser.parse(this.input); + var rendered = this.renderer.render(parsed); + + this.renderer.paragraph = real_paragraph; + + return rendered; + } + + toPlaintext() { + const real_paragraph = this.renderer.paragraph; + + // The default `out` function only sends the input through an XML + // escaping function, which causes messages to be entity encoded, + // which we don't want in this case. + this.renderer.out = function(s) { + // The `lit` function adds a string literal to the output buffer. + this.lit(s); + } + + this.renderer.paragraph = function(node, entering) { + // If there is only one top level node, just return the + // bare text: it's a single line of text and so should be + // 'inline', rather than unnecessarily wrapped in its own + // p tag. If, however, we have multiple nodes, each gets + // its own p tag to keep them as separate paragraphs. + var par = node; + while (par.parent) { + node = par; + par = par.parent; + } + if (node != par.lastChild) { + if (!entering) { + this.lit('\n\n'); } } } - var parsed = parser.parse(this.input); - return renderer.render(parsed); + var parsed = this.parser.parse(this.input); + var rendered = this.renderer.render(parsed); + + this.renderer.paragraph = real_paragraph; + + return rendered; } } diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 5e8df592da..41b27c1394 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -401,7 +401,7 @@ export default class MessageComposerInput extends React.Component { let contentState = null; if (enabled) { const md = new Markdown(this.state.editorState.getCurrentContent().getPlainText()); - contentState = RichText.HTMLtoContentState(md.render(true)); + contentState = RichText.HTMLtoContentState(md.toHTML()); } else { let markdown = stateToMarkdown(this.state.editorState.getCurrentContent()); if (markdown[markdown.length - 1] === '\n') { @@ -524,9 +524,9 @@ export default class MessageComposerInput extends React.Component { } else { const md = new Markdown(contentText); if (md.isPlainText()) { - contentText = md.render(false); + contentText = md.toPlaintext(); } else { - contentHTML = md.render(true); + contentHTML = md.toHTML(true); } } diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index 3b0100278b..91abd5a2a8 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -325,13 +325,13 @@ module.exports = React.createClass({ } if (send_markdown) { - const htmlText = mdown.render(true); + const htmlText = mdown.toHTML(); sendMessagePromise = isEmote ? MatrixClientPeg.get().sendHtmlEmote(this.props.room.roomId, contentText, htmlText) : MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText); } else { - const contentText = mdown.render(false); + const contentText = mdown.toPlaintext(false); sendMessagePromise = isEmote ? MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) : MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText); From 7b7728c93abec435c44ae66f218fa23ee23e35ef Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 18 Jan 2017 18:32:38 +0000 Subject: [PATCH 25/57] Make behaviour of ChatInviteDialog more consistent * Pressing enter now always adds whatever was in the input box to the invite list, if it's a valid address (previously it added it to the list of it was a search result but submitted the form straight away if there were no results). * Remove isValidAddress as it was only used in the context of testing whether its return value was true or null (where null meant "unsure") so just use getAddressType instead. --- src/Invite.js | 22 ----------------- .../views/dialogs/ChatInviteDialog.js | 24 +++++++++++++++---- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/src/Invite.js b/src/Invite.js index 6422812734..6bfc646977 100644 --- a/src/Invite.js +++ b/src/Invite.js @@ -59,25 +59,3 @@ export function inviteMultipleToRoom(roomId, addrs) { return this.inviter.invite(addrs); } -/** - * Checks is the supplied address is valid - * - * @param {addr} The mx userId or email address to check - * @returns true, false, or null for unsure - */ -export function isValidAddress(addr) { - // Check if the addr is a valid type - var addrType = this.getAddressType(addr); - if (addrType === "mx") { - let user = MatrixClientPeg.get().getUser(addr); - if (user) { - return true; - } else { - return null; - } - } else if (addrType === "email") { - return true; - } else { - return false; - } -} diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 9ca3ff635d..767620d93f 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -74,8 +74,8 @@ module.exports = React.createClass({ var inviteList = this.state.inviteList.slice(); // Check the text input field to see if user has an unconverted address // If there is and it's valid add it to the local inviteList - var check = Invite.isValidAddress(this.refs.textinput.value); - if (check === true || check === null) { + const addrType = Invite.getAddressType(this.refs.textinput.value); + if (addrType !== null) { inviteList.push(this.refs.textinput.value); } else if (this.refs.textinput.value.length > 0) { this.setState({ error: true }); @@ -135,12 +135,26 @@ module.exports = React.createClass({ } else if (e.keyCode === 13) { // enter e.stopPropagation(); e.preventDefault(); - this.onButtonClick(); + if (this.state.queryList.length > 0) { + this.addressSelector.chooseSelection(); + } else { + const addrType = Invite.getAddressType(this.refs.textinput.value); + if (addrType !== null) { + const inviteList = this.state.inviteList.slice(); + inviteList.push(this.refs.textinput.value.trim()); + this.setState({ + inviteList: inviteList, + queryList: [], + }); + } else { + this.setState({ error: true }); + } + } } else if (e.keyCode === 188 || e.keyCode === 9) { // comma or tab e.stopPropagation(); e.preventDefault(); - var check = Invite.isValidAddress(this.refs.textinput.value); - if (check === true || check === null) { + const addrType = Invite.getAddressType(this.refs.textinput.value); + if (addrType !== null) { var inviteList = this.state.inviteList.slice(); inviteList.push(this.refs.textinput.value.trim()); this.setState({ From 14ead373e2e8c6d1ed612000ddbee668b84f8989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Wed, 18 Jan 2017 20:54:34 +0100 Subject: [PATCH 26/57] Add markdown test-cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johannes Löthberg --- .../views/rooms/MessageComposerInput-test.js | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/test/components/views/rooms/MessageComposerInput-test.js b/test/components/views/rooms/MessageComposerInput-test.js index 8d33e0ead3..ca2bbba2eb 100644 --- a/test/components/views/rooms/MessageComposerInput-test.js +++ b/test/components/views/rooms/MessageComposerInput-test.js @@ -158,4 +158,85 @@ describe('MessageComposerInput', () => { expect(['__', '**']).toContain(spy.args[0][1]); }); + it('should not entity-encode " in Markdown mode', () => { + const spy = sinon.spy(client, 'sendTextMessage'); + mci.enableRichtext(false); + addTextToDraft('"'); + mci.handleReturn(sinon.stub()); + + expect(spy.calledOnce).toEqual(true); + expect(spy.args[0][1]).toEqual('"'); + }); + + it('should escape characters without other markup in Markdown mode', () => { + const spy = sinon.spy(client, 'sendTextMessage'); + mci.enableRichtext(false); + addTextToDraft('\\*escaped\\*'); + mci.handleReturn(sinon.stub()); + + expect(spy.calledOnce).toEqual(true); + expect(spy.args[0][1]).toEqual('*escaped*'); + }); + + it('should escape characters with other markup in Markdown mode', () => { + const spy = sinon.spy(client, 'sendHtmlMessage'); + mci.enableRichtext(false); + addTextToDraft('\\*escaped\\* *italic*'); + mci.handleReturn(sinon.stub()); + + expect(spy.calledOnce).toEqual(true); + expect(spy.args[0][1]).toEqual('\\*escaped\\* *italic*'); + expect(spy.args[0][2]).toEqual('*escaped* italic'); + }); + + it('should not convert -_- into a horizontal rule in Markdown mode', () => { + const spy = sinon.spy(client, 'sendTextMessage'); + mci.enableRichtext(false); + addTextToDraft('-_-'); + mci.handleReturn(sinon.stub()); + + expect(spy.calledOnce).toEqual(true); + expect(spy.args[0][1]).toEqual('-_-'); + }); + + it('should not strip tags in Markdown mode', () => { + const spy = sinon.spy(client, 'sendHtmlMessage'); + mci.enableRichtext(false); + addTextToDraft('striked-out'); + mci.handleReturn(sinon.stub()); + + expect(spy.calledOnce).toEqual(true); + expect(spy.args[0][1]).toEqual('striked-out'); + expect(spy.args[0][2]).toEqual('striked-out'); + }); + + it('should not strike-through ~~~ in Markdown mode', () => { + const spy = sinon.spy(client, 'sendTextMessage'); + mci.enableRichtext(false); + addTextToDraft('~~~striked-out~~~'); + mci.handleReturn(sinon.stub()); + + expect(spy.calledOnce).toEqual(true); + expect(spy.args[0][1]).toEqual('~~~striked-out~~~'); + }); + + it('should not mark single unmarkedup paragraphs as HTML in Markdown mode', () => { + const spy = sinon.spy(client, 'sendTextMessage'); + mci.enableRichtext(false); + addTextToDraft('Lorem ipsum dolor sit amet, consectetur adipiscing elit.'); + mci.handleReturn(sinon.stub()); + + expect(spy.calledOnce).toEqual(true); + expect(spy.args[0][1]).toEqual('Lorem ipsum dolor sit amet, consectetur adipiscing elit.'); + }); + + it('should not mark two unmarkedup paragraphs as HTML in Markdown mode', () => { + const spy = sinon.spy(client, 'sendTextMessage'); + mci.enableRichtext(false); + addTextToDraft('Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\nFusce congue sapien sed neque molestie volutpat.'); + mci.handleReturn(sinon.stub()); + + expect(spy.calledOnce).toEqual(true); + expect(spy.args[0][1]).toEqual('Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\nFusce congue sapien sed neque molestie volutpat.'); + }); }); From 4df968ecdf36334313f30d27aff409d40cdbe859 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 18 Jan 2017 20:06:44 +0000 Subject: [PATCH 27/57] fix css snafu --- src/components/structures/UserSettings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 5ce9ab1a15..498acc1917 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -605,7 +605,7 @@ module.exports = React.createClass({
- +
Remove From 4e5689082de009c3e6214e578831804942c120bd Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 18 Jan 2017 20:06:54 +0000 Subject: [PATCH 28/57] correctly load synced themes without NPE --- src/components/structures/MatrixChat.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 8917f0535e..0336dc99d5 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -588,13 +588,6 @@ module.exports = React.createClass({ _onLoadCompleted: function() { this.props.onLoadCompleted(); this.setState({loading: false}); - - // set up the right theme. - // XXX: this will temporarily flicker the wrong CSS. - dis.dispatch({ - action: 'set_theme', - value: UserSettingsStore.getSyncedSetting('theme') - }); }, /** @@ -730,6 +723,16 @@ module.exports = React.createClass({ action: 'logout' }); }); + cli.on("accountData", function(ev) { + if (ev.getType() === 'im.vector.web.settings') { + if (ev.getContent() && ev.getContent().theme) { + dis.dispatch({ + action: 'set_theme', + value: ev.getContent().theme, + }); + } + } + }); }, onFocus: function(ev) { From e06dd6e34ae5b70723d7f83603e0a100565fd641 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Thu, 19 Jan 2017 10:51:40 +0100 Subject: [PATCH 29/57] Implement auto-join rooms on registration Also: This fixes registration with a team: only the email localpart was being used to register. When a registration is successful, the user will be joined to rooms specified in the config.json teamsConfig: "teamsConfig" : { "supportEmail": "support@riot.im", "teams": [ { "name" : "matrix", "emailSuffix" : "matrix.org", "rooms" : [ { "id" : "#irc_matrix:matrix.org", "autoJoin" : true } ] } ] } autoJoin can of course be set to false if the room should only be displayed on the (forthcoming) welcome page for each team, and not auto-joined. --- src/components/structures/login/Registration.js | 17 +++++++++++++++++ src/components/views/login/RegistrationForm.js | 6 +++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index fb24b61504..f89b627e8d 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -179,6 +179,23 @@ module.exports = React.createClass({ accessToken: response.access_token }); + // Auto-join rooms + if (self.props.teamsConfig) { + for (let i = 0; i < self.props.teamsConfig.teams.length; i++) { + let team = self.props.teamsConfig.teams[i]; + if (self.state.formVals.email.endsWith(team.emailSuffix)) { + console.log("User successfully registered with team " + team.name); + team.rooms.forEach((room) => { + if (room.autoJoin) { + console.log("Auto-joining " + room.id); + MatrixClientPeg.get().joinRoom(room.id); + } + }); + break; + } + } + } + if (self.props.brand) { MatrixClientPeg.get().getPushers().done((resp)=>{ var pushers = resp.pushers; diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 3e07302a91..4be40bc53a 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -116,10 +116,14 @@ module.exports = React.createClass({ }, _doSubmit: function() { + let email = this.refs.email.value.trim(); + if (this.state.selectedTeam) { + email += "@" + this.state.selectedTeam.emailSuffix; + } var promise = this.props.onRegisterClick({ username: this.refs.username.value.trim() || this.props.guestUsername, password: this.refs.password.value.trim(), - email: this.refs.email.value.trim() + email: email, }); if (promise) { From e9eb38fd74cb13e85a0d31cd359894a1eea5d535 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Thu, 19 Jan 2017 11:05:08 +0100 Subject: [PATCH 30/57] Update propTypes and do null checks --- src/components/structures/login/Registration.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index f89b627e8d..b092e0a9fb 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -57,6 +57,11 @@ module.exports = React.createClass({ "name": React.PropTypes.string, // The suffix with which every team email address ends "emailSuffix": React.PropTypes.string, + // The rooms to use during auto-join + "rooms": React.PropTypes.arrayOf(React.PropTypes.shape({ + "id": React.PropTypes.string, + "autoJoin": React.PropTypes.bool, + })), })).required, }), @@ -180,11 +185,14 @@ module.exports = React.createClass({ }); // Auto-join rooms - if (self.props.teamsConfig) { + if (self.props.teamsConfig && self.props.teamsConfig.teams) { for (let i = 0; i < self.props.teamsConfig.teams.length; i++) { let team = self.props.teamsConfig.teams[i]; if (self.state.formVals.email.endsWith(team.emailSuffix)) { console.log("User successfully registered with team " + team.name); + if (!team.rooms) { + break; + } team.rooms.forEach((room) => { if (room.autoJoin) { console.log("Auto-joining " + room.id); From 242f5e03016e6aad73890c18914c0d2eaafcc8d4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Jan 2017 10:24:21 +0000 Subject: [PATCH 31/57] PR feedback * Doc & properly indent escapeRegExp * Add close bracket to the list of punctuation chars we search after --- src/components/views/dialogs/ChatInviteDialog.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 59e95d1538..dfeb2a3978 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -27,8 +27,13 @@ var Modal = require('../../../Modal'); const TRUNCATE_QUERY_LIST = 40; +/* + * Escapes a string so it can be used in a RegExp + * Basically just replaces: \ ^ $ * + ? . ( ) | { } [ ] + * From http://stackoverflow.com/a/6969486 + */ function escapeRegExp(str) { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); } module.exports = React.createClass({ @@ -326,7 +331,7 @@ module.exports = React.createClass({ // * The start of the string // * Whitespace, or // * A fixed number of punctuation characters - let expr = new RegExp("(?:^|[\\s\\('\",\.-])" + escapeRegExp(query)); + let expr = new RegExp("(?:^|[\\s\\(\)'\",\.-])" + escapeRegExp(query)); if (expr.test(name)) { return true; } From 8b60cb9df0594bb032a560428dfa1f67f068c08c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 19 Jan 2017 10:43:41 +0000 Subject: [PATCH 32/57] Megolm export: Clear bit 63 of the salt --- src/utils/MegolmExportEncryption.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js index e3ca7e68f2..983ec2c75f 100644 --- a/src/utils/MegolmExportEncryption.js +++ b/src/utils/MegolmExportEncryption.js @@ -106,6 +106,12 @@ export function encryptMegolmKeyFile(data, password, options) { const salt = new Uint8Array(16); window.crypto.getRandomValues(salt); + + // clear bit 63 of the salt to stop us hitting the 64-bit counter boundary + // (which would mean we wouldn't be able to decrypt on Android). The loss + // of a single bit of salt is a price we have to pay. + salt[9] &= 0x7f; + const iv = new Uint8Array(16); window.crypto.getRandomValues(iv); From fdc213cbb80f84a202c7d55fa79a7c45a66af4d0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 19 Jan 2017 10:44:01 +0000 Subject: [PATCH 33/57] Megolm export: fix test --- test/utils/MegolmExportEncryption-test.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/utils/MegolmExportEncryption-test.js b/test/utils/MegolmExportEncryption-test.js index db38a931ed..28752ae529 100644 --- a/test/utils/MegolmExportEncryption-test.js +++ b/test/utils/MegolmExportEncryption-test.js @@ -21,12 +21,6 @@ import * as MegolmExportEncryption from 'utils/MegolmExportEncryption'; import * as testUtils from '../test-utils'; import expect from 'expect'; -// polyfill textencoder if necessary -let TextEncoder = window.TextEncoder; -if (!TextEncoder) { - TextEncoder = require('utils/TextEncoderPolyfill'); -} - const TEST_VECTORS=[ [ "plain", From b58a67f6b19fec62731a57656c7a1f27246d9fc4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Jan 2017 10:51:43 +0000 Subject: [PATCH 34/57] Add more punctuation. Also s/let/const/ --- src/components/views/dialogs/ChatInviteDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index dfeb2a3978..b5222d77d0 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -331,7 +331,7 @@ module.exports = React.createClass({ // * The start of the string // * Whitespace, or // * A fixed number of punctuation characters - let expr = new RegExp("(?:^|[\\s\\(\)'\",\.-])" + escapeRegExp(query)); + const expr = new RegExp("(?:^|[\\s\\(\)'\",\.-_@\?;:{}\\[\\]\\#~`\\*\\&\\$])" + escapeRegExp(query)); if (expr.test(name)) { return true; } From 9c1c657a1edf157ec6039bf12b0f8cc848bca5f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Thu, 19 Jan 2017 11:55:36 +0100 Subject: [PATCH 35/57] Markdown: delete remaining pre-split relics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johannes Löthberg --- src/Markdown.js | 2 +- src/components/views/rooms/MessageComposerInput.js | 2 +- src/components/views/rooms/MessageComposerInputOld.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Markdown.js b/src/Markdown.js index 80d1aa4335..3506e3cb59 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -59,7 +59,7 @@ export default class Markdown { return is_plain; } - toHTML(html) { + toHTML() { const real_paragraph = this.renderer.paragraph; this.renderer.paragraph = function(node, entering) { diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 41b27c1394..b6af5a9f09 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -526,7 +526,7 @@ export default class MessageComposerInput extends React.Component { if (md.isPlainText()) { contentText = md.toPlaintext(); } else { - contentHTML = md.toHTML(true); + contentHTML = md.toHTML(); } } diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index 91abd5a2a8..ed4533737f 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -331,7 +331,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText); } else { - const contentText = mdown.toPlaintext(false); + const contentText = mdown.toPlaintext(); sendMessagePromise = isEmote ? MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) : MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText); From 1b6735d7292e7e0ee6a4331f687818b1f5fb4506 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 19 Jan 2017 12:51:20 +0000 Subject: [PATCH 36/57] Configure travis to test riot-web after building --- .travis-test-riot.sh | 15 +++++++++++++++ .travis.yml | 3 +++ 2 files changed, 18 insertions(+) create mode 100755 .travis-test-riot.sh diff --git a/.travis-test-riot.sh b/.travis-test-riot.sh new file mode 100755 index 0000000000..b488045df1 --- /dev/null +++ b/.travis-test-riot.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# +# script which is run by the travis build (after `npm run test`). +# +# clones riot-web develop and runs the tests against our version of react-sdk. + +set -ev + +git clone --depth=1 https://github.com/vector-im/riot-web.git riot-web +cd riot-web +mkdir node_modules +ln -s ../.. node_modules/matrix-react-sdk +npm install +(cd node_modules/matrix-js-sdk && npm install) +npm run test diff --git a/.travis.yml b/.travis.yml index 6d68b66a1c..9a8f804644 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,3 +4,6 @@ node_js: install: - npm install - (cd node_modules/matrix-js-sdk && npm install) +script: + - npm run test + - ./.travis-test-riot.sh From a88f9fdd73095c03423f2c5d649ea4930aa55b0a Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 19 Jan 2017 15:36:57 +0000 Subject: [PATCH 37/57] (hopefully) fix theming on Chrome Jump through some hoops to make Chrome behave sensibly on non-default themes. --- src/components/structures/MatrixChat.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 0336dc99d5..61a02057ea 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -598,20 +598,32 @@ module.exports = React.createClass({ theme = 'light'; } + // look for the stylesheet elements. + // styleElements is a map from style name to HTMLLinkElement. + var styleElements = Object.create(null); var i, a; for (i = 0; (a = document.getElementsByTagName("link")[i]); i++) { var href = a.getAttribute("href"); + // shouldn't we be using the 'title' tag rather than the href? var match = href.match(/^bundles\/.*\/theme-(.*)\.css$/); if (match) { - if (match[1] === theme) { - a.disabled = false; - } - else { - a.disabled = true; - } + styleElements[match[1]] = a; } } + if (!(theme in styleElements)) { + throw new Error("Unknown theme " + theme); + } + + // disable all of them first, then enable the one we want. Chrome only + // bothers to do an update on a true->false transition, so this ensures + // that we get exactly one update, at the right time. + + Object.values(styleElements).forEach((a) => { + a.disabled = true; + }); + styleElements[theme].disabled = false; + if (theme === 'dark') { // abuse the tinter to change all the SVG's #fff to #2d2d2d // XXX: obviously this shouldn't be hardcoded here. From 2a08abaa955a9e60a06584ef7203e07743d70928 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Jan 2017 16:35:40 +0000 Subject: [PATCH 38/57] Keep old behaviour of submitting on enter if input is empty --- src/components/views/dialogs/ChatInviteDialog.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 767620d93f..c117944482 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -135,7 +135,10 @@ module.exports = React.createClass({ } else if (e.keyCode === 13) { // enter e.stopPropagation(); e.preventDefault(); - if (this.state.queryList.length > 0) { + if (this.refs.textinput.value == '') { + // if there's nothing in the input box, submit the form + this.onButtonClick(); + } else if (this.state.queryList.length > 0) { this.addressSelector.chooseSelection(); } else { const addrType = Invite.getAddressType(this.refs.textinput.value); From ee1f6c772e164d24e0896c88be200f75d8e371aa Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Jan 2017 16:50:09 +0000 Subject: [PATCH 39/57] Remove duplicate case handled above And fix typo where it was handled --- src/components/views/dialogs/ChatInviteDialog.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index c117944482..5cbb0d4503 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -124,7 +124,7 @@ module.exports = React.createClass({ e.stopPropagation(); e.preventDefault(); this.addressSelector.moveSelectionDown(); - } else if (this.state.queryList.length > 0 && (e.keyCode === 188, e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab + } else if (this.state.queryList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab e.stopPropagation(); e.preventDefault(); this.addressSelector.chooseSelection(); @@ -138,8 +138,6 @@ module.exports = React.createClass({ if (this.refs.textinput.value == '') { // if there's nothing in the input box, submit the form this.onButtonClick(); - } else if (this.state.queryList.length > 0) { - this.addressSelector.chooseSelection(); } else { const addrType = Invite.getAddressType(this.refs.textinput.value); if (addrType !== null) { From a2ff1cd8e6578d237720fb0ac77885677c4e4775 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Jan 2017 17:03:16 +0000 Subject: [PATCH 40/57] Factor out adding the input field to the list --- .../views/dialogs/ChatInviteDialog.js | 51 ++++++++----------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 5cbb0d4503..9928030d7f 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -71,15 +71,12 @@ module.exports = React.createClass({ }, onButtonClick: function() { - var inviteList = this.state.inviteList.slice(); + let inviteList = this.state.inviteList.slice(); // Check the text input field to see if user has an unconverted address // If there is and it's valid add it to the local inviteList - const addrType = Invite.getAddressType(this.refs.textinput.value); - if (addrType !== null) { - inviteList.push(this.refs.textinput.value); - } else if (this.refs.textinput.value.length > 0) { - this.setState({ error: true }); - return; + if (this.refs.textinput.value !== '') { + inviteList = this._addInputToList(); + if (inviteList === false) return; } if (inviteList.length > 0) { @@ -139,32 +136,12 @@ module.exports = React.createClass({ // if there's nothing in the input box, submit the form this.onButtonClick(); } else { - const addrType = Invite.getAddressType(this.refs.textinput.value); - if (addrType !== null) { - const inviteList = this.state.inviteList.slice(); - inviteList.push(this.refs.textinput.value.trim()); - this.setState({ - inviteList: inviteList, - queryList: [], - }); - } else { - this.setState({ error: true }); - } + this._addInputToList(); } } else if (e.keyCode === 188 || e.keyCode === 9) { // comma or tab e.stopPropagation(); e.preventDefault(); - const addrType = Invite.getAddressType(this.refs.textinput.value); - if (addrType !== null) { - var inviteList = this.state.inviteList.slice(); - inviteList.push(this.refs.textinput.value.trim()); - this.setState({ - inviteList: inviteList, - queryList: [], - }); - } else { - this.setState({ error: true }); - } + this._addInputToList(); } }, @@ -376,6 +353,22 @@ module.exports = React.createClass({ return addrs; }, + _addInputToList: function() { + const addrType = Invite.getAddressType(this.refs.textinput.value); + if (addrType !== null) { + const inviteList = this.state.inviteList.slice(); + inviteList.push(this.refs.textinput.value.trim()); + this.setState({ + inviteList: inviteList, + queryList: [], + }); + return inviteList; + } else { + this.setState({ error: true }); + return false; + } + }, + render: function() { var TintableSvg = sdk.getComponent("elements.TintableSvg"); var AddressSelector = sdk.getComponent("elements.AddressSelector"); From dee3495d51b850f7765da766cc063f52284f0c1d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 19 Jan 2017 17:21:03 +0000 Subject: [PATCH 41/57] Run riot-web develop, not master --- .travis-test-riot.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis-test-riot.sh b/.travis-test-riot.sh index b488045df1..c483ea7b4e 100755 --- a/.travis-test-riot.sh +++ b/.travis-test-riot.sh @@ -6,7 +6,7 @@ set -ev -git clone --depth=1 https://github.com/vector-im/riot-web.git riot-web +git clone --depth=1 --branch develop https://github.com/vector-im/riot-web.git riot-web cd riot-web mkdir node_modules ln -s ../.. node_modules/matrix-react-sdk From af4ef1da8bb99e1b4cb2ac0cd22bac39351b2ce9 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 19 Jan 2017 17:41:58 +0000 Subject: [PATCH 42/57] Install source-map-loader before building riot-web --- .travis-test-riot.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis-test-riot.sh b/.travis-test-riot.sh index c483ea7b4e..482b4a21c1 100755 --- a/.travis-test-riot.sh +++ b/.travis-test-riot.sh @@ -11,5 +11,10 @@ cd riot-web mkdir node_modules ln -s ../.. node_modules/matrix-react-sdk npm install + (cd node_modules/matrix-js-sdk && npm install) + +# https://github.com/webpack/webpack/issues/1472 workaround +(cd node_modules/matrix-react-sdk && npm install source-map-loader) + npm run test From afa384c4f39e466c22dc6b0552ad9552a80c28bd Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Jan 2017 18:13:27 +0000 Subject: [PATCH 43/57] Use null instead of false --- src/components/views/dialogs/ChatInviteDialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 9928030d7f..18f4ce36ba 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -76,7 +76,7 @@ module.exports = React.createClass({ // If there is and it's valid add it to the local inviteList if (this.refs.textinput.value !== '') { inviteList = this._addInputToList(); - if (inviteList === false) return; + if (inviteList === null) return; } if (inviteList.length > 0) { @@ -365,7 +365,7 @@ module.exports = React.createClass({ return inviteList; } else { this.setState({ error: true }); - return false; + return null; } }, From 33d741e83d826b8540277f4869e84b08b010449d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 19 Jan 2017 18:23:34 +0000 Subject: [PATCH 44/57] Do riot-web setup in a different order ... in an effort to stop npm cocking it up. --- .travis-test-riot.sh | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.travis-test-riot.sh b/.travis-test-riot.sh index 482b4a21c1..c280044246 100755 --- a/.travis-test-riot.sh +++ b/.travis-test-riot.sh @@ -6,15 +6,20 @@ set -ev -git clone --depth=1 --branch develop https://github.com/vector-im/riot-web.git riot-web -cd riot-web +RIOT_WEB_DIR=riot-web +REACT_SDK_DIR=`pwd` + +git clone --depth=1 --branch develop https://github.com/vector-im/riot-web.git \ + "$RIOT_WEB_DIR" + +cd "$RIOT_WEB_DIR" + mkdir node_modules -ln -s ../.. node_modules/matrix-react-sdk npm install (cd node_modules/matrix-js-sdk && npm install) -# https://github.com/webpack/webpack/issues/1472 workaround -(cd node_modules/matrix-react-sdk && npm install source-map-loader) +rm -r node_modules/matrix-react-sdk +ln -s "$REACT_SDK_DIR" node_modules/matrix-react-sdk npm run test From b76b0f755d0d5698383e0bbe9ecbf2e9d090e86a Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 20 Jan 2017 02:47:47 +0000 Subject: [PATCH 45/57] fix context menu bg --- src/components/structures/MatrixChat.js | 1 - src/components/views/rooms/RoomTile.js | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 61a02057ea..170fa6ff84 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -25,7 +25,6 @@ var SdkConfig = require("../../SdkConfig"); var ContextualMenu = require("./ContextualMenu"); var RoomListSorter = require("../../RoomListSorter"); var UserActivity = require("../../UserActivity"); -var UserSettingsStore = require('../../UserSettingsStore'); var Presence = require("../../Presence"); var dis = require("../../dispatcher"); diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 84916f8ab8..834a06d8e6 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -26,6 +26,7 @@ var sdk = require('../../../index'); var ContextualMenu = require('../../structures/ContextualMenu'); var RoomNotifs = require('../../../RoomNotifs'); var FormattingUtils = require('../../../utils/FormattingUtils'); +var UserSettingsStore = require('../../../UserSettingsStore'); module.exports = React.createClass({ displayName: 'RoomTile', @@ -176,7 +177,8 @@ module.exports = React.createClass({ var self = this; ContextualMenu.createMenu(RoomTagMenu, { chevronOffset: 10, - menuColour: "#FFFFFF", + // XXX: fix horrid hardcoding + menuColour: UserSettingsStore.getSyncedSettings().theme === 'dark' ? "#2d2d2d" : "#FFFFFF", left: x, top: y, room: this.props.room, From 2d060c8d2e91669ebc7a17ba0745e99d72c196dc Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 20 Jan 2017 11:04:09 +0000 Subject: [PATCH 46/57] Fix error display in account deactivate dialog --- src/components/views/dialogs/DeactivateAccountDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.js index 926e4059d2..9a15841061 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.js +++ b/src/components/views/dialogs/DeactivateAccountDialog.js @@ -80,7 +80,7 @@ export default class DeactivateAccountDialog extends React.Component { let error = null; if (this.state.errStr) { error =
- {this.state.err_str} + {this.state.errStr}
passwordBoxClass = 'error'; } From a16aeeef2a0f16efedf7e6616cdf3c2c8752a077 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 18 Jan 2017 17:22:41 +0000 Subject: [PATCH 47/57] Temporarily add more logging, to try to catch failures on travis --- src/components/structures/MatrixChat.js | 6 ++++-- src/dispatcher.js | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 170fa6ff84..ceca3591c5 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -259,6 +259,8 @@ module.exports = React.createClass({ }, onAction: function(payload) { + console.log("onAction: "+payload.action); + var roomIndexDelta = 1; var self = this; @@ -1006,8 +1008,8 @@ module.exports = React.createClass({ var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); var LoggedInView = sdk.getComponent('structures.LoggedInView'); - // console.log("rendering; loading="+this.state.loading+"; screen="+this.state.screen + - // "; logged_in="+this.state.logged_in+"; ready="+this.state.ready); + console.log("rendering; loading="+this.state.loading+"; screen="+this.state.screen + + "; logged_in="+this.state.logged_in+"; ready="+this.state.ready); if (this.state.loading) { var Spinner = sdk.getComponent('elements.Spinner'); diff --git a/src/dispatcher.js b/src/dispatcher.js index f35639c3be..22ef712b6c 100644 --- a/src/dispatcher.js +++ b/src/dispatcher.js @@ -28,6 +28,7 @@ class MatrixDispatcher extends flux.Dispatcher { * for. */ dispatch(payload, sync) { + console.log("Dispatch: "+payload.action); if (sync) { super.dispatch(payload); } else { From 4ba224aac3cccf26537b5bb339dc438239b6b741 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 20 Jan 2017 14:08:14 +0000 Subject: [PATCH 48/57] Use eslint config from the js sdk Extend the js sdk's eslint config to give as consistent a code style as possible. Add react/jsx/flow stuff that we use here. --- .eslintrc | 117 --------------------------------------------------- .eslintrc.js | 62 +++++++++++++++++++++++++++ package.json | 9 ++-- 3 files changed, 67 insertions(+), 121 deletions(-) delete mode 100644 .eslintrc create mode 100644 .eslintrc.js diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index e2baaed5a6..0000000000 --- a/.eslintrc +++ /dev/null @@ -1,117 +0,0 @@ -{ - "parser": "babel-eslint", - "plugins": [ - "react", - "flowtype" - ], - "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module", - "ecmaFeatures": { - "jsx": true, - "impliedStrict": true - } - }, - "env": { - "browser": true, - "amd": true, - "es6": true, - "node": true, - "mocha": true - }, - "extends": ["eslint:recommended", "plugin:react/recommended"], - "rules": { - "no-undef": ["warn"], - "global-strict": ["off"], - "no-extra-semi": ["warn"], - "no-underscore-dangle": ["off"], - "no-console": ["off"], - "no-unused-vars": ["off"], - "no-trailing-spaces": ["warn", { - "skipBlankLines": true - }], - "no-unreachable": ["warn"], - "no-spaced-func": ["warn"], - "no-new-func": ["error"], - "no-new-wrappers": ["error"], - "no-invalid-regexp": ["error"], - "no-extra-bind": ["error"], - "no-magic-numbers": ["error", { - "ignore": [-1, 0, 1], // usually used in array/string indexing - "ignoreArrayIndexes": true, - "enforceConst": true, - "detectObjects": true - }], - "consistent-return": ["error"], - "valid-jsdoc": ["error"], - "no-use-before-define": ["error"], - "camelcase": ["warn"], - "array-callback-return": ["error"], - "dot-location": ["warn", "property"], - "guard-for-in": ["error"], - "no-useless-call": ["warn"], - "no-useless-escape": ["warn"], - "no-useless-concat": ["warn"], - "brace-style": ["warn", "1tbs"], - "comma-style": ["warn", "last"], - "space-before-function-paren": ["warn", "never"], - "space-before-blocks": ["warn", "always"], - "keyword-spacing": ["warn", { - "before": true, - "after": true - }], - - // dangling commas required, but only for multiline objects/arrays - "comma-dangle": ["warn", "always-multiline"], - // always === instead of ==, unless dealing with null/undefined - "eqeqeq": ["error", "smart"], - // always use curly braces, even with single statements - "curly": ["error", "all"], - // phasing out var in favour of let/const is a good idea - "no-var": ["warn"], - // always require semicolons - "semi": ["error", "always"], - // prefer rest and spread over the Old Ways - "prefer-spread": ["warn"], - "prefer-rest-params": ["warn"], - - /** react **/ - - // bind or arrow function in props causes performance issues - "react/jsx-no-bind": ["error", { - "ignoreRefs": true - }], - "react/jsx-key": ["error"], - "react/prefer-stateless-function": ["warn"], - - /** flowtype **/ - "flowtype/require-parameter-type": [ - 1, - { - "excludeArrowFunctions": true - } - ], - "flowtype/define-flow-type": 1, - "flowtype/require-return-type": [ - 1, - "always", - { - "annotateUndefined": "never", - "excludeArrowFunctions": true - } - ], - "flowtype/space-after-type-colon": [ - 1, - "always" - ], - "flowtype/space-before-type-colon": [ - 1, - "never" - ] - }, - "settings": { - "flowtype": { - "onlyFilesWithFlowAnnotation": true - } - } -} diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000000..e41106d695 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,62 @@ +module.exports = { + parser: "babel-eslint", + extends: ["./node_modules/matrix-js-sdk/.eslintrc.js"], + plugins: [ + "react", + "flowtype", + ], + env: { + es6: true, + }, + parserOptions: { + ecmaFeatures: { + jsx: true, + } + }, + rules: { + /** react **/ + // This just uses the react plugin to help eslint known when + // variables have been used in JSX + "react/jsx-uses-vars": "error", + + // bind or arrow function in props causes performance issues + "react/jsx-no-bind": ["error", { + "ignoreRefs": true, + }], + "react/jsx-key": ["error"], + + /** flowtype **/ + "flowtype/require-parameter-type": ["warn", { + "excludeArrowFunctions": true, + }], + "flowtype/define-flow-type": "warn", + "flowtype/require-return-type": ["warn", + "always", + { + "annotateUndefined": "never", + "excludeArrowFunctions": true, + } + ], + "flowtype/space-after-type-colon": ["warn", "always"], + "flowtype/space-before-type-colon": ["warn", "never"], + + /* + * things that are errors in the js-sdk config that the current + * code does not adhere to, turned down to warn + */ + "max-len": ["warn"], + "valid-jsdoc": ["warn"], + "new-cap": ["warn"], + "key-spacing": ["warn"], + "arrow-parens": ["warn"], + "prefer-const": ["warn"], + + // crashes currently: https://github.com/eslint/eslint/issues/6274 + "generator-star-spacing": "off", + }, + settings: { + flowtype: { + onlyFilesWithFlowAnnotation: true + }, + }, +}; diff --git a/package.json b/package.json index e2a8fe2c35..8e1ead4b9e 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "devDependencies": { "babel-cli": "^6.5.2", "babel-core": "^6.14.0", - "babel-eslint": "^6.1.0", + "babel-eslint": "^6.1.2", "babel-loader": "^6.2.5", "babel-plugin-add-module-exports": "^0.2.1", "babel-plugin-transform-async-to-generator": "^6.16.0", @@ -86,9 +86,10 @@ "babel-preset-es2016": "^6.11.3", "babel-preset-es2017": "^6.14.0", "babel-preset-react": "^6.11.1", - "eslint": "^2.13.1", - "eslint-plugin-flowtype": "^2.17.0", - "eslint-plugin-react": "^6.2.1", + "eslint": "^3.13.1", + "eslint-config-google": "^0.7.1", + "eslint-plugin-flowtype": "^2.30.0", + "eslint-plugin-react": "^6.9.0", "expect": "^1.16.0", "json-loader": "^0.5.3", "karma": "^0.13.22", From 18d4d3392a13b56d683ff0a536e723d7ead76921 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 20 Jan 2017 14:22:27 +0000 Subject: [PATCH 49/57] Fix a bunch of linting errors eslint --fix and a few manual ones --- src/AddThreepid.js | 2 +- src/Avatar.js | 4 +-- src/BasePlatform.js | 6 ++-- src/CallHandler.js | 4 +-- src/ContentMessages.js | 2 +- src/DateUtils.js | 2 +- src/Entities.js | 2 +- src/HtmlUtils.js | 10 +++--- src/ImageUtils.js | 2 +- src/Invite.js | 4 +-- src/Lifecycle.js | 4 +-- src/Markdown.js | 14 ++++---- src/Modal.js | 8 ++--- src/Notifier.js | 4 +-- src/ObjectUtils.js | 2 +- src/Presence.js | 2 +- src/RichText.js | 4 +-- src/RoomListSorter.js | 2 +- src/RoomNotifs.js | 2 +- src/Rooms.js | 2 +- src/ScalarMessaging.js | 2 +- src/SlashCommands.js | 12 +++---- src/TabComplete.js | 6 ++-- src/TabCompleteEntries.js | 5 ++- src/TextForEvent.js | 3 +- src/Tinter.js | 9 ++--- src/Velociraptor.js | 6 ++-- src/VelocityBounce.js | 6 ++-- src/WhoIsTyping.js | 2 +- src/autocomplete/AutocompleteProvider.js | 2 +- src/autocomplete/CommandProvider.js | 2 +- src/autocomplete/DuckDuckGoProvider.js | 2 +- src/autocomplete/EmojiProvider.js | 2 +- src/autocomplete/RoomProvider.js | 4 +-- src/components/structures/ContextualMenu.js | 8 ++--- src/components/structures/CreateRoom.js | 4 +-- src/components/structures/FilePanel.js | 2 +- src/components/structures/LoggedInView.js | 18 +++++----- src/components/structures/MatrixChat.js | 4 +-- src/components/structures/MessagePanel.js | 6 ++-- src/components/structures/RoomView.js | 34 ++++++++++--------- src/components/structures/ScrollPanel.js | 4 +-- src/components/structures/TimelinePanel.js | 20 ++++++----- src/components/structures/UploadBar.js | 6 ++-- src/components/structures/UserSettings.js | 20 +++++------ .../structures/login/ForgotPassword.js | 6 ++-- src/components/structures/login/Login.js | 8 ++--- .../structures/login/Registration.js | 2 +- src/components/views/avatars/BaseAvatar.js | 2 +- src/components/views/avatars/MemberAvatar.js | 6 ++-- src/components/views/avatars/RoomAvatar.js | 4 +-- src/components/views/create_room/Presets.js | 2 +- .../views/dialogs/ChatInviteDialog.js | 6 ++-- .../views/dialogs/DeactivateAccountDialog.js | 4 +-- .../views/elements/AddressSelector.js | 4 +-- src/components/views/elements/AddressTile.js | 2 +- .../views/elements/DeviceVerifyButtons.js | 4 +-- src/components/views/elements/EditableText.js | 8 ++--- .../views/elements/PowerSelector.js | 8 ++--- src/components/views/elements/ProgressBar.js | 2 +- .../views/elements/TruncatedList.js | 2 +- src/components/views/elements/UserSelector.js | 2 +- src/components/views/login/CaptchaForm.js | 4 +-- .../login/InteractiveAuthEntryComponents.js | 4 +-- .../views/login/RegistrationForm.js | 16 ++++----- src/components/views/login/ServerConfig.js | 4 +-- src/components/views/messages/MAudioBody.js | 2 +- src/components/views/messages/MFileBody.js | 4 +-- src/components/views/messages/MImageBody.js | 2 +- src/components/views/messages/MVideoBody.js | 2 +- src/components/views/messages/TextualBody.js | 6 ++-- .../views/room_settings/AliasSettings.js | 18 +++++----- .../views/room_settings/ColorSettings.js | 2 +- .../views/room_settings/UrlPreviewSettings.js | 6 ++-- src/components/views/rooms/AuxPanel.js | 4 +-- src/components/views/rooms/EventTile.js | 10 +++--- .../views/rooms/LinkPreviewWidget.js | 8 +++-- .../views/rooms/MemberDeviceInfo.js | 2 +- src/components/views/rooms/MemberInfo.js | 12 +++---- src/components/views/rooms/MemberList.js | 8 ++--- src/components/views/rooms/MemberTile.js | 2 +- src/components/views/rooms/MessageComposer.js | 2 +- .../views/rooms/MessageComposerInput.js | 19 ++++++----- .../views/rooms/MessageComposerInputOld.js | 2 +- .../views/rooms/ReadReceiptMarker.js | 4 +-- src/components/views/rooms/RoomHeader.js | 10 +++--- src/components/views/rooms/RoomList.js | 8 ++--- src/components/views/rooms/RoomPreviewBar.js | 6 ++-- src/components/views/rooms/RoomSettings.js | 14 ++++---- src/components/views/rooms/RoomTile.js | 2 +- .../views/rooms/SearchableEntityList.js | 6 ++-- .../views/rooms/SimpleRoomHeader.js | 2 +- src/components/views/rooms/UserTile.js | 2 +- src/components/views/settings/ChangeAvatar.js | 6 ++-- .../views/settings/ChangePassword.js | 4 +-- src/components/views/settings/DevicesPanel.js | 4 +-- .../views/settings/DevicesPanelEntry.js | 7 ++-- .../settings/EnableNotificationsButton.js | 1 - src/components/views/voip/CallView.js | 2 +- src/components/views/voip/VideoView.js | 2 +- src/createRoom.js | 4 +-- src/dispatcher.js | 2 +- src/extend.js | 2 +- src/linkify-matrix.js | 4 +-- src/utils/FormattingUtils.js | 2 +- src/utils/MegolmExportEncryption.js | 2 +- src/wrappers/WithMatrixClient.js | 2 +- 107 files changed, 290 insertions(+), 290 deletions(-) diff --git a/src/AddThreepid.js b/src/AddThreepid.js index 5593d46ff7..d6a1d58aa0 100644 --- a/src/AddThreepid.js +++ b/src/AddThreepid.js @@ -21,7 +21,7 @@ var MatrixClientPeg = require("./MatrixClientPeg"); * optionally, the identity servers. * * This involves getting an email token from the identity server to "prove" that - * the client owns the given email address, which is then passed to the + * the client owns the given email address, which is then passed to the * add threepid API on the homeserver. */ class AddThreepid { diff --git a/src/Avatar.js b/src/Avatar.js index 0ef6c8d07b..76f5e55ff0 100644 --- a/src/Avatar.js +++ b/src/Avatar.js @@ -49,12 +49,12 @@ module.exports = { }, defaultAvatarUrlForString: function(s) { - var images = [ '76cfa6', '50e2c2', 'f4c371' ]; + var images = ['76cfa6', '50e2c2', 'f4c371']; var total = 0; for (var i = 0; i < s.length; ++i) { total += s.charCodeAt(i); } return 'img/' + images[total % images.length] + '.png'; } -} +}; diff --git a/src/BasePlatform.js b/src/BasePlatform.js index 815133c334..8bdf7d0391 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -41,7 +41,7 @@ export default class BasePlatform { * Returns true if the platform supports displaying * notifications, otherwise false. */ - supportsNotifications() : boolean { + supportsNotifications(): boolean { return false; } @@ -49,7 +49,7 @@ export default class BasePlatform { * Returns true if the application currently has permission * to display notifications. Otherwise false. */ - maySendNotifications() : boolean { + maySendNotifications(): boolean { return false; } @@ -60,7 +60,7 @@ export default class BasePlatform { * that is 'granted' if the user allowed the request or * 'denied' otherwise. */ - requestNotificationPermission() : Promise { + requestNotificationPermission(): Promise { } displayNotification(title: string, msg: string, avatarUrl: string, room: Object) { diff --git a/src/CallHandler.js b/src/CallHandler.js index 31b52b65a3..268a599d8e 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -159,10 +159,10 @@ function _setCallState(call, roomId, status) { calls[roomId] = call; if (status === "ringing") { - play("ringAudio") + play("ringAudio"); } else if (call && call.call_state === "ringing") { - pause("ringAudio") + pause("ringAudio"); } if (call) { diff --git a/src/ContentMessages.js b/src/ContentMessages.js index c169ce64b5..17c8155c1b 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -256,7 +256,7 @@ function uploadFile(matrixClient, roomId, file) { }); }); } else { - const basePromise = matrixClient.uploadContent(file); + const basePromise = matrixClient.uploadContent(file); const promise1 = basePromise.then(function(url) { // If the attachment isn't encrypted then include the URL directly. return {"url": url}; diff --git a/src/DateUtils.js b/src/DateUtils.js index 2b51c5903f..07bab4ae7b 100644 --- a/src/DateUtils.js +++ b/src/DateUtils.js @@ -48,5 +48,5 @@ module.exports = { //return pad(date.getHours()) + ':' + pad(date.getMinutes()); return ('00' + date.getHours()).slice(-2) + ':' + ('00' + date.getMinutes()).slice(-2); } -} +}; diff --git a/src/Entities.js b/src/Entities.js index ac3c976797..7c3909f36f 100644 --- a/src/Entities.js +++ b/src/Entities.js @@ -136,6 +136,6 @@ module.exports = { fromUsers: function(users, showInviteButton, inviteFn) { return users.map(function(u) { return new UserEntity(u, showInviteButton, inviteFn); - }) + }); } }; diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index fc1630b6fb..c7b13bc071 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -91,16 +91,16 @@ var sanitizeHtmlParams = { ], allowedAttributes: { // custom ones first: - font: [ 'color' ], // custom to matrix - a: [ 'href', 'name', 'target', 'rel' ], // remote target: custom to matrix + font: ['color'], // custom to matrix + a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix // We don't currently allow img itself by default, but this // would make sense if we did - img: [ 'src' ], + img: ['src'], }, // Lots of these won't come up by default because we don't allow them - selfClosing: [ 'img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta' ], + selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'], // URL schemes we permit - allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ], + allowedSchemes: ['http', 'https', 'ftp', 'mailto'], // DO NOT USE. sanitize-html allows all URL starting with '//' // so this will always allow links to whatever scheme the diff --git a/src/ImageUtils.js b/src/ImageUtils.js index fdb12c7608..3744241874 100644 --- a/src/ImageUtils.js +++ b/src/ImageUtils.js @@ -53,5 +53,5 @@ module.exports = { return Math.floor(heightMulti * fullHeight); } }, -} +}; diff --git a/src/Invite.js b/src/Invite.js index 6bfc646977..6ad929e33b 100644 --- a/src/Invite.js +++ b/src/Invite.js @@ -55,7 +55,7 @@ export function inviteToRoom(roomId, addr) { * @returns Promise */ export function inviteMultipleToRoom(roomId, addrs) { - this.inviter = new MultiInviter(roomId); - return this.inviter.invite(addrs); + const inviter = new MultiInviter(roomId); + return inviter.invite(addrs); } diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 0a61dc6105..493bbf12aa 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -18,7 +18,7 @@ import q from 'q'; import Matrix from 'matrix-js-sdk'; import MatrixClientPeg from './MatrixClientPeg'; -import Notifier from './Notifier' +import Notifier from './Notifier'; import UserActivity from './UserActivity'; import Presence from './Presence'; import dis from './dispatcher'; @@ -140,7 +140,7 @@ function _loginWithToken(queryParams, defaultDeviceDisplayName) { homeserverUrl: queryParams.homeserver, identityServerUrl: queryParams.identityServer, guest: false, - }) + }); }, (err) => { console.error("Failed to log in with login token: " + err + " " + err.data); diff --git a/src/Markdown.js b/src/Markdown.js index 3506e3cb59..2f278183a3 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -49,9 +49,9 @@ export default class Markdown { dummy_renderer[k] = setNotPlain; } // text and paragraph are just text - dummy_renderer.text = function(t) { return t; } - dummy_renderer.softbreak = function(t) { return t; } - dummy_renderer.paragraph = function(t) { return t; } + dummy_renderer.text = function(t) { return t; }; + dummy_renderer.softbreak = function(t) { return t; }; + dummy_renderer.paragraph = function(t) { return t; }; const dummy_parser = new commonmark.Parser(); dummy_renderer.render(dummy_parser.parse(this.input)); @@ -70,12 +70,12 @@ export default class Markdown { // its own p tag to keep them as separate paragraphs. var par = node; while (par.parent) { - par = par.parent + par = par.parent; } if (par.firstChild != par.lastChild) { real_paragraph.call(this, node, entering); } - } + }; var parsed = this.parser.parse(this.input); var rendered = this.renderer.render(parsed); @@ -94,7 +94,7 @@ export default class Markdown { this.renderer.out = function(s) { // The `lit` function adds a string literal to the output buffer. this.lit(s); - } + }; this.renderer.paragraph = function(node, entering) { // If there is only one top level node, just return the @@ -112,7 +112,7 @@ export default class Markdown { this.lit('\n\n'); } } - } + }; var parsed = this.parser.parse(this.input); var rendered = this.renderer.render(parsed); diff --git a/src/Modal.js b/src/Modal.js index c2ce04c4e8..862e4befc5 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -36,7 +36,7 @@ const AsyncWrapper = React.createClass({ getInitialState: function() { return { component: null, - } + }; }, componentWillMount: function() { @@ -82,8 +82,8 @@ module.exports = { return container; }, - createDialog: function (Element, props, className) { - return this.createDialogAsync((cb) => {cb(Element)}, props, className); + createDialog: function(Element, props, className) { + return this.createDialogAsync((cb) => {cb(Element);}, props, className); }, /** @@ -105,7 +105,7 @@ module.exports = { * * @param {String} className CSS class to apply to the modal wrapper */ - createDialogAsync: function (loader, props, className) { + createDialogAsync: function(loader, props, className) { var self = this; // never call this via modal.close() from onFinished() otherwise it will loop var closeDialog = function() { diff --git a/src/Notifier.js b/src/Notifier.js index a58fc0132f..67642e734a 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -53,7 +53,7 @@ var Notifier = { if (!msg) return; var title; - if (!ev.sender || room.name == ev.sender.name) { + if (!ev.sender || room.name == ev.sender.name) { title = room.name; // notificationMessageForEvent includes sender, // but we already have the sender here @@ -88,7 +88,7 @@ var Notifier = { if (e) { e.load(); e.play(); - }; + } }, start: function() { diff --git a/src/ObjectUtils.js b/src/ObjectUtils.js index 07a16df501..5fac588a4f 100644 --- a/src/ObjectUtils.js +++ b/src/ObjectUtils.js @@ -64,7 +64,7 @@ module.exports.getKeyValueArrayDiffs = function(before, after) { } else if (itemDelta[item] === -1) { results.push({ place: "del", key: muxedKey, val: item }); } else { - // itemDelta of 0 means it was unchanged between before/after + // itemDelta of 0 means it was unchanged between before/after } }); break; diff --git a/src/Presence.js b/src/Presence.js index 4152d7a487..c45d571217 100644 --- a/src/Presence.js +++ b/src/Presence.js @@ -111,7 +111,7 @@ class Presence { this.timer = setTimeout(function() { self._onUnavailableTimerFire(); }, UNAVAILABLE_TIME_MS); - } + } } module.exports = new Presence(); diff --git a/src/RichText.js b/src/RichText.js index 5fe920fe50..b1793d0ddf 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -12,7 +12,7 @@ import { SelectionState, Entity, } from 'draft-js'; -import * as sdk from './index'; +import * as sdk from './index'; import * as emojione from 'emojione'; import {stateToHTML} from 'draft-js-export-html'; import {SelectionRange} from "./autocomplete/Autocompleter"; @@ -109,7 +109,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator { return {avatar}{props.children}; } }; - + let roomDecorator = { strategy: (contentBlock, callback) => { findWithRegex(ROOM_REGEX, contentBlock, callback); diff --git a/src/RoomListSorter.js b/src/RoomListSorter.js index 09f178dd3f..7a43c1891e 100644 --- a/src/RoomListSorter.js +++ b/src/RoomListSorter.js @@ -26,7 +26,7 @@ function tsOfNewestEvent(room) { } function mostRecentActivityFirst(roomList) { - return roomList.sort(function(a,b) { + return roomList.sort(function(a, b) { return tsOfNewestEvent(b) - tsOfNewestEvent(a); }); } diff --git a/src/RoomNotifs.js b/src/RoomNotifs.js index d0cdd6ead7..7cb7d4b9de 100644 --- a/src/RoomNotifs.js +++ b/src/RoomNotifs.js @@ -146,7 +146,7 @@ function isRuleForRoom(roomId, rule) { } const cond = rule.conditions[0]; if ( - cond.kind == 'event_match' && + cond.kind == 'event_match' && cond.key == 'room_id' && cond.pattern == roomId ) { diff --git a/src/Rooms.js b/src/Rooms.js index cf62f2dda0..fbcc843ad2 100644 --- a/src/Rooms.js +++ b/src/Rooms.js @@ -37,7 +37,7 @@ export function getOnlyOtherMember(room, me) { if (joinedMembers.length === 2) { return joinedMembers.filter(function(m) { - return m.userId !== me.userId + return m.userId !== me.userId; })[0]; } diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 75062daaa2..dbb7e405df 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -371,7 +371,7 @@ const onMessage = function(event) { }, (err) => { console.error(err); sendError(event, "Failed to lookup current room."); - }) + }); }; module.exports = { diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 523d1d8f3c..1ddcf4832d 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -41,7 +41,7 @@ class Command { } getUsage() { - return "Usage: " + this.getCommandWithArgs() + return "Usage: " + this.getCommandWithArgs(); } } @@ -84,7 +84,7 @@ var commands = { var matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/); if (matches) { Tinter.tint(matches[1], matches[4]); - var colorScheme = {} + var colorScheme = {}; colorScheme.primary_color = matches[1]; if (matches[4]) { colorScheme.secondary_color = matches[4]; @@ -288,7 +288,7 @@ var commands = { // helpful aliases var aliases = { j: "join" -} +}; module.exports = { /** @@ -331,9 +331,9 @@ module.exports = { // Return all the commands plus /me and /markdown which aren't handled like normal commands var cmds = Object.keys(commands).sort().map(function(cmdKey) { return commands[cmdKey]; - }) - cmds.push(new Command("me", "", function(){})); - cmds.push(new Command("markdown", "", function(){})); + }); + cmds.push(new Command("me", "", function() {})); + cmds.push(new Command("markdown", "", function() {})); return cmds; } diff --git a/src/TabComplete.js b/src/TabComplete.js index a0380f36c4..59ecc2ae20 100644 --- a/src/TabComplete.js +++ b/src/TabComplete.js @@ -112,7 +112,7 @@ class TabComplete { return; } // ES6 destructuring; ignore first element (the complete match) - var [ , boundaryGroup, partialGroup] = res; + var [, boundaryGroup, partialGroup] = res; if (partialGroup.length === 0 && passive) { return; @@ -254,7 +254,7 @@ class TabComplete { if (ev.ctrlKey || ev.metaKey || ev.altKey) return; // tab key has been pressed at this point - this.handleTabPress(false, ev.shiftKey) + this.handleTabPress(false, ev.shiftKey); // prevent the default TAB operation (typically focus shifting) ev.preventDefault(); @@ -386,6 +386,6 @@ class TabComplete { this.memberTabOrder[ev.getSender()] = this.memberOrderSeq++; } } -}; +} module.exports = TabComplete; diff --git a/src/TabCompleteEntries.js b/src/TabCompleteEntries.js index 2a8c7b383a..e6adec0d7d 100644 --- a/src/TabCompleteEntries.js +++ b/src/TabCompleteEntries.js @@ -13,7 +13,6 @@ 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. */ -var React = require("react"); var sdk = require("./index"); class Entry { @@ -90,7 +89,7 @@ CommandEntry.fromCommands = function(commandArray) { return commandArray.map(function(cmd) { return new CommandEntry(cmd.getCommand(), cmd.getCommandWithArgs()); }); -} +}; class MemberEntry extends Entry { constructor(member) { @@ -119,7 +118,7 @@ MemberEntry.fromMemberList = function(members) { return members.map(function(m) { return new MemberEntry(m); }); -} +}; module.exports.Entry = Entry; module.exports.MemberEntry = MemberEntry; diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 2ffd33167f..3f772e9cfb 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -75,7 +75,6 @@ function textForMemberEvent(ev) { return targetName + " joined the room."; } } - return ''; case 'leave': if (ev.getSender() === ev.getStateKey()) { if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) { @@ -203,4 +202,4 @@ module.exports = { if (!hdlr) return ""; return hdlr(ev); } -} +}; diff --git a/src/Tinter.js b/src/Tinter.js index 4a5e4e453c..5bf13e6d4a 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -14,9 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -var dis = require("./dispatcher"); -var sdk = require("./index"); - // FIXME: these vars should be bundled up and attached to // module.exports otherwise this will break when included by both // react-sdk and apps layered on top. @@ -152,7 +149,7 @@ function hexToRgb(color) { function rgbToHex(rgb) { var val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2]; - return '#' + (0x1000000 + val).toString(16).slice(1) + return '#' + (0x1000000 + val).toString(16).slice(1); } // List of functions to call when the tint changes. @@ -187,7 +184,7 @@ module.exports = { } if (!secondaryColor) { - var x = 0.16; // average weighting factor calculated from vector green & light green + const x = 0.16; // average weighting factor calculated from vector green & light green var rgb = hexToRgb(primaryColor); rgb[0] = x * rgb[0] + (1 - x) * 255; rgb[1] = x * rgb[1] + (1 - x) * 255; @@ -196,7 +193,7 @@ module.exports = { } if (!tertiaryColor) { - var x = 0.19; + const x = 0.19; var rgb1 = hexToRgb(primaryColor); var rgb2 = hexToRgb(secondaryColor); rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0]; diff --git a/src/Velociraptor.js b/src/Velociraptor.js index d9b6b3d5dc..006dbcb0ac 100644 --- a/src/Velociraptor.js +++ b/src/Velociraptor.js @@ -76,7 +76,7 @@ module.exports = React.createClass({ var startStyles = self.props.startStyles; if (startStyles.length > 0) { - var startStyle = startStyles[0] + var startStyle = startStyles[0]; newProps.style = startStyle; // console.log("mounted@startstyle0: "+JSON.stringify(startStyle)); } @@ -105,7 +105,7 @@ module.exports = React.createClass({ ) { var startStyles = this.props.startStyles; var transitionOpts = this.props.enterTransitionOpts; - var domNode = ReactDom.findDOMNode(node); + const domNode = ReactDom.findDOMNode(node); // start from startStyle 1: 0 is the one we gave it // to start with, so now we animate 1 etc. for (var i = 1; i < startStyles.length; ++i) { @@ -145,7 +145,7 @@ module.exports = React.createClass({ // and the FAQ entry, "Preventing memory leaks when // creating/destroying large numbers of elements" // (https://github.com/julianshapiro/velocity/issues/47) - var domNode = ReactDom.findDOMNode(this.nodes[k]); + const domNode = ReactDom.findDOMNode(this.nodes[k]); Velocity.Utilities.removeData(domNode); } this.nodes[k] = node; diff --git a/src/VelocityBounce.js b/src/VelocityBounce.js index 168b0b14af..3ad7d207a9 100644 --- a/src/VelocityBounce.js +++ b/src/VelocityBounce.js @@ -6,10 +6,12 @@ function bounce( p ) { var pow2, bounce = 4; - while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {} + while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) { + // just sets pow2 + } return 1 / Math.pow( 4, 3 - bounce ) - 7.5625 * Math.pow( ( pow2 * 3 - 2 ) / 22 - p, 2 ); } Velocity.Easings.easeOutBounce = function(p) { return 1 - bounce(1 - p); -} +}; diff --git a/src/WhoIsTyping.js b/src/WhoIsTyping.js index 4fb5399027..8c3838d615 100644 --- a/src/WhoIsTyping.js +++ b/src/WhoIsTyping.js @@ -46,4 +46,4 @@ module.exports = { return names.join(', ') + ' and ' + lastPerson + ' are typing'; } } -} +}; diff --git a/src/autocomplete/AutocompleteProvider.js b/src/autocomplete/AutocompleteProvider.js index 9cdb774cac..5c90990295 100644 --- a/src/autocomplete/AutocompleteProvider.js +++ b/src/autocomplete/AutocompleteProvider.js @@ -26,7 +26,7 @@ export default class AutocompleteProvider { } commandRegex.lastIndex = 0; - + let match; while ((match = commandRegex.exec(query)) != null) { let matchStart = match.index, diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index 7d032006db..60171bc72f 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -83,7 +83,7 @@ export default class CommandProvider extends AutocompleteProvider { static getInstance(): CommandProvider { if (instance == null) - instance = new CommandProvider(); + {instance = new CommandProvider();} return instance; } diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index 46aa4b0f03..bffd924976 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -13,7 +13,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { constructor() { super(DDG_REGEX); } - + static getQueryUri(query: String) { return `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}` + `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`; diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index 4c8bf60b83..a2d77f02a1 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -44,7 +44,7 @@ export default class EmojiProvider extends AutocompleteProvider { static getInstance() { if (instance == null) - instance = new EmojiProvider(); + {instance = new EmojiProvider();} return instance; } diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index f3401cf1bb..8d1e555e56 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -52,12 +52,12 @@ export default class RoomProvider extends AutocompleteProvider { getName() { return '💬 Rooms'; } - + static getInstance() { if (instance == null) { instance = new RoomProvider(); } - + return instance; } diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index da419897dc..e5a62b8345 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -47,7 +47,7 @@ module.exports = { return container; }, - createMenu: function (Element, props) { + createMenu: function(Element, props) { var self = this; var closeMenu = function() { @@ -78,15 +78,15 @@ module.exports = { .mx_ContextualMenu_chevron_right:after { border-left-color: ${props.menuColour}; } - ` + `; } var chevron = null; if (props.left) { - chevron =
+ chevron =
; position.left = props.left; } else { - chevron =
+ chevron =
; position.right = props.right; } diff --git a/src/components/structures/CreateRoom.js b/src/components/structures/CreateRoom.js index ce4c0916d4..24ebfea07f 100644 --- a/src/components/structures/CreateRoom.js +++ b/src/components/structures/CreateRoom.js @@ -118,7 +118,7 @@ module.exports = React.createClass({ var self = this; - deferred.then(function (resp) { + deferred.then(function(resp) { self.setState({ phase: self.phases.CREATED, }); @@ -210,7 +210,7 @@ module.exports = React.createClass({ onAliasChanged: function(alias) { this.setState({ alias: alias - }) + }); }, onEncryptChanged: function(ev) { diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index 0dd16a7e99..5166619d48 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -35,7 +35,7 @@ var FilePanel = React.createClass({ getInitialState: function() { return { timelineSet: null, - } + }; }, componentWillMount: function() { diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 7c0fe14edd..57a4d4c721 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -160,8 +160,8 @@ export default React.createClass({ collapsedRhs={this.props.collapse_rhs} ConferenceHandler={this.props.ConferenceHandler} scrollStateMap={this._scrollStateMap} - /> - if (!this.props.collapse_rhs) right_panel = + />; + if (!this.props.collapse_rhs) right_panel = ; break; case PageTypes.UserSettings: @@ -170,28 +170,28 @@ export default React.createClass({ brand={this.props.config.brand} collapsedRhs={this.props.collapse_rhs} enableLabs={this.props.config.enableLabs} - /> - if (!this.props.collapse_rhs) right_panel = + />; + if (!this.props.collapse_rhs) right_panel = ; break; case PageTypes.CreateRoom: page_element = - if (!this.props.collapse_rhs) right_panel = + />; + if (!this.props.collapse_rhs) right_panel = ; break; case PageTypes.RoomDirectory: page_element = - if (!this.props.collapse_rhs) right_panel = + />; + if (!this.props.collapse_rhs) right_panel = ; break; case PageTypes.UserView: page_element = null; // deliberately null for now - right_panel = + right_panel = ; break; } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index ceca3591c5..20d59e22ec 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -77,7 +77,7 @@ module.exports = React.createClass({ getChildContext: function() { return { appConfig: this.props.config, - } + }; }, getInitialState: function() { @@ -1038,7 +1038,7 @@ module.exports = React.createClass({ {...this.props} {...this.state} /> - ) + ); } else if (this.state.logged_in) { // we think we are logged in, but are still waiting for the /sync to complete var Spinner = sdk.getComponent('elements.Spinner'); diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index c04bec4b35..64b0a8e875 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -19,7 +19,7 @@ var ReactDOM = require("react-dom"); var dis = require("../../dispatcher"); var sdk = require('../../index'); -var MatrixClientPeg = require('../../MatrixClientPeg') +var MatrixClientPeg = require('../../MatrixClientPeg'); const MILLIS_IN_DAY = 86400000; @@ -282,7 +282,7 @@ module.exports = React.createClass({ var isMembershipChange = (e) => e.getType() === 'm.room.member' && ['join', 'leave'].indexOf(e.getContent().membership) !== -1 - && (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership); + && (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership); for (i = 0; i < this.props.events.length; i++) { var mxEv = this.props.events[i]; @@ -340,7 +340,7 @@ module.exports = React.createClass({ prevEvent = e; return ret; } - ).reduce((a,b) => a.concat(b)); + ).reduce((a, b) => a.concat(b)); if (eventTiles.length === 0) { eventTiles = null; diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 8059cd9372..1f35e41817 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -48,7 +48,7 @@ if (DEBUG) { // using bind means that we get to keep useful line numbers in the console var debuglog = console.log.bind(console); } else { - var debuglog = function () {}; + var debuglog = function() {}; } module.exports = React.createClass({ @@ -146,7 +146,7 @@ module.exports = React.createClass({ showTopUnreadMessagesBar: false, auxPanelMaxHeight: undefined, - } + }; }, componentWillMount: function() { @@ -674,8 +674,9 @@ module.exports = React.createClass({ }, onSearchResultsFillRequest: function(backwards) { - if (!backwards) + if (!backwards) { return q(false); + } if (this.state.searchResults.next_batch) { debuglog("requesting more search results"); @@ -758,7 +759,7 @@ module.exports = React.createClass({ }).then(() => { var sign_url = this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : undefined; return MatrixClientPeg.get().joinRoom(this.props.roomAddress, - { inviteSignUrl: sign_url } ) + { inviteSignUrl: sign_url } ); }).then(function(resp) { var roomId = resp.roomId; @@ -962,7 +963,7 @@ module.exports = React.createClass({ // For overlapping highlights, // favour longer (more specific) terms first highlights = highlights.sort(function(a, b) { - return b.length - a.length }); + return b.length - a.length; }); self.setState({ searchHighlights: highlights, @@ -1025,7 +1026,7 @@ module.exports = React.createClass({ if (scrollPanel) { scrollPanel.checkScroll(); } - } + }; var lastRoomId; @@ -1090,7 +1091,7 @@ module.exports = React.createClass({ } this.refs.room_settings.save().then((results) => { - var fails = results.filter(function(result) { return result.state !== "fulfilled" }); + var fails = results.filter(function(result) { return result.state !== "fulfilled"; }); console.log("Settings saved with %s errors", fails.length); if (fails.length) { fails.forEach(function(result) { @@ -1099,7 +1100,7 @@ module.exports = React.createClass({ var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Failed to save settings", - description: fails.map(function(result) { return result.reason }).join("\n"), + description: fails.map(function(result) { return result.reason; }).join("\n"), }); // still editing room settings } @@ -1183,7 +1184,7 @@ module.exports = React.createClass({ this.setState({ searching: true }); }, - onCancelSearchClick: function () { + onCancelSearchClick: function() { this.setState({ searching: false, searchResults: null, @@ -1208,8 +1209,9 @@ module.exports = React.createClass({ // decide whether or not the top 'unread messages' bar should be shown _updateTopUnreadMessagesBar: function() { - if (!this.refs.messagePanel) + if (!this.refs.messagePanel) { return; + } var pos = this.refs.messagePanel.getReadMarkerPosition(); @@ -1498,7 +1500,7 @@ module.exports = React.createClass({ if (ContentMessages.getCurrentUploads().length > 0) { var UploadBar = sdk.getComponent('structures.UploadBar'); - statusBar = + statusBar = ; } else if (!this.state.searchResults) { var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar'); @@ -1513,7 +1515,7 @@ module.exports = React.createClass({ onCancelAllClick={this.onCancelAllClick} onScrollToBottomClick={this.jumpToLiveTimeline} onResize={this.onChildResize} - /> + />; } var aux = null; @@ -1569,7 +1571,7 @@ module.exports = React.createClass({ messageComposer = + callState={this.state.callState} tabComplete={this.tabComplete} opacity={ this.props.opacity }/>; } // TODO: Why aren't we storing the term/scope/count in this format @@ -1597,14 +1599,14 @@ module.exports = React.createClass({ {call.isLocalVideoMuted() -
+
; } voiceMuteButton =
{call.isMicrophoneMuted() -
+ ; // wrap the existing status bar into a 'callStatusBar' which adds more knobs. statusBar = @@ -1614,7 +1616,7 @@ module.exports = React.createClass({ { zoomButton } { statusBar } - + ; } // if we have search results, we keep the messagepanel (so that it preserves its diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index a9e16d364c..1391d2b740 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -34,7 +34,7 @@ if (DEBUG_SCROLL) { // using bind means that we get to keep useful line numbers in the console var debuglog = console.log.bind(console); } else { - var debuglog = function () {}; + var debuglog = function() {}; } /* This component implements an intelligent scrolling list. @@ -600,7 +600,7 @@ module.exports = React.createClass({ stuckAtBottom: false, trackedScrollToken: node.dataset.scrollToken, pixelOffset: wrapperRect.bottom - boundingRect.bottom, - } + }; debuglog("Saved scroll state", this.scrollState); return; } diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index bf31f44315..490b83f2bf 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -38,7 +38,7 @@ if (DEBUG) { // using bind means that we get to keep useful line numbers in the console var debuglog = console.log.bind(console); } else { - var debuglog = function () {}; + var debuglog = function() {}; } /* @@ -322,7 +322,7 @@ var TimelinePanel = React.createClass({ }); }, - onMessageListScroll: function () { + onMessageListScroll: function() { if (this.props.onScroll) { this.props.onScroll(); } @@ -387,7 +387,7 @@ var TimelinePanel = React.createClass({ // if we're at the end of the live timeline, append the pending events if (this.props.timelineSet.room && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) { - events.push(... this.props.timelineSet.room.getPendingEvents()); + events.push(...this.props.timelineSet.room.getPendingEvents()); } var updatedState = {events: events}; @@ -564,8 +564,9 @@ var TimelinePanel = React.createClass({ // first find where the current RM is for (var i = 0; i < events.length; i++) { - if (events[i].getId() == this.state.readMarkerEventId) + if (events[i].getId() == this.state.readMarkerEventId) { break; + } } if (i >= events.length) { return; @@ -644,7 +645,7 @@ var TimelinePanel = React.createClass({ var tl = this.props.timelineSet.getTimelineForEvent(rmId); var rmTs; if (tl) { - var event = tl.getEvents().find((e) => { return e.getId() == rmId }); + var event = tl.getEvents().find((e) => { return e.getId() == rmId; }); if (event) { rmTs = event.getTs(); } @@ -821,7 +822,7 @@ var TimelinePanel = React.createClass({ description: message, onFinished: onFinished, }); - } + }; var prom = this._timelineWindow.load(eventId, INITIAL_SIZE); @@ -843,7 +844,7 @@ var TimelinePanel = React.createClass({ timelineLoading: true, }); - prom = prom.then(onLoaded, onError) + prom = prom.then(onLoaded, onError); } prom.done(); @@ -868,7 +869,7 @@ var TimelinePanel = React.createClass({ // if we're at the end of the live timeline, append the pending events if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) { - events.push(... this.props.timelineSet.getPendingEvents()); + events.push(...this.props.timelineSet.getPendingEvents()); } return events; @@ -930,8 +931,9 @@ var TimelinePanel = React.createClass({ _getCurrentReadReceipt: function(ignoreSynthesized) { var client = MatrixClientPeg.get(); // the client can be null on logout - if (client == null) + if (client == null) { return null; + } var myUserId = client.credentials.userId; return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized); diff --git a/src/components/structures/UploadBar.js b/src/components/structures/UploadBar.js index 794fcffec7..e91e558cb2 100644 --- a/src/components/structures/UploadBar.js +++ b/src/components/structures/UploadBar.js @@ -57,7 +57,7 @@ module.exports = React.createClass({displayName: 'UploadBar', // }]; if (uploads.length == 0) { - return
+ return
; } var upload; @@ -68,7 +68,7 @@ module.exports = React.createClass({displayName: 'UploadBar', } } if (!upload) { - return
+ return
; } var innerProgressStyle = { @@ -76,7 +76,7 @@ module.exports = React.createClass({displayName: 'UploadBar', }; var uploadedSize = filesize(upload.loaded); var totalSize = filesize(upload.total); - if (uploadedSize.replace(/^.* /,'') === totalSize.replace(/^.* /,'')) { + if (uploadedSize.replace(/^.* /, '') === totalSize.replace(/^.* /, '')) { uploadedSize = uploadedSize.replace(/ .*/, ''); } diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 498acc1917..8f6473a181 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -346,8 +346,8 @@ module.exports = React.createClass({ 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. " - message += "Please check your email and click on the link it contains. Once this is done, click continue." + var 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", description: message, @@ -417,7 +417,7 @@ module.exports = React.createClass({ -
+
; }, _renderSyncedSetting: function(setting) { @@ -430,7 +430,7 @@ module.exports = React.createClass({ -
+
; }, _renderThemeSelector: function(setting) { @@ -442,7 +442,7 @@ module.exports = React.createClass({ defaultChecked={ this._syncedSettings[setting.id] === setting.value } onChange={ e => { if (e.target.checked) { - UserSettingsStore.setSyncedSetting(setting.id, setting.value) + UserSettingsStore.setSyncedSetting(setting.id, setting.value); } dis.dispatch({ action: 'set_theme', @@ -454,7 +454,7 @@ module.exports = React.createClass({ - + ; }, _renderCryptoInfo: function() { @@ -467,8 +467,8 @@ module.exports = React.createClass({

Cryptography

    -
  • {deviceId}
  • -
  • {identityKey}
  • +
  • {deviceId}
  • +
  • {identityKey}
@@ -485,7 +485,7 @@ module.exports = React.createClass({ ); }, - _renderLabs: function () { + _renderLabs: function() { // default to enabled if undefined if (this.props.enableLabs === false) return null; @@ -521,7 +521,7 @@ module.exports = React.createClass({ {features} - ) + ); }, _renderDeactivateAccount: function() { diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js index 1868c2ee73..5037136b1d 100644 --- a/src/components/structures/login/ForgotPassword.js +++ b/src/components/structures/login/ForgotPassword.js @@ -58,7 +58,7 @@ module.exports = React.createClass({ this.setState({ progress: null }); - }) + }); }, onVerify: function(ev) { @@ -71,7 +71,7 @@ module.exports = React.createClass({ this.setState({ progress: "complete" }); }, (err) => { this.showErrorDialog(err.message); - }) + }); }, onSubmitForm: function(ev) { @@ -129,7 +129,7 @@ module.exports = React.createClass({ var resetPasswordJsx; if (this.state.progress === "sending_email") { - resetPasswordJsx = + resetPasswordJsx = ; } else if (this.state.progress === "sent_email") { resetPasswordJsx = ( diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index c0d0c08d2c..fe9b544751 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -173,7 +173,7 @@ module.exports = React.createClass({ }, _getCurrentFlowStep: function() { - return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null + return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null; }, _setStateFromError: function(err, isLoginAttempt) { @@ -195,7 +195,7 @@ module.exports = React.createClass({ } let errorText = "Error: Problem communicating with the given homeserver " + - (errCode ? "(" + errCode + ")" : "") + (errCode ? "(" + errCode + ")" : ""); if (err.cors === 'rejected') { if (window.location.protocol === 'https:' && @@ -258,7 +258,7 @@ module.exports = React.createClass({ loginAsGuestJsx = Login as guest - + ; } var returnToAppJsx; @@ -266,7 +266,7 @@ module.exports = React.createClass({ returnToAppJsx = Return to app - + ; } return ( diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index b092e0a9fb..90140b3280 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -333,7 +333,7 @@ module.exports = React.createClass({ returnToAppJsx = Return to app - + ; } return ( diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 47f0a76891..363f340fad 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -41,7 +41,7 @@ module.exports = React.createClass({ height: 40, resizeMethod: 'crop', defaultToInitialLetter: true - } + }; }, getInitialState: function() { diff --git a/src/components/views/avatars/MemberAvatar.js b/src/components/views/avatars/MemberAvatar.js index c8a9abb4fe..9fb522a5f1 100644 --- a/src/components/views/avatars/MemberAvatar.js +++ b/src/components/views/avatars/MemberAvatar.js @@ -42,7 +42,7 @@ module.exports = React.createClass({ height: 40, resizeMethod: 'crop', viewUserOnClick: false, - } + }; }, getInitialState: function() { @@ -64,7 +64,7 @@ module.exports = React.createClass({ props.width, props.height, props.resizeMethod) - } + }; }, render: function() { @@ -78,7 +78,7 @@ module.exports = React.createClass({ action: 'view_user', member: this.props.member, }); - } + }; } return ( diff --git a/src/components/views/avatars/RoomAvatar.js b/src/components/views/avatars/RoomAvatar.js index dcb25eff61..bfa7575b0c 100644 --- a/src/components/views/avatars/RoomAvatar.js +++ b/src/components/views/avatars/RoomAvatar.js @@ -39,7 +39,7 @@ module.exports = React.createClass({ height: 36, resizeMethod: 'crop', oobData: {}, - } + }; }, getInitialState: function() { @@ -51,7 +51,7 @@ module.exports = React.createClass({ componentWillReceiveProps: function(newProps) { this.setState({ urls: this.getImageUrls(newProps) - }) + }); }, getImageUrls: function(props) { diff --git a/src/components/views/create_room/Presets.js b/src/components/views/create_room/Presets.js index 0cce4a6644..6d40be9d32 100644 --- a/src/components/views/create_room/Presets.js +++ b/src/components/views/create_room/Presets.js @@ -40,7 +40,7 @@ module.exports = React.createClass({ }, onValueChanged: function(ev) { - this.props.onChange(ev.target.value) + this.props.onChange(ev.target.value); }, render: function() { diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 18f4ce36ba..64f90c1a30 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -171,7 +171,7 @@ module.exports = React.createClass({ inviteList: inviteList, queryList: [], }); - } + }; }, onClick: function(index) { @@ -402,10 +402,10 @@ module.exports = React.createClass({ var error; var addressSelector; if (this.state.error) { - error =
You have entered an invalid contact. Try using their Matrix ID or email address.
+ error =
You have entered an invalid contact. Try using their Matrix ID or email address.
; } else { addressSelector = ( - {this.addressSelector = ref}} + {this.addressSelector = ref;}} addressList={ this.state.queryList } onSelected={ this.onSelected } truncateAt={ TRUNCATE_QUERY_LIST } /> diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.js index 9a15841061..54a4e99424 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.js +++ b/src/components/views/dialogs/DeactivateAccountDialog.js @@ -81,7 +81,7 @@ export default class DeactivateAccountDialog extends React.Component { if (this.state.errStr) { error =
{this.state.errStr} -
+ ; passwordBoxClass = 'error'; } @@ -92,7 +92,7 @@ export default class DeactivateAccountDialog extends React.Component { if (!this.state.busy) { cancelButton = + ; } return ( diff --git a/src/components/views/elements/AddressSelector.js b/src/components/views/elements/AddressSelector.js index c477b8e7eb..d42637c9e5 100644 --- a/src/components/views/elements/AddressSelector.js +++ b/src/components/views/elements/AddressSelector.js @@ -129,7 +129,7 @@ module.exports = React.createClass({ _maxSelected: function(list) { var listSize = list.length === 0 ? 0 : list.length - 1; - var maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize + var maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize; return maxSelected; }, @@ -140,7 +140,7 @@ module.exports = React.createClass({ }); return ( -
{this.scrollElement = ref}}> +
{this.scrollElement = ref;}}> { this.createAddressListTiles() }
); diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js index 2799f10a41..9fc45d582c 100644 --- a/src/components/views/elements/AddressTile.js +++ b/src/components/views/elements/AddressTile.js @@ -108,7 +108,7 @@ module.exports = React.createClass({ info = (
{ this.props.address }
); - } else if (email) { + } else if (email) { var emailClasses = classNames({ "mx_AddressTile_email": true, "mx_AddressTile_justified": this.props.justified, diff --git a/src/components/views/elements/DeviceVerifyButtons.js b/src/components/views/elements/DeviceVerifyButtons.js index aeb93e866c..da3975e4db 100644 --- a/src/components/views/elements/DeviceVerifyButtons.js +++ b/src/components/views/elements/DeviceVerifyButtons.js @@ -42,8 +42,8 @@ export default React.createClass({
  • { this.props.device.getDisplayName() }
  • -
  • { this.props.device.deviceId}
  • -
  • { this.props.device.getFingerprint() }
  • +
  • { this.props.device.deviceId}
  • +
  • { this.props.device.getFingerprint() }

diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.js index 15118f249e..2c74567698 100644 --- a/src/components/views/elements/EditableText.js +++ b/src/components/views/elements/EditableText.js @@ -57,7 +57,7 @@ module.exports = React.createClass({ getInitialState: function() { return { phase: this.Phases.Display, - } + }; }, componentWillReceiveProps: function(nextProps) { @@ -164,7 +164,7 @@ module.exports = React.createClass({ this.setState({ phase: this.Phases.Edit, - }) + }); }, onFocus: function(ev) { @@ -197,9 +197,9 @@ module.exports = React.createClass({ sel.removeAllRanges(); if (this.props.blurToCancel) - this.cancelEdit(); + {this.cancelEdit();} else - this.onFinish(ev); + {this.onFinish(ev);} this.showPlaceholder(!this.value); }, diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.js index 993f2b965a..c7bfd4eec1 100644 --- a/src/components/views/elements/PowerSelector.js +++ b/src/components/views/elements/PowerSelector.js @@ -73,7 +73,7 @@ module.exports = React.createClass({ getValue: function() { var value; if (this.refs.select) { - value = reverseRoles[ this.refs.select.value ]; + value = reverseRoles[this.refs.select.value]; if (this.refs.custom) { if (value === undefined) value = parseInt( this.refs.custom.value ); } @@ -86,10 +86,10 @@ module.exports = React.createClass({ if (this.state.custom) { var input; if (this.props.disabled) { - input = { this.props.value } + input = { this.props.value }; } else { - input = + input = ; } customPicker = of { input }; } @@ -115,7 +115,7 @@ module.exports = React.createClass({ - + ; } return ( diff --git a/src/components/views/elements/ProgressBar.js b/src/components/views/elements/ProgressBar.js index 12b34480f1..a39e8e48f9 100644 --- a/src/components/views/elements/ProgressBar.js +++ b/src/components/views/elements/ProgressBar.js @@ -35,4 +35,4 @@ module.exports = React.createClass({

); } -}); \ No newline at end of file +}); diff --git a/src/components/views/elements/TruncatedList.js b/src/components/views/elements/TruncatedList.js index 3e174848d3..0ec2c15f0a 100644 --- a/src/components/views/elements/TruncatedList.js +++ b/src/components/views/elements/TruncatedList.js @@ -55,7 +55,7 @@ module.exports = React.createClass({ overflowJsx = this.props.createOverflowElement( overflowCount, childCount ); - + // cut out the overflow elements childArray.splice(childCount - overflowCount, overflowCount); childsJsx = childArray; // use what is left diff --git a/src/components/views/elements/UserSelector.js b/src/components/views/elements/UserSelector.js index 5f176a3e54..266e10154f 100644 --- a/src/components/views/elements/UserSelector.js +++ b/src/components/views/elements/UserSelector.js @@ -56,7 +56,7 @@ module.exports = React.createClass({
    {this.props.selected_users.map(function(user_id, i) { - return
  • {user_id} - X
  • + return
  • {user_id} - X
  • ; })}
diff --git a/src/components/views/login/CaptchaForm.js b/src/components/views/login/CaptchaForm.js index d50e0dee26..0977f947aa 100644 --- a/src/components/views/login/CaptchaForm.js +++ b/src/components/views/login/CaptchaForm.js @@ -52,7 +52,7 @@ module.exports = React.createClass({ this._onCaptchaLoaded(); } else { console.log("Loading recaptcha script..."); - window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded()}; + window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();}; var protocol = global.location.protocol; if (protocol === "file:") { var warning = document.createElement('div'); @@ -101,7 +101,7 @@ module.exports = React.createClass({ } catch (e) { this.setState({ errorText: e.toString(), - }) + }); } }, diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index 23e2b442ef..ec184ca09f 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -70,7 +70,7 @@ export const PasswordAuthEntry = React.createClass({ }); }, - _onPasswordFieldChange: function (ev) { + _onPasswordFieldChange: function(ev) { // enable the submit button iff the password is non-empty this.props.setSubmitButtonEnabled(Boolean(ev.target.value)); }, @@ -209,4 +209,4 @@ export function getEntryComponentForLoginType(loginType) { } } return FallbackAuthEntry; -}; +} diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 4be40bc53a..f8a0863f70 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -169,7 +169,7 @@ module.exports = React.createClass({ validateField: function(field_id) { var pwd1 = this.refs.password.value.trim(); - var pwd2 = this.refs.passwordConfirm.value.trim() + var pwd2 = this.refs.passwordConfirm.value.trim(); switch (field_id) { case FIELD_EMAIL: @@ -283,7 +283,7 @@ module.exports = React.createClass({ autoFocus={true} placeholder="Email address (optional)" defaultValue={this.props.defaultEmail} className={this._classForField(FIELD_EMAIL, 'mx_Login_field')} - onBlur={function() {self.validateField(FIELD_EMAIL)}} + onBlur={function() {self.validateField(FIELD_EMAIL);}} value={self.state.email}/> {emailSuffix ? : null }
@@ -293,8 +293,8 @@ module.exports = React.createClass({ + onBlur={function() {self.validateField(FIELD_USERNAME);}} />
{ this.props.guestUsername ?
Setting a user name will create a fresh account
: null }

{registerButton} diff --git a/src/components/views/login/ServerConfig.js b/src/components/views/login/ServerConfig.js index a18cfbbcef..4e6ed12f9e 100644 --- a/src/components/views/login/ServerConfig.js +++ b/src/components/views/login/ServerConfig.js @@ -64,10 +64,10 @@ module.exports = React.createClass({ hs_url: this.props.customHsUrl, is_url: this.props.customIsUrl, // if withToggleButton is false, then show the config all the time given we have no way otherwise of making it visible - configVisible: !this.props.withToggleButton || + configVisible: !this.props.withToggleButton || (this.props.customHsUrl !== this.props.defaultHsUrl) || (this.props.customIsUrl !== this.props.defaultIsUrl) - } + }; }, onHomeserverChanged: function(ev) { diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js index 7e338e8466..73b9bdb200 100644 --- a/src/components/views/messages/MAudioBody.js +++ b/src/components/views/messages/MAudioBody.js @@ -31,7 +31,7 @@ export default class MAudioBody extends React.Component { decryptedUrl: null, decryptedBlob: null, error: null, - } + }; } onPlayToggle() { this.setState({ diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index 4f5ca2d3be..86aee28269 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -281,7 +281,7 @@ module.exports = React.createClass({ decryptedBlob: blob, }); }).catch((err) => { - console.warn("Unable to decrypt attachment: ", err) + console.warn("Unable to decrypt attachment: ", err); Modal.createDialog(ErrorDialog, { description: "Error decrypting attachment" }); @@ -372,7 +372,7 @@ module.exports = React.createClass({ var extra = text ? (': ' + text) : ''; return Invalid file{extra} - + ; } }, }); diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 5e7cb6e800..10941e0f7f 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -119,7 +119,7 @@ module.exports = React.createClass({ if (content.info.thumbnail_file) { thumbnailPromise = decryptFile( content.info.thumbnail_file - ).then(function (blob) { + ).then(function(blob) { return readBlobAsDataUri(blob); }); } diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index 18552a973d..e2d4af9e69 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -111,7 +111,7 @@ module.exports = React.createClass({ this.props.onWidgetLoad(); }); }).catch((err) => { - console.warn("Unable to decrypt attachment: ", err) + console.warn("Unable to decrypt attachment: ", err); // Set a placeholder image when we can't decrypt the image. this.setState({ error: err, diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index d005ef0cca..fd26ae58da 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -200,7 +200,7 @@ module.exports = React.createClass({ global.localStorage.removeItem("hide_preview_" + self.props.mxEvent.getId()); } }, - } + }; }, onStarterLinkClick: function(starterLink, ev) { @@ -230,8 +230,8 @@ module.exports = React.createClass({ if (!confirmed) { return; } - let width = window.screen.width > 1024 ? 1024 : window.screen.width; - let height = window.screen.height > 800 ? 800 : window.screen.height; + let width = window.screen.width > 1024 ? 1024 : window.screen.width; + let height = window.screen.height > 800 ? 800 : window.screen.height; let left = (window.screen.width - width) / 2; let top = (window.screen.height - height) / 2; window.open(completeUrl, '_blank', `height=${height}, width=${width}, top=${top}, left=${left},`); diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index 96618b613a..5935298634 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.js @@ -103,13 +103,13 @@ module.exports = React.createClass({ } if (oldCanonicalAlias !== this.state.canonicalAlias) { console.log("AliasSettings: Updating canonical alias"); - promises = [ q.all(promises).then( + promises = [q.all(promises).then( MatrixClientPeg.get().sendStateEvent( this.props.roomId, "m.room.canonical_alias", { alias: this.state.canonicalAlias }, "" ) - ) ]; + )]; } return promises; @@ -144,7 +144,7 @@ module.exports = React.createClass({ // XXX: do we need to deep copy aliases before editing it? this.state.domainToAliases[domain] = this.state.domainToAliases[domain] || []; this.state.domainToAliases[domain].push(alias); - this.setState({ + this.setState({ domainToAliases: this.state.domainToAliases }); @@ -152,9 +152,9 @@ module.exports = React.createClass({ this.refs.add_alias.setValue(''); // FIXME } else { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Invalid alias format", + title: "Invalid alias format", description: "'" + alias + "' is not a valid format for an alias", }); } @@ -168,9 +168,9 @@ module.exports = React.createClass({ this.state.domainToAliases[domain][index] = alias; } else { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Invalid address format", + title: "Invalid address format", description: "'" + alias + "' is not a valid format for an address", }); } @@ -183,7 +183,7 @@ module.exports = React.createClass({ // would be to arbitrarily deepcopy to a temp variable and then setState // that, but why bother when we can cut this corner. var alias = this.state.domainToAliases[domain].splice(index, 1); - this.setState({ + this.setState({ domainToAliases: this.state.domainToAliases }); }, @@ -310,4 +310,4 @@ module.exports = React.createClass({
); } -}); \ No newline at end of file +}); diff --git a/src/components/views/room_settings/ColorSettings.js b/src/components/views/room_settings/ColorSettings.js index 6d147b1f63..6a455d9c3c 100644 --- a/src/components/views/room_settings/ColorSettings.js +++ b/src/components/views/room_settings/ColorSettings.js @@ -135,7 +135,7 @@ module.exports = React.createClass({ ); } - var boundClick = this._onColorSchemeChanged.bind(this, i) + var boundClick = this._onColorSchemeChanged.bind(this, i); return (
Disable URL previews by default for participants in this room - + ; } else { disableRoomPreviewUrls = + ; } return ( @@ -154,4 +154,4 @@ module.exports = React.createClass({ ); } -}); \ No newline at end of file +}); diff --git a/src/components/views/rooms/AuxPanel.js b/src/components/views/rooms/AuxPanel.js index f7c3052ea8..365cc18f99 100644 --- a/src/components/views/rooms/AuxPanel.js +++ b/src/components/views/rooms/AuxPanel.js @@ -93,8 +93,8 @@ module.exports = React.createClass({ } else { joinText = ( - Join as { this.onConferenceNotificationClick(event, 'voice')}} - href="#">voice or { this.onConferenceNotificationClick(event, 'video') }} + Join as { this.onConferenceNotificationClick(event, 'voice');}} + href="#">voice or { this.onConferenceNotificationClick(event, 'video'); }} href="#">video. ); diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 42393ad87b..c6a766509a 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -149,13 +149,13 @@ module.exports = WithMatrixClient(React.createClass({ this.props.mxEvent.on("Event.decrypted", this._onDecrypted); }, - componentWillReceiveProps: function (nextProps) { + componentWillReceiveProps: function(nextProps) { if (nextProps.mxEvent !== this.props.mxEvent) { this._verifyEvent(nextProps.mxEvent); } }, - shouldComponentUpdate: function (nextProps, nextState) { + shouldComponentUpdate: function(nextProps, nextState) { if (!ObjectUtils.shallowEqual(this.state, nextState)) { return true; } @@ -259,7 +259,7 @@ module.exports = WithMatrixClient(React.createClass({ onEditClicked: function(e) { var MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu'); - var buttonRect = e.target.getBoundingClientRect() + var buttonRect = e.target.getBoundingClientRect(); // The window X and Y offsets are to adjust position when zoomed in to page var x = buttonRect.right + window.pageXOffset; @@ -293,7 +293,7 @@ module.exports = WithMatrixClient(React.createClass({ // If it is, we want to display the complete date along with the HH:MM:SS, // rather than just HH:MM:SS. let dayAfterEvent = new Date(this.props.mxEvent.getTs()); - dayAfterEvent.setDate(dayAfterEvent.getDate() + 1) + dayAfterEvent.setDate(dayAfterEvent.getDate() + 1); dayAfterEvent.setHours(0); dayAfterEvent.setMinutes(0); dayAfterEvent.setSeconds(0); @@ -369,7 +369,7 @@ module.exports = WithMatrixClient(React.createClass({ var event = this.props.mxEvent; Modal.createDialogAsync((cb) => { - require(['../../../async-components/views/dialogs/EncryptedEventDialog'], cb) + require(['../../../async-components/views/dialogs/EncryptedEventDialog'], cb); }, { event: event, }); diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index 60f4f8abc0..ef8fb29cbc 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -60,13 +60,15 @@ module.exports = React.createClass({ }, componentDidMount: function() { - if (this.refs.description) + if (this.refs.description) { linkifyElement(this.refs.description, linkifyMatrix.options); + } }, componentDidUpdate: function() { - if (this.refs.description) + if (this.refs.description) { linkifyElement(this.refs.description, linkifyMatrix.options); + } }, componentWillUnmount: function() { @@ -116,7 +118,7 @@ module.exports = React.createClass({ if (image) { img =
-
+
; } return ( diff --git a/src/components/views/rooms/MemberDeviceInfo.js b/src/components/views/rooms/MemberDeviceInfo.js index 1e7850ab44..d4c00dda76 100644 --- a/src/components/views/rooms/MemberDeviceInfo.js +++ b/src/components/views/rooms/MemberDeviceInfo.js @@ -60,7 +60,7 @@ export default class MemberDeviceInfo extends React.Component { ); } -}; +} MemberDeviceInfo.displayName = 'MemberDeviceInfo'; MemberDeviceInfo.propTypes = { diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 1f4d392461..16a047f72d 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -64,7 +64,7 @@ module.exports = WithMatrixClient(React.createClass({ updating: 0, devicesLoading: true, devices: null, - } + }; }, componentWillMount: function() { @@ -202,7 +202,7 @@ module.exports = WithMatrixClient(React.createClass({ } var cancelled = false; - this._cancelDeviceList = function() { cancelled = true; } + this._cancelDeviceList = function() { cancelled = true; }; var client = this.props.matrixClient; var self = this; @@ -529,7 +529,7 @@ module.exports = WithMatrixClient(React.createClass({ }); }, - onMemberAvatarClick: function () { + onMemberAvatarClick: function() { var avatarUrl = this.props.member.user ? this.props.member.user.avatarUrl : this.props.member.events.member.getContent().avatar_url; if(!avatarUrl) return; @@ -620,7 +620,7 @@ module.exports = WithMatrixClient(React.createClass({
Start new chat
- + ; startChat =

Direct chats

@@ -654,7 +654,7 @@ module.exports = WithMatrixClient(React.createClass({ var giveOpLabel = this.state.isTargetMod ? "Revoke Moderator" : "Make Moderator"; giveModButton =
{giveOpLabel} -
+
; } // TODO: we should have an invite button if this MemberInfo is showing a user who isn't actually in the current room yet @@ -672,7 +672,7 @@ module.exports = WithMatrixClient(React.createClass({ {banButton} {giveModButton} - + ; } const memberName = this.props.member.name; diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index deedded4fa..bd386ed1bb 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -32,7 +32,7 @@ var SHARE_HISTORY_WARNING = Newly invited users will see the history of this room.
If you'd prefer invited users not to see messages that were sent before they joined,
turn off, 'Share message history with new users' in the settings for this room. -
+
; module.exports = React.createClass({ displayName: 'MemberList', @@ -207,7 +207,7 @@ module.exports = React.createClass({ // For now we'll pretend this is any entity. It should probably be a separate tile. var EntityTile = sdk.getComponent("rooms.EntityTile"); var BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); - var text = "and " + overflowCount + " other" + (overflowCount > 1 ? "s" : "") + "..."; + var text = "and " + overflowCount + " other" + (overflowCount > 1 ? "s" : "") + "..."; return ( @@ -338,8 +338,8 @@ module.exports = React.createClass({ } memberList.push( - ) - }) + ); + }); } } diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index cf79394228..5becef9ede 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -46,7 +46,7 @@ module.exports = React.createClass({ (this.user_last_modified_time === undefined || this.user_last_modified_time < nextProps.member.user.getLastModifiedTime()) ) { - return true + return true; } return false; }, diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index ee9c49d52a..4dfb95f5d0 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -367,7 +367,7 @@ export default class MessageComposer extends React.Component { ); } -}; +} MessageComposer.propTypes = { tabComplete: React.PropTypes.any, diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index b6af5a9f09..634939331f 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -443,12 +443,12 @@ export default class MessageComposerInput extends React.Component { selection = this.state.editorState.getSelection(); let modifyFn = { - bold: text => `**${text}**`, - italic: text => `*${text}*`, - underline: text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug* - strike: text => `~~${text}~~`, - code: text => `\`${text}\``, - blockquote: text => text.split('\n').map(line => `> ${line}\n`).join(''), + 'bold': text => `**${text}**`, + 'italic': text => `*${text}*`, + 'underline': text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug* + 'strike': text => `~~${text}~~`, + 'code': text => `\`${text}\``, + 'blockquote': text => text.split('\n').map(line => `> ${line}\n`).join(''), 'unordered-list-item': text => text.split('\n').map(line => `- ${line}\n`).join(''), 'ordered-list-item': text => text.split('\n').map((line, i) => `${i+1}. ${line}\n`).join(''), }[command]; @@ -462,8 +462,9 @@ export default class MessageComposerInput extends React.Component { } } - if (newState == null) + if (newState == null) { newState = RichUtils.handleKeyCommand(this.state.editorState, command); + } if (newState != null) { this.setEditorState(newState); @@ -665,7 +666,7 @@ export default class MessageComposerInput extends React.Component { const blockName = { 'code-block': 'code', - blockquote: 'quote', + 'blockquote': 'quote', 'unordered-list-item': 'bullet', 'ordered-list-item': 'numbullet', }; @@ -740,7 +741,7 @@ export default class MessageComposerInput extends React.Component { ); } -}; +} MessageComposerInput.propTypes = { tabComplete: React.PropTypes.any, diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index ed4533737f..c5d5f083c1 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -192,7 +192,7 @@ module.exports = React.createClass({ } }, - onKeyDown: function (ev) { + onKeyDown: function(ev) { if (ev.keyCode === KeyCode.ENTER && !ev.shiftKey) { var input = this.refs.textarea.value; if (input.length === 0) { diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js index 47875bd7fb..c1fe4431bf 100644 --- a/src/components/views/rooms/ReadReceiptMarker.js +++ b/src/components/views/rooms/ReadReceiptMarker.js @@ -71,7 +71,7 @@ module.exports = React.createClass({ getDefaultProps: function() { return { leftOffset: 0, - } + }; }, getInitialState: function() { @@ -81,7 +81,7 @@ module.exports = React.createClass({ // position. return { suppressDisplay: !this.props.suppressAnimation, - } + }; }, componentWillUnmount: function() { diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index db3c7bb3d9..a066bfd337 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -182,8 +182,8 @@ module.exports = React.createClass({ 'm.room.name', user_id ); - save_button =
Save
- cancel_button =
Cancel
+ save_button =
Save
; + cancel_button =
Cancel
; } if (this.props.saving) { @@ -193,7 +193,7 @@ module.exports = React.createClass({ if (can_set_room_name) { var RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor"); - name = + name = ; } else { var searchStatus; @@ -232,7 +232,7 @@ module.exports = React.createClass({ if (can_set_room_topic) { var RoomTopicEditor = sdk.getComponent("rooms.RoomTopicEditor"); - topic_el = + topic_el = ; } else { var topic; if (this.props.room) { @@ -301,7 +301,7 @@ module.exports = React.createClass({ rightPanel_buttons =
-
+ ; } var right_row; diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 3ced4102d4..c3ee5f1730 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -46,7 +46,7 @@ module.exports = React.createClass({ isLoadingLeftRooms: false, lists: {}, incomingCall: null, - } + }; }, componentWillMount: function() { @@ -338,7 +338,7 @@ module.exports = React.createClass({ // as this is used to calculate the CSS fixed top position for the stickies var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height; - var top = (incomingCallBox.parentElement.getBoundingClientRect().top + window.pageYOffset) + var top = (incomingCallBox.parentElement.getBoundingClientRect().top + window.pageYOffset); // Make sure we don't go too far up, if the headers aren't sticky top = (top < scrollAreaOffset) ? scrollAreaOffset : top; // make sure we don't go too far down, if the headers aren't sticky @@ -401,7 +401,7 @@ module.exports = React.createClass({ var stickyHeight = sticky.dataset.originalHeight; var stickyHeader = sticky.childNodes[0]; var topStuckHeight = stickyHeight * i; - var bottomStuckHeight = stickyHeight * (stickyWrappers.length - i) + var bottomStuckHeight = stickyHeight * (stickyWrappers.length - i); if (self.scrollAreaSufficient && stickyPosition < (scrollArea.scrollTop + topStuckHeight)) { // Top stickies @@ -520,7 +520,7 @@ module.exports = React.createClass({ collapsed={ self.props.collapsed } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } - onShowMoreRooms={ self.onShowMoreRooms } /> + onShowMoreRooms={ self.onShowMoreRooms } />; } }) } diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index b9912b6fcc..218bac48aa 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -58,7 +58,7 @@ module.exports = React.createClass({ getInitialState: function() { return { busy: false - } + }; }, componentWillMount: function() { @@ -96,7 +96,7 @@ module.exports = React.createClass({ emailMatchBlock =
Unable to ascertain that the address this invite was sent to matches one associated with your account. -
+ ; } else if (this.state.invitedEmailMxid != MatrixClientPeg.get().credentials.userId) { emailMatchBlock =
@@ -107,7 +107,7 @@ module.exports = React.createClass({ This invitation was sent to {this.props.invitedEmail}, which is not associated with this account.
You may wish to login with a different account, or add this email to this account.
- + ; } } joinBlock = ( diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 04ea05843d..e14a929ebe 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -252,7 +252,7 @@ module.exports = React.createClass({ return this.refs.url_preview_settings.saveSettings(); }, - saveEncryption: function () { + saveEncryption: function() { if (!this.refs.encrypt) { return q(); } var encrypt = this.refs.encrypt.checked; @@ -404,7 +404,7 @@ module.exports = React.createClass({ var cli = MatrixClientPeg.get(); var roomState = this.props.room.currentState; return (roomState.mayClientSendStateEvent("m.room.join_rules", cli) && - roomState.mayClientSendStateEvent("m.room.guest_access", cli)) + roomState.mayClientSendStateEvent("m.room.guest_access", cli)); }, onManageIntegrations(ev) { @@ -510,7 +510,7 @@ module.exports = React.createClass({ var UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings"); var EditableText = sdk.getComponent('elements.EditableText'); var PowerSelector = sdk.getComponent('elements.PowerSelector'); - var Loader = sdk.getComponent("elements.Spinner") + var Loader = sdk.getComponent("elements.Spinner"); var cli = MatrixClientPeg.get(); var roomState = this.props.room.currentState; @@ -557,7 +557,7 @@ module.exports = React.createClass({ ; } else { - userLevelsSection =
No users have specific privileges in this room.
+ userLevelsSection =
No users have specific privileges in this room.
; } var banned = this.props.room.getMembersWithMembership("ban"); @@ -635,7 +635,7 @@ module.exports = React.createClass({ ); })) : (self.state.tags && self.state.tags.join) ? self.state.tags.join(", ") : "" } - + ; } // If there is no history_visibility, it is assumed to be 'shared'. @@ -653,7 +653,7 @@ module.exports = React.createClass({ addressWarning =
To link to a room it must have an address. -
+ ; } var inviteGuestWarning; @@ -664,7 +664,7 @@ module.exports = React.createClass({ this.setState({ join_rule: "invite", guest_access: "can_join" }); e.preventDefault(); }}>Click here to fix. - + ; } var integrationsButton; diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 834a06d8e6..83b9cf3c6f 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -221,7 +221,7 @@ module.exports = React.createClass({ var avatarContainerClasses = classNames({ 'mx_RoomTile_avatar_container': true, 'mx_RoomTile_avatar_roomTagMenu': this.state.roomTagMenu, - }) + }); var badgeClasses = classNames({ 'mx_RoomTile_badge': true, diff --git a/src/components/views/rooms/SearchableEntityList.js b/src/components/views/rooms/SearchableEntityList.js index ccf733b985..50169edad5 100644 --- a/src/components/views/rooms/SearchableEntityList.js +++ b/src/components/views/rooms/SearchableEntityList.js @@ -118,7 +118,7 @@ var SearchableEntityList = React.createClass({ _createOverflowEntity: function(overflowCount, totalCount) { var EntityTile = sdk.getComponent("rooms.EntityTile"); var BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); - var text = "and " + overflowCount + " other" + (overflowCount > 1 ? "s" : "") + "..."; + var text = "and " + overflowCount + " other" + (overflowCount > 1 ? "s" : "") + "..."; return ( @@ -135,8 +135,8 @@ var SearchableEntityList = React.createClass({ { this.setState({ focused: true }) }} - onBlur= {() => { this.setState({ focused: false }) }} + onFocus= {() => { this.setState({ focused: true }); }} + onBlur= {() => { this.setState({ focused: false }); }} placeholder={this.props.searchPlaceholderText} /> ); diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.js index 7f2bb0048a..4c63be5b99 100644 --- a/src/components/views/rooms/SimpleRoomHeader.js +++ b/src/components/views/rooms/SimpleRoomHeader.js @@ -44,7 +44,7 @@ module.exports = React.createClass({ var cancelButton; if (this.props.onCancelClick) { - cancelButton =
Cancel
+ cancelButton =
Cancel
; } var showRhsButton; diff --git a/src/components/views/rooms/UserTile.js b/src/components/views/rooms/UserTile.js index 9608247d5e..66d736f3bb 100644 --- a/src/components/views/rooms/UserTile.js +++ b/src/components/views/rooms/UserTile.js @@ -38,7 +38,7 @@ module.exports = React.createClass({ var active = -1; // FIXME: make presence data update whenever User.presence changes... - active = user.lastActiveAgo ? + active = user.lastActiveAgo ? (Date.now() - (user.lastPresenceTs - user.lastActiveAgo)) : -1; var BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js index 9b03aba1a3..de30b51f1b 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -49,7 +49,7 @@ module.exports = React.createClass({ return { avatarUrl: this.props.initialAvatarUrl, phase: this.Phases.Display, - } + }; }, componentWillReceiveProps: function(newProps) { @@ -120,7 +120,7 @@ module.exports = React.createClass({ var BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); // XXX: FIXME: once we track in the JS what our own displayname is(!) then use it here rather than ? avatarImg = + name='?' idName={ MatrixClientPeg.get().getUserIdLocalpart() } url={this.state.avatarUrl} />; } var uploadSection; @@ -130,7 +130,7 @@ module.exports = React.createClass({ Upload new: {this.state.errorText} - + ); } diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 1ef3eff205..a011d5262e 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -59,7 +59,7 @@ module.exports = React.createClass({ getInitialState: function() { return { phase: this.Phases.Edit - } + }; }, changePassword: function(old_password, new_password) { @@ -105,7 +105,7 @@ module.exports = React.createClass({ render: function() { var rowClassName = this.props.rowClassName; var rowLabelClassName = this.props.rowLabelClassName; - var rowInputClassName = this.props.rowInputClassName + var rowInputClassName = this.props.rowInputClassName; var buttonClassName = this.props.buttonClassName; switch (this.state.phase) { diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.js index f48d4bec85..28eee55527 100644 --- a/src/components/views/settings/DevicesPanel.js +++ b/src/components/views/settings/DevicesPanel.js @@ -88,7 +88,7 @@ export default class DevicesPanel extends React.Component { const removed_id = device.device_id; this.setState((state, props) => { const newDevices = state.devices.filter( - d => { return d.device_id != removed_id } + d => { return d.device_id != removed_id; } ); return { devices: newDevices }; }); @@ -98,7 +98,7 @@ export default class DevicesPanel extends React.Component { var DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry'); return ( {this._onDeviceDeleted(device)}} /> + onDeleted={()=>{this._onDeviceDeleted(device);}} /> ); } diff --git a/src/components/views/settings/DevicesPanelEntry.js b/src/components/views/settings/DevicesPanelEntry.js index f9f0e49a5e..4fa7d961ac 100644 --- a/src/components/views/settings/DevicesPanelEntry.js +++ b/src/components/views/settings/DevicesPanelEntry.js @@ -15,12 +15,9 @@ limitations under the License. */ import React from 'react'; -import classNames from 'classnames'; -import q from 'q'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; -import DateUtils from '../../../DateUtils'; import Modal from '../../../Modal'; export default class DevicesPanelEntry extends React.Component { @@ -61,7 +58,7 @@ export default class DevicesPanelEntry extends React.Component { if (this._unmounted) { return; } if (error.httpStatus !== 401 || !error.data || !error.data.flows) { // doesn't look like an interactive-auth failure - throw e; + throw error; } // pop up an interactive auth dialog @@ -121,7 +118,7 @@ export default class DevicesPanelEntry extends React.Component { let deleteButton; if (this.state.deleteError) { - deleteButton =
{this.state.deleteError}
+ deleteButton =
{this.state.deleteError}
; } else { deleteButton = (
-
+
diff --git a/src/createRoom.js b/src/createRoom.js index a1512e23f6..2a23fb0787 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -45,7 +45,7 @@ function createRoom(opts) { Modal.createDialog(NeedToRegisterDialog, { title: "Please Register", description: "Guest users can't create new rooms. Please register to create room and start a chat." - }) + }); }, 0); return q(null); } @@ -78,7 +78,7 @@ function createRoom(opts) { let modal; setTimeout(()=>{ - modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner') + modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); }, 0); let roomId; diff --git a/src/dispatcher.js b/src/dispatcher.js index 22ef712b6c..ed0350fe54 100644 --- a/src/dispatcher.js +++ b/src/dispatcher.js @@ -40,7 +40,7 @@ class MatrixDispatcher extends flux.Dispatcher { setTimeout(super.dispatch.bind(this, payload), 0); } } -}; +} if (global.mxDispatcher === undefined) { global.mxDispatcher = new MatrixDispatcher(); diff --git a/src/extend.js b/src/extend.js index e39e9e9be9..cc3c33b2e7 100644 --- a/src/extend.js +++ b/src/extend.js @@ -23,4 +23,4 @@ module.exports = function(dest, src) { } } return dest; -} +}; diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index e7d798c9ff..68f7a66bda 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -129,7 +129,7 @@ matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/ matrixLinkify.MATRIXTO_BASE_URL= "https://matrix.to"; matrixLinkify.options = { - events: function (href, type) { + events: function(href, type) { switch (type) { case "userid": return { @@ -146,7 +146,7 @@ matrixLinkify.options = { } }, - formatHref: function (href, type) { + formatHref: function(href, type) { switch (type) { case 'roomalias': case 'userid': diff --git a/src/utils/FormattingUtils.js b/src/utils/FormattingUtils.js index 44dcb2aa22..414784d101 100644 --- a/src/utils/FormattingUtils.js +++ b/src/utils/FormattingUtils.js @@ -25,4 +25,4 @@ export function formatCount(count) { if (count < 10000000) return (count / 1000000).toFixed(1) + "M"; if (count < 100000000) return (count / 1000000).toFixed(0) + "M"; return (count / 1000000000).toFixed(1) + "B"; // 10B is enough for anyone, right? :S -} \ No newline at end of file +} diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js index 983ec2c75f..abae81e5ad 100644 --- a/src/utils/MegolmExportEncryption.js +++ b/src/utils/MegolmExportEncryption.js @@ -71,7 +71,7 @@ export function decryptMegolmKeyFile(data, password) { toVerify, ).then((isValid) => { if (!isValid) { - throw new Error('Authentication check failed: incorrect password?') + throw new Error('Authentication check failed: incorrect password?'); } return subtleCrypto.decrypt( diff --git a/src/wrappers/WithMatrixClient.js b/src/wrappers/WithMatrixClient.js index c9c9e7adb7..8e56d17dff 100644 --- a/src/wrappers/WithMatrixClient.js +++ b/src/wrappers/WithMatrixClient.js @@ -36,4 +36,4 @@ export default function(WrappedComponent) { return ; }, }); -}; +} From 0c5762b91da9f13402018f7a914e83b14c16678c Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Fri, 20 Jan 2017 17:51:35 +0100 Subject: [PATCH 50/57] Implement "someone is typing" avatars (#631) When users are typing, their avatars can be seen instead of "..." in the RoomView StatusBar --- src/components/structures/RoomStatusBar.js | 39 +++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index c6f2d6500b..618989a75c 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -19,6 +19,9 @@ var sdk = require('../../index'); var dis = require("../../dispatcher"); var WhoIsTyping = require("../../WhoIsTyping"); var MatrixClientPeg = require("../../MatrixClientPeg"); +const MemberAvatar = require("../views/avatars/MemberAvatar"); + +const TYPING_AVATARS_LIMIT = 2; module.exports = React.createClass({ displayName: 'RoomStatusBar', @@ -173,10 +176,8 @@ module.exports = React.createClass({ if (wantPlaceholder) { return ( -
- . - . - . +
+ {this._renderTypingIndicatorAvatars(TYPING_AVATARS_LIMIT)}
); } @@ -184,6 +185,36 @@ module.exports = React.createClass({ return null; }, + _renderTypingIndicatorAvatars: function(limit) { + let users = WhoIsTyping.usersTyping(this.props.room); + + let othersCount = Math.max(users.length - limit, 0); + users = users.slice(0, limit); + + let avatars = users.map((u, index) => { + let showInitial = othersCount === 0 && index === users.length - 1; + return ( + + ); + }); + + if (othersCount > 0) { + avatars.push( + + +{othersCount} + + ); + } + + return avatars; + }, // return suitable content for the main (text) part of the status bar. _getContent: function() { From 937f13d578c0b63b911d4f68728dd7e28cab60d5 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 20 Jan 2017 21:00:22 +0000 Subject: [PATCH 51/57] fix a bunch of dark-theme buttons --- src/components/structures/UserSettings.js | 8 +++---- .../views/room_settings/AliasSettings.js | 4 ++-- src/components/views/rooms/MessageComposer.js | 21 +++++++++++-------- .../views/rooms/MessageComposerInput.js | 2 +- src/components/views/rooms/RoomHeader.js | 2 +- 5 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 8f6473a181..4a1332be8c 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -607,8 +607,8 @@ module.exports = React.createClass({
-
- Remove +
+ Remove
); @@ -630,7 +630,7 @@ module.exports = React.createClass({ blurToCancel={ false } onValueChanged={ this.onAddThreepidClicked } />
-
+
Add
@@ -711,7 +711,7 @@ module.exports = React.createClass({
diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index 5935298634..6543f2a17d 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.js @@ -281,7 +281,7 @@ module.exports = React.createClass({ onValueChanged={ self.onAliasChanged.bind(self, localDomain, i) } editable={ self.props.canSetAliases } initialValue={ alias } /> -
+
{ deleteButton }
@@ -297,7 +297,7 @@ module.exports = React.createClass({ placeholder={ "New address (e.g. #foo:" + localDomain + ")" } blurToCancel={ false } onValueChanged={ self.onAliasAdded } /> -
+
Add
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 4dfb95f5d0..113224666d 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -222,20 +222,22 @@ export default class MessageComposer extends React.Component {
); - let e2eimg, e2etitle; + let e2eImg, e2eTitle, e2eClass; if (MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId)) { // FIXME: show a /!\ if there are untrusted devices in the room... - e2eimg = 'img/e2e-verified.svg'; - e2etitle = 'Encrypted room'; + e2eImg = 'img/e2e-verified.svg'; + e2eTitle = 'Encrypted room'; + e2eClass = 'mx_MessageComposer_e2eIcon'; } else { - e2eimg = 'img/e2e-unencrypted.svg'; - e2etitle = 'Unencrypted room'; + e2eImg = 'img/e2e-unencrypted.svg'; + e2eTitle = 'Unencrypted room'; + e2eClass = 'mx_MessageComposer_e2eIcon mx_filterFlipColor'; } controls.push( - {e2etitle} ); var callButton, videoCallButton, hangupButton; @@ -331,6 +333,7 @@ export default class MessageComposer extends React.Component { const disabled = !this.state.inputState.isRichtextEnabled && 'underline' === name; const className = classNames("mx_MessageComposer_format_button", { mx_MessageComposer_format_button_disabled: disabled, + mx_filterFlipColor: true, }); return
: null diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 634939331f..bf936a2c13 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -719,7 +719,7 @@ export default class MessageComposerInput extends React.Component { selection={selection} />
- diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index a066bfd337..e345918f07 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -183,7 +183,7 @@ module.exports = React.createClass({ ); save_button =
Save
; - cancel_button =
Cancel
; + cancel_button =
Cancel
; } if (this.props.saving) { From d97fc0a99a66f564d3d64c98b6a947d47fd89266 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 23 Jan 2017 10:25:33 +0000 Subject: [PATCH 52/57] Fix typing avatars displaying "me" --- src/components/structures/RoomStatusBar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 618989a75c..d6d41f21d8 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -186,7 +186,7 @@ module.exports = React.createClass({ }, _renderTypingIndicatorAvatars: function(limit) { - let users = WhoIsTyping.usersTyping(this.props.room); + let users = WhoIsTyping.usersTypingApartFromMe(this.props.room); let othersCount = Math.max(users.length - limit, 0); users = users.slice(0, limit); From dc08d9dfdfb75d154f1a7464dc50e822a369e5e0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 23 Jan 2017 12:18:41 +0000 Subject: [PATCH 53/57] Fix device verification from e2e info Don't attempt to reuse the same AsyncWrapper for different dialogs - which ends up pushing the props for the new dialog into the old dialog. Fixes https://github.com/vector-im/riot-web/issues/3020 --- src/Modal.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Modal.js b/src/Modal.js index 862e4befc5..f0ab97a91e 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -67,6 +67,8 @@ const AsyncWrapper = React.createClass({ }, }); +let _counter = 0; + module.exports = { DialogContainerId: "mx_Dialog_Container", @@ -113,12 +115,16 @@ module.exports = { ReactDOM.unmountComponentAtNode(self.getOrCreateContainer()); }; + // don't attempt to reuse the same AsyncWrapper for different dialogs, + // otherwise we'll get confused. + const modalCount = _counter++; + // FIXME: If a dialog uses getDefaultProps it clobbers the onFinished // property set here so you can't close the dialog from a button click! var dialog = (
- +
From 4d12a65529e5fb08726e56328177b410686d3595 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 23 Jan 2017 14:16:25 +0000 Subject: [PATCH 54/57] Add mocha env for tests in eslint config --- test/.eslintrc.js | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 test/.eslintrc.js diff --git a/test/.eslintrc.js b/test/.eslintrc.js new file mode 100644 index 0000000000..4cc4659d7d --- /dev/null +++ b/test/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + env: { + mocha: true, + }, +} From a06ecb87bc4c0d10a5d516e3a89395a437327283 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 23 Jan 2017 16:01:39 +0100 Subject: [PATCH 55/57] Hide RoomStatusBar when it displays nothing (#615) Use CSS class `mx_RoomView_statusArea_expanded` to indicate an expanded status bar. Without this, the status bar may be hidden from view. A 10s debounce will prevent it from bouncing frequently. --- src/components/structures/RoomStatusBar.js | 67 +++++++++++++--------- src/components/structures/RoomView.js | 24 +++++++- 2 files changed, 62 insertions(+), 29 deletions(-) diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index d6d41f21d8..0eb50161ec 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -23,6 +23,11 @@ const MemberAvatar = require("../views/avatars/MemberAvatar"); const TYPING_AVATARS_LIMIT = 2; +const HIDE_DEBOUNCE_MS = 10000; +const STATUS_BAR_HIDDEN = 0; +const STATUS_BAR_EXPANDED = 1; +const STATUS_BAR_EXPANDED_LARGE = 2; + module.exports = React.createClass({ displayName: 'RoomStatusBar', @@ -63,6 +68,13 @@ module.exports = React.createClass({ // status bar. This is used to trigger a re-layout in the parent // component. onResize: React.PropTypes.func, + + // callback for when the status bar can be hidden from view, as it is + // not displaying anything + onHidden: React.PropTypes.func, + // callback for when the status bar is displaying something and should + // be visible + onVisible: React.PropTypes.func, }, getInitialState: function() { @@ -81,6 +93,18 @@ module.exports = React.createClass({ if(this.props.onResize && this._checkForResize(prevProps, prevState)) { this.props.onResize(); } + + const size = this._getSize(this.state, this.props); + if (size > 0) { + this.props.onVisible(); + } else { + if (this.hideDebouncer) { + clearTimeout(this.hideDebouncer); + } + this.hideDebouncer = setTimeout(() => { + this.props.onHidden(); + }, HIDE_DEBOUNCE_MS); + } }, componentWillUnmount: function() { @@ -107,35 +131,24 @@ module.exports = React.createClass({ }); }, + // We don't need the actual height - just whether it is likely to have + // changed - so we use '0' to indicate normal size, and other values to + // indicate other sizes. + _getSize: function(state, props) { + if (state.syncState === "ERROR" || state.whoisTypingString) { + return STATUS_BAR_EXPANDED; + } else if (props.tabCompleteEntries) { + return STATUS_BAR_HIDDEN; + } else if (props.hasUnsentMessages) { + return STATUS_BAR_EXPANDED_LARGE; + } + return STATUS_BAR_HIDDEN; + }, + // determine if we need to call onResize _checkForResize: function(prevProps, prevState) { - // figure out the old height and the new height of the status bar. We - // don't need the actual height - just whether it is likely to have - // changed - so we use '0' to indicate normal size, and other values to - // indicate other sizes. - var oldSize, newSize; - - if (prevState.syncState === "ERROR") { - oldSize = 1; - } else if (prevProps.tabCompleteEntries) { - oldSize = 0; - } else if (prevProps.hasUnsentMessages) { - oldSize = 2; - } else { - oldSize = 0; - } - - if (this.state.syncState === "ERROR") { - newSize = 1; - } else if (this.props.tabCompleteEntries) { - newSize = 0; - } else if (this.props.hasUnsentMessages) { - newSize = 2; - } else { - newSize = 0; - } - - return newSize != oldSize; + // figure out the old height and the new height of the status bar. + return this._getSize(prevProps, prevState) !== this._getSize(this.props, this.state); }, // return suitable content for the image on the left of the status bar. diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 1f35e41817..8753540e48 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -146,6 +146,8 @@ module.exports = React.createClass({ showTopUnreadMessagesBar: false, auxPanelMaxHeight: undefined, + + statusBarVisible: false, }; }, @@ -1333,6 +1335,18 @@ module.exports = React.createClass({ // no longer anything to do here }, + onStatusBarVisible: function() { + this.setState({ + statusBarVisible: true, + }); + }, + + onStatusBarHidden: function() { + this.setState({ + statusBarVisible: false, + }); + }, + showSettings: function(show) { // XXX: this is a bit naughty; we should be doing this via props if (show) { @@ -1515,7 +1529,9 @@ module.exports = React.createClass({ onCancelAllClick={this.onCancelAllClick} onScrollToBottomClick={this.jumpToLiveTimeline} onResize={this.onChildResize} - />; + onVisible={this.onStatusBarVisible} + onHidden={this.onStatusBarHidden} + />; } var aux = null; @@ -1669,6 +1685,10 @@ module.exports = React.createClass({
); } + let statusBarAreaClass = "mx_RoomView_statusArea mx_fadable"; + if (this.state.statusBarVisible) { + statusBarAreaClass += " mx_RoomView_statusArea_expanded"; + } return (
@@ -1691,7 +1711,7 @@ module.exports = React.createClass({ { topUnreadMessagesBar } { messagePanel } { searchResultsPanel } -
+
{ statusBar } From 94fdd2bcd286760613d66e2675031b1a57bd3c3a Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 23 Jan 2017 15:41:33 +0000 Subject: [PATCH 56/57] lint all --- jenkins.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins.sh b/jenkins.sh index 3b4e31fd7f..c1fba19e94 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -19,7 +19,7 @@ npm install npm run test # run eslint -npm run lint -- -f checkstyle -o eslint.xml || true +npm run lintall -- -f checkstyle -o eslint.xml || true # delete the old tarball, if it exists rm -f matrix-react-sdk-*.tgz From 1e77b2eba33f8b084dd43030d1929c50cb870cc0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 23 Jan 2017 16:19:29 +0000 Subject: [PATCH 57/57] Bundle eslint config --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 8e1ead4b9e..0a8c09f984 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "main": "lib/index.js", "files": [ + ".eslintrc.js", "CHANGELOG.md", "CONTRIBUTING.rst", "LICENSE",