Merge branch 'develop' into matthew/warn-unknown-devices
This commit is contained in:
commit
a2dd1fa0a9
45 changed files with 2021 additions and 589 deletions
1
.eslintignore
Normal file
1
.eslintignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
src/component-index.js
|
11
.eslintrc.js
11
.eslintrc.js
|
@ -1,6 +1,15 @@
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// get the path of the js-sdk so we can extend the config
|
||||||
|
// eslint supports loading extended configs by module,
|
||||||
|
// but only if they come from a module that starts with eslint-config-
|
||||||
|
// So we load the filename directly (and it could be in node_modules/
|
||||||
|
// or or ../node_modules/ etc)
|
||||||
|
const matrixJsSdkPath = path.dirname(require.resolve('matrix-js-sdk'));
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
parser: "babel-eslint",
|
parser: "babel-eslint",
|
||||||
extends: ["./node_modules/matrix-js-sdk/.eslintrc.js"],
|
extends: [matrixJsSdkPath + "/.eslintrc.js"],
|
||||||
plugins: [
|
plugins: [
|
||||||
"react",
|
"react",
|
||||||
"flowtype",
|
"flowtype",
|
||||||
|
|
|
@ -19,7 +19,7 @@ npm install
|
||||||
npm run test
|
npm run test
|
||||||
|
|
||||||
# run eslint
|
# 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
|
# delete the old tarball, if it exists
|
||||||
rm -f matrix-react-sdk-*.tgz
|
rm -f matrix-react-sdk-*.tgz
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"files": [
|
"files": [
|
||||||
|
".eslintrc.js",
|
||||||
"CHANGELOG.md",
|
"CHANGELOG.md",
|
||||||
"CONTRIBUTING.rst",
|
"CONTRIBUTING.rst",
|
||||||
"LICENSE",
|
"LICENSE",
|
||||||
|
@ -46,10 +47,12 @@
|
||||||
"browser-encrypt-attachment": "^0.3.0",
|
"browser-encrypt-attachment": "^0.3.0",
|
||||||
"browser-request": "^0.3.3",
|
"browser-request": "^0.3.3",
|
||||||
"classnames": "^2.1.2",
|
"classnames": "^2.1.2",
|
||||||
|
"commonmark": "^0.27.0",
|
||||||
"draft-js": "^0.8.1",
|
"draft-js": "^0.8.1",
|
||||||
"draft-js-export-html": "^0.5.0",
|
"draft-js-export-html": "^0.5.0",
|
||||||
"draft-js-export-markdown": "^0.2.0",
|
"draft-js-export-markdown": "^0.2.0",
|
||||||
"emojione": "2.2.3",
|
"emojione": "2.2.3",
|
||||||
|
"file-saver": "^1.3.3",
|
||||||
"filesize": "^3.1.2",
|
"filesize": "^3.1.2",
|
||||||
"flux": "^2.0.3",
|
"flux": "^2.0.3",
|
||||||
"fuse.js": "^2.2.0",
|
"fuse.js": "^2.2.0",
|
||||||
|
@ -58,7 +61,6 @@
|
||||||
"isomorphic-fetch": "^2.2.1",
|
"isomorphic-fetch": "^2.2.1",
|
||||||
"linkifyjs": "^2.1.3",
|
"linkifyjs": "^2.1.3",
|
||||||
"lodash": "^4.13.1",
|
"lodash": "^4.13.1",
|
||||||
"commonmark": "^0.27.0",
|
|
||||||
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
|
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
"q": "^1.4.1",
|
"q": "^1.4.1",
|
||||||
|
|
|
@ -19,9 +19,12 @@ import MultiInviter from './utils/MultiInviter';
|
||||||
|
|
||||||
const emailRegex = /^\S+@\S+\.\S+$/;
|
const emailRegex = /^\S+@\S+\.\S+$/;
|
||||||
|
|
||||||
|
// We allow localhost for mxids to avoid confusion
|
||||||
|
const mxidRegex = /^@\S+:(?:\S+\.\S+|localhost)$/
|
||||||
|
|
||||||
export function getAddressType(inputText) {
|
export function getAddressType(inputText) {
|
||||||
const isEmailAddress = /^\S+@\S+\.\S+$/.test(inputText);
|
const isEmailAddress = emailRegex.test(inputText);
|
||||||
const isMatrixId = inputText[0] === '@' && inputText.indexOf(":") > 0;
|
const isMatrixId = mxidRegex.test(inputText);
|
||||||
|
|
||||||
// sanity check the input for user IDs
|
// sanity check the input for user IDs
|
||||||
if (isEmailAddress) {
|
if (isEmailAddress) {
|
||||||
|
|
|
@ -20,6 +20,7 @@ module.exports = {
|
||||||
TAB: 9,
|
TAB: 9,
|
||||||
ENTER: 13,
|
ENTER: 13,
|
||||||
SHIFT: 16,
|
SHIFT: 16,
|
||||||
|
ESCAPE: 27,
|
||||||
PAGE_UP: 33,
|
PAGE_UP: 33,
|
||||||
PAGE_DOWN: 34,
|
PAGE_DOWN: 34,
|
||||||
END: 35,
|
END: 35,
|
||||||
|
|
|
@ -67,6 +67,8 @@ const AsyncWrapper = React.createClass({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let _counter = 0;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
DialogContainerId: "mx_Dialog_Container",
|
DialogContainerId: "mx_Dialog_Container",
|
||||||
|
|
||||||
|
@ -113,12 +115,16 @@ module.exports = {
|
||||||
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
|
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
|
// FIXME: If a dialog uses getDefaultProps it clobbers the onFinished
|
||||||
// property set here so you can't close the dialog from a button click!
|
// property set here so you can't close the dialog from a button click!
|
||||||
var dialog = (
|
var dialog = (
|
||||||
<div className={"mx_Dialog_wrapper " + className}>
|
<div className={"mx_Dialog_wrapper " + className}>
|
||||||
<div className="mx_Dialog">
|
<div className="mx_Dialog">
|
||||||
<AsyncWrapper loader={loader} {...props} onFinished={closeDialog}/>
|
<AsyncWrapper key={modalCount} loader={loader} {...props} onFinished={closeDialog}/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_background" onClick={ closeDialog.bind(this, false) }></div>
|
<div className="mx_Dialog_background" onClick={ closeDialog.bind(this, false) }></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -32,17 +32,24 @@ module.exports = {
|
||||||
return whoIsTyping;
|
return whoIsTyping;
|
||||||
},
|
},
|
||||||
|
|
||||||
whoIsTypingString: function(room) {
|
whoIsTypingString: function(room, limit) {
|
||||||
var whoIsTyping = this.usersTypingApartFromMe(room);
|
const whoIsTyping = this.usersTypingApartFromMe(room);
|
||||||
|
const othersCount = limit === undefined ?
|
||||||
|
0 : Math.max(whoIsTyping.length - limit, 0);
|
||||||
if (whoIsTyping.length == 0) {
|
if (whoIsTyping.length == 0) {
|
||||||
return null;
|
return '';
|
||||||
} else if (whoIsTyping.length == 1) {
|
} else if (whoIsTyping.length == 1) {
|
||||||
return whoIsTyping[0].name + ' is typing';
|
return whoIsTyping[0].name + ' is typing';
|
||||||
|
}
|
||||||
|
const names = whoIsTyping.map(function(m) {
|
||||||
|
return m.name;
|
||||||
|
});
|
||||||
|
if (othersCount) {
|
||||||
|
const other = ' other' + (othersCount > 1 ? 's' : '');
|
||||||
|
return names.slice(0, limit).join(', ') + ' and ' +
|
||||||
|
othersCount + other + ' are typing';
|
||||||
} else {
|
} else {
|
||||||
var names = whoIsTyping.map(function(m) {
|
const lastPerson = names.pop();
|
||||||
return m.name;
|
|
||||||
});
|
|
||||||
var lastPerson = names.shift();
|
|
||||||
return names.join(', ') + ' and ' + lastPerson + ' are typing';
|
return names.join(', ') + ' and ' + lastPerson + ' are typing';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,71 +14,158 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import FileSaver from 'file-saver';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import * as Matrix from 'matrix-js-sdk';
|
||||||
|
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
|
||||||
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
|
const PHASE_EDIT = 1;
|
||||||
|
const PHASE_EXPORTING = 2;
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'ExportE2eKeysDialog',
|
displayName: 'ExportE2eKeysDialog',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
||||||
|
onFinished: React.PropTypes.func.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
collectedPassword: false,
|
phase: PHASE_EDIT,
|
||||||
|
errStr: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
this._unmounted = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
this._unmounted = true;
|
||||||
|
},
|
||||||
|
|
||||||
_onPassphraseFormSubmit: function(ev) {
|
_onPassphraseFormSubmit: function(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
console.log(this.refs.passphrase1.value);
|
|
||||||
|
const passphrase = this.refs.passphrase1.value;
|
||||||
|
if (passphrase !== this.refs.passphrase2.value) {
|
||||||
|
this.setState({errStr: 'Passphrases must match'});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!passphrase) {
|
||||||
|
this.setState({errStr: 'Passphrase must not be empty'});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._startExport(passphrase);
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
_startExport: function(passphrase) {
|
||||||
let content;
|
// extra Promise.resolve() to turn synchronous exceptions into
|
||||||
if (!this.state.collectedPassword) {
|
// asynchronous ones.
|
||||||
content = (
|
Promise.resolve().then(() => {
|
||||||
<div className="mx_Dialog_content">
|
return this.props.matrixClient.exportRoomKeys();
|
||||||
<p>
|
}).then((k) => {
|
||||||
This process will allow you to export the keys for messages
|
return MegolmExportEncryption.encryptMegolmKeyFile(
|
||||||
you have received in encrypted rooms to a local file. You
|
JSON.stringify(k), passphrase
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
<form onSubmit={this._onPassphraseFormSubmit}>
|
|
||||||
<div className="mx_TextInputDialog_label">
|
|
||||||
<label htmlFor="passphrase1">Enter passphrase</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input ref="passphrase1" id="passphrase1"
|
|
||||||
className="mx_TextInputDialog_input"
|
|
||||||
autoFocus={true} size="64" type="password"/>
|
|
||||||
</div>
|
|
||||||
<div className="mx_Dialog_buttons">
|
|
||||||
<input className="mx_Dialog_primary" type="submit" value="Export" />
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}).then((f) => {
|
||||||
|
const blob = new Blob([f], {
|
||||||
|
type: 'text/plain;charset=us-ascii',
|
||||||
|
});
|
||||||
|
FileSaver.saveAs(blob, 'riot-keys.txt');
|
||||||
|
this.props.onFinished(true);
|
||||||
|
}).catch((e) => {
|
||||||
|
if (this._unmounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
errStr: e.message,
|
||||||
|
phase: PHASE_EDIT,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
errStr: null,
|
||||||
|
phase: PHASE_EXPORTING,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
|
||||||
|
|
||||||
|
const disableForm = (this.state.phase === PHASE_EXPORTING);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_exportE2eKeysDialog">
|
<BaseDialog className='mx_exportE2eKeysDialog'
|
||||||
<div className="mx_Dialog_title">
|
onFinished={this.props.onFinished}
|
||||||
Export room keys
|
title="Export room keys"
|
||||||
</div>
|
>
|
||||||
{content}
|
<form onSubmit={this._onPassphraseFormSubmit}>
|
||||||
</div>
|
<div className="mx_Dialog_content">
|
||||||
|
<p>
|
||||||
|
This process allows 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.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<div className='error'>
|
||||||
|
{this.state.errStr}
|
||||||
|
</div>
|
||||||
|
<div className='mx_E2eKeysDialog_inputTable'>
|
||||||
|
<div className='mx_E2eKeysDialog_inputRow'>
|
||||||
|
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||||
|
<label htmlFor='passphrase1'>
|
||||||
|
Enter passphrase
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
|
<input ref='passphrase1' id='passphrase1'
|
||||||
|
autoFocus={true} size='64' type='password'
|
||||||
|
disabled={disableForm}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='mx_E2eKeysDialog_inputRow'>
|
||||||
|
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||||
|
<label htmlFor='passphrase2'>
|
||||||
|
Confirm passphrase
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
|
<input ref='passphrase2' id='passphrase2'
|
||||||
|
size='64' type='password'
|
||||||
|
disabled={disableForm}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='mx_Dialog_buttons'>
|
||||||
|
<input className='mx_Dialog_primary' type='submit' value='Export'
|
||||||
|
disabled={disableForm}
|
||||||
|
/>
|
||||||
|
<AccessibleButton element='button' onClick={this.props.onFinished}
|
||||||
|
disabled={disableForm}>
|
||||||
|
Cancel
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
170
src/async-components/views/dialogs/ImportE2eKeysDialog.js
Normal file
170
src/async-components/views/dialogs/ImportE2eKeysDialog.js
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
/*
|
||||||
|
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 * as Matrix from 'matrix-js-sdk';
|
||||||
|
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
|
||||||
|
function readFileAsArrayBuffer(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
resolve(e.target.result);
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const PHASE_EDIT = 1;
|
||||||
|
const PHASE_IMPORTING = 2;
|
||||||
|
|
||||||
|
export default React.createClass({
|
||||||
|
displayName: 'ImportE2eKeysDialog',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
||||||
|
onFinished: React.PropTypes.func.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
enableSubmit: false,
|
||||||
|
phase: PHASE_EDIT,
|
||||||
|
errStr: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
this._unmounted = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
this._unmounted = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
_onFormChange: function(ev) {
|
||||||
|
const files = this.refs.file.files || [];
|
||||||
|
this.setState({
|
||||||
|
enableSubmit: (this.refs.passphrase.value !== "" && files.length > 0),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onFormSubmit: function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this._startImport(this.refs.file.files[0], this.refs.passphrase.value);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
_startImport: function(file, passphrase) {
|
||||||
|
this.setState({
|
||||||
|
errStr: null,
|
||||||
|
phase: PHASE_IMPORTING,
|
||||||
|
});
|
||||||
|
|
||||||
|
return readFileAsArrayBuffer(file).then((arrayBuffer) => {
|
||||||
|
return MegolmExportEncryption.decryptMegolmKeyFile(
|
||||||
|
arrayBuffer, passphrase
|
||||||
|
);
|
||||||
|
}).then((keys) => {
|
||||||
|
return this.props.matrixClient.importRoomKeys(JSON.parse(keys));
|
||||||
|
}).then(() => {
|
||||||
|
// TODO: it would probably be nice to give some feedback about what we've imported here.
|
||||||
|
this.props.onFinished(true);
|
||||||
|
}).catch((e) => {
|
||||||
|
if (this._unmounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
errStr: e.message,
|
||||||
|
phase: PHASE_EDIT,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
|
||||||
|
|
||||||
|
const disableForm = (this.state.phase !== PHASE_EDIT);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseDialog className='mx_importE2eKeysDialog'
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
|
title="Import room keys"
|
||||||
|
>
|
||||||
|
<form onSubmit={this._onFormSubmit}>
|
||||||
|
<div className="mx_Dialog_content">
|
||||||
|
<p>
|
||||||
|
This process allows you to import encryption keys
|
||||||
|
that you had previously exported from another Matrix
|
||||||
|
client. You will then be able to decrypt any
|
||||||
|
messages that the other client could decrypt.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The export file will be protected with a passphrase.
|
||||||
|
You should enter the passphrase here, to decrypt the
|
||||||
|
file.
|
||||||
|
</p>
|
||||||
|
<div className='error'>
|
||||||
|
{this.state.errStr}
|
||||||
|
</div>
|
||||||
|
<div className='mx_E2eKeysDialog_inputTable'>
|
||||||
|
<div className='mx_E2eKeysDialog_inputRow'>
|
||||||
|
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||||
|
<label htmlFor='importFile'>
|
||||||
|
File to import
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
|
<input ref='file' id='importFile' type='file'
|
||||||
|
autoFocus={true}
|
||||||
|
onChange={this._onFormChange}
|
||||||
|
disabled={disableForm} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='mx_E2eKeysDialog_inputRow'>
|
||||||
|
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||||
|
<label htmlFor='passphrase'>
|
||||||
|
Enter passphrase
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className='mx_E2eKeysDialog_inputCell'>
|
||||||
|
<input ref='passphrase' id='passphrase'
|
||||||
|
size='64' type='password'
|
||||||
|
onChange={this._onFormChange}
|
||||||
|
disabled={disableForm}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='mx_Dialog_buttons'>
|
||||||
|
<input className='mx_Dialog_primary' type='submit' value='Import'
|
||||||
|
disabled={!this.state.enableSubmit || disableForm}
|
||||||
|
/>
|
||||||
|
<AccessibleButton element='button' onClick={this.props.onFinished}
|
||||||
|
disabled={disableForm}>
|
||||||
|
Cancel
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -71,6 +71,8 @@ import views$create_room$Presets from './components/views/create_room/Presets';
|
||||||
views$create_room$Presets && (module.exports.components['views.create_room.Presets'] = views$create_room$Presets);
|
views$create_room$Presets && (module.exports.components['views.create_room.Presets'] = views$create_room$Presets);
|
||||||
import views$create_room$RoomAlias from './components/views/create_room/RoomAlias';
|
import views$create_room$RoomAlias from './components/views/create_room/RoomAlias';
|
||||||
views$create_room$RoomAlias && (module.exports.components['views.create_room.RoomAlias'] = views$create_room$RoomAlias);
|
views$create_room$RoomAlias && (module.exports.components['views.create_room.RoomAlias'] = views$create_room$RoomAlias);
|
||||||
|
import views$dialogs$BaseDialog from './components/views/dialogs/BaseDialog';
|
||||||
|
views$dialogs$BaseDialog && (module.exports.components['views.dialogs.BaseDialog'] = views$dialogs$BaseDialog);
|
||||||
import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog';
|
import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog';
|
||||||
views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog);
|
views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog);
|
||||||
import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog';
|
import views$dialogs$DeactivateAccountDialog from './components/views/dialogs/DeactivateAccountDialog';
|
||||||
|
@ -79,8 +81,6 @@ import views$dialogs$ErrorDialog from './components/views/dialogs/ErrorDialog';
|
||||||
views$dialogs$ErrorDialog && (module.exports.components['views.dialogs.ErrorDialog'] = views$dialogs$ErrorDialog);
|
views$dialogs$ErrorDialog && (module.exports.components['views.dialogs.ErrorDialog'] = views$dialogs$ErrorDialog);
|
||||||
import views$dialogs$InteractiveAuthDialog from './components/views/dialogs/InteractiveAuthDialog';
|
import views$dialogs$InteractiveAuthDialog from './components/views/dialogs/InteractiveAuthDialog';
|
||||||
views$dialogs$InteractiveAuthDialog && (module.exports.components['views.dialogs.InteractiveAuthDialog'] = views$dialogs$InteractiveAuthDialog);
|
views$dialogs$InteractiveAuthDialog && (module.exports.components['views.dialogs.InteractiveAuthDialog'] = views$dialogs$InteractiveAuthDialog);
|
||||||
import views$dialogs$LogoutPrompt from './components/views/dialogs/LogoutPrompt';
|
|
||||||
views$dialogs$LogoutPrompt && (module.exports.components['views.dialogs.LogoutPrompt'] = views$dialogs$LogoutPrompt);
|
|
||||||
import views$dialogs$NeedToRegisterDialog from './components/views/dialogs/NeedToRegisterDialog';
|
import views$dialogs$NeedToRegisterDialog from './components/views/dialogs/NeedToRegisterDialog';
|
||||||
views$dialogs$NeedToRegisterDialog && (module.exports.components['views.dialogs.NeedToRegisterDialog'] = views$dialogs$NeedToRegisterDialog);
|
views$dialogs$NeedToRegisterDialog && (module.exports.components['views.dialogs.NeedToRegisterDialog'] = views$dialogs$NeedToRegisterDialog);
|
||||||
import views$dialogs$QuestionDialog from './components/views/dialogs/QuestionDialog';
|
import views$dialogs$QuestionDialog from './components/views/dialogs/QuestionDialog';
|
||||||
|
@ -91,6 +91,8 @@ import views$dialogs$TextInputDialog from './components/views/dialogs/TextInputD
|
||||||
views$dialogs$TextInputDialog && (module.exports.components['views.dialogs.TextInputDialog'] = views$dialogs$TextInputDialog);
|
views$dialogs$TextInputDialog && (module.exports.components['views.dialogs.TextInputDialog'] = views$dialogs$TextInputDialog);
|
||||||
import views$dialogs$UnknownDeviceDialog from './components/views/dialogs/UnknownDeviceDialog';
|
import views$dialogs$UnknownDeviceDialog from './components/views/dialogs/UnknownDeviceDialog';
|
||||||
views$dialogs$UnknownDeviceDialog && (module.exports.components['views.dialogs.UnknownDeviceDialog'] = views$dialogs$UnknownDeviceDialog);
|
views$dialogs$UnknownDeviceDialog && (module.exports.components['views.dialogs.UnknownDeviceDialog'] = views$dialogs$UnknownDeviceDialog);
|
||||||
|
import views$elements$AccessibleButton from './components/views/elements/AccessibleButton';
|
||||||
|
views$elements$AccessibleButton && (module.exports.components['views.elements.AccessibleButton'] = views$elements$AccessibleButton);
|
||||||
import views$elements$AddressSelector from './components/views/elements/AddressSelector';
|
import views$elements$AddressSelector from './components/views/elements/AddressSelector';
|
||||||
views$elements$AddressSelector && (module.exports.components['views.elements.AddressSelector'] = views$elements$AddressSelector);
|
views$elements$AddressSelector && (module.exports.components['views.elements.AddressSelector'] = views$elements$AddressSelector);
|
||||||
import views$elements$AddressTile from './components/views/elements/AddressTile';
|
import views$elements$AddressTile from './components/views/elements/AddressTile';
|
||||||
|
|
|
@ -21,6 +21,7 @@ import KeyCode from '../../KeyCode';
|
||||||
import Notifier from '../../Notifier';
|
import Notifier from '../../Notifier';
|
||||||
import PageTypes from '../../PageTypes';
|
import PageTypes from '../../PageTypes';
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
|
import dis from '../../dispatcher';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is what our MatrixChat shows when we are logged in. The precise view is
|
* This is what our MatrixChat shows when we are logged in. The precise view is
|
||||||
|
|
|
@ -259,8 +259,6 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onAction: function(payload) {
|
onAction: function(payload) {
|
||||||
console.log("onAction: "+payload.action);
|
|
||||||
|
|
||||||
var roomIndexDelta = 1;
|
var roomIndexDelta = 1;
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
@ -1008,8 +1006,8 @@ module.exports = React.createClass({
|
||||||
var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
|
var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
|
||||||
var LoggedInView = sdk.getComponent('structures.LoggedInView');
|
var LoggedInView = sdk.getComponent('structures.LoggedInView');
|
||||||
|
|
||||||
console.log("rendering; loading="+this.state.loading+"; screen="+this.state.screen +
|
// console.log("rendering; loading="+this.state.loading+"; screen="+this.state.screen +
|
||||||
"; logged_in="+this.state.logged_in+"; ready="+this.state.ready);
|
// "; logged_in="+this.state.logged_in+"; ready="+this.state.ready);
|
||||||
|
|
||||||
if (this.state.loading) {
|
if (this.state.loading) {
|
||||||
var Spinner = sdk.getComponent('elements.Spinner');
|
var Spinner = sdk.getComponent('elements.Spinner');
|
||||||
|
|
|
@ -281,7 +281,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var isMembershipChange = (e) =>
|
var isMembershipChange = (e) =>
|
||||||
e.getType() === 'm.room.member'
|
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++) {
|
for (i = 0; i < this.props.events.length; i++) {
|
||||||
|
|
|
@ -21,7 +21,10 @@ var WhoIsTyping = require("../../WhoIsTyping");
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||||
const MemberAvatar = require("../views/avatars/MemberAvatar");
|
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({
|
module.exports = React.createClass({
|
||||||
displayName: 'RoomStatusBar',
|
displayName: 'RoomStatusBar',
|
||||||
|
@ -48,6 +51,10 @@ module.exports = React.createClass({
|
||||||
// more interesting)
|
// more interesting)
|
||||||
hasActiveCall: React.PropTypes.bool,
|
hasActiveCall: React.PropTypes.bool,
|
||||||
|
|
||||||
|
// Number of names to display in typing indication. E.g. set to 3, will
|
||||||
|
// result in "X, Y, Z and 100 others are typing."
|
||||||
|
whoIsTypingLimit: React.PropTypes.number,
|
||||||
|
|
||||||
// callback for when the user clicks on the 'resend all' button in the
|
// callback for when the user clicks on the 'resend all' button in the
|
||||||
// 'unsent messages' bar
|
// 'unsent messages' bar
|
||||||
onResendAllClick: React.PropTypes.func,
|
onResendAllClick: React.PropTypes.func,
|
||||||
|
@ -63,12 +70,28 @@ module.exports = React.createClass({
|
||||||
// status bar. This is used to trigger a re-layout in the parent
|
// status bar. This is used to trigger a re-layout in the parent
|
||||||
// component.
|
// component.
|
||||||
onResize: React.PropTypes.func,
|
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,
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
whoIsTypingLimit: 2,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
syncState: MatrixClientPeg.get().getSyncState(),
|
syncState: MatrixClientPeg.get().getSyncState(),
|
||||||
whoisTypingString: WhoIsTyping.whoIsTypingString(this.props.room),
|
whoisTypingString: WhoIsTyping.whoIsTypingString(
|
||||||
|
this.props.room,
|
||||||
|
this.props.whoIsTypingLimit
|
||||||
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -81,6 +104,18 @@ module.exports = React.createClass({
|
||||||
if(this.props.onResize && this._checkForResize(prevProps, prevState)) {
|
if(this.props.onResize && this._checkForResize(prevProps, prevState)) {
|
||||||
this.props.onResize();
|
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() {
|
componentWillUnmount: function() {
|
||||||
|
@ -103,39 +138,35 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onRoomMemberTyping: function(ev, member) {
|
onRoomMemberTyping: function(ev, member) {
|
||||||
this.setState({
|
this.setState({
|
||||||
whoisTypingString: WhoIsTyping.whoIsTypingString(this.props.room),
|
whoisTypingString: WhoIsTyping.whoIsTypingString(
|
||||||
|
this.props.room,
|
||||||
|
this.props.whoIsTypingLimit
|
||||||
|
),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 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 ||
|
||||||
|
props.numUnreadMessages ||
|
||||||
|
!props.atEndOfLiveTimeline ||
|
||||||
|
props.hasActiveCall) {
|
||||||
|
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
|
// determine if we need to call onResize
|
||||||
_checkForResize: function(prevProps, prevState) {
|
_checkForResize: function(prevProps, prevState) {
|
||||||
// figure out the old height and the new height of the status bar. We
|
// figure out the old height and the new height of the status bar.
|
||||||
// don't need the actual height - just whether it is likely to have
|
return this._getSize(prevProps, prevState) !== this._getSize(this.props, this.state);
|
||||||
// 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;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// return suitable content for the image on the left of the status bar.
|
// return suitable content for the image on the left of the status bar.
|
||||||
|
@ -177,7 +208,7 @@ module.exports = React.createClass({
|
||||||
if (wantPlaceholder) {
|
if (wantPlaceholder) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomStatusBar_typingIndicatorAvatars">
|
<div className="mx_RoomStatusBar_typingIndicatorAvatars">
|
||||||
{this._renderTypingIndicatorAvatars(TYPING_AVATARS_LIMIT)}
|
{this._renderTypingIndicatorAvatars(this.props.whoIsTypingLimit)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -186,7 +217,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderTypingIndicatorAvatars: function(limit) {
|
_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);
|
let othersCount = Math.max(users.length - limit, 0);
|
||||||
users = users.slice(0, limit);
|
users = users.slice(0, limit);
|
||||||
|
|
|
@ -146,6 +146,8 @@ module.exports = React.createClass({
|
||||||
showTopUnreadMessagesBar: false,
|
showTopUnreadMessagesBar: false,
|
||||||
|
|
||||||
auxPanelMaxHeight: undefined,
|
auxPanelMaxHeight: undefined,
|
||||||
|
|
||||||
|
statusBarVisible: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -720,15 +722,11 @@ module.exports = React.createClass({
|
||||||
if (!result.displayname) {
|
if (!result.displayname) {
|
||||||
var SetDisplayNameDialog = sdk.getComponent('views.dialogs.SetDisplayNameDialog');
|
var SetDisplayNameDialog = sdk.getComponent('views.dialogs.SetDisplayNameDialog');
|
||||||
var dialog_defer = q.defer();
|
var dialog_defer = q.defer();
|
||||||
var dialog_ref;
|
|
||||||
Modal.createDialog(SetDisplayNameDialog, {
|
Modal.createDialog(SetDisplayNameDialog, {
|
||||||
currentDisplayName: result.displayname,
|
currentDisplayName: result.displayname,
|
||||||
ref: (r) => {
|
onFinished: (submitted, newDisplayName) => {
|
||||||
dialog_ref = r;
|
|
||||||
},
|
|
||||||
onFinished: (submitted) => {
|
|
||||||
if (submitted) {
|
if (submitted) {
|
||||||
cli.setDisplayName(dialog_ref.getValue()).done(() => {
|
cli.setDisplayName(newDisplayName).done(() => {
|
||||||
dialog_defer.resolve();
|
dialog_defer.resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1333,6 +1331,18 @@ module.exports = React.createClass({
|
||||||
// no longer anything to do here
|
// no longer anything to do here
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onStatusBarVisible: function() {
|
||||||
|
this.setState({
|
||||||
|
statusBarVisible: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onStatusBarHidden: function() {
|
||||||
|
this.setState({
|
||||||
|
statusBarVisible: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
showSettings: function(show) {
|
showSettings: function(show) {
|
||||||
// XXX: this is a bit naughty; we should be doing this via props
|
// XXX: this is a bit naughty; we should be doing this via props
|
||||||
if (show) {
|
if (show) {
|
||||||
|
@ -1515,7 +1525,10 @@ module.exports = React.createClass({
|
||||||
onCancelAllClick={this.onCancelAllClick}
|
onCancelAllClick={this.onCancelAllClick}
|
||||||
onScrollToBottomClick={this.jumpToLiveTimeline}
|
onScrollToBottomClick={this.jumpToLiveTimeline}
|
||||||
onResize={this.onChildResize}
|
onResize={this.onChildResize}
|
||||||
/>;
|
onVisible={this.onStatusBarVisible}
|
||||||
|
onHidden={this.onStatusBarHidden}
|
||||||
|
whoIsTypingLimit={2}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
var aux = null;
|
var aux = null;
|
||||||
|
@ -1669,6 +1682,10 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let statusBarAreaClass = "mx_RoomView_statusArea mx_fadable";
|
||||||
|
if (this.state.statusBarVisible) {
|
||||||
|
statusBarAreaClass += " mx_RoomView_statusArea_expanded";
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") } ref="roomView">
|
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") } ref="roomView">
|
||||||
|
@ -1691,7 +1708,7 @@ module.exports = React.createClass({
|
||||||
{ topUnreadMessagesBar }
|
{ topUnreadMessagesBar }
|
||||||
{ messagePanel }
|
{ messagePanel }
|
||||||
{ searchResultsPanel }
|
{ searchResultsPanel }
|
||||||
<div className="mx_RoomView_statusArea mx_fadable" style={{ opacity: this.props.opacity }}>
|
<div className={statusBarAreaClass} style={{opacity: this.props.opacity}}>
|
||||||
<div className="mx_RoomView_statusAreaBox">
|
<div className="mx_RoomView_statusAreaBox">
|
||||||
<div className="mx_RoomView_statusAreaBox_line"></div>
|
<div className="mx_RoomView_statusAreaBox_line"></div>
|
||||||
{ statusBar }
|
{ statusBar }
|
||||||
|
|
|
@ -26,6 +26,7 @@ var UserSettingsStore = require('../../UserSettingsStore');
|
||||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||||
var Email = require('../../email');
|
var Email = require('../../email');
|
||||||
var AddThreepid = require('../../AddThreepid');
|
var AddThreepid = require('../../AddThreepid');
|
||||||
|
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||||
|
|
||||||
// if this looks like a release, use the 'version' from package.json; else use
|
// if this looks like a release, use the 'version' from package.json; else use
|
||||||
// the git sha.
|
// the git sha.
|
||||||
|
@ -228,8 +229,26 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onLogoutClicked: function(ev) {
|
onLogoutClicked: function(ev) {
|
||||||
var LogoutPrompt = sdk.getComponent('dialogs.LogoutPrompt');
|
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
this.logoutModal = Modal.createDialog(LogoutPrompt);
|
Modal.createDialog(QuestionDialog, {
|
||||||
|
title: "Sign out?",
|
||||||
|
description:
|
||||||
|
<div>
|
||||||
|
For security, logging out will delete any end-to-end encryption keys from this browser,
|
||||||
|
making previous encrypted chat history unreadable if you log back in.
|
||||||
|
In future this <a href="https://github.com/vector-im/riot-web/issues/2108">will be improved</a>,
|
||||||
|
but for now be warned.
|
||||||
|
</div>,
|
||||||
|
button: "Sign out",
|
||||||
|
onFinished: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
dis.dispatch({action: 'logout'});
|
||||||
|
if (this.props.onFinished) {
|
||||||
|
this.props.onFinished();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onPasswordChangeError: function(err) {
|
onPasswordChangeError: function(err) {
|
||||||
|
@ -392,6 +411,30 @@ module.exports = React.createClass({
|
||||||
}).done();
|
}).done();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onExportE2eKeysClicked: function() {
|
||||||
|
Modal.createDialogAsync(
|
||||||
|
(cb) => {
|
||||||
|
require.ensure(['../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
|
||||||
|
cb(require('../../async-components/views/dialogs/ExportE2eKeysDialog'));
|
||||||
|
}, "e2e-export");
|
||||||
|
}, {
|
||||||
|
matrixClient: MatrixClientPeg.get(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onImportE2eKeysClicked: function() {
|
||||||
|
Modal.createDialogAsync(
|
||||||
|
(cb) => {
|
||||||
|
require.ensure(['../../async-components/views/dialogs/ImportE2eKeysDialog'], () => {
|
||||||
|
cb(require('../../async-components/views/dialogs/ImportE2eKeysDialog'));
|
||||||
|
}, "e2e-export");
|
||||||
|
}, {
|
||||||
|
matrixClient: MatrixClientPeg.get(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
_renderUserInterfaceSettings: function() {
|
_renderUserInterfaceSettings: function() {
|
||||||
var client = MatrixClientPeg.get();
|
var client = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
@ -462,6 +505,23 @@ module.exports = React.createClass({
|
||||||
const deviceId = client.deviceId;
|
const deviceId = client.deviceId;
|
||||||
const identityKey = client.getDeviceEd25519Key() || "<not supported>";
|
const identityKey = client.getDeviceEd25519Key() || "<not supported>";
|
||||||
|
|
||||||
|
let exportButton = null,
|
||||||
|
importButton = null;
|
||||||
|
|
||||||
|
if (client.isCryptoEnabled) {
|
||||||
|
exportButton = (
|
||||||
|
<AccessibleButton className="mx_UserSettings_button"
|
||||||
|
onClick={this._onExportE2eKeysClicked}>
|
||||||
|
Export E2E room keys
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
importButton = (
|
||||||
|
<AccessibleButton className="mx_UserSettings_button"
|
||||||
|
onClick={this._onImportE2eKeysClicked}>
|
||||||
|
Import E2E room keys
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3>Cryptography</h3>
|
<h3>Cryptography</h3>
|
||||||
|
@ -470,6 +530,8 @@ module.exports = React.createClass({
|
||||||
<li><label>Device ID:</label> <span><code>{deviceId}</code></span></li>
|
<li><label>Device ID:</label> <span><code>{deviceId}</code></span></li>
|
||||||
<li><label>Device key:</label> <span><code><b>{identityKey}</b></code></span></li>
|
<li><label>Device key:</label> <span><code><b>{identityKey}</b></code></span></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
{exportButton}
|
||||||
|
{importButton}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -531,9 +593,9 @@ module.exports = React.createClass({
|
||||||
return <div>
|
return <div>
|
||||||
<h3>Deactivate Account</h3>
|
<h3>Deactivate Account</h3>
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
<button className="mx_UserSettings_button danger"
|
<AccessibleButton className="mx_UserSettings_button danger"
|
||||||
onClick={this._onDeactivateAccountClicked}>Deactivate my account
|
onClick={this._onDeactivateAccountClicked}>Deactivate my account
|
||||||
</button>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
@ -553,10 +615,10 @@ module.exports = React.createClass({
|
||||||
// bind() the invited rooms so any new invites that may come in as this button is clicked
|
// bind() the invited rooms so any new invites that may come in as this button is clicked
|
||||||
// don't inadvertently get rejected as well.
|
// don't inadvertently get rejected as well.
|
||||||
reject = (
|
reject = (
|
||||||
<button className="mx_UserSettings_button danger"
|
<AccessibleButton className="mx_UserSettings_button danger"
|
||||||
onClick={this._onRejectAllInvitesClicked.bind(this, invitedRooms)}>
|
onClick={this._onRejectAllInvitesClicked.bind(this, invitedRooms)}>
|
||||||
Reject all {invitedRooms.length} invites
|
Reject all {invitedRooms.length} invites
|
||||||
</button>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -724,9 +786,9 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
|
|
||||||
<div className="mx_UserSettings_logout mx_UserSettings_button" onClick={this.onLogoutClicked}>
|
<AccessibleButton className="mx_UserSettings_logout mx_UserSettings_button" onClick={this.onLogoutClicked}>
|
||||||
Sign out
|
Sign out
|
||||||
</div>
|
</AccessibleButton>
|
||||||
|
|
||||||
{accountJsx}
|
{accountJsx}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -87,10 +87,26 @@ module.exports = React.createClass({
|
||||||
this.showErrorDialog("New passwords must match each other.");
|
this.showErrorDialog("New passwords must match each other.");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.submitPasswordReset(
|
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
this.state.enteredHomeserverUrl, this.state.enteredIdentityServerUrl,
|
Modal.createDialog(QuestionDialog, {
|
||||||
this.state.email, this.state.password
|
title: "Warning",
|
||||||
);
|
description:
|
||||||
|
<div>
|
||||||
|
Resetting password will currently reset any end-to-end encryption keys on all devices,
|
||||||
|
making encrypted chat history unreadable.
|
||||||
|
In future this <a href="https://github.com/vector-im/riot-web/issues/2671">may be improved</a>,
|
||||||
|
but for now be warned.
|
||||||
|
</div>,
|
||||||
|
button: "Continue",
|
||||||
|
onFinished: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.submitPasswordReset(
|
||||||
|
this.state.enteredHomeserverUrl, this.state.enteredIdentityServerUrl,
|
||||||
|
this.state.email, this.state.password
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var AvatarLogic = require("../../../Avatar");
|
var AvatarLogic = require("../../../Avatar");
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'BaseAvatar',
|
displayName: 'BaseAvatar',
|
||||||
|
@ -138,7 +139,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
const {
|
const {
|
||||||
name, idName, title, url, urls, width, height, resizeMethod,
|
name, idName, title, url, urls, width, height, resizeMethod,
|
||||||
defaultToInitialLetter,
|
defaultToInitialLetter, onClick,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -156,12 +157,24 @@ module.exports = React.createClass({
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
if (onClick != null) {
|
||||||
<img className="mx_BaseAvatar mx_BaseAvatar_image" src={imageUrl}
|
return (
|
||||||
onError={this.onError}
|
<AccessibleButton className="mx_BaseAvatar" onClick={onClick}>
|
||||||
width={width} height={height}
|
<img className="mx_BaseAvatar_image" src={imageUrl}
|
||||||
title={title} alt=""
|
onError={this.onError}
|
||||||
{...otherProps} />
|
width={width} height={height}
|
||||||
);
|
title={title} alt=""
|
||||||
|
{...otherProps} />
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<img className="mx_BaseAvatar mx_BaseAvatar_image" src={imageUrl}
|
||||||
|
onError={this.onError}
|
||||||
|
width={width} height={height}
|
||||||
|
title={title} alt=""
|
||||||
|
{...otherProps} />
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
72
src/components/views/dialogs/BaseDialog.js
Normal file
72
src/components/views/dialogs/BaseDialog.js
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
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 * as KeyCode from '../../../KeyCode';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic container for modal dialogs.
|
||||||
|
*
|
||||||
|
* Includes a div for the title, and a keypress handler which cancels the
|
||||||
|
* dialog on escape.
|
||||||
|
*/
|
||||||
|
export default React.createClass({
|
||||||
|
displayName: 'BaseDialog',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
// onFinished callback to call when Escape is pressed
|
||||||
|
onFinished: React.PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
// callback to call when Enter is pressed
|
||||||
|
onEnterPressed: React.PropTypes.func,
|
||||||
|
|
||||||
|
// CSS class to apply to dialog div
|
||||||
|
className: React.PropTypes.string,
|
||||||
|
|
||||||
|
// Title for the dialog.
|
||||||
|
// (could probably actually be something more complicated than a string if desired)
|
||||||
|
title: React.PropTypes.string.isRequired,
|
||||||
|
|
||||||
|
// children should be the content of the dialog
|
||||||
|
children: React.PropTypes.node,
|
||||||
|
},
|
||||||
|
|
||||||
|
_onKeyDown: function(e) {
|
||||||
|
if (e.keyCode === KeyCode.ESCAPE) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onFinished();
|
||||||
|
} else if (e.keyCode === KeyCode.ENTER) {
|
||||||
|
if (this.props.onEnterPressed) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onEnterPressed(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div onKeyDown={this._onKeyDown} className={this.props.className}>
|
||||||
|
<div className='mx_Dialog_title'>
|
||||||
|
{ this.props.title }
|
||||||
|
</div>
|
||||||
|
{ this.props.children }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -24,9 +24,19 @@ var DMRoomMap = require('../../../utils/DMRoomMap');
|
||||||
var rate_limited_func = require("../../../ratelimitedfunc");
|
var rate_limited_func = require("../../../ratelimitedfunc");
|
||||||
var dis = require("../../../dispatcher");
|
var dis = require("../../../dispatcher");
|
||||||
var Modal = require('../../../Modal');
|
var Modal = require('../../../Modal');
|
||||||
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
|
||||||
const TRUNCATE_QUERY_LIST = 40;
|
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, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: "ChatInviteDialog",
|
displayName: "ChatInviteDialog",
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
@ -57,7 +67,14 @@ module.exports = React.createClass({
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
error: false,
|
error: false,
|
||||||
|
|
||||||
|
// List of AddressTile.InviteAddressType objects represeting
|
||||||
|
// the list of addresses we're going to invite
|
||||||
inviteList: [],
|
inviteList: [],
|
||||||
|
|
||||||
|
// List of AddressTile.InviteAddressType objects represeting
|
||||||
|
// the set of autocompletion results for the current search
|
||||||
|
// query.
|
||||||
queryList: [],
|
queryList: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -146,14 +163,38 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onQueryChanged: function(ev) {
|
onQueryChanged: function(ev) {
|
||||||
var query = ev.target.value;
|
const query = ev.target.value;
|
||||||
var queryList = [];
|
let queryList = [];
|
||||||
|
|
||||||
// Only do search if there is something to search
|
// Only do search if there is something to search
|
||||||
if (query.length > 0) {
|
if (query.length > 0 && query != '@') {
|
||||||
|
// filter the known users list
|
||||||
queryList = this._userList.filter((user) => {
|
queryList = this._userList.filter((user) => {
|
||||||
return this._matches(query, user);
|
return this._matches(query, user);
|
||||||
|
}).map((user) => {
|
||||||
|
// Return objects, structure of which is defined
|
||||||
|
// by InviteAddressType
|
||||||
|
return {
|
||||||
|
addressType: 'mx',
|
||||||
|
address: user.userId,
|
||||||
|
displayName: user.displayName,
|
||||||
|
avatarMxc: user.avatarUrl,
|
||||||
|
isKnown: true,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If the query isn't a user we know about, but is a
|
||||||
|
// valid address, add an entry for that
|
||||||
|
if (queryList.length == 0) {
|
||||||
|
const addrType = Invite.getAddressType(query);
|
||||||
|
if (addrType !== null) {
|
||||||
|
queryList.push({
|
||||||
|
addressType: addrType,
|
||||||
|
address: query,
|
||||||
|
isKnown: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -183,7 +224,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onSelected: function(index) {
|
onSelected: function(index) {
|
||||||
var inviteList = this.state.inviteList.slice();
|
var inviteList = this.state.inviteList.slice();
|
||||||
inviteList.push(this.state.queryList[index].userId);
|
inviteList.push(this.state.queryList[index]);
|
||||||
this.setState({
|
this.setState({
|
||||||
inviteList: inviteList,
|
inviteList: inviteList,
|
||||||
queryList: [],
|
queryList: [],
|
||||||
|
@ -218,10 +259,14 @@ module.exports = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addrTexts = addrs.map((addr) => {
|
||||||
|
return addr.address;
|
||||||
|
});
|
||||||
|
|
||||||
if (this.props.roomId) {
|
if (this.props.roomId) {
|
||||||
// Invite new user to a room
|
// Invite new user to a room
|
||||||
var self = this;
|
var self = this;
|
||||||
Invite.inviteMultipleToRoom(this.props.roomId, addrs)
|
Invite.inviteMultipleToRoom(this.props.roomId, addrTexts)
|
||||||
.then(function(addrs) {
|
.then(function(addrs) {
|
||||||
var room = MatrixClientPeg.get().getRoom(self.props.roomId);
|
var room = MatrixClientPeg.get().getRoom(self.props.roomId);
|
||||||
return self._showAnyInviteErrors(addrs, room);
|
return self._showAnyInviteErrors(addrs, room);
|
||||||
|
@ -236,9 +281,9 @@ module.exports = React.createClass({
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
.done();
|
.done();
|
||||||
} else if (this._isDmChat(addrs)) {
|
} else if (this._isDmChat(addrTexts)) {
|
||||||
// Start the DM chat
|
// Start the DM chat
|
||||||
createRoom({dmUserId: addrs[0]})
|
createRoom({dmUserId: addrTexts[0]})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
console.error(err.stack);
|
console.error(err.stack);
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
@ -255,7 +300,7 @@ module.exports = React.createClass({
|
||||||
var room;
|
var room;
|
||||||
createRoom().then(function(roomId) {
|
createRoom().then(function(roomId) {
|
||||||
room = MatrixClientPeg.get().getRoom(roomId);
|
room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
return Invite.inviteMultipleToRoom(roomId, addrs);
|
return Invite.inviteMultipleToRoom(roomId, addrTexts);
|
||||||
})
|
})
|
||||||
.then(function(addrs) {
|
.then(function(addrs) {
|
||||||
return self._showAnyInviteErrors(addrs, room);
|
return self._showAnyInviteErrors(addrs, room);
|
||||||
|
@ -273,7 +318,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close - this will happen before the above, as that is async
|
// Close - this will happen before the above, as that is async
|
||||||
this.props.onFinished(true, addrs);
|
this.props.onFinished(true, addrTexts);
|
||||||
},
|
},
|
||||||
|
|
||||||
_updateUserList: new rate_limited_func(function() {
|
_updateUserList: new rate_limited_func(function() {
|
||||||
|
@ -307,19 +352,27 @@ module.exports = React.createClass({
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// split spaces in name and try matching constituent parts
|
// Try to find the query following a "word boundary", except that
|
||||||
var parts = name.split(" ");
|
// this does avoids using \b because it only considers letters from
|
||||||
for (var i = 0; i < parts.length; i++) {
|
// the roman alphabet to be word characters.
|
||||||
if (parts[i].indexOf(query) === 0) {
|
// Instead, we look for the query following either:
|
||||||
return true;
|
// * The start of the string
|
||||||
}
|
// * Whitespace, or
|
||||||
|
// * A fixed number of punctuation characters
|
||||||
|
const expr = new RegExp("(?:^|[\\s\\(\)'\",\.-_@\?;:{}\\[\\]\\#~`\\*\\&\\$])" + escapeRegExp(query));
|
||||||
|
if (expr.test(name)) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
_isOnInviteList: function(uid) {
|
_isOnInviteList: function(uid) {
|
||||||
for (let i = 0; i < this.state.inviteList.length; i++) {
|
for (let i = 0; i < this.state.inviteList.length; i++) {
|
||||||
if (this.state.inviteList[i].toLowerCase() === uid) {
|
if (
|
||||||
|
this.state.inviteList[i].addressType == 'mx' &&
|
||||||
|
this.state.inviteList[i].address.toLowerCase() === uid
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -354,24 +407,37 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_addInputToList: function() {
|
_addInputToList: function() {
|
||||||
const addrType = Invite.getAddressType(this.refs.textinput.value);
|
const addressText = this.refs.textinput.value.trim();
|
||||||
if (addrType !== null) {
|
const addrType = Invite.getAddressType(addressText);
|
||||||
const inviteList = this.state.inviteList.slice();
|
const addrObj = {
|
||||||
inviteList.push(this.refs.textinput.value.trim());
|
addressType: addrType,
|
||||||
this.setState({
|
address: addressText,
|
||||||
inviteList: inviteList,
|
isKnown: false,
|
||||||
queryList: [],
|
};
|
||||||
});
|
if (addrType == null) {
|
||||||
return inviteList;
|
|
||||||
} else {
|
|
||||||
this.setState({ error: true });
|
this.setState({ error: true });
|
||||||
return null;
|
return null;
|
||||||
|
} else if (addrType == 'mx') {
|
||||||
|
const user = MatrixClientPeg.get().getUser(addrObj.address);
|
||||||
|
if (user) {
|
||||||
|
addrObj.displayName = user.displayName;
|
||||||
|
addrObj.avatarMxc = user.avatarUrl;
|
||||||
|
addrObj.isKnown = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inviteList = this.state.inviteList.slice();
|
||||||
|
inviteList.push(addrObj);
|
||||||
|
this.setState({
|
||||||
|
inviteList: inviteList,
|
||||||
|
queryList: [],
|
||||||
|
});
|
||||||
|
return inviteList;
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
var AddressSelector = sdk.getComponent("elements.AddressSelector");
|
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
||||||
this.scrollElement = null;
|
this.scrollElement = null;
|
||||||
|
|
||||||
var query = [];
|
var query = [];
|
||||||
|
@ -404,11 +470,16 @@ module.exports = React.createClass({
|
||||||
if (this.state.error) {
|
if (this.state.error) {
|
||||||
error = <div className="mx_ChatInviteDialog_error">You have entered an invalid contact. Try using their Matrix ID or email address.</div>;
|
error = <div className="mx_ChatInviteDialog_error">You have entered an invalid contact. Try using their Matrix ID or email address.</div>;
|
||||||
} else {
|
} else {
|
||||||
|
const addressSelectorHeader = <div className="mx_ChatInviteDialog_addressSelectHeader">
|
||||||
|
Searching known users
|
||||||
|
</div>;
|
||||||
addressSelector = (
|
addressSelector = (
|
||||||
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
|
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
|
||||||
addressList={ this.state.queryList }
|
addressList={ this.state.queryList }
|
||||||
onSelected={ this.onSelected }
|
onSelected={ this.onSelected }
|
||||||
truncateAt={ TRUNCATE_QUERY_LIST } />
|
truncateAt={ TRUNCATE_QUERY_LIST }
|
||||||
|
header={ addressSelectorHeader }
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -417,9 +488,10 @@ module.exports = React.createClass({
|
||||||
<div className="mx_Dialog_title">
|
<div className="mx_Dialog_title">
|
||||||
{this.props.title}
|
{this.props.title}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_ChatInviteDialog_cancel" onClick={this.onCancel} >
|
<AccessibleButton className="mx_ChatInviteDialog_cancel"
|
||||||
|
onClick={this.onCancel} >
|
||||||
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
||||||
</div>
|
</AccessibleButton>
|
||||||
<div className="mx_ChatInviteDialog_label">
|
<div className="mx_ChatInviteDialog_label">
|
||||||
<label htmlFor="textinput">{ this.props.description }</label>
|
<label htmlFor="textinput">{ this.props.description }</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,9 +25,10 @@ limitations under the License.
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require("react");
|
import React from 'react';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'ErrorDialog',
|
displayName: 'ErrorDialog',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
title: React.PropTypes.string,
|
title: React.PropTypes.string,
|
||||||
|
@ -49,20 +50,11 @@ module.exports = React.createClass({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeyDown: function(e) {
|
|
||||||
if (e.keyCode === 27) { // escape
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onFinished(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
return (
|
return (
|
||||||
<div className="mx_ErrorDialog" onKeyDown={ this.onKeyDown }>
|
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||||
<div className="mx_Dialog_title">
|
title={this.props.title}>
|
||||||
{this.props.title}
|
|
||||||
</div>
|
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
{this.props.description}
|
{this.props.description}
|
||||||
</div>
|
</div>
|
||||||
|
@ -71,7 +63,7 @@ module.exports = React.createClass({
|
||||||
{this.props.button}
|
{this.props.button}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -111,20 +111,9 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onKeyDown: function(e) {
|
_onEnterPressed: function(e) {
|
||||||
if (e.keyCode === 27) { // escape
|
if (this.state.submitButtonEnabled && !this.state.busy) {
|
||||||
e.stopPropagation();
|
this._onSubmit();
|
||||||
e.preventDefault();
|
|
||||||
if (!this.state.busy) {
|
|
||||||
this._onCancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (e.keyCode === 13) { // enter
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
if (this.state.submitButtonEnabled && !this.state.busy) {
|
|
||||||
this._onSubmit();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -171,6 +160,7 @@ export default React.createClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
|
||||||
let error = null;
|
let error = null;
|
||||||
if (this.state.errorText) {
|
if (this.state.errorText) {
|
||||||
|
@ -200,10 +190,11 @@ export default React.createClass({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_InteractiveAuthDialog" onKeyDown={this._onKeyDown}>
|
<BaseDialog className="mx_InteractiveAuthDialog"
|
||||||
<div className="mx_Dialog_title">
|
onEnterPressed={this._onEnterPressed}
|
||||||
{this.props.title}
|
onFinished={this.props.onFinished}
|
||||||
</div>
|
title={this.props.title}
|
||||||
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<p>This operation requires additional authentication.</p>
|
<p>This operation requires additional authentication.</p>
|
||||||
{this._renderCurrentStage()}
|
{this._renderCurrentStage()}
|
||||||
|
@ -213,7 +204,7 @@ export default React.createClass({
|
||||||
{submitButton}
|
{submitButton}
|
||||||
{cancelButton}
|
{cancelButton}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
var React = require('react');
|
|
||||||
var dis = require("../../../dispatcher");
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
|
||||||
displayName: 'LogoutPrompt',
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
onFinished: React.PropTypes.func,
|
|
||||||
},
|
|
||||||
|
|
||||||
logOut: function() {
|
|
||||||
dis.dispatch({action: 'logout'});
|
|
||||||
if (this.props.onFinished) {
|
|
||||||
this.props.onFinished();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
cancelPrompt: function() {
|
|
||||||
if (this.props.onFinished) {
|
|
||||||
this.props.onFinished();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onKeyDown: function(e) {
|
|
||||||
if (e.keyCode === 27) { // escape
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
this.cancelPrompt();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="mx_Dialog_content">
|
|
||||||
Sign out?
|
|
||||||
</div>
|
|
||||||
<div className="mx_Dialog_buttons" onKeyDown={ this.onKeyDown }>
|
|
||||||
<button className="mx_Dialog_primary" autoFocus onClick={this.logOut}>Sign Out</button>
|
|
||||||
<button onClick={this.cancelPrompt}>Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
|
@ -23,8 +23,9 @@ limitations under the License.
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require("react");
|
import React from 'react';
|
||||||
var dis = require("../../../dispatcher");
|
import dis from '../../../dispatcher';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'NeedToRegisterDialog',
|
displayName: 'NeedToRegisterDialog',
|
||||||
|
@ -54,11 +55,12 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
return (
|
return (
|
||||||
<div className="mx_NeedToRegisterDialog">
|
<BaseDialog className="mx_NeedToRegisterDialog"
|
||||||
<div className="mx_Dialog_title">
|
onFinished={this.props.onFinished}
|
||||||
{this.props.title}
|
title={this.props.title}
|
||||||
</div>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
{this.props.description}
|
{this.props.description}
|
||||||
</div>
|
</div>
|
||||||
|
@ -70,7 +72,7 @@ module.exports = React.createClass({
|
||||||
Register
|
Register
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require("react");
|
import React from 'react';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'QuestionDialog',
|
displayName: 'QuestionDialog',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
title: React.PropTypes.string,
|
title: React.PropTypes.string,
|
||||||
|
@ -46,25 +47,13 @@ module.exports = React.createClass({
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeyDown: function(e) {
|
|
||||||
if (e.keyCode === 27) { // escape
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onFinished(false);
|
|
||||||
}
|
|
||||||
else if (e.keyCode === 13) { // enter
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onFinished(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
return (
|
return (
|
||||||
<div className="mx_QuestionDialog" onKeyDown={ this.onKeyDown }>
|
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
||||||
<div className="mx_Dialog_title">
|
onEnterPressed={ this.onOk }
|
||||||
{this.props.title}
|
title={this.props.title}
|
||||||
</div>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
{this.props.description}
|
{this.props.description}
|
||||||
</div>
|
</div>
|
||||||
|
@ -77,7 +66,7 @@ module.exports = React.createClass({
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,11 +14,16 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require("react");
|
import React from 'react';
|
||||||
var sdk = require("../../../index.js");
|
import sdk from '../../../index';
|
||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
/**
|
||||||
|
* Prompt the user to set a display name.
|
||||||
|
*
|
||||||
|
* On success, `onFinished(true, newDisplayName)` is called.
|
||||||
|
*/
|
||||||
|
export default React.createClass({
|
||||||
displayName: 'SetDisplayNameDialog',
|
displayName: 'SetDisplayNameDialog',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
onFinished: React.PropTypes.func.isRequired,
|
||||||
|
@ -42,10 +47,6 @@ module.exports = React.createClass({
|
||||||
this.refs.input_value.select();
|
this.refs.input_value.select();
|
||||||
},
|
},
|
||||||
|
|
||||||
getValue: function() {
|
|
||||||
return this.state.value;
|
|
||||||
},
|
|
||||||
|
|
||||||
onValueChange: function(ev) {
|
onValueChange: function(ev) {
|
||||||
this.setState({
|
this.setState({
|
||||||
value: ev.target.value
|
value: ev.target.value
|
||||||
|
@ -54,16 +55,17 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onFormSubmit: function(ev) {
|
onFormSubmit: function(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.props.onFinished(true);
|
this.props.onFinished(true, this.state.value);
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
return (
|
return (
|
||||||
<div className="mx_SetDisplayNameDialog">
|
<BaseDialog className="mx_SetDisplayNameDialog"
|
||||||
<div className="mx_Dialog_title">
|
onFinished={this.props.onFinished}
|
||||||
Set a Display Name
|
title="Set a Display Name"
|
||||||
</div>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
Your display name is how you'll appear to others when you speak in rooms.<br/>
|
Your display name is how you'll appear to others when you speak in rooms.<br/>
|
||||||
What would you like it to be?
|
What would you like it to be?
|
||||||
|
@ -79,7 +81,7 @@ module.exports = React.createClass({
|
||||||
<input className="mx_Dialog_primary" type="submit" value="Set" />
|
<input className="mx_Dialog_primary" type="submit" value="Set" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require("react");
|
import React from 'react';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'TextInputDialog',
|
displayName: 'TextInputDialog',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
title: React.PropTypes.string,
|
title: React.PropTypes.string,
|
||||||
|
@ -27,7 +28,7 @@ module.exports = React.createClass({
|
||||||
value: React.PropTypes.string,
|
value: React.PropTypes.string,
|
||||||
button: React.PropTypes.string,
|
button: React.PropTypes.string,
|
||||||
focus: React.PropTypes.bool,
|
focus: React.PropTypes.bool,
|
||||||
onFinished: React.PropTypes.func.isRequired
|
onFinished: React.PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -36,7 +37,7 @@ module.exports = React.createClass({
|
||||||
value: "",
|
value: "",
|
||||||
description: "",
|
description: "",
|
||||||
button: "OK",
|
button: "OK",
|
||||||
focus: true
|
focus: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -55,25 +56,13 @@ module.exports = React.createClass({
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeyDown: function(e) {
|
|
||||||
if (e.keyCode === 27) { // escape
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onFinished(false);
|
|
||||||
}
|
|
||||||
else if (e.keyCode === 13) { // enter
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onFinished(true, this.refs.textinput.value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
return (
|
return (
|
||||||
<div className="mx_TextInputDialog">
|
<BaseDialog className="mx_TextInputDialog" onFinished={this.props.onFinished}
|
||||||
<div className="mx_Dialog_title">
|
onEnterPressed={this.onOk}
|
||||||
{this.props.title}
|
title={this.props.title}
|
||||||
</div>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<div className="mx_TextInputDialog_label">
|
<div className="mx_TextInputDialog_label">
|
||||||
<label htmlFor="textinput"> {this.props.description} </label>
|
<label htmlFor="textinput"> {this.props.description} </label>
|
||||||
|
@ -90,7 +79,7 @@ module.exports = React.createClass({
|
||||||
{this.props.button}
|
{this.props.button}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
54
src/components/views/elements/AccessibleButton.js
Normal file
54
src/components/views/elements/AccessibleButton.js
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
Copyright 2016 Jani Mustonen
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AccessibleButton is a generic wrapper for any element that should be treated
|
||||||
|
* as a button. Identifies the element as a button, setting proper tab
|
||||||
|
* indexing and keyboard activation behavior.
|
||||||
|
*
|
||||||
|
* @param {Object} props react element properties
|
||||||
|
* @returns {Object} rendered react
|
||||||
|
*/
|
||||||
|
export default function AccessibleButton(props) {
|
||||||
|
const {element, onClick, children, ...restProps} = props;
|
||||||
|
restProps.onClick = onClick;
|
||||||
|
restProps.onKeyDown = function(e) {
|
||||||
|
if (e.keyCode == 13 || e.keyCode == 32) return onClick();
|
||||||
|
};
|
||||||
|
restProps.tabIndex = restProps.tabIndex || "0";
|
||||||
|
restProps.role = "button";
|
||||||
|
return React.createElement(element, restProps, children);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* children: React's magic prop. Represents all children given to the element.
|
||||||
|
* element: (optional) The base element type. "div" by default.
|
||||||
|
* onClick: (required) Event handler for button activation. Should be
|
||||||
|
* implemented exactly like a normal onClick handler.
|
||||||
|
*/
|
||||||
|
AccessibleButton.propTypes = {
|
||||||
|
children: React.PropTypes.node,
|
||||||
|
element: React.PropTypes.string,
|
||||||
|
onClick: React.PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
AccessibleButton.defaultProps = {
|
||||||
|
element: 'div',
|
||||||
|
};
|
||||||
|
|
||||||
|
AccessibleButton.displayName = "AccessibleButton";
|
|
@ -16,18 +16,24 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var React = require("react");
|
import React from 'react';
|
||||||
var sdk = require("../../../index");
|
import sdk from '../../../index';
|
||||||
var classNames = require('classnames');
|
import classNames from 'classnames';
|
||||||
|
import { InviteAddressType } from './AddressTile';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'AddressSelector',
|
displayName: 'AddressSelector',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onSelected: React.PropTypes.func.isRequired,
|
onSelected: React.PropTypes.func.isRequired,
|
||||||
addressList: React.PropTypes.array.isRequired,
|
|
||||||
|
// List of the addresses to display
|
||||||
|
addressList: React.PropTypes.arrayOf(InviteAddressType).isRequired,
|
||||||
truncateAt: React.PropTypes.number.isRequired,
|
truncateAt: React.PropTypes.number.isRequired,
|
||||||
selected: React.PropTypes.number,
|
selected: React.PropTypes.number,
|
||||||
|
|
||||||
|
// Element to put as a header on top of the list
|
||||||
|
header: React.PropTypes.node,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -119,7 +125,7 @@ module.exports = React.createClass({
|
||||||
// method, how far to scroll when using the arrow keys
|
// method, how far to scroll when using the arrow keys
|
||||||
addressList.push(
|
addressList.push(
|
||||||
<div className={classes} onClick={this.onClick.bind(this, i)} onMouseEnter={this.onMouseEnter.bind(this, i)} onMouseLeave={this.onMouseLeave} key={i} ref={(ref) => { this.addressListElement = ref; }} >
|
<div className={classes} onClick={this.onClick.bind(this, i)} onMouseEnter={this.onMouseEnter.bind(this, i)} onMouseLeave={this.onMouseLeave} key={i} ref={(ref) => { this.addressListElement = ref; }} >
|
||||||
<AddressTile address={this.props.addressList[i].userId} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />
|
<AddressTile address={this.props.addressList[i]} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -141,6 +147,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes} ref={(ref) => {this.scrollElement = ref;}}>
|
<div className={classes} ref={(ref) => {this.scrollElement = ref;}}>
|
||||||
|
{ this.props.header }
|
||||||
{ this.createAddressListTiles() }
|
{ this.createAddressListTiles() }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -23,16 +23,33 @@ var Invite = require("../../../Invite");
|
||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
var Avatar = require('../../../Avatar');
|
var Avatar = require('../../../Avatar');
|
||||||
|
|
||||||
module.exports = React.createClass({
|
// React PropType definition for an object describing
|
||||||
|
// an address that can be invited to a room (which
|
||||||
|
// could be a third party identifier or a matrix ID)
|
||||||
|
// along with some additional information about the
|
||||||
|
// address / target.
|
||||||
|
export const InviteAddressType = React.PropTypes.shape({
|
||||||
|
addressType: React.PropTypes.oneOf([
|
||||||
|
'mx', 'email'
|
||||||
|
]).isRequired,
|
||||||
|
address: React.PropTypes.string.isRequired,
|
||||||
|
displayName: React.PropTypes.string,
|
||||||
|
avatarMxc: React.PropTypes.string,
|
||||||
|
// true if the address is known to be a valid address (eg. is a real
|
||||||
|
// user we've seen) or false otherwise (eg. is just an address the
|
||||||
|
// user has entered)
|
||||||
|
isKnown: React.PropTypes.bool,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default React.createClass({
|
||||||
displayName: 'AddressTile',
|
displayName: 'AddressTile',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
address: React.PropTypes.string.isRequired,
|
address: InviteAddressType.isRequired,
|
||||||
canDismiss: React.PropTypes.bool,
|
canDismiss: React.PropTypes.bool,
|
||||||
onDismissed: React.PropTypes.func,
|
onDismissed: React.PropTypes.func,
|
||||||
justified: React.PropTypes.bool,
|
justified: React.PropTypes.bool,
|
||||||
networkName: React.PropTypes.string,
|
|
||||||
networkUrl: React.PropTypes.string,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -40,37 +57,30 @@ module.exports = React.createClass({
|
||||||
canDismiss: false,
|
canDismiss: false,
|
||||||
onDismissed: function() {}, // NOP
|
onDismissed: function() {}, // NOP
|
||||||
justified: false,
|
justified: false,
|
||||||
networkName: "",
|
|
||||||
networkUrl: "",
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var userId, name, imgUrl, email;
|
const address = this.props.address;
|
||||||
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
const name = address.displayName || address.address;
|
||||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
|
||||||
|
|
||||||
// Check if the addr is a valid type
|
let imgUrl;
|
||||||
var addrType = Invite.getAddressType(this.props.address);
|
if (address.avatarMxc) {
|
||||||
if (addrType === "mx") {
|
imgUrl = MatrixClientPeg.get().mxcUrlToHttp(
|
||||||
let user = MatrixClientPeg.get().getUser(this.props.address);
|
address.avatarMxc, 25, 25, 'crop'
|
||||||
if (user) {
|
);
|
||||||
userId = user.userId;
|
|
||||||
name = user.rawDisplayName || userId;
|
|
||||||
imgUrl = Avatar.avatarUrlForUser(user, 25, 25, "crop");
|
|
||||||
} else {
|
|
||||||
name=this.props.address;
|
|
||||||
imgUrl = "img/icon-mx-user.svg";
|
|
||||||
}
|
|
||||||
} else if (addrType === "email") {
|
|
||||||
email = this.props.address;
|
|
||||||
name="email";
|
|
||||||
imgUrl = "img/icon-email-user.svg";
|
|
||||||
} else {
|
|
||||||
name="Unknown";
|
|
||||||
imgUrl = "img/avatar-error.svg";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (address.addressType === "mx") {
|
||||||
|
if (!imgUrl) imgUrl = 'img/icon-mx-user.svg';
|
||||||
|
} else if (address.addressType === 'email') {
|
||||||
|
if (!imgUrl) imgUrl = 'img/icon-email-user.svg';
|
||||||
|
} else {
|
||||||
|
if (!imgUrl) imgUrl = "img/avatar-error.svg";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removing networks for now as they're not really supported
|
||||||
|
/*
|
||||||
var network;
|
var network;
|
||||||
if (this.props.networkUrl !== "") {
|
if (this.props.networkUrl !== "") {
|
||||||
network = (
|
network = (
|
||||||
|
@ -79,16 +89,20 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
var info;
|
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
var error = false;
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
if (addrType === "mx" && userId) {
|
|
||||||
var nameClasses = classNames({
|
let info;
|
||||||
|
let error = false;
|
||||||
|
if (address.addressType === "mx" && address.isKnown) {
|
||||||
|
const nameClasses = classNames({
|
||||||
"mx_AddressTile_name": true,
|
"mx_AddressTile_name": true,
|
||||||
"mx_AddressTile_justified": this.props.justified,
|
"mx_AddressTile_justified": this.props.justified,
|
||||||
});
|
});
|
||||||
|
|
||||||
var idClasses = classNames({
|
const idClasses = classNames({
|
||||||
"mx_AddressTile_id": true,
|
"mx_AddressTile_id": true,
|
||||||
"mx_AddressTile_justified": this.props.justified,
|
"mx_AddressTile_justified": this.props.justified,
|
||||||
});
|
});
|
||||||
|
@ -96,26 +110,26 @@ module.exports = React.createClass({
|
||||||
info = (
|
info = (
|
||||||
<div className="mx_AddressTile_mx">
|
<div className="mx_AddressTile_mx">
|
||||||
<div className={nameClasses}>{ name }</div>
|
<div className={nameClasses}>{ name }</div>
|
||||||
<div className={idClasses}>{ userId }</div>
|
<div className={idClasses}>{ address.address }</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (addrType === "mx") {
|
} else if (address.addressType === "mx") {
|
||||||
var unknownMxClasses = classNames({
|
const unknownMxClasses = classNames({
|
||||||
"mx_AddressTile_unknownMx": true,
|
"mx_AddressTile_unknownMx": true,
|
||||||
"mx_AddressTile_justified": this.props.justified,
|
"mx_AddressTile_justified": this.props.justified,
|
||||||
});
|
});
|
||||||
|
|
||||||
info = (
|
info = (
|
||||||
<div className={unknownMxClasses}>{ this.props.address }</div>
|
<div className={unknownMxClasses}>{ this.props.address.address }</div>
|
||||||
);
|
);
|
||||||
} else if (email) {
|
} else if (address.addressType === "email") {
|
||||||
var emailClasses = classNames({
|
var emailClasses = classNames({
|
||||||
"mx_AddressTile_email": true,
|
"mx_AddressTile_email": true,
|
||||||
"mx_AddressTile_justified": this.props.justified,
|
"mx_AddressTile_justified": this.props.justified,
|
||||||
});
|
});
|
||||||
|
|
||||||
info = (
|
info = (
|
||||||
<div className={emailClasses}>{ email }</div>
|
<div className={emailClasses}>{ address.address }</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
error = true;
|
error = true;
|
||||||
|
@ -129,12 +143,12 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var classes = classNames({
|
const classes = classNames({
|
||||||
"mx_AddressTile": true,
|
"mx_AddressTile": true,
|
||||||
"mx_AddressTile_error": error,
|
"mx_AddressTile_error": error,
|
||||||
});
|
});
|
||||||
|
|
||||||
var dismiss;
|
let dismiss;
|
||||||
if (this.props.canDismiss) {
|
if (this.props.canDismiss) {
|
||||||
dismiss = (
|
dismiss = (
|
||||||
<div className="mx_AddressTile_dismiss" onClick={this.props.onDismissed} >
|
<div className="mx_AddressTile_dismiss" onClick={this.props.onDismissed} >
|
||||||
|
@ -145,7 +159,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
{ network }
|
|
||||||
<div className="mx_AddressTile_avatar">
|
<div className="mx_AddressTile_avatar">
|
||||||
<BaseAvatar width={25} height={25} name={name} title={name} url={imgUrl} />
|
<BaseAvatar width={25} height={25} name={name} title={name} url={imgUrl} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,7 +24,7 @@ module.exports = React.createClass({
|
||||||
events: React.PropTypes.array.isRequired,
|
events: React.PropTypes.array.isRequired,
|
||||||
// An array of EventTiles to render when expanded
|
// An array of EventTiles to render when expanded
|
||||||
children: React.PropTypes.array.isRequired,
|
children: React.PropTypes.array.isRequired,
|
||||||
// The maximum number of names to show in either the join or leave summaries
|
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
|
||||||
summaryLength: React.PropTypes.number,
|
summaryLength: React.PropTypes.number,
|
||||||
// The maximum number of avatars to display in the summary
|
// The maximum number of avatars to display in the summary
|
||||||
avatarsMaxLength: React.PropTypes.number,
|
avatarsMaxLength: React.PropTypes.number,
|
||||||
|
@ -40,110 +40,12 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
return {
|
return {
|
||||||
summaryLength: 3,
|
summaryLength: 1,
|
||||||
threshold: 3,
|
threshold: 3,
|
||||||
avatarsMaxLength: 5,
|
avatarsMaxLength: 5,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
_toggleSummary: function() {
|
|
||||||
this.setState({
|
|
||||||
expanded: !this.state.expanded,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_getEventSenderName: function(ev) {
|
|
||||||
if (!ev) {
|
|
||||||
return 'undefined';
|
|
||||||
}
|
|
||||||
return ev.sender.name || ev.event.content.displayname || ev.getSender();
|
|
||||||
},
|
|
||||||
|
|
||||||
_renderNameList: function(events) {
|
|
||||||
if (events.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let originalNumber = events.length;
|
|
||||||
events = events.slice(0, this.props.summaryLength);
|
|
||||||
let lastEvent = events.pop();
|
|
||||||
|
|
||||||
let names = events.map((ev) => {
|
|
||||||
return this._getEventSenderName(ev);
|
|
||||||
}).join(', ');
|
|
||||||
|
|
||||||
let lastName = this._getEventSenderName(lastEvent);
|
|
||||||
if (names.length === 0) {
|
|
||||||
// special-case for a single event
|
|
||||||
return lastName;
|
|
||||||
}
|
|
||||||
|
|
||||||
let remaining = originalNumber - this.props.summaryLength;
|
|
||||||
if (remaining > 0) {
|
|
||||||
// name1, name2, name3, and 100 others
|
|
||||||
return names + ', ' + lastName + ', and ' + remaining + ' others';
|
|
||||||
} else {
|
|
||||||
// name1, name2 and name3
|
|
||||||
return names + ' and ' + lastName;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_renderSummary: function(joinEvents, leaveEvents) {
|
|
||||||
let joiners = this._renderNameList(joinEvents);
|
|
||||||
let leavers = this._renderNameList(leaveEvents);
|
|
||||||
|
|
||||||
let joinSummary = null;
|
|
||||||
if (joiners) {
|
|
||||||
joinSummary = (
|
|
||||||
<span>
|
|
||||||
{joiners} joined the room
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let leaveSummary = null;
|
|
||||||
if (leavers) {
|
|
||||||
leaveSummary = (
|
|
||||||
<span>
|
|
||||||
{leavers} left the room
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The joinEvents and leaveEvents are representative of the net movement
|
|
||||||
// per-user, and so it is possible that the total net movement is nil,
|
|
||||||
// whilst there are some events in the expanded list. If the total net
|
|
||||||
// movement is nil, then neither joinSummary nor leaveSummary will be
|
|
||||||
// truthy, so return null.
|
|
||||||
if (!joinSummary && !leaveSummary) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
{joinSummary}{joinSummary && leaveSummary?'; ':''}
|
|
||||||
{leaveSummary}.
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
_renderAvatars: function(events) {
|
|
||||||
let avatars = events.slice(0, this.props.avatarsMaxLength).map((e) => {
|
|
||||||
return (
|
|
||||||
<MemberAvatar
|
|
||||||
key={e.getId()}
|
|
||||||
member={e.sender}
|
|
||||||
width={14}
|
|
||||||
height={14}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
{avatars}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
shouldComponentUpdate: function(nextProps, nextState) {
|
shouldComponentUpdate: function(nextProps, nextState) {
|
||||||
// Update if
|
// Update if
|
||||||
// - The number of summarised events has changed
|
// - The number of summarised events has changed
|
||||||
|
@ -157,10 +59,296 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_toggleSummary: function() {
|
||||||
|
this.setState({
|
||||||
|
expanded: !this.state.expanded,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the JSX for users aggregated by their transition sequences (`eventAggregates`) where
|
||||||
|
* the sequences are ordered by `orderedTransitionSequences`.
|
||||||
|
* @param {object[]} eventAggregates a map of transition sequence to array of user display names
|
||||||
|
* or user IDs.
|
||||||
|
* @param {string[]} orderedTransitionSequences an array which is some ordering of
|
||||||
|
* `Object.keys(eventAggregates)`.
|
||||||
|
* @returns {ReactElement} a single <span> containing the textual summary of the aggregated
|
||||||
|
* events that occurred.
|
||||||
|
*/
|
||||||
|
_renderSummary: function(eventAggregates, orderedTransitionSequences) {
|
||||||
|
const summaries = orderedTransitionSequences.map((transitions) => {
|
||||||
|
const userNames = eventAggregates[transitions];
|
||||||
|
const nameList = this._renderNameList(userNames);
|
||||||
|
const plural = userNames.length > 1;
|
||||||
|
|
||||||
|
const splitTransitions = transitions.split(',');
|
||||||
|
|
||||||
|
// Some neighbouring transitions are common, so canonicalise some into "pair"
|
||||||
|
// transitions
|
||||||
|
const canonicalTransitions = this._getCanonicalTransitions(splitTransitions);
|
||||||
|
// Transform into consecutive repetitions of the same transition (like 5
|
||||||
|
// consecutive 'joined_and_left's)
|
||||||
|
const coalescedTransitions = this._coalesceRepeatedTransitions(
|
||||||
|
canonicalTransitions
|
||||||
|
);
|
||||||
|
|
||||||
|
const descs = coalescedTransitions.map((t) => {
|
||||||
|
return this._getDescriptionForTransition(
|
||||||
|
t.transitionType, plural, t.repeats
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const desc = this._renderCommaSeparatedList(descs);
|
||||||
|
|
||||||
|
return nameList + " " + desc;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!summaries) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{summaries.join(", ")}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} users an array of user display names or user IDs.
|
||||||
|
* @returns {string} a comma-separated list that ends with "and [n] others" if there are
|
||||||
|
* more items in `users` than `this.props.summaryLength`, which is the number of names
|
||||||
|
* included before "and [n] others".
|
||||||
|
*/
|
||||||
|
_renderNameList: function(users) {
|
||||||
|
return this._renderCommaSeparatedList(users, this.props.summaryLength);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonicalise an array of transitions such that some pairs of transitions become
|
||||||
|
* single transitions. For example an input ['joined','left'] would result in an output
|
||||||
|
* ['joined_and_left'].
|
||||||
|
* @param {string[]} transitions an array of transitions.
|
||||||
|
* @returns {string[]} an array of transitions.
|
||||||
|
*/
|
||||||
|
_getCanonicalTransitions: function(transitions) {
|
||||||
|
const modMap = {
|
||||||
|
'joined': {
|
||||||
|
'after': 'left',
|
||||||
|
'newTransition': 'joined_and_left',
|
||||||
|
},
|
||||||
|
'left': {
|
||||||
|
'after': 'joined',
|
||||||
|
'newTransition': 'left_and_joined',
|
||||||
|
},
|
||||||
|
// $currentTransition : {
|
||||||
|
// 'after' : $nextTransition,
|
||||||
|
// 'newTransition' : 'new_transition_type',
|
||||||
|
// },
|
||||||
|
};
|
||||||
|
const res = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < transitions.length; i++) {
|
||||||
|
const t = transitions[i];
|
||||||
|
const t2 = transitions[i + 1];
|
||||||
|
|
||||||
|
let transition = t;
|
||||||
|
|
||||||
|
if (i < transitions.length - 1 && modMap[t] && modMap[t].after === t2) {
|
||||||
|
transition = modMap[t].newTransition;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.push(transition);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform an array of transitions into an array of transitions and how many times
|
||||||
|
* they are repeated consecutively.
|
||||||
|
*
|
||||||
|
* An array of 123 "joined_and_left" transitions, would result in:
|
||||||
|
* ```
|
||||||
|
* [{
|
||||||
|
* transitionType: "joined_and_left"
|
||||||
|
* repeats: 123
|
||||||
|
* }]
|
||||||
|
* ```
|
||||||
|
* @param {string[]} transitions the array of transitions to transform.
|
||||||
|
* @returns {object[]} an array of coalesced transitions.
|
||||||
|
*/
|
||||||
|
_coalesceRepeatedTransitions: function(transitions) {
|
||||||
|
const res = [];
|
||||||
|
for (let i = 0; i < transitions.length; i++) {
|
||||||
|
if (res.length > 0 && res[res.length - 1].transitionType === transitions[i]) {
|
||||||
|
res[res.length - 1].repeats += 1;
|
||||||
|
} else {
|
||||||
|
res.push({
|
||||||
|
transitionType: transitions[i],
|
||||||
|
repeats: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For a certain transition, t, describe what happened to the users that
|
||||||
|
* underwent the transition.
|
||||||
|
* @param {string} t the transition type.
|
||||||
|
* @param {boolean} plural whether there were multiple users undergoing the same
|
||||||
|
* transition.
|
||||||
|
* @param {number} repeats the number of times the transition was repeated in a row.
|
||||||
|
* @returns {string} the written English equivalent of the transition.
|
||||||
|
*/
|
||||||
|
_getDescriptionForTransition(t, plural, repeats) {
|
||||||
|
const beConjugated = plural ? "were" : "was";
|
||||||
|
const invitation = "their invitation" + (plural || (repeats > 1) ? "s" : "");
|
||||||
|
|
||||||
|
let res = null;
|
||||||
|
const map = {
|
||||||
|
"joined": "joined",
|
||||||
|
"left": "left",
|
||||||
|
"joined_and_left": "joined and left",
|
||||||
|
"left_and_joined": "left and rejoined",
|
||||||
|
"invite_reject": "rejected " + invitation,
|
||||||
|
"invite_withdrawal": "had " + invitation + " withdrawn",
|
||||||
|
"invited": beConjugated + " invited",
|
||||||
|
"banned": beConjugated + " banned",
|
||||||
|
"unbanned": beConjugated + " unbanned",
|
||||||
|
"kicked": beConjugated + " kicked",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Object.keys(map).includes(t)) {
|
||||||
|
res = map[t] + (repeats > 1 ? " " + repeats + " times" : "" );
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a written English string representing `items`, with an optional limit on
|
||||||
|
* the number of items included in the result. If specified and if the length of
|
||||||
|
*`items` is greater than the limit, the string "and n others" will be appended onto
|
||||||
|
* the result.
|
||||||
|
* If `items` is empty, returns the empty string. If there is only one item, return
|
||||||
|
* it.
|
||||||
|
* @param {string[]} items the items to construct a string from.
|
||||||
|
* @param {number?} itemLimit the number by which to limit the list.
|
||||||
|
* @returns {string} a string constructed by joining `items` with a comma between each
|
||||||
|
* item, but with the last item appended as " and [lastItem]".
|
||||||
|
*/
|
||||||
|
_renderCommaSeparatedList(items, itemLimit) {
|
||||||
|
const remaining = itemLimit === undefined ? 0 : Math.max(
|
||||||
|
items.length - itemLimit, 0
|
||||||
|
);
|
||||||
|
if (items.length === 0) {
|
||||||
|
return "";
|
||||||
|
} else if (items.length === 1) {
|
||||||
|
return items[0];
|
||||||
|
} else if (remaining) {
|
||||||
|
items = items.slice(0, itemLimit);
|
||||||
|
const other = " other" + (remaining > 1 ? "s" : "");
|
||||||
|
return items.join(', ') + ' and ' + remaining + other;
|
||||||
|
} else {
|
||||||
|
const lastItem = items.pop();
|
||||||
|
return items.join(', ') + ' and ' + lastItem;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_renderAvatars: function(roomMembers) {
|
||||||
|
const avatars = roomMembers.slice(0, this.props.avatarsMaxLength).map((m) => {
|
||||||
|
return (
|
||||||
|
<MemberAvatar key={m.userId} member={m} width={14} height={14} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{avatars}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
_getTransitionSequence: function(events) {
|
||||||
|
return events.map(this._getTransition);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Label a given membership event, `e`, where `getContent().membership` has
|
||||||
|
* changed for each transition allowed by the Matrix protocol. This attempts to
|
||||||
|
* label the membership changes that occur in `../../../TextForEvent.js`.
|
||||||
|
* @param {MatrixEvent} e the membership change event to label.
|
||||||
|
* @returns {string?} the transition type given to this event. This defaults to `null`
|
||||||
|
* if a transition is not recognised.
|
||||||
|
*/
|
||||||
|
_getTransition: function(e) {
|
||||||
|
switch (e.mxEvent.getContent().membership) {
|
||||||
|
case 'invite': return 'invited';
|
||||||
|
case 'ban': return 'banned';
|
||||||
|
case 'join': return 'joined';
|
||||||
|
case 'leave':
|
||||||
|
if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) {
|
||||||
|
switch (e.mxEvent.getPrevContent().membership) {
|
||||||
|
case 'invite': return 'invite_reject';
|
||||||
|
default: return 'left';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (e.mxEvent.getPrevContent().membership) {
|
||||||
|
case 'invite': return 'invite_withdrawal';
|
||||||
|
case 'ban': return 'unbanned';
|
||||||
|
case 'join': return 'kicked';
|
||||||
|
default: return 'left';
|
||||||
|
}
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_getAggregate: function(userEvents) {
|
||||||
|
// A map of aggregate type to arrays of display names. Each aggregate type
|
||||||
|
// is a comma-delimited string of transitions, e.g. "joined,left,kicked".
|
||||||
|
// The array of display names is the array of users who went through that
|
||||||
|
// sequence during eventsToRender.
|
||||||
|
const aggregate = {
|
||||||
|
// $aggregateType : []:string
|
||||||
|
};
|
||||||
|
// A map of aggregate types to the indices that order them (the index of
|
||||||
|
// the first event for a given transition sequence)
|
||||||
|
const aggregateIndices = {
|
||||||
|
// $aggregateType : int
|
||||||
|
};
|
||||||
|
|
||||||
|
const users = Object.keys(userEvents);
|
||||||
|
users.forEach(
|
||||||
|
(userId) => {
|
||||||
|
const firstEvent = userEvents[userId][0];
|
||||||
|
const displayName = firstEvent.displayName;
|
||||||
|
|
||||||
|
const seq = this._getTransitionSequence(userEvents[userId]);
|
||||||
|
if (!aggregate[seq]) {
|
||||||
|
aggregate[seq] = [];
|
||||||
|
aggregateIndices[seq] = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregate[seq].push(displayName);
|
||||||
|
|
||||||
|
if (aggregateIndices[seq] === -1 ||
|
||||||
|
firstEvent.index < aggregateIndices[seq]) {
|
||||||
|
aggregateIndices[seq] = firstEvent.index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
names: aggregate,
|
||||||
|
indices: aggregateIndices,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
let eventsToRender = this.props.events;
|
const eventsToRender = this.props.events;
|
||||||
let fewEvents = eventsToRender.length < this.props.threshold;
|
const fewEvents = eventsToRender.length < this.props.threshold;
|
||||||
let expanded = this.state.expanded || fewEvents;
|
const expanded = this.state.expanded || fewEvents;
|
||||||
|
|
||||||
let expandedEvents = null;
|
let expandedEvents = null;
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
|
@ -175,70 +363,56 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map user IDs to the first and last member events in eventsToRender for each user
|
// Map user IDs to an array of objects:
|
||||||
let userEvents = {
|
const userEvents = {
|
||||||
// $userId : {first : e0, last : e1}
|
// $userId : [{
|
||||||
|
// // The original event
|
||||||
|
// mxEvent: e,
|
||||||
|
// // The display name of the user (if not, then user ID)
|
||||||
|
// displayName: e.target.name || userId,
|
||||||
|
// // The original index of the event in this.props.events
|
||||||
|
// index: index,
|
||||||
|
// }]
|
||||||
};
|
};
|
||||||
|
|
||||||
eventsToRender.forEach((e) => {
|
const avatarMembers = [];
|
||||||
|
eventsToRender.forEach((e, index) => {
|
||||||
const userId = e.getStateKey();
|
const userId = e.getStateKey();
|
||||||
// Initialise a user's events
|
// Initialise a user's events
|
||||||
if (!userEvents[userId]) {
|
if (!userEvents[userId]) {
|
||||||
userEvents[userId] = {first: null, last: null};
|
userEvents[userId] = [];
|
||||||
|
avatarMembers.push(e.target);
|
||||||
}
|
}
|
||||||
if (!userEvents[userId].first) {
|
userEvents[userId].push({
|
||||||
userEvents[userId].first = e;
|
mxEvent: e,
|
||||||
}
|
displayName: e.target.name || userId,
|
||||||
userEvents[userId].last = e;
|
index: index,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Populate the join/leave event arrays with events that represent what happened
|
const aggregate = this._getAggregate(userEvents);
|
||||||
// overall to a user's membership. If no events are added to either array for a
|
|
||||||
// particular user, they will be considered a user that "joined and left".
|
|
||||||
let joinEvents = [];
|
|
||||||
let leaveEvents = [];
|
|
||||||
let joinedAndLeft = 0;
|
|
||||||
let senders = Object.keys(userEvents);
|
|
||||||
senders.forEach(
|
|
||||||
(userId) => {
|
|
||||||
let firstEvent = userEvents[userId].first;
|
|
||||||
let lastEvent = userEvents[userId].last;
|
|
||||||
|
|
||||||
// Membership BEFORE eventsToRender
|
// Sort types by order of lowest event index within sequence
|
||||||
let previousMembership = firstEvent.getPrevContent().membership || "leave";
|
const orderedTransitionSequences = Object.keys(aggregate.names).sort(
|
||||||
|
(seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2]
|
||||||
// If the last membership event differs from previousMembership, use that.
|
|
||||||
if (previousMembership !== lastEvent.getContent().membership) {
|
|
||||||
if (lastEvent.event.content.membership === 'join') {
|
|
||||||
joinEvents.push(lastEvent);
|
|
||||||
} else if (lastEvent.event.content.membership === 'leave') {
|
|
||||||
leaveEvents.push(lastEvent);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Increment the number of users whose membership change was nil overall
|
|
||||||
joinedAndLeft++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let avatars = this._renderAvatars(joinEvents.concat(leaveEvents));
|
const avatars = this._renderAvatars(avatarMembers);
|
||||||
let summary = this._renderSummary(joinEvents, leaveEvents);
|
const summary = this._renderSummary(aggregate.names, orderedTransitionSequences);
|
||||||
let toggleButton = (
|
const toggleButton = (
|
||||||
<a className="mx_MemberEventListSummary_toggle" onClick={this._toggleSummary}>
|
<a className="mx_MemberEventListSummary_toggle" onClick={this._toggleSummary}>
|
||||||
{expanded ? 'collapse' : 'expand'}
|
{expanded ? 'collapse' : 'expand'}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
let plural = (joinEvents.length + leaveEvents.length > 0) ? 'others' : 'users';
|
|
||||||
let noun = (joinedAndLeft === 1 ? 'user' : plural);
|
|
||||||
|
|
||||||
let summaryContainer = (
|
const summaryContainer = (
|
||||||
<div className="mx_EventTile_line">
|
<div className="mx_EventTile_line">
|
||||||
<div className="mx_EventTile_info">
|
<div className="mx_EventTile_info">
|
||||||
<span className="mx_MemberEventListSummary_avatars">
|
<span className="mx_MemberEventListSummary_avatars">
|
||||||
{avatars}
|
{avatars}
|
||||||
</span>
|
</span>
|
||||||
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
|
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
|
||||||
{summary}{joinedAndLeft ? joinedAndLeft + ' ' + noun + ' joined and left' : ''}
|
{summary}
|
||||||
</span>
|
</span>
|
||||||
{toggleButton}
|
{toggleButton}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -69,6 +69,7 @@ var TintableSvg = React.createClass({
|
||||||
width={ this.props.width }
|
width={ this.props.width }
|
||||||
height={ this.props.height }
|
height={ this.props.height }
|
||||||
onLoad={ this.onLoad }
|
onLoad={ this.onLoad }
|
||||||
|
tabIndex="-1"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ var React = require('react');
|
||||||
|
|
||||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
var sdk = require('../../../index');
|
var sdk = require('../../../index');
|
||||||
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
|
||||||
|
|
||||||
var PRESENCE_CLASS = {
|
var PRESENCE_CLASS = {
|
||||||
|
@ -152,7 +153,7 @@ module.exports = React.createClass({
|
||||||
var av = this.props.avatarJsx || <BaseAvatar name={this.props.name} width={36} height={36} />;
|
var av = this.props.avatarJsx || <BaseAvatar name={this.props.name} width={36} height={36} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={mainClassName} title={ this.props.title }
|
<AccessibleButton className={mainClassName} title={ this.props.title }
|
||||||
onClick={ this.props.onClick } onMouseEnter={ this.mouseEnter }
|
onClick={ this.props.onClick } onMouseEnter={ this.mouseEnter }
|
||||||
onMouseLeave={ this.mouseLeave }>
|
onMouseLeave={ this.mouseLeave }>
|
||||||
<div className="mx_EntityTile_avatar">
|
<div className="mx_EntityTile_avatar">
|
||||||
|
@ -161,7 +162,7 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
{ nameEl }
|
{ nameEl }
|
||||||
{ inviteButton }
|
{ inviteButton }
|
||||||
</div>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -35,6 +35,7 @@ var DMRoomMap = require('../../../utils/DMRoomMap');
|
||||||
var Unread = require('../../../Unread');
|
var Unread = require('../../../Unread');
|
||||||
var Receipt = require('../../../utils/Receipt');
|
var Receipt = require('../../../utils/Receipt');
|
||||||
var WithMatrixClient = require('../../../wrappers/WithMatrixClient');
|
var WithMatrixClient = require('../../../wrappers/WithMatrixClient');
|
||||||
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
|
||||||
module.exports = WithMatrixClient(React.createClass({
|
module.exports = WithMatrixClient(React.createClass({
|
||||||
displayName: 'MemberInfo',
|
displayName: 'MemberInfo',
|
||||||
|
@ -612,7 +613,7 @@ module.exports = WithMatrixClient(React.createClass({
|
||||||
mx_MemberInfo_createRoom_label: true,
|
mx_MemberInfo_createRoom_label: true,
|
||||||
mx_RoomTile_name: true,
|
mx_RoomTile_name: true,
|
||||||
});
|
});
|
||||||
const startNewChat = <div
|
const startNewChat = <AccessibleButton
|
||||||
className="mx_MemberInfo_createRoom"
|
className="mx_MemberInfo_createRoom"
|
||||||
onClick={this.onNewDMClick}
|
onClick={this.onNewDMClick}
|
||||||
>
|
>
|
||||||
|
@ -620,7 +621,7 @@ module.exports = WithMatrixClient(React.createClass({
|
||||||
<img src="img/create-big.svg" width="26" height="26" />
|
<img src="img/create-big.svg" width="26" height="26" />
|
||||||
</div>
|
</div>
|
||||||
<div className={labelClasses}><i>Start new chat</i></div>
|
<div className={labelClasses}><i>Start new chat</i></div>
|
||||||
</div>;
|
</AccessibleButton>;
|
||||||
|
|
||||||
startChat = <div>
|
startChat = <div>
|
||||||
<h3>Direct chats</h3>
|
<h3>Direct chats</h3>
|
||||||
|
@ -635,26 +636,37 @@ module.exports = WithMatrixClient(React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.can.kick) {
|
if (this.state.can.kick) {
|
||||||
kickButton = <div className="mx_MemberInfo_field" onClick={this.onKick}>
|
const membership = this.props.member.membership;
|
||||||
{ this.props.member.membership === "invite" ? "Disinvite" : "Kick" }
|
const kickLabel = membership === "invite" ? "Disinvite" : "Kick";
|
||||||
</div>;
|
kickButton = (
|
||||||
|
<AccessibleButton className="mx_MemberInfo_field"
|
||||||
|
onClick={this.onKick}>
|
||||||
|
{kickLabel}
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (this.state.can.ban) {
|
if (this.state.can.ban) {
|
||||||
banButton = <div className="mx_MemberInfo_field" onClick={this.onBan}>
|
banButton = (
|
||||||
Ban
|
<AccessibleButton className="mx_MemberInfo_field"
|
||||||
</div>;
|
onClick={this.onBan}>
|
||||||
|
Ban
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (this.state.can.mute) {
|
if (this.state.can.mute) {
|
||||||
var muteLabel = this.state.muted ? "Unmute" : "Mute";
|
const muteLabel = this.state.muted ? "Unmute" : "Mute";
|
||||||
muteButton = <div className="mx_MemberInfo_field" onClick={this.onMuteToggle}>
|
muteButton = (
|
||||||
{muteLabel}
|
<AccessibleButton className="mx_MemberInfo_field"
|
||||||
</div>;
|
onClick={this.onMuteToggle}>
|
||||||
|
{muteLabel}
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (this.state.can.toggleMod) {
|
if (this.state.can.toggleMod) {
|
||||||
var giveOpLabel = this.state.isTargetMod ? "Revoke Moderator" : "Make Moderator";
|
var giveOpLabel = this.state.isTargetMod ? "Revoke Moderator" : "Make Moderator";
|
||||||
giveModButton = <div className="mx_MemberInfo_field" onClick={this.onModToggle}>
|
giveModButton = <AccessibleButton className="mx_MemberInfo_field" onClick={this.onModToggle}>
|
||||||
{giveOpLabel}
|
{giveOpLabel}
|
||||||
</div>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: we should have an invite button if this MemberInfo is showing a user who isn't actually in the current room yet
|
// TODO: we should have an invite button if this MemberInfo is showing a user who isn't actually in the current room yet
|
||||||
|
@ -682,7 +694,7 @@ module.exports = WithMatrixClient(React.createClass({
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||||
return (
|
return (
|
||||||
<div className="mx_MemberInfo">
|
<div className="mx_MemberInfo">
|
||||||
<img className="mx_MemberInfo_cancel" src="img/cancel.svg" width="18" height="18" onClick={this.onCancel}/>
|
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}> <img src="img/cancel.svg" width="18" height="18"/></AccessibleButton>
|
||||||
<div className="mx_MemberInfo_avatar">
|
<div className="mx_MemberInfo_avatar">
|
||||||
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
|
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -192,9 +192,9 @@ module.exports = React.createClass({
|
||||||
width={14} height={14} resizeMethod="crop"
|
width={14} height={14} resizeMethod="crop"
|
||||||
style={style}
|
style={style}
|
||||||
title={title}
|
title={title}
|
||||||
onClick={this.props.onClick}
|
|
||||||
/>
|
/>
|
||||||
</Velociraptor>
|
</Velociraptor>
|
||||||
);
|
);
|
||||||
|
/* onClick={this.props.onClick} */
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -26,6 +26,7 @@ var rate_limited_func = require('../../../ratelimitedfunc');
|
||||||
var linkify = require('linkifyjs');
|
var linkify = require('linkifyjs');
|
||||||
var linkifyElement = require('linkifyjs/element');
|
var linkifyElement = require('linkifyjs/element');
|
||||||
var linkifyMatrix = require('../../../linkify-matrix');
|
var linkifyMatrix = require('../../../linkify-matrix');
|
||||||
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
|
||||||
linkifyMatrix(linkify);
|
linkifyMatrix(linkify);
|
||||||
|
|
||||||
|
@ -182,8 +183,8 @@ module.exports = React.createClass({
|
||||||
'm.room.name', user_id
|
'm.room.name', user_id
|
||||||
);
|
);
|
||||||
|
|
||||||
save_button = <div className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save</div>;
|
save_button = <AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save</AccessibleButton>;
|
||||||
cancel_button = <div className="mx_RoomHeader_cancelButton mx_filterFlipColor" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" alt="Cancel"/> </div>;
|
cancel_button = <AccessibleButton className="mx_RoomHeader_cancelButton" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" alt="Cancel"/> </AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.saving) {
|
if (this.props.saving) {
|
||||||
|
@ -275,9 +276,9 @@ module.exports = React.createClass({
|
||||||
var settings_button;
|
var settings_button;
|
||||||
if (this.props.onSettingsClick) {
|
if (this.props.onSettingsClick) {
|
||||||
settings_button =
|
settings_button =
|
||||||
<div className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title="Settings">
|
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title="Settings">
|
||||||
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
|
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
|
||||||
</div>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// var leave_button;
|
// var leave_button;
|
||||||
|
@ -291,17 +292,17 @@ module.exports = React.createClass({
|
||||||
var forget_button;
|
var forget_button;
|
||||||
if (this.props.onForgetClick) {
|
if (this.props.onForgetClick) {
|
||||||
forget_button =
|
forget_button =
|
||||||
<div className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title="Forget room">
|
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title="Forget room">
|
||||||
<TintableSvg src="img/leave.svg" width="26" height="20"/>
|
<TintableSvg src="img/leave.svg" width="26" height="20"/>
|
||||||
</div>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
var rightPanel_buttons;
|
var rightPanel_buttons;
|
||||||
if (this.props.collapsedRhs) {
|
if (this.props.collapsedRhs) {
|
||||||
rightPanel_buttons =
|
rightPanel_buttons =
|
||||||
<div className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title="<">
|
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title="<">
|
||||||
<TintableSvg src="img/minimise.svg" width="10" height="16"/>
|
<TintableSvg src="img/minimise.svg" width="10" height="16"/>
|
||||||
</div>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
var right_row;
|
var right_row;
|
||||||
|
@ -310,9 +311,9 @@ module.exports = React.createClass({
|
||||||
<div className="mx_RoomHeader_rightRow">
|
<div className="mx_RoomHeader_rightRow">
|
||||||
{ settings_button }
|
{ settings_button }
|
||||||
{ forget_button }
|
{ forget_button }
|
||||||
<div className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search">
|
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search">
|
||||||
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
|
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
|
||||||
</div>
|
</AccessibleButton>
|
||||||
{ rightPanel_buttons }
|
{ rightPanel_buttons }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ var sdk = require('../../../index');
|
||||||
var ContextualMenu = require('../../structures/ContextualMenu');
|
var ContextualMenu = require('../../structures/ContextualMenu');
|
||||||
var RoomNotifs = require('../../../RoomNotifs');
|
var RoomNotifs = require('../../../RoomNotifs');
|
||||||
var FormattingUtils = require('../../../utils/FormattingUtils');
|
var FormattingUtils = require('../../../utils/FormattingUtils');
|
||||||
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
var UserSettingsStore = require('../../../UserSettingsStore');
|
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
@ -288,8 +289,10 @@ module.exports = React.createClass({
|
||||||
var connectDragSource = this.props.connectDragSource;
|
var connectDragSource = this.props.connectDragSource;
|
||||||
var connectDropTarget = this.props.connectDropTarget;
|
var connectDropTarget = this.props.connectDropTarget;
|
||||||
|
|
||||||
|
|
||||||
let ret = (
|
let ret = (
|
||||||
<div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
<div> { /* Only native elements can be wrapped in a DnD object. */}
|
||||||
|
<AccessibleButton className={classes} tabIndex="0" onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
||||||
<div className={avatarClasses}>
|
<div className={avatarClasses}>
|
||||||
<div className="mx_RoomTile_avatar_menu" onClick={this.onAvatarClicked}>
|
<div className="mx_RoomTile_avatar_menu" onClick={this.onAvatarClicked}>
|
||||||
<div className={avatarContainerClasses}>
|
<div className={avatarContainerClasses}>
|
||||||
|
@ -304,6 +307,7 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
{/* { incomingCallBox } */}
|
{/* { incomingCallBox } */}
|
||||||
{ tooltip }
|
{ tooltip }
|
||||||
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var sdk = require('../../../index');
|
var sdk = require('../../../index');
|
||||||
var dis = require("../../../dispatcher");
|
var dis = require("../../../dispatcher");
|
||||||
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* A stripped-down room header used for things like the user settings
|
* A stripped-down room header used for things like the user settings
|
||||||
|
@ -44,7 +45,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var cancelButton;
|
var cancelButton;
|
||||||
if (this.props.onCancelClick) {
|
if (this.props.onCancelClick) {
|
||||||
cancelButton = <div className="mx_RoomHeader_cancelButton" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" alt="Cancel"/> </div>;
|
cancelButton = <AccessibleButton className="mx_RoomHeader_cancelButton" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" alt="Cancel"/> </AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
var showRhsButton;
|
var showRhsButton;
|
||||||
|
@ -70,4 +71,3 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,9 @@ limitations under the License.
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
|
var Modal = require("../../../Modal");
|
||||||
var sdk = require("../../../index");
|
var sdk = require("../../../index");
|
||||||
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'ChangePassword',
|
displayName: 'ChangePassword',
|
||||||
|
@ -65,26 +67,42 @@ module.exports = React.createClass({
|
||||||
changePassword: function(old_password, new_password) {
|
changePassword: function(old_password, new_password) {
|
||||||
var cli = MatrixClientPeg.get();
|
var cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
var authDict = {
|
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
type: 'm.login.password',
|
Modal.createDialog(QuestionDialog, {
|
||||||
user: cli.credentials.userId,
|
title: "Warning",
|
||||||
password: old_password
|
description:
|
||||||
};
|
<div>
|
||||||
|
Changing password will currently reset any end-to-end encryption keys on all devices,
|
||||||
|
making encrypted chat history unreadable.
|
||||||
|
This will be <a href="https://github.com/vector-im/riot-web/issues/2671">improved shortly</a>,
|
||||||
|
but for now be warned.
|
||||||
|
</div>,
|
||||||
|
button: "Continue",
|
||||||
|
onFinished: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
var authDict = {
|
||||||
|
type: 'm.login.password',
|
||||||
|
user: cli.credentials.userId,
|
||||||
|
password: old_password
|
||||||
|
};
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: this.Phases.Uploading
|
phase: this.Phases.Uploading
|
||||||
|
});
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
cli.setPassword(authDict, new_password).then(function() {
|
||||||
|
self.props.onFinished();
|
||||||
|
}, function(err) {
|
||||||
|
self.props.onError(err);
|
||||||
|
}).finally(function() {
|
||||||
|
self.setState({
|
||||||
|
phase: self.Phases.Edit
|
||||||
|
});
|
||||||
|
}).done();
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
var self = this;
|
|
||||||
cli.setPassword(authDict, new_password).then(function() {
|
|
||||||
self.props.onFinished();
|
|
||||||
}, function(err) {
|
|
||||||
self.props.onError(err);
|
|
||||||
}).finally(function() {
|
|
||||||
self.setState({
|
|
||||||
phase: self.Phases.Edit
|
|
||||||
});
|
|
||||||
}).done();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onClickChange: function() {
|
onClickChange: function() {
|
||||||
|
@ -136,9 +154,10 @@ module.exports = React.createClass({
|
||||||
<input id="password2" type="password" ref="confirm_input" />
|
<input id="password2" type="password" ref="confirm_input" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={buttonClassName} onClick={this.onClickChange}>
|
<AccessibleButton className={buttonClassName}
|
||||||
|
onClick={this.onClickChange}>
|
||||||
Change Password
|
Change Password
|
||||||
</div>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case this.Phases.Uploading:
|
case this.Phases.Uploading:
|
||||||
|
|
|
@ -28,7 +28,6 @@ class MatrixDispatcher extends flux.Dispatcher {
|
||||||
* for.
|
* for.
|
||||||
*/
|
*/
|
||||||
dispatch(payload, sync) {
|
dispatch(payload, sync) {
|
||||||
console.log("Dispatch: "+payload.action);
|
|
||||||
if (sync) {
|
if (sync) {
|
||||||
super.dispatch(payload);
|
super.dispatch(payload);
|
||||||
} else {
|
} else {
|
||||||
|
@ -42,6 +41,9 @@ class MatrixDispatcher extends flux.Dispatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XXX this is a big anti-pattern, and makes testing hard. Because dispatches
|
||||||
|
// happen asynchronously, it is possible for actions dispatched in one thread
|
||||||
|
// to arrive in another, with *hilarious* consequences.
|
||||||
if (global.mxDispatcher === undefined) {
|
if (global.mxDispatcher === undefined) {
|
||||||
global.mxDispatcher = new MatrixDispatcher();
|
global.mxDispatcher = new MatrixDispatcher();
|
||||||
}
|
}
|
||||||
|
|
25
src/index.js
25
src/index.js
|
@ -27,28 +27,3 @@ module.exports.resetSkin = function() {
|
||||||
module.exports.getComponent = function(componentName) {
|
module.exports.getComponent = function(componentName) {
|
||||||
return Skinner.getComponent(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);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
5
test/.eslintrc.js
Normal file
5
test/.eslintrc.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
mocha: true,
|
||||||
|
},
|
||||||
|
}
|
681
test/components/views/elements/MemberEventListSummary-test.js
Normal file
681
test/components/views/elements/MemberEventListSummary-test.js
Normal file
|
@ -0,0 +1,681 @@
|
||||||
|
const expect = require('expect');
|
||||||
|
const React = require('react');
|
||||||
|
const ReactDOM = require("react-dom");
|
||||||
|
const ReactTestUtils = require('react-addons-test-utils');
|
||||||
|
const sdk = require('matrix-react-sdk');
|
||||||
|
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
|
||||||
|
|
||||||
|
const testUtils = require('../../../test-utils');
|
||||||
|
describe('MemberEventListSummary', function() {
|
||||||
|
let sandbox;
|
||||||
|
|
||||||
|
// Generate dummy event tiles for use in simulating an expanded MELS
|
||||||
|
const generateTiles = (events) => {
|
||||||
|
return events.map((e) => {
|
||||||
|
return (
|
||||||
|
<div key={e.getId()} className="event_tile">
|
||||||
|
Expanded membership
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a membership event with the target of the event set as a mocked
|
||||||
|
* RoomMember based on `parameters.userId`.
|
||||||
|
* @param {string} eventId the ID of the event.
|
||||||
|
* @param {object} parameters the parameters to use to create the event.
|
||||||
|
* @param {string} parameters.membership the membership to assign to
|
||||||
|
* `content.membership`
|
||||||
|
* @param {string} parameters.userId the state key and target userId of the event. If
|
||||||
|
* `parameters.senderId` is not specified, this is also used as the event sender.
|
||||||
|
* @param {string} parameters.prevMembership the membership to assign to
|
||||||
|
* `prev_content.membership`.
|
||||||
|
* @param {string} parameters.senderId the user ID of the sender of the event.
|
||||||
|
* Optional. Defaults to `parameters.userId`.
|
||||||
|
* @returns {MatrixEvent} the event created.
|
||||||
|
*/
|
||||||
|
const generateMembershipEvent = (eventId, parameters) => {
|
||||||
|
const e = testUtils.mkMembership({
|
||||||
|
event: true,
|
||||||
|
user: parameters.senderId || parameters.userId,
|
||||||
|
skey: parameters.userId,
|
||||||
|
mship: parameters.membership,
|
||||||
|
prevMship: parameters.prevMembership,
|
||||||
|
target: {
|
||||||
|
// Use localpart as display name
|
||||||
|
name: parameters.userId.match(/@([^:]*):/)[1],
|
||||||
|
userId: parameters.userId,
|
||||||
|
getAvatarUrl: () => {
|
||||||
|
return "avatar.jpeg";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Override random event ID to allow for equality tests against tiles from
|
||||||
|
// generateTiles
|
||||||
|
e.event.event_id = eventId;
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate mock MatrixEvents from the array of parameters
|
||||||
|
const generateEvents = (parameters) => {
|
||||||
|
const res = [];
|
||||||
|
for (let i = 0; i < parameters.length; i++) {
|
||||||
|
res.push(generateMembershipEvent(`event${i}`, parameters[i]));
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate the same sequence of `events` for `n` users, where each user ID
|
||||||
|
// is created by replacing the first "$" in userIdTemplate with `i` for
|
||||||
|
// `i = 0 .. n`.
|
||||||
|
const generateEventsForUsers = (userIdTemplate, n, events) => {
|
||||||
|
let eventsForUsers = [];
|
||||||
|
let userId = "";
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
userId = userIdTemplate.replace('$', i);
|
||||||
|
events.forEach((e) => {
|
||||||
|
e.userId = userId;
|
||||||
|
});
|
||||||
|
eventsForUsers = eventsForUsers.concat(generateEvents(events));
|
||||||
|
}
|
||||||
|
return eventsForUsers;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
testUtils.beforeEach(this);
|
||||||
|
sandbox = testUtils.stubClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders expanded events if there are less than props.threshold', function() {
|
||||||
|
const events = generateEvents([
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
]);
|
||||||
|
const props = {
|
||||||
|
events: events,
|
||||||
|
children: generateTiles(events),
|
||||||
|
summaryLength: 1,
|
||||||
|
avatarsMaxLength: 5,
|
||||||
|
threshold: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderer = ReactTestUtils.createRenderer();
|
||||||
|
renderer.render(<MemberEventListSummary {...props} />);
|
||||||
|
const result = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
expect(result.type).toBe('div');
|
||||||
|
expect(result.props.children).toEqual([
|
||||||
|
<div className="event_tile" key="event0">Expanded membership</div>,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders expanded events if there are less than props.threshold', function() {
|
||||||
|
const events = generateEvents([
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
]);
|
||||||
|
const props = {
|
||||||
|
events: events,
|
||||||
|
children: generateTiles(events),
|
||||||
|
summaryLength: 1,
|
||||||
|
avatarsMaxLength: 5,
|
||||||
|
threshold: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderer = ReactTestUtils.createRenderer();
|
||||||
|
renderer.render(<MemberEventListSummary {...props} />);
|
||||||
|
const result = renderer.getRenderOutput();
|
||||||
|
|
||||||
|
expect(result.type).toBe('div');
|
||||||
|
expect(result.props.children).toEqual([
|
||||||
|
<div className="event_tile" key="event0">Expanded membership</div>,
|
||||||
|
<div className="event_tile" key="event1">Expanded membership</div>,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders collapsed events if events.length = props.threshold', function() {
|
||||||
|
const events = generateEvents([
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
]);
|
||||||
|
const props = {
|
||||||
|
events: events,
|
||||||
|
children: generateTiles(events),
|
||||||
|
summaryLength: 1,
|
||||||
|
avatarsMaxLength: 5,
|
||||||
|
threshold: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const instance = ReactTestUtils.renderIntoDocument(
|
||||||
|
<MemberEventListSummary {...props} />
|
||||||
|
);
|
||||||
|
const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
|
||||||
|
instance, "mx_MemberEventListSummary_summary"
|
||||||
|
);
|
||||||
|
const summaryText = summary.innerText;
|
||||||
|
|
||||||
|
expect(summaryText).toBe("user_1 joined and left and joined");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('truncates long join,leave repetitions', function() {
|
||||||
|
const events = generateEvents([
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
]);
|
||||||
|
const props = {
|
||||||
|
events: events,
|
||||||
|
children: generateTiles(events),
|
||||||
|
summaryLength: 1,
|
||||||
|
avatarsMaxLength: 5,
|
||||||
|
threshold: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const instance = ReactTestUtils.renderIntoDocument(
|
||||||
|
<MemberEventListSummary {...props} />
|
||||||
|
);
|
||||||
|
const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
|
||||||
|
instance, "mx_MemberEventListSummary_summary"
|
||||||
|
);
|
||||||
|
const summaryText = summary.innerText;
|
||||||
|
|
||||||
|
expect(summaryText).toBe("user_1 joined and left 7 times");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('truncates long join,leave repetitions between other events', function() {
|
||||||
|
const events = generateEvents([
|
||||||
|
{
|
||||||
|
userId: "@user_1:some.domain",
|
||||||
|
prevMembership: "ban",
|
||||||
|
membership: "leave",
|
||||||
|
senderId: "@some_other_user:some.domain",
|
||||||
|
},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{
|
||||||
|
userId: "@user_1:some.domain",
|
||||||
|
prevMembership: "leave",
|
||||||
|
membership: "invite",
|
||||||
|
senderId: "@some_other_user:some.domain",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const props = {
|
||||||
|
events: events,
|
||||||
|
children: generateTiles(events),
|
||||||
|
summaryLength: 1,
|
||||||
|
avatarsMaxLength: 5,
|
||||||
|
threshold: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const instance = ReactTestUtils.renderIntoDocument(
|
||||||
|
<MemberEventListSummary {...props} />
|
||||||
|
);
|
||||||
|
const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
|
||||||
|
instance, "mx_MemberEventListSummary_summary"
|
||||||
|
);
|
||||||
|
const summaryText = summary.innerText;
|
||||||
|
|
||||||
|
expect(summaryText).toBe(
|
||||||
|
"user_1 was unbanned, joined and left 7 times and was invited"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('truncates multiple sequences of repetitions with other events between',
|
||||||
|
function() {
|
||||||
|
const events = generateEvents([
|
||||||
|
{
|
||||||
|
userId: "@user_1:some.domain",
|
||||||
|
prevMembership: "ban",
|
||||||
|
membership: "leave",
|
||||||
|
senderId: "@some_other_user:some.domain",
|
||||||
|
},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{
|
||||||
|
userId: "@user_1:some.domain",
|
||||||
|
prevMembership: "leave",
|
||||||
|
membership: "ban",
|
||||||
|
senderId: "@some_other_user:some.domain",
|
||||||
|
},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "ban", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{
|
||||||
|
userId: "@user_1:some.domain",
|
||||||
|
prevMembership: "leave",
|
||||||
|
membership: "invite",
|
||||||
|
senderId: "@some_other_user:some.domain",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const props = {
|
||||||
|
events: events,
|
||||||
|
children: generateTiles(events),
|
||||||
|
summaryLength: 1,
|
||||||
|
avatarsMaxLength: 5,
|
||||||
|
threshold: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const instance = ReactTestUtils.renderIntoDocument(
|
||||||
|
<MemberEventListSummary {...props} />
|
||||||
|
);
|
||||||
|
const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
|
||||||
|
instance, "mx_MemberEventListSummary_summary"
|
||||||
|
);
|
||||||
|
const summaryText = summary.innerText;
|
||||||
|
|
||||||
|
expect(summaryText).toBe(
|
||||||
|
"user_1 was unbanned, joined and left 2 times, was banned, " +
|
||||||
|
"joined and left 3 times and was invited"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles multiple users following the same sequence of memberships', function() {
|
||||||
|
const events = generateEvents([
|
||||||
|
// user_1
|
||||||
|
{
|
||||||
|
userId: "@user_1:some.domain",
|
||||||
|
prevMembership: "ban",
|
||||||
|
membership: "leave",
|
||||||
|
senderId: "@some_other_user:some.domain",
|
||||||
|
},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{
|
||||||
|
userId: "@user_1:some.domain",
|
||||||
|
prevMembership: "leave",
|
||||||
|
membership: "ban",
|
||||||
|
senderId: "@some_other_user:some.domain",
|
||||||
|
},
|
||||||
|
// user_2
|
||||||
|
{
|
||||||
|
userId: "@user_2:some.domain",
|
||||||
|
prevMembership: "ban",
|
||||||
|
membership: "leave",
|
||||||
|
senderId: "@some_other_user:some.domain",
|
||||||
|
},
|
||||||
|
{userId: "@user_2:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_2:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{userId: "@user_2:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_2:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{
|
||||||
|
userId: "@user_2:some.domain",
|
||||||
|
prevMembership: "leave",
|
||||||
|
membership: "ban",
|
||||||
|
senderId: "@some_other_user:some.domain",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const props = {
|
||||||
|
events: events,
|
||||||
|
children: generateTiles(events),
|
||||||
|
summaryLength: 1,
|
||||||
|
avatarsMaxLength: 5,
|
||||||
|
threshold: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const instance = ReactTestUtils.renderIntoDocument(
|
||||||
|
<MemberEventListSummary {...props} />
|
||||||
|
);
|
||||||
|
const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
|
||||||
|
instance, "mx_MemberEventListSummary_summary"
|
||||||
|
);
|
||||||
|
const summaryText = summary.innerText;
|
||||||
|
|
||||||
|
expect(summaryText).toBe(
|
||||||
|
"user_1 and 1 other were unbanned, joined and left 2 times and were banned"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles many users following the same sequence of memberships', function() {
|
||||||
|
const events = generateEventsForUsers("@user_$:some.domain", 20, [
|
||||||
|
{
|
||||||
|
prevMembership: "ban",
|
||||||
|
membership: "leave",
|
||||||
|
senderId: "@some_other_user:some.domain",
|
||||||
|
},
|
||||||
|
{prevMembership: "leave", membership: "join"},
|
||||||
|
{prevMembership: "join", membership: "leave"},
|
||||||
|
{prevMembership: "leave", membership: "join"},
|
||||||
|
{prevMembership: "join", membership: "leave"},
|
||||||
|
{
|
||||||
|
prevMembership: "leave",
|
||||||
|
membership: "ban",
|
||||||
|
senderId: "@some_other_user:some.domain",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const props = {
|
||||||
|
events: events,
|
||||||
|
children: generateTiles(events),
|
||||||
|
summaryLength: 1,
|
||||||
|
avatarsMaxLength: 5,
|
||||||
|
threshold: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const instance = ReactTestUtils.renderIntoDocument(
|
||||||
|
<MemberEventListSummary {...props} />
|
||||||
|
);
|
||||||
|
const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
|
||||||
|
instance, "mx_MemberEventListSummary_summary"
|
||||||
|
);
|
||||||
|
const summaryText = summary.innerText;
|
||||||
|
|
||||||
|
expect(summaryText).toBe(
|
||||||
|
"user_0 and 19 others were unbanned, joined and left 2 times and were banned"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly orders sequences of transitions by the order of their first event',
|
||||||
|
function() {
|
||||||
|
const events = generateEvents([
|
||||||
|
{
|
||||||
|
userId: "@user_2:some.domain",
|
||||||
|
prevMembership: "ban",
|
||||||
|
membership: "leave",
|
||||||
|
senderId: "@some_other_user:some.domain",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: "@user_1:some.domain",
|
||||||
|
prevMembership: "ban",
|
||||||
|
membership: "leave",
|
||||||
|
senderId: "@some_other_user:some.domain",
|
||||||
|
},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{
|
||||||
|
userId: "@user_1:some.domain",
|
||||||
|
prevMembership: "leave",
|
||||||
|
membership: "ban",
|
||||||
|
senderId: "@some_other_user:some.domain",
|
||||||
|
},
|
||||||
|
{userId: "@user_2:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_2:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
{userId: "@user_2:some.domain", prevMembership: "leave", membership: "join"},
|
||||||
|
{userId: "@user_2:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
]);
|
||||||
|
const props = {
|
||||||
|
events: events,
|
||||||
|
children: generateTiles(events),
|
||||||
|
summaryLength: 1,
|
||||||
|
avatarsMaxLength: 5,
|
||||||
|
threshold: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const instance = ReactTestUtils.renderIntoDocument(
|
||||||
|
<MemberEventListSummary {...props} />
|
||||||
|
);
|
||||||
|
const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
|
||||||
|
instance, "mx_MemberEventListSummary_summary"
|
||||||
|
);
|
||||||
|
const summaryText = summary.innerText;
|
||||||
|
|
||||||
|
expect(summaryText).toBe(
|
||||||
|
"user_2 was unbanned and joined and left 2 times, user_1 was unbanned, " +
|
||||||
|
"joined and left 2 times and was banned"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly identifies transitions', function() {
|
||||||
|
const events = generateEvents([
|
||||||
|
// invited
|
||||||
|
{userId: "@user_1:some.domain", membership: "invite"},
|
||||||
|
// banned
|
||||||
|
{userId: "@user_1:some.domain", membership: "ban"},
|
||||||
|
// joined
|
||||||
|
{userId: "@user_1:some.domain", membership: "join"},
|
||||||
|
// invite_reject
|
||||||
|
{
|
||||||
|
userId: "@user_1:some.domain",
|
||||||
|
prevMembership: "invite",
|
||||||
|
membership: "leave",
|
||||||
|
},
|
||||||
|
// left
|
||||||
|
{userId: "@user_1:some.domain", prevMembership: "join", membership: "leave"},
|
||||||
|
// invite_withdrawal
|
||||||
|
{
|
||||||
|
userId: "@user_1:some.domain",
|
||||||
|
prevMembership: "invite",
|
||||||
|
membership: "leave",
|
||||||
|
senderId: "@some_other_user:some.domain",
|
||||||
|
},
|
||||||
|
// unbanned
|
||||||
|
{
|
||||||
|
userId: "@user_1:some.domain",
|
||||||
|
prevMembership: "ban",
|
||||||
|
membership: "leave",
|
||||||
|
senderId: "@some_other_user:some.domain",
|
||||||
|
},
|
||||||
|
// kicked
|
||||||
|
{
|
||||||
|
userId: "@user_1:some.domain",
|
||||||
|
prevMembership: "join",
|
||||||
|
membership: "leave",
|
||||||
|
senderId: "@some_other_user:some.domain",
|
||||||
|
},
|
||||||
|
// default = left
|
||||||
|
{
|
||||||
|
userId: "@user_1:some.domain",
|
||||||
|
prevMembership: "????",
|
||||||
|
membership: "leave",
|
||||||
|
senderId: "@some_other_user:some.domain",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const props = {
|
||||||
|
events: events,
|
||||||
|
children: generateTiles(events),
|
||||||
|
summaryLength: 1,
|
||||||
|
avatarsMaxLength: 5,
|
||||||
|
threshold: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const instance = ReactTestUtils.renderIntoDocument(
|
||||||
|
<MemberEventListSummary {...props} />
|
||||||
|
);
|
||||||
|
const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
|
||||||
|
instance, "mx_MemberEventListSummary_summary"
|
||||||
|
);
|
||||||
|
const summaryText = summary.innerText;
|
||||||
|
|
||||||
|
expect(summaryText).toBe(
|
||||||
|
"user_1 was invited, was banned, joined, rejected their invitation, left, " +
|
||||||
|
"had their invitation withdrawn, was unbanned, was kicked and left"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles invitation plurals correctly when there are multiple users', function() {
|
||||||
|
const events = generateEvents([
|
||||||
|
{
|
||||||
|
userId: "@user_1:some.domain",
|
||||||
|
prevMembership: "invite",
|
||||||
|
membership: "leave",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: "@user_1:some.domain",
|
||||||
|
prevMembership: "invite",
|
||||||
|
membership: "leave",
|
||||||
|
senderId: "@some_other_user:some.domain",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: "@user_2:some.domain",
|
||||||
|
prevMembership: "invite",
|
||||||
|
membership: "leave",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: "@user_2:some.domain",
|
||||||
|
prevMembership: "invite",
|
||||||
|
membership: "leave",
|
||||||
|
senderId: "@some_other_user:some.domain",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const props = {
|
||||||
|
events: events,
|
||||||
|
children: generateTiles(events),
|
||||||
|
summaryLength: 1,
|
||||||
|
avatarsMaxLength: 5,
|
||||||
|
threshold: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const instance = ReactTestUtils.renderIntoDocument(
|
||||||
|
<MemberEventListSummary {...props} />
|
||||||
|
);
|
||||||
|
const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
|
||||||
|
instance, "mx_MemberEventListSummary_summary"
|
||||||
|
);
|
||||||
|
const summaryText = summary.innerText;
|
||||||
|
|
||||||
|
expect(summaryText).toBe(
|
||||||
|
"user_1 and 1 other rejected their invitations and " +
|
||||||
|
"had their invitations withdrawn"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles invitation plurals correctly when there are multiple invites',
|
||||||
|
function() {
|
||||||
|
const events = generateEvents([
|
||||||
|
{
|
||||||
|
userId: "@user_1:some.domain",
|
||||||
|
prevMembership: "invite",
|
||||||
|
membership: "leave",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: "@user_1:some.domain",
|
||||||
|
prevMembership: "invite",
|
||||||
|
membership: "leave",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const props = {
|
||||||
|
events: events,
|
||||||
|
children: generateTiles(events),
|
||||||
|
summaryLength: 1,
|
||||||
|
avatarsMaxLength: 5,
|
||||||
|
threshold: 1, // threshold = 1 to force collapse
|
||||||
|
};
|
||||||
|
|
||||||
|
const instance = ReactTestUtils.renderIntoDocument(
|
||||||
|
<MemberEventListSummary {...props} />
|
||||||
|
);
|
||||||
|
const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
|
||||||
|
instance, "mx_MemberEventListSummary_summary"
|
||||||
|
);
|
||||||
|
const summaryText = summary.innerText;
|
||||||
|
|
||||||
|
expect(summaryText).toBe(
|
||||||
|
"user_1 rejected their invitations 2 times"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles a summary length = 2, with no "others"', function() {
|
||||||
|
const events = generateEvents([
|
||||||
|
{userId: "@user_1:some.domain", membership: "join"},
|
||||||
|
{userId: "@user_1:some.domain", membership: "join"},
|
||||||
|
{userId: "@user_2:some.domain", membership: "join"},
|
||||||
|
{userId: "@user_2:some.domain", membership: "join"},
|
||||||
|
]);
|
||||||
|
const props = {
|
||||||
|
events: events,
|
||||||
|
children: generateTiles(events),
|
||||||
|
summaryLength: 2,
|
||||||
|
avatarsMaxLength: 5,
|
||||||
|
threshold: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const instance = ReactTestUtils.renderIntoDocument(
|
||||||
|
<MemberEventListSummary {...props} />
|
||||||
|
);
|
||||||
|
const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
|
||||||
|
instance, "mx_MemberEventListSummary_summary"
|
||||||
|
);
|
||||||
|
const summaryText = summary.innerText;
|
||||||
|
|
||||||
|
expect(summaryText).toBe(
|
||||||
|
"user_1 and user_2 joined 2 times"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles a summary length = 2, with 1 "other"', function() {
|
||||||
|
const events = generateEvents([
|
||||||
|
{userId: "@user_1:some.domain", membership: "join"},
|
||||||
|
{userId: "@user_2:some.domain", membership: "join"},
|
||||||
|
{userId: "@user_3:some.domain", membership: "join"},
|
||||||
|
]);
|
||||||
|
const props = {
|
||||||
|
events: events,
|
||||||
|
children: generateTiles(events),
|
||||||
|
summaryLength: 2,
|
||||||
|
avatarsMaxLength: 5,
|
||||||
|
threshold: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const instance = ReactTestUtils.renderIntoDocument(
|
||||||
|
<MemberEventListSummary {...props} />
|
||||||
|
);
|
||||||
|
const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
|
||||||
|
instance, "mx_MemberEventListSummary_summary"
|
||||||
|
);
|
||||||
|
const summaryText = summary.innerText;
|
||||||
|
|
||||||
|
expect(summaryText).toBe(
|
||||||
|
"user_1, user_2 and 1 other joined"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles a summary length = 2, with many "others"', function() {
|
||||||
|
const events = generateEventsForUsers("@user_$:some.domain", 20, [
|
||||||
|
{membership: "join"},
|
||||||
|
]);
|
||||||
|
const props = {
|
||||||
|
events: events,
|
||||||
|
children: generateTiles(events),
|
||||||
|
summaryLength: 2,
|
||||||
|
avatarsMaxLength: 5,
|
||||||
|
threshold: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const instance = ReactTestUtils.renderIntoDocument(
|
||||||
|
<MemberEventListSummary {...props} />
|
||||||
|
);
|
||||||
|
const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
|
||||||
|
instance, "mx_MemberEventListSummary_summary"
|
||||||
|
);
|
||||||
|
const summaryText = summary.innerText;
|
||||||
|
|
||||||
|
expect(summaryText).toBe(
|
||||||
|
"user_0, user_1 and 18 others joined"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -108,6 +108,7 @@ export function mkEvent(opts) {
|
||||||
room_id: opts.room,
|
room_id: opts.room,
|
||||||
sender: opts.user,
|
sender: opts.user,
|
||||||
content: opts.content,
|
content: opts.content,
|
||||||
|
prev_content: opts.prev_content,
|
||||||
event_id: "$" + Math.random() + "-" + Math.random(),
|
event_id: "$" + Math.random() + "-" + Math.random(),
|
||||||
origin_server_ts: opts.ts,
|
origin_server_ts: opts.ts,
|
||||||
};
|
};
|
||||||
|
@ -150,7 +151,9 @@ export function mkPresence(opts) {
|
||||||
* @param {Object} opts Values for the membership.
|
* @param {Object} opts Values for the membership.
|
||||||
* @param {string} opts.room The room ID for the event.
|
* @param {string} opts.room The room ID for the event.
|
||||||
* @param {string} opts.mship The content.membership for the event.
|
* @param {string} opts.mship The content.membership for the event.
|
||||||
|
* @param {string} opts.prevMship The prev_content.membership for the event.
|
||||||
* @param {string} opts.user The user ID for the event.
|
* @param {string} opts.user The user ID for the event.
|
||||||
|
* @param {RoomMember} opts.target The target of the event.
|
||||||
* @param {string} opts.skey The other user ID for the event if applicable
|
* @param {string} opts.skey The other user ID for the event if applicable
|
||||||
* e.g. for invites/bans.
|
* e.g. for invites/bans.
|
||||||
* @param {string} opts.name The content.displayname for the event.
|
* @param {string} opts.name The content.displayname for the event.
|
||||||
|
@ -169,9 +172,16 @@ export function mkMembership(opts) {
|
||||||
opts.content = {
|
opts.content = {
|
||||||
membership: opts.mship
|
membership: opts.mship
|
||||||
};
|
};
|
||||||
|
if (opts.prevMship) {
|
||||||
|
opts.prev_content = { membership: opts.prevMship };
|
||||||
|
}
|
||||||
if (opts.name) { opts.content.displayname = opts.name; }
|
if (opts.name) { opts.content.displayname = opts.name; }
|
||||||
if (opts.url) { opts.content.avatar_url = opts.url; }
|
if (opts.url) { opts.content.avatar_url = opts.url; }
|
||||||
return mkEvent(opts);
|
let e = mkEvent(opts);
|
||||||
|
if (opts.target) {
|
||||||
|
e.target = opts.target;
|
||||||
|
}
|
||||||
|
return e;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in a new issue