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 = {
|
||||
parser: "babel-eslint",
|
||||
extends: ["./node_modules/matrix-js-sdk/.eslintrc.js"],
|
||||
extends: [matrixJsSdkPath + "/.eslintrc.js"],
|
||||
plugins: [
|
||||
"react",
|
||||
"flowtype",
|
||||
|
|
|
@ -19,7 +19,7 @@ npm install
|
|||
npm run test
|
||||
|
||||
# run eslint
|
||||
npm run lint -- -f checkstyle -o eslint.xml || true
|
||||
npm run lintall -- -f checkstyle -o eslint.xml || true
|
||||
|
||||
# delete the old tarball, if it exists
|
||||
rm -f matrix-react-sdk-*.tgz
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"license": "Apache-2.0",
|
||||
"main": "lib/index.js",
|
||||
"files": [
|
||||
".eslintrc.js",
|
||||
"CHANGELOG.md",
|
||||
"CONTRIBUTING.rst",
|
||||
"LICENSE",
|
||||
|
@ -46,10 +47,12 @@
|
|||
"browser-encrypt-attachment": "^0.3.0",
|
||||
"browser-request": "^0.3.3",
|
||||
"classnames": "^2.1.2",
|
||||
"commonmark": "^0.27.0",
|
||||
"draft-js": "^0.8.1",
|
||||
"draft-js-export-html": "^0.5.0",
|
||||
"draft-js-export-markdown": "^0.2.0",
|
||||
"emojione": "2.2.3",
|
||||
"file-saver": "^1.3.3",
|
||||
"filesize": "^3.1.2",
|
||||
"flux": "^2.0.3",
|
||||
"fuse.js": "^2.2.0",
|
||||
|
@ -58,7 +61,6 @@
|
|||
"isomorphic-fetch": "^2.2.1",
|
||||
"linkifyjs": "^2.1.3",
|
||||
"lodash": "^4.13.1",
|
||||
"commonmark": "^0.27.0",
|
||||
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
|
||||
"optimist": "^0.6.1",
|
||||
"q": "^1.4.1",
|
||||
|
|
|
@ -19,9 +19,12 @@ import MultiInviter from './utils/MultiInviter';
|
|||
|
||||
const emailRegex = /^\S+@\S+\.\S+$/;
|
||||
|
||||
// We allow localhost for mxids to avoid confusion
|
||||
const mxidRegex = /^@\S+:(?:\S+\.\S+|localhost)$/
|
||||
|
||||
export function getAddressType(inputText) {
|
||||
const isEmailAddress = /^\S+@\S+\.\S+$/.test(inputText);
|
||||
const isMatrixId = inputText[0] === '@' && inputText.indexOf(":") > 0;
|
||||
const isEmailAddress = emailRegex.test(inputText);
|
||||
const isMatrixId = mxidRegex.test(inputText);
|
||||
|
||||
// sanity check the input for user IDs
|
||||
if (isEmailAddress) {
|
||||
|
|
|
@ -20,6 +20,7 @@ module.exports = {
|
|||
TAB: 9,
|
||||
ENTER: 13,
|
||||
SHIFT: 16,
|
||||
ESCAPE: 27,
|
||||
PAGE_UP: 33,
|
||||
PAGE_DOWN: 34,
|
||||
END: 35,
|
||||
|
|
|
@ -67,6 +67,8 @@ const AsyncWrapper = React.createClass({
|
|||
},
|
||||
});
|
||||
|
||||
let _counter = 0;
|
||||
|
||||
module.exports = {
|
||||
DialogContainerId: "mx_Dialog_Container",
|
||||
|
||||
|
@ -113,12 +115,16 @@ module.exports = {
|
|||
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
|
||||
};
|
||||
|
||||
// don't attempt to reuse the same AsyncWrapper for different dialogs,
|
||||
// otherwise we'll get confused.
|
||||
const modalCount = _counter++;
|
||||
|
||||
// FIXME: If a dialog uses getDefaultProps it clobbers the onFinished
|
||||
// property set here so you can't close the dialog from a button click!
|
||||
var dialog = (
|
||||
<div className={"mx_Dialog_wrapper " + className}>
|
||||
<div className="mx_Dialog">
|
||||
<AsyncWrapper loader={loader} {...props} onFinished={closeDialog}/>
|
||||
<AsyncWrapper key={modalCount} loader={loader} {...props} onFinished={closeDialog}/>
|
||||
</div>
|
||||
<div className="mx_Dialog_background" onClick={ closeDialog.bind(this, false) }></div>
|
||||
</div>
|
||||
|
|
|
@ -32,17 +32,24 @@ module.exports = {
|
|||
return whoIsTyping;
|
||||
},
|
||||
|
||||
whoIsTypingString: function(room) {
|
||||
var whoIsTyping = this.usersTypingApartFromMe(room);
|
||||
whoIsTypingString: function(room, limit) {
|
||||
const whoIsTyping = this.usersTypingApartFromMe(room);
|
||||
const othersCount = limit === undefined ?
|
||||
0 : Math.max(whoIsTyping.length - limit, 0);
|
||||
if (whoIsTyping.length == 0) {
|
||||
return null;
|
||||
return '';
|
||||
} else if (whoIsTyping.length == 1) {
|
||||
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 {
|
||||
var names = whoIsTyping.map(function(m) {
|
||||
return m.name;
|
||||
});
|
||||
var lastPerson = names.shift();
|
||||
const lastPerson = names.pop();
|
||||
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.
|
||||
*/
|
||||
|
||||
import FileSaver from 'file-saver';
|
||||
import React from 'react';
|
||||
|
||||
import * as Matrix from 'matrix-js-sdk';
|
||||
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
|
||||
import sdk from '../../../index';
|
||||
|
||||
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
|
||||
const PHASE_EDIT = 1;
|
||||
const PHASE_EXPORTING = 2;
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'ExportE2eKeysDialog',
|
||||
|
||||
propTypes: {
|
||||
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
collectedPassword: false,
|
||||
phase: PHASE_EDIT,
|
||||
errStr: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
},
|
||||
|
||||
_onPassphraseFormSubmit: function(ev) {
|
||||
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;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let content;
|
||||
if (!this.state.collectedPassword) {
|
||||
content = (
|
||||
<div className="mx_Dialog_content">
|
||||
<p>
|
||||
This process will allow you to export the keys for messages
|
||||
you have received in encrypted rooms to a local file. You
|
||||
will then be able to import the file into another Matrix
|
||||
client in the future, so that client will also be able to
|
||||
decrypt these messages.
|
||||
</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>
|
||||
_startExport: function(passphrase) {
|
||||
// extra Promise.resolve() to turn synchronous exceptions into
|
||||
// asynchronous ones.
|
||||
Promise.resolve().then(() => {
|
||||
return this.props.matrixClient.exportRoomKeys();
|
||||
}).then((k) => {
|
||||
return MegolmExportEncryption.encryptMegolmKeyFile(
|
||||
JSON.stringify(k), passphrase
|
||||
);
|
||||
}
|
||||
}).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 (
|
||||
<div className="mx_exportE2eKeysDialog">
|
||||
<div className="mx_Dialog_title">
|
||||
Export room keys
|
||||
</div>
|
||||
{content}
|
||||
</div>
|
||||
<BaseDialog className='mx_exportE2eKeysDialog'
|
||||
onFinished={this.props.onFinished}
|
||||
title="Export room keys"
|
||||
>
|
||||
<form onSubmit={this._onPassphraseFormSubmit}>
|
||||
<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);
|
||||
import views$create_room$RoomAlias from './components/views/create_room/RoomAlias';
|
||||
views$create_room$RoomAlias && (module.exports.components['views.create_room.RoomAlias'] = views$create_room$RoomAlias);
|
||||
import views$dialogs$BaseDialog from './components/views/dialogs/BaseDialog';
|
||||
views$dialogs$BaseDialog && (module.exports.components['views.dialogs.BaseDialog'] = views$dialogs$BaseDialog);
|
||||
import views$dialogs$ChatInviteDialog from './components/views/dialogs/ChatInviteDialog';
|
||||
views$dialogs$ChatInviteDialog && (module.exports.components['views.dialogs.ChatInviteDialog'] = views$dialogs$ChatInviteDialog);
|
||||
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);
|
||||
import views$dialogs$InteractiveAuthDialog from './components/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';
|
||||
views$dialogs$NeedToRegisterDialog && (module.exports.components['views.dialogs.NeedToRegisterDialog'] = views$dialogs$NeedToRegisterDialog);
|
||||
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);
|
||||
import views$dialogs$UnknownDeviceDialog from './components/views/dialogs/UnknownDeviceDialog';
|
||||
views$dialogs$UnknownDeviceDialog && (module.exports.components['views.dialogs.UnknownDeviceDialog'] = views$dialogs$UnknownDeviceDialog);
|
||||
import views$elements$AccessibleButton from './components/views/elements/AccessibleButton';
|
||||
views$elements$AccessibleButton && (module.exports.components['views.elements.AccessibleButton'] = views$elements$AccessibleButton);
|
||||
import views$elements$AddressSelector from './components/views/elements/AddressSelector';
|
||||
views$elements$AddressSelector && (module.exports.components['views.elements.AddressSelector'] = views$elements$AddressSelector);
|
||||
import views$elements$AddressTile from './components/views/elements/AddressTile';
|
||||
|
|
|
@ -21,6 +21,7 @@ import KeyCode from '../../KeyCode';
|
|||
import Notifier from '../../Notifier';
|
||||
import PageTypes from '../../PageTypes';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
console.log("onAction: "+payload.action);
|
||||
|
||||
var roomIndexDelta = 1;
|
||||
|
||||
var self = this;
|
||||
|
@ -1008,8 +1006,8 @@ module.exports = React.createClass({
|
|||
var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
|
||||
var LoggedInView = sdk.getComponent('structures.LoggedInView');
|
||||
|
||||
console.log("rendering; loading="+this.state.loading+"; screen="+this.state.screen +
|
||||
"; logged_in="+this.state.logged_in+"; ready="+this.state.ready);
|
||||
// console.log("rendering; loading="+this.state.loading+"; screen="+this.state.screen +
|
||||
// "; logged_in="+this.state.logged_in+"; ready="+this.state.ready);
|
||||
|
||||
if (this.state.loading) {
|
||||
var Spinner = sdk.getComponent('elements.Spinner');
|
||||
|
|
|
@ -281,7 +281,6 @@ module.exports = React.createClass({
|
|||
|
||||
var isMembershipChange = (e) =>
|
||||
e.getType() === 'm.room.member'
|
||||
&& ['join', 'leave'].indexOf(e.getContent().membership) !== -1
|
||||
&& (!e.getPrevContent() || e.getContent().membership !== e.getPrevContent().membership);
|
||||
|
||||
for (i = 0; i < this.props.events.length; i++) {
|
||||
|
|
|
@ -21,7 +21,10 @@ var WhoIsTyping = require("../../WhoIsTyping");
|
|||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
const MemberAvatar = require("../views/avatars/MemberAvatar");
|
||||
|
||||
const TYPING_AVATARS_LIMIT = 2;
|
||||
const HIDE_DEBOUNCE_MS = 10000;
|
||||
const STATUS_BAR_HIDDEN = 0;
|
||||
const STATUS_BAR_EXPANDED = 1;
|
||||
const STATUS_BAR_EXPANDED_LARGE = 2;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomStatusBar',
|
||||
|
@ -48,6 +51,10 @@ module.exports = React.createClass({
|
|||
// more interesting)
|
||||
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
|
||||
// 'unsent messages' bar
|
||||
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
|
||||
// component.
|
||||
onResize: React.PropTypes.func,
|
||||
|
||||
// callback for when the status bar can be hidden from view, as it is
|
||||
// not displaying anything
|
||||
onHidden: React.PropTypes.func,
|
||||
// callback for when the status bar is displaying something and should
|
||||
// be visible
|
||||
onVisible: React.PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
whoIsTypingLimit: 2,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
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)) {
|
||||
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() {
|
||||
|
@ -103,39 +138,35 @@ module.exports = React.createClass({
|
|||
|
||||
onRoomMemberTyping: function(ev, member) {
|
||||
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
|
||||
_checkForResize: function(prevProps, prevState) {
|
||||
// figure out the old height and the new height of the status bar. We
|
||||
// don't need the actual height - just whether it is likely to have
|
||||
// changed - so we use '0' to indicate normal size, and other values to
|
||||
// indicate other sizes.
|
||||
var oldSize, newSize;
|
||||
|
||||
if (prevState.syncState === "ERROR") {
|
||||
oldSize = 1;
|
||||
} else if (prevProps.tabCompleteEntries) {
|
||||
oldSize = 0;
|
||||
} else if (prevProps.hasUnsentMessages) {
|
||||
oldSize = 2;
|
||||
} else {
|
||||
oldSize = 0;
|
||||
}
|
||||
|
||||
if (this.state.syncState === "ERROR") {
|
||||
newSize = 1;
|
||||
} else if (this.props.tabCompleteEntries) {
|
||||
newSize = 0;
|
||||
} else if (this.props.hasUnsentMessages) {
|
||||
newSize = 2;
|
||||
} else {
|
||||
newSize = 0;
|
||||
}
|
||||
|
||||
return newSize != oldSize;
|
||||
// figure out the old height and the new height of the status bar.
|
||||
return this._getSize(prevProps, prevState) !== this._getSize(this.props, this.state);
|
||||
},
|
||||
|
||||
// return suitable content for the image on the left of the status bar.
|
||||
|
@ -177,7 +208,7 @@ module.exports = React.createClass({
|
|||
if (wantPlaceholder) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_typingIndicatorAvatars">
|
||||
{this._renderTypingIndicatorAvatars(TYPING_AVATARS_LIMIT)}
|
||||
{this._renderTypingIndicatorAvatars(this.props.whoIsTypingLimit)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -186,7 +217,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_renderTypingIndicatorAvatars: function(limit) {
|
||||
let users = WhoIsTyping.usersTyping(this.props.room);
|
||||
let users = WhoIsTyping.usersTypingApartFromMe(this.props.room);
|
||||
|
||||
let othersCount = Math.max(users.length - limit, 0);
|
||||
users = users.slice(0, limit);
|
||||
|
|
|
@ -146,6 +146,8 @@ module.exports = React.createClass({
|
|||
showTopUnreadMessagesBar: false,
|
||||
|
||||
auxPanelMaxHeight: undefined,
|
||||
|
||||
statusBarVisible: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -720,15 +722,11 @@ module.exports = React.createClass({
|
|||
if (!result.displayname) {
|
||||
var SetDisplayNameDialog = sdk.getComponent('views.dialogs.SetDisplayNameDialog');
|
||||
var dialog_defer = q.defer();
|
||||
var dialog_ref;
|
||||
Modal.createDialog(SetDisplayNameDialog, {
|
||||
currentDisplayName: result.displayname,
|
||||
ref: (r) => {
|
||||
dialog_ref = r;
|
||||
},
|
||||
onFinished: (submitted) => {
|
||||
onFinished: (submitted, newDisplayName) => {
|
||||
if (submitted) {
|
||||
cli.setDisplayName(dialog_ref.getValue()).done(() => {
|
||||
cli.setDisplayName(newDisplayName).done(() => {
|
||||
dialog_defer.resolve();
|
||||
});
|
||||
}
|
||||
|
@ -1333,6 +1331,18 @@ module.exports = React.createClass({
|
|||
// no longer anything to do here
|
||||
},
|
||||
|
||||
onStatusBarVisible: function() {
|
||||
this.setState({
|
||||
statusBarVisible: true,
|
||||
});
|
||||
},
|
||||
|
||||
onStatusBarHidden: function() {
|
||||
this.setState({
|
||||
statusBarVisible: false,
|
||||
});
|
||||
},
|
||||
|
||||
showSettings: function(show) {
|
||||
// XXX: this is a bit naughty; we should be doing this via props
|
||||
if (show) {
|
||||
|
@ -1515,7 +1525,10 @@ module.exports = React.createClass({
|
|||
onCancelAllClick={this.onCancelAllClick}
|
||||
onScrollToBottomClick={this.jumpToLiveTimeline}
|
||||
onResize={this.onChildResize}
|
||||
/>;
|
||||
onVisible={this.onStatusBarVisible}
|
||||
onHidden={this.onStatusBarHidden}
|
||||
whoIsTypingLimit={2}
|
||||
/>;
|
||||
}
|
||||
|
||||
var aux = null;
|
||||
|
@ -1669,6 +1682,10 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
let statusBarAreaClass = "mx_RoomView_statusArea mx_fadable";
|
||||
if (this.state.statusBarVisible) {
|
||||
statusBarAreaClass += " mx_RoomView_statusArea_expanded";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") } ref="roomView">
|
||||
|
@ -1691,7 +1708,7 @@ module.exports = React.createClass({
|
|||
{ topUnreadMessagesBar }
|
||||
{ messagePanel }
|
||||
{ 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_line"></div>
|
||||
{ statusBar }
|
||||
|
|
|
@ -26,6 +26,7 @@ var UserSettingsStore = require('../../UserSettingsStore');
|
|||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
var Email = require('../../email');
|
||||
var AddThreepid = require('../../AddThreepid');
|
||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||
|
||||
// if this looks like a release, use the 'version' from package.json; else use
|
||||
// the git sha.
|
||||
|
@ -228,8 +229,26 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onLogoutClicked: function(ev) {
|
||||
var LogoutPrompt = sdk.getComponent('dialogs.LogoutPrompt');
|
||||
this.logoutModal = Modal.createDialog(LogoutPrompt);
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
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) {
|
||||
|
@ -392,6 +411,30 @@ module.exports = React.createClass({
|
|||
}).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() {
|
||||
var client = MatrixClientPeg.get();
|
||||
|
||||
|
@ -462,6 +505,23 @@ module.exports = React.createClass({
|
|||
const deviceId = client.deviceId;
|
||||
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 (
|
||||
<div>
|
||||
<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 key:</label> <span><code><b>{identityKey}</b></code></span></li>
|
||||
</ul>
|
||||
{exportButton}
|
||||
{importButton}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -531,9 +593,9 @@ module.exports = React.createClass({
|
|||
return <div>
|
||||
<h3>Deactivate Account</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
<button className="mx_UserSettings_button danger"
|
||||
<AccessibleButton className="mx_UserSettings_button danger"
|
||||
onClick={this._onDeactivateAccountClicked}>Deactivate my account
|
||||
</button>
|
||||
</AccessibleButton>
|
||||
</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
|
||||
// don't inadvertently get rejected as well.
|
||||
reject = (
|
||||
<button className="mx_UserSettings_button danger"
|
||||
<AccessibleButton className="mx_UserSettings_button danger"
|
||||
onClick={this._onRejectAllInvitesClicked.bind(this, invitedRooms)}>
|
||||
Reject all {invitedRooms.length} invites
|
||||
</button>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -724,9 +786,9 @@ module.exports = React.createClass({
|
|||
|
||||
<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
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
|
||||
{accountJsx}
|
||||
</div>
|
||||
|
|
|
@ -87,10 +87,26 @@ module.exports = React.createClass({
|
|||
this.showErrorDialog("New passwords must match each other.");
|
||||
}
|
||||
else {
|
||||
this.submitPasswordReset(
|
||||
this.state.enteredHomeserverUrl, this.state.enteredIdentityServerUrl,
|
||||
this.state.email, this.state.password
|
||||
);
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
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 AvatarLogic = require("../../../Avatar");
|
||||
import sdk from '../../../index';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'BaseAvatar',
|
||||
|
@ -138,7 +139,7 @@ module.exports = React.createClass({
|
|||
|
||||
const {
|
||||
name, idName, title, url, urls, width, height, resizeMethod,
|
||||
defaultToInitialLetter,
|
||||
defaultToInitialLetter, onClick,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
|
@ -156,12 +157,24 @@ module.exports = React.createClass({
|
|||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<img className="mx_BaseAvatar mx_BaseAvatar_image" src={imageUrl}
|
||||
onError={this.onError}
|
||||
width={width} height={height}
|
||||
title={title} alt=""
|
||||
{...otherProps} />
|
||||
);
|
||||
if (onClick != null) {
|
||||
return (
|
||||
<AccessibleButton className="mx_BaseAvatar" onClick={onClick}>
|
||||
<img className="mx_BaseAvatar_image" src={imageUrl}
|
||||
onError={this.onError}
|
||||
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 dis = require("../../../dispatcher");
|
||||
var Modal = require('../../../Modal');
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
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({
|
||||
displayName: "ChatInviteDialog",
|
||||
propTypes: {
|
||||
|
@ -57,7 +67,14 @@ module.exports = React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
error: false,
|
||||
|
||||
// List of AddressTile.InviteAddressType objects represeting
|
||||
// the list of addresses we're going to invite
|
||||
inviteList: [],
|
||||
|
||||
// List of AddressTile.InviteAddressType objects represeting
|
||||
// the set of autocompletion results for the current search
|
||||
// query.
|
||||
queryList: [],
|
||||
};
|
||||
},
|
||||
|
@ -146,14 +163,38 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onQueryChanged: function(ev) {
|
||||
var query = ev.target.value;
|
||||
var queryList = [];
|
||||
const query = ev.target.value;
|
||||
let queryList = [];
|
||||
|
||||
// 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) => {
|
||||
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({
|
||||
|
@ -183,7 +224,7 @@ module.exports = React.createClass({
|
|||
|
||||
onSelected: function(index) {
|
||||
var inviteList = this.state.inviteList.slice();
|
||||
inviteList.push(this.state.queryList[index].userId);
|
||||
inviteList.push(this.state.queryList[index]);
|
||||
this.setState({
|
||||
inviteList: inviteList,
|
||||
queryList: [],
|
||||
|
@ -218,10 +259,14 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
const addrTexts = addrs.map((addr) => {
|
||||
return addr.address;
|
||||
});
|
||||
|
||||
if (this.props.roomId) {
|
||||
// Invite new user to a room
|
||||
var self = this;
|
||||
Invite.inviteMultipleToRoom(this.props.roomId, addrs)
|
||||
Invite.inviteMultipleToRoom(this.props.roomId, addrTexts)
|
||||
.then(function(addrs) {
|
||||
var room = MatrixClientPeg.get().getRoom(self.props.roomId);
|
||||
return self._showAnyInviteErrors(addrs, room);
|
||||
|
@ -236,9 +281,9 @@ module.exports = React.createClass({
|
|||
return null;
|
||||
})
|
||||
.done();
|
||||
} else if (this._isDmChat(addrs)) {
|
||||
} else if (this._isDmChat(addrTexts)) {
|
||||
// Start the DM chat
|
||||
createRoom({dmUserId: addrs[0]})
|
||||
createRoom({dmUserId: addrTexts[0]})
|
||||
.catch(function(err) {
|
||||
console.error(err.stack);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
@ -255,7 +300,7 @@ module.exports = React.createClass({
|
|||
var room;
|
||||
createRoom().then(function(roomId) {
|
||||
room = MatrixClientPeg.get().getRoom(roomId);
|
||||
return Invite.inviteMultipleToRoom(roomId, addrs);
|
||||
return Invite.inviteMultipleToRoom(roomId, addrTexts);
|
||||
})
|
||||
.then(function(addrs) {
|
||||
return self._showAnyInviteErrors(addrs, room);
|
||||
|
@ -273,7 +318,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
// 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() {
|
||||
|
@ -307,19 +352,27 @@ module.exports = React.createClass({
|
|||
return true;
|
||||
}
|
||||
|
||||
// split spaces in name and try matching constituent parts
|
||||
var parts = name.split(" ");
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
if (parts[i].indexOf(query) === 0) {
|
||||
return true;
|
||||
}
|
||||
// Try to find the query following a "word boundary", except that
|
||||
// this does avoids using \b because it only considers letters from
|
||||
// the roman alphabet to be word characters.
|
||||
// Instead, we look for the query following either:
|
||||
// * The start of the string
|
||||
// * Whitespace, or
|
||||
// * A fixed number of punctuation characters
|
||||
const expr = new RegExp("(?:^|[\\s\\(\)'\",\.-_@\?;:{}\\[\\]\\#~`\\*\\&\\$])" + escapeRegExp(query));
|
||||
if (expr.test(name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
_isOnInviteList: function(uid) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -354,24 +407,37 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_addInputToList: function() {
|
||||
const addrType = Invite.getAddressType(this.refs.textinput.value);
|
||||
if (addrType !== null) {
|
||||
const inviteList = this.state.inviteList.slice();
|
||||
inviteList.push(this.refs.textinput.value.trim());
|
||||
this.setState({
|
||||
inviteList: inviteList,
|
||||
queryList: [],
|
||||
});
|
||||
return inviteList;
|
||||
} else {
|
||||
const addressText = this.refs.textinput.value.trim();
|
||||
const addrType = Invite.getAddressType(addressText);
|
||||
const addrObj = {
|
||||
addressType: addrType,
|
||||
address: addressText,
|
||||
isKnown: false,
|
||||
};
|
||||
if (addrType == null) {
|
||||
this.setState({ error: true });
|
||||
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() {
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
var AddressSelector = sdk.getComponent("elements.AddressSelector");
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
||||
this.scrollElement = null;
|
||||
|
||||
var query = [];
|
||||
|
@ -404,11 +470,16 @@ module.exports = React.createClass({
|
|||
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>;
|
||||
} else {
|
||||
const addressSelectorHeader = <div className="mx_ChatInviteDialog_addressSelectHeader">
|
||||
Searching known users
|
||||
</div>;
|
||||
addressSelector = (
|
||||
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
|
||||
addressList={ this.state.queryList }
|
||||
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">
|
||||
{this.props.title}
|
||||
</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" />
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
<div className="mx_ChatInviteDialog_label">
|
||||
<label htmlFor="textinput">{ this.props.description }</label>
|
||||
</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',
|
||||
propTypes: {
|
||||
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() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<div className="mx_ErrorDialog" onKeyDown={ this.onKeyDown }>
|
||||
<div className="mx_Dialog_title">
|
||||
{this.props.title}
|
||||
</div>
|
||||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||
title={this.props.title}>
|
||||
<div className="mx_Dialog_content">
|
||||
{this.props.description}
|
||||
</div>
|
||||
|
@ -71,7 +63,7 @@ module.exports = React.createClass({
|
|||
{this.props.button}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -111,20 +111,9 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onKeyDown: function(e) {
|
||||
if (e.keyCode === 27) { // escape
|
||||
e.stopPropagation();
|
||||
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();
|
||||
}
|
||||
_onEnterPressed: function(e) {
|
||||
if (this.state.submitButtonEnabled && !this.state.busy) {
|
||||
this._onSubmit();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -171,6 +160,7 @@ export default React.createClass({
|
|||
|
||||
render: function() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
let error = null;
|
||||
if (this.state.errorText) {
|
||||
|
@ -200,10 +190,11 @@ export default React.createClass({
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="mx_InteractiveAuthDialog" onKeyDown={this._onKeyDown}>
|
||||
<div className="mx_Dialog_title">
|
||||
{this.props.title}
|
||||
</div>
|
||||
<BaseDialog className="mx_InteractiveAuthDialog"
|
||||
onEnterPressed={this._onEnterPressed}
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>This operation requires additional authentication.</p>
|
||||
{this._renderCurrentStage()}
|
||||
|
@ -213,7 +204,7 @@ export default React.createClass({
|
|||
{submitButton}
|
||||
{cancelButton}
|
||||
</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");
|
||||
var dis = require("../../../dispatcher");
|
||||
import React from 'react';
|
||||
import dis from '../../../dispatcher';
|
||||
import sdk from '../../../index';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'NeedToRegisterDialog',
|
||||
|
@ -54,11 +55,12 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<div className="mx_NeedToRegisterDialog">
|
||||
<div className="mx_Dialog_title">
|
||||
{this.props.title}
|
||||
</div>
|
||||
<BaseDialog className="mx_NeedToRegisterDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
{this.props.description}
|
||||
</div>
|
||||
|
@ -70,7 +72,7 @@ module.exports = React.createClass({
|
|||
Register
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
|
|||
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',
|
||||
propTypes: {
|
||||
title: React.PropTypes.string,
|
||||
|
@ -46,25 +47,13 @@ module.exports = React.createClass({
|
|||
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() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<div className="mx_QuestionDialog" onKeyDown={ this.onKeyDown }>
|
||||
<div className="mx_Dialog_title">
|
||||
{this.props.title}
|
||||
</div>
|
||||
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={ this.onOk }
|
||||
title={this.props.title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
{this.props.description}
|
||||
</div>
|
||||
|
@ -77,7 +66,7 @@ module.exports = React.createClass({
|
|||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,11 +14,16 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require("react");
|
||||
var sdk = require("../../../index.js");
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
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',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
|
@ -42,10 +47,6 @@ module.exports = React.createClass({
|
|||
this.refs.input_value.select();
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
return this.state.value;
|
||||
},
|
||||
|
||||
onValueChange: function(ev) {
|
||||
this.setState({
|
||||
value: ev.target.value
|
||||
|
@ -54,16 +55,17 @@ module.exports = React.createClass({
|
|||
|
||||
onFormSubmit: function(ev) {
|
||||
ev.preventDefault();
|
||||
this.props.onFinished(true);
|
||||
this.props.onFinished(true, this.state.value);
|
||||
return false;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<div className="mx_SetDisplayNameDialog">
|
||||
<div className="mx_Dialog_title">
|
||||
Set a Display Name
|
||||
</div>
|
||||
<BaseDialog className="mx_SetDisplayNameDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title="Set a Display Name"
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
Your display name is how you'll appear to others when you speak in rooms.<br/>
|
||||
What would you like it to be?
|
||||
|
@ -79,7 +81,7 @@ module.exports = React.createClass({
|
|||
<input className="mx_Dialog_primary" type="submit" value="Set" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
|
|||
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',
|
||||
propTypes: {
|
||||
title: React.PropTypes.string,
|
||||
|
@ -27,7 +28,7 @@ module.exports = React.createClass({
|
|||
value: React.PropTypes.string,
|
||||
button: React.PropTypes.string,
|
||||
focus: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -36,7 +37,7 @@ module.exports = React.createClass({
|
|||
value: "",
|
||||
description: "",
|
||||
button: "OK",
|
||||
focus: true
|
||||
focus: true,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -55,25 +56,13 @@ module.exports = React.createClass({
|
|||
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() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<div className="mx_TextInputDialog">
|
||||
<div className="mx_Dialog_title">
|
||||
{this.props.title}
|
||||
</div>
|
||||
<BaseDialog className="mx_TextInputDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={this.onOk}
|
||||
title={this.props.title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_TextInputDialog_label">
|
||||
<label htmlFor="textinput"> {this.props.description} </label>
|
||||
|
@ -90,7 +79,7 @@ module.exports = React.createClass({
|
|||
{this.props.button}
|
||||
</button>
|
||||
</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';
|
||||
|
||||
var React = require("react");
|
||||
var sdk = require("../../../index");
|
||||
var classNames = require('classnames');
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import classNames from 'classnames';
|
||||
import { InviteAddressType } from './AddressTile';
|
||||
|
||||
module.exports = React.createClass({
|
||||
export default React.createClass({
|
||||
displayName: 'AddressSelector',
|
||||
|
||||
propTypes: {
|
||||
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,
|
||||
selected: React.PropTypes.number,
|
||||
|
||||
// Element to put as a header on top of the list
|
||||
header: React.PropTypes.node,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -119,7 +125,7 @@ module.exports = React.createClass({
|
|||
// method, how far to scroll when using the arrow keys
|
||||
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; }} >
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
@ -141,6 +147,7 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<div className={classes} ref={(ref) => {this.scrollElement = ref;}}>
|
||||
{ this.props.header }
|
||||
{ this.createAddressListTiles() }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -23,16 +23,33 @@ var Invite = require("../../../Invite");
|
|||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
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',
|
||||
|
||||
propTypes: {
|
||||
address: React.PropTypes.string.isRequired,
|
||||
address: InviteAddressType.isRequired,
|
||||
canDismiss: React.PropTypes.bool,
|
||||
onDismissed: React.PropTypes.func,
|
||||
justified: React.PropTypes.bool,
|
||||
networkName: React.PropTypes.string,
|
||||
networkUrl: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -40,37 +57,30 @@ module.exports = React.createClass({
|
|||
canDismiss: false,
|
||||
onDismissed: function() {}, // NOP
|
||||
justified: false,
|
||||
networkName: "",
|
||||
networkUrl: "",
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var userId, name, imgUrl, email;
|
||||
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const address = this.props.address;
|
||||
const name = address.displayName || address.address;
|
||||
|
||||
// Check if the addr is a valid type
|
||||
var addrType = Invite.getAddressType(this.props.address);
|
||||
if (addrType === "mx") {
|
||||
let user = MatrixClientPeg.get().getUser(this.props.address);
|
||||
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";
|
||||
let imgUrl;
|
||||
if (address.avatarMxc) {
|
||||
imgUrl = MatrixClientPeg.get().mxcUrlToHttp(
|
||||
address.avatarMxc, 25, 25, 'crop'
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
if (this.props.networkUrl !== "") {
|
||||
network = (
|
||||
|
@ -79,16 +89,20 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
var info;
|
||||
var error = false;
|
||||
if (addrType === "mx" && userId) {
|
||||
var nameClasses = classNames({
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
let info;
|
||||
let error = false;
|
||||
if (address.addressType === "mx" && address.isKnown) {
|
||||
const nameClasses = classNames({
|
||||
"mx_AddressTile_name": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
});
|
||||
|
||||
var idClasses = classNames({
|
||||
const idClasses = classNames({
|
||||
"mx_AddressTile_id": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
});
|
||||
|
@ -96,26 +110,26 @@ module.exports = React.createClass({
|
|||
info = (
|
||||
<div className="mx_AddressTile_mx">
|
||||
<div className={nameClasses}>{ name }</div>
|
||||
<div className={idClasses}>{ userId }</div>
|
||||
<div className={idClasses}>{ address.address }</div>
|
||||
</div>
|
||||
);
|
||||
} else if (addrType === "mx") {
|
||||
var unknownMxClasses = classNames({
|
||||
} else if (address.addressType === "mx") {
|
||||
const unknownMxClasses = classNames({
|
||||
"mx_AddressTile_unknownMx": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
});
|
||||
|
||||
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({
|
||||
"mx_AddressTile_email": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
});
|
||||
|
||||
info = (
|
||||
<div className={emailClasses}>{ email }</div>
|
||||
<div className={emailClasses}>{ address.address }</div>
|
||||
);
|
||||
} else {
|
||||
error = true;
|
||||
|
@ -129,12 +143,12 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
var classes = classNames({
|
||||
const classes = classNames({
|
||||
"mx_AddressTile": true,
|
||||
"mx_AddressTile_error": error,
|
||||
});
|
||||
|
||||
var dismiss;
|
||||
let dismiss;
|
||||
if (this.props.canDismiss) {
|
||||
dismiss = (
|
||||
<div className="mx_AddressTile_dismiss" onClick={this.props.onDismissed} >
|
||||
|
@ -145,7 +159,6 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<div className={classes}>
|
||||
{ network }
|
||||
<div className="mx_AddressTile_avatar">
|
||||
<BaseAvatar width={25} height={25} name={name} title={name} url={imgUrl} />
|
||||
</div>
|
||||
|
|
|
@ -24,7 +24,7 @@ module.exports = React.createClass({
|
|||
events: React.PropTypes.array.isRequired,
|
||||
// An array of EventTiles to render when expanded
|
||||
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,
|
||||
// The maximum number of avatars to display in the summary
|
||||
avatarsMaxLength: React.PropTypes.number,
|
||||
|
@ -40,110 +40,12 @@ module.exports = React.createClass({
|
|||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
summaryLength: 3,
|
||||
summaryLength: 1,
|
||||
threshold: 3,
|
||||
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) {
|
||||
// Update if
|
||||
// - 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() {
|
||||
let eventsToRender = this.props.events;
|
||||
let fewEvents = eventsToRender.length < this.props.threshold;
|
||||
let expanded = this.state.expanded || fewEvents;
|
||||
const eventsToRender = this.props.events;
|
||||
const fewEvents = eventsToRender.length < this.props.threshold;
|
||||
const expanded = this.state.expanded || fewEvents;
|
||||
|
||||
let expandedEvents = null;
|
||||
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
|
||||
let userEvents = {
|
||||
// $userId : {first : e0, last : e1}
|
||||
// Map user IDs to an array of objects:
|
||||
const userEvents = {
|
||||
// $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();
|
||||
// Initialise a user's events
|
||||
if (!userEvents[userId]) {
|
||||
userEvents[userId] = {first: null, last: null};
|
||||
userEvents[userId] = [];
|
||||
avatarMembers.push(e.target);
|
||||
}
|
||||
if (!userEvents[userId].first) {
|
||||
userEvents[userId].first = e;
|
||||
}
|
||||
userEvents[userId].last = e;
|
||||
userEvents[userId].push({
|
||||
mxEvent: e,
|
||||
displayName: e.target.name || userId,
|
||||
index: index,
|
||||
});
|
||||
});
|
||||
|
||||
// Populate the join/leave event arrays with events that represent what happened
|
||||
// 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;
|
||||
const aggregate = this._getAggregate(userEvents);
|
||||
|
||||
// Membership BEFORE eventsToRender
|
||||
let previousMembership = firstEvent.getPrevContent().membership || "leave";
|
||||
|
||||
// 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++;
|
||||
}
|
||||
}
|
||||
// Sort types by order of lowest event index within sequence
|
||||
const orderedTransitionSequences = Object.keys(aggregate.names).sort(
|
||||
(seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2]
|
||||
);
|
||||
|
||||
let avatars = this._renderAvatars(joinEvents.concat(leaveEvents));
|
||||
let summary = this._renderSummary(joinEvents, leaveEvents);
|
||||
let toggleButton = (
|
||||
const avatars = this._renderAvatars(avatarMembers);
|
||||
const summary = this._renderSummary(aggregate.names, orderedTransitionSequences);
|
||||
const toggleButton = (
|
||||
<a className="mx_MemberEventListSummary_toggle" onClick={this._toggleSummary}>
|
||||
{expanded ? 'collapse' : 'expand'}
|
||||
</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_info">
|
||||
<span className="mx_MemberEventListSummary_avatars">
|
||||
{avatars}
|
||||
</span>
|
||||
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
|
||||
{summary}{joinedAndLeft ? joinedAndLeft + ' ' + noun + ' joined and left' : ''}
|
||||
{summary}
|
||||
</span>
|
||||
{toggleButton}
|
||||
</div>
|
||||
|
|
|
@ -69,6 +69,7 @@ var TintableSvg = React.createClass({
|
|||
width={ this.props.width }
|
||||
height={ this.props.height }
|
||||
onLoad={ this.onLoad }
|
||||
tabIndex="-1"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ var React = require('react');
|
|||
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var sdk = require('../../../index');
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
|
||||
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} />;
|
||||
|
||||
return (
|
||||
<div className={mainClassName} title={ this.props.title }
|
||||
<AccessibleButton className={mainClassName} title={ this.props.title }
|
||||
onClick={ this.props.onClick } onMouseEnter={ this.mouseEnter }
|
||||
onMouseLeave={ this.mouseLeave }>
|
||||
<div className="mx_EntityTile_avatar">
|
||||
|
@ -161,7 +162,7 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
{ nameEl }
|
||||
{ inviteButton }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -35,6 +35,7 @@ var DMRoomMap = require('../../../utils/DMRoomMap');
|
|||
var Unread = require('../../../Unread');
|
||||
var Receipt = require('../../../utils/Receipt');
|
||||
var WithMatrixClient = require('../../../wrappers/WithMatrixClient');
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
module.exports = WithMatrixClient(React.createClass({
|
||||
displayName: 'MemberInfo',
|
||||
|
@ -612,7 +613,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
mx_MemberInfo_createRoom_label: true,
|
||||
mx_RoomTile_name: true,
|
||||
});
|
||||
const startNewChat = <div
|
||||
const startNewChat = <AccessibleButton
|
||||
className="mx_MemberInfo_createRoom"
|
||||
onClick={this.onNewDMClick}
|
||||
>
|
||||
|
@ -620,7 +621,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
<img src="img/create-big.svg" width="26" height="26" />
|
||||
</div>
|
||||
<div className={labelClasses}><i>Start new chat</i></div>
|
||||
</div>;
|
||||
</AccessibleButton>;
|
||||
|
||||
startChat = <div>
|
||||
<h3>Direct chats</h3>
|
||||
|
@ -635,26 +636,37 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
}
|
||||
|
||||
if (this.state.can.kick) {
|
||||
kickButton = <div className="mx_MemberInfo_field" onClick={this.onKick}>
|
||||
{ this.props.member.membership === "invite" ? "Disinvite" : "Kick" }
|
||||
</div>;
|
||||
const membership = this.props.member.membership;
|
||||
const kickLabel = membership === "invite" ? "Disinvite" : "Kick";
|
||||
kickButton = (
|
||||
<AccessibleButton className="mx_MemberInfo_field"
|
||||
onClick={this.onKick}>
|
||||
{kickLabel}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
if (this.state.can.ban) {
|
||||
banButton = <div className="mx_MemberInfo_field" onClick={this.onBan}>
|
||||
Ban
|
||||
</div>;
|
||||
banButton = (
|
||||
<AccessibleButton className="mx_MemberInfo_field"
|
||||
onClick={this.onBan}>
|
||||
Ban
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
if (this.state.can.mute) {
|
||||
var muteLabel = this.state.muted ? "Unmute" : "Mute";
|
||||
muteButton = <div className="mx_MemberInfo_field" onClick={this.onMuteToggle}>
|
||||
{muteLabel}
|
||||
</div>;
|
||||
const muteLabel = this.state.muted ? "Unmute" : "Mute";
|
||||
muteButton = (
|
||||
<AccessibleButton className="mx_MemberInfo_field"
|
||||
onClick={this.onMuteToggle}>
|
||||
{muteLabel}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
if (this.state.can.toggleMod) {
|
||||
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}
|
||||
</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
|
||||
|
@ -682,7 +694,7 @@ module.exports = WithMatrixClient(React.createClass({
|
|||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
return (
|
||||
<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">
|
||||
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
|
||||
</div>
|
||||
|
|
|
@ -192,9 +192,9 @@ module.exports = React.createClass({
|
|||
width={14} height={14} resizeMethod="crop"
|
||||
style={style}
|
||||
title={title}
|
||||
onClick={this.props.onClick}
|
||||
/>
|
||||
</Velociraptor>
|
||||
);
|
||||
/* onClick={this.props.onClick} */
|
||||
},
|
||||
});
|
||||
|
|
|
@ -26,6 +26,7 @@ var rate_limited_func = require('../../../ratelimitedfunc');
|
|||
var linkify = require('linkifyjs');
|
||||
var linkifyElement = require('linkifyjs/element');
|
||||
var linkifyMatrix = require('../../../linkify-matrix');
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
|
@ -182,8 +183,8 @@ module.exports = React.createClass({
|
|||
'm.room.name', user_id
|
||||
);
|
||||
|
||||
save_button = <div className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save</div>;
|
||||
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>;
|
||||
save_button = <AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save</AccessibleButton>;
|
||||
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) {
|
||||
|
@ -275,9 +276,9 @@ module.exports = React.createClass({
|
|||
var settings_button;
|
||||
if (this.props.onSettingsClick) {
|
||||
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"/>
|
||||
</div>;
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
// var leave_button;
|
||||
|
@ -291,17 +292,17 @@ module.exports = React.createClass({
|
|||
var forget_button;
|
||||
if (this.props.onForgetClick) {
|
||||
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"/>
|
||||
</div>;
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
var rightPanel_buttons;
|
||||
if (this.props.collapsedRhs) {
|
||||
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"/>
|
||||
</div>;
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
var right_row;
|
||||
|
@ -310,9 +311,9 @@ module.exports = React.createClass({
|
|||
<div className="mx_RoomHeader_rightRow">
|
||||
{ settings_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"/>
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
{ rightPanel_buttons }
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ var sdk = require('../../../index');
|
|||
var ContextualMenu = require('../../structures/ContextualMenu');
|
||||
var RoomNotifs = require('../../../RoomNotifs');
|
||||
var FormattingUtils = require('../../../utils/FormattingUtils');
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -288,8 +289,10 @@ module.exports = React.createClass({
|
|||
var connectDragSource = this.props.connectDragSource;
|
||||
var connectDropTarget = this.props.connectDropTarget;
|
||||
|
||||
|
||||
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="mx_RoomTile_avatar_menu" onClick={this.onAvatarClicked}>
|
||||
<div className={avatarContainerClasses}>
|
||||
|
@ -304,6 +307,7 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
{/* { incomingCallBox } */}
|
||||
{ tooltip }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
|||
var React = require('react');
|
||||
var sdk = require('../../../index');
|
||||
var dis = require("../../../dispatcher");
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
/*
|
||||
* A stripped-down room header used for things like the user settings
|
||||
|
@ -44,7 +45,7 @@ module.exports = React.createClass({
|
|||
|
||||
var cancelButton;
|
||||
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;
|
||||
|
@ -70,4 +71,3 @@ module.exports = React.createClass({
|
|||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -18,7 +18,9 @@ limitations under the License.
|
|||
|
||||
var React = require('react');
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var Modal = require("../../../Modal");
|
||||
var sdk = require("../../../index");
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ChangePassword',
|
||||
|
@ -65,26 +67,42 @@ module.exports = React.createClass({
|
|||
changePassword: function(old_password, new_password) {
|
||||
var cli = MatrixClientPeg.get();
|
||||
|
||||
var authDict = {
|
||||
type: 'm.login.password',
|
||||
user: cli.credentials.userId,
|
||||
password: old_password
|
||||
};
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "Warning",
|
||||
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({
|
||||
phase: this.Phases.Uploading
|
||||
this.setState({
|
||||
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() {
|
||||
|
@ -136,9 +154,10 @@ module.exports = React.createClass({
|
|||
<input id="password2" type="password" ref="confirm_input" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={buttonClassName} onClick={this.onClickChange}>
|
||||
<AccessibleButton className={buttonClassName}
|
||||
onClick={this.onClickChange}>
|
||||
Change Password
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
case this.Phases.Uploading:
|
||||
|
|
|
@ -28,7 +28,6 @@ class MatrixDispatcher extends flux.Dispatcher {
|
|||
* for.
|
||||
*/
|
||||
dispatch(payload, sync) {
|
||||
console.log("Dispatch: "+payload.action);
|
||||
if (sync) {
|
||||
super.dispatch(payload);
|
||||
} 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) {
|
||||
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) {
|
||||
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,
|
||||
sender: opts.user,
|
||||
content: opts.content,
|
||||
prev_content: opts.prev_content,
|
||||
event_id: "$" + Math.random() + "-" + Math.random(),
|
||||
origin_server_ts: opts.ts,
|
||||
};
|
||||
|
@ -150,7 +151,9 @@ export function mkPresence(opts) {
|
|||
* @param {Object} opts Values for the membership.
|
||||
* @param {string} opts.room The room ID 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 {RoomMember} opts.target The target of the event.
|
||||
* @param {string} opts.skey The other user ID for the event if applicable
|
||||
* e.g. for invites/bans.
|
||||
* @param {string} opts.name The content.displayname for the event.
|
||||
|
@ -169,9 +172,16 @@ export function mkMembership(opts) {
|
|||
opts.content = {
|
||||
membership: opts.mship
|
||||
};
|
||||
if (opts.prevMship) {
|
||||
opts.prev_content = { membership: opts.prevMship };
|
||||
}
|
||||
if (opts.name) { opts.content.displayname = opts.name; }
|
||||
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