Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit a2dd1fa

Browse files
committed
Merge branch 'develop' into matthew/warn-unknown-devices
2 parents 21f3aea + a3b9384 commit a2dd1fa

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2001
-569
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
src/component-index.js

.eslintrc.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1+
const path = require('path');
2+
3+
// get the path of the js-sdk so we can extend the config
4+
// eslint supports loading extended configs by module,
5+
// but only if they come from a module that starts with eslint-config-
6+
// So we load the filename directly (and it could be in node_modules/
7+
// or or ../node_modules/ etc)
8+
const matrixJsSdkPath = path.dirname(require.resolve('matrix-js-sdk'));
9+
110
module.exports = {
211
parser: "babel-eslint",
3-
extends: ["./node_modules/matrix-js-sdk/.eslintrc.js"],
12+
extends: [matrixJsSdkPath + "/.eslintrc.js"],
413
plugins: [
514
"react",
615
"flowtype",

jenkins.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ npm install
1919
npm run test
2020

2121
# run eslint
22-
npm run lint -- -f checkstyle -o eslint.xml || true
22+
npm run lintall -- -f checkstyle -o eslint.xml || true
2323

2424
# delete the old tarball, if it exists
2525
rm -f matrix-react-sdk-*.tgz

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"license": "Apache-2.0",
1111
"main": "lib/index.js",
1212
"files": [
13+
".eslintrc.js",
1314
"CHANGELOG.md",
1415
"CONTRIBUTING.rst",
1516
"LICENSE",
@@ -46,10 +47,12 @@
4647
"browser-encrypt-attachment": "^0.3.0",
4748
"browser-request": "^0.3.3",
4849
"classnames": "^2.1.2",
50+
"commonmark": "^0.27.0",
4951
"draft-js": "^0.8.1",
5052
"draft-js-export-html": "^0.5.0",
5153
"draft-js-export-markdown": "^0.2.0",
5254
"emojione": "2.2.3",
55+
"file-saver": "^1.3.3",
5356
"filesize": "^3.1.2",
5457
"flux": "^2.0.3",
5558
"fuse.js": "^2.2.0",
@@ -58,7 +61,6 @@
5861
"isomorphic-fetch": "^2.2.1",
5962
"linkifyjs": "^2.1.3",
6063
"lodash": "^4.13.1",
61-
"commonmark": "^0.27.0",
6264
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
6365
"optimist": "^0.6.1",
6466
"q": "^1.4.1",

src/Invite.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ import MultiInviter from './utils/MultiInviter';
1919

2020
const emailRegex = /^\S+@\S+\.\S+$/;
2121

22+
// We allow localhost for mxids to avoid confusion
23+
const mxidRegex = /^@\S+:(?:\S+\.\S+|localhost)$/
24+
2225
export function getAddressType(inputText) {
23-
const isEmailAddress = /^\S+@\S+\.\S+$/.test(inputText);
24-
const isMatrixId = inputText[0] === '@' && inputText.indexOf(":") > 0;
26+
const isEmailAddress = emailRegex.test(inputText);
27+
const isMatrixId = mxidRegex.test(inputText);
2528

2629
// sanity check the input for user IDs
2730
if (isEmailAddress) {

src/KeyCode.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ module.exports = {
2020
TAB: 9,
2121
ENTER: 13,
2222
SHIFT: 16,
23+
ESCAPE: 27,
2324
PAGE_UP: 33,
2425
PAGE_DOWN: 34,
2526
END: 35,

src/Modal.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ const AsyncWrapper = React.createClass({
6767
},
6868
});
6969

70+
let _counter = 0;
71+
7072
module.exports = {
7173
DialogContainerId: "mx_Dialog_Container",
7274

@@ -113,12 +115,16 @@ module.exports = {
113115
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
114116
};
115117

118+
// don't attempt to reuse the same AsyncWrapper for different dialogs,
119+
// otherwise we'll get confused.
120+
const modalCount = _counter++;
121+
116122
// FIXME: If a dialog uses getDefaultProps it clobbers the onFinished
117123
// property set here so you can't close the dialog from a button click!
118124
var dialog = (
119125
<div className={"mx_Dialog_wrapper " + className}>
120126
<div className="mx_Dialog">
121-
<AsyncWrapper loader={loader} {...props} onFinished={closeDialog}/>
127+
<AsyncWrapper key={modalCount} loader={loader} {...props} onFinished={closeDialog}/>
122128
</div>
123129
<div className="mx_Dialog_background" onClick={ closeDialog.bind(this, false) }></div>
124130
</div>

src/WhoIsTyping.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,24 @@ module.exports = {
3232
return whoIsTyping;
3333
},
3434

35-
whoIsTypingString: function(room) {
36-
var whoIsTyping = this.usersTypingApartFromMe(room);
35+
whoIsTypingString: function(room, limit) {
36+
const whoIsTyping = this.usersTypingApartFromMe(room);
37+
const othersCount = limit === undefined ?
38+
0 : Math.max(whoIsTyping.length - limit, 0);
3739
if (whoIsTyping.length == 0) {
38-
return null;
40+
return '';
3941
} else if (whoIsTyping.length == 1) {
4042
return whoIsTyping[0].name + ' is typing';
43+
}
44+
const names = whoIsTyping.map(function(m) {
45+
return m.name;
46+
});
47+
if (othersCount) {
48+
const other = ' other' + (othersCount > 1 ? 's' : '');
49+
return names.slice(0, limit).join(', ') + ' and ' +
50+
othersCount + other + ' are typing';
4151
} else {
42-
var names = whoIsTyping.map(function(m) {
43-
return m.name;
44-
});
45-
var lastPerson = names.shift();
52+
const lastPerson = names.pop();
4653
return names.join(', ') + ' and ' + lastPerson + ' are typing';
4754
}
4855
}

src/async-components/views/dialogs/ExportE2eKeysDialog.js

Lines changed: 131 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,71 +14,158 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
import FileSaver from 'file-saver';
1718
import React from 'react';
1819

20+
import * as Matrix from 'matrix-js-sdk';
21+
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
1922
import sdk from '../../../index';
2023

21-
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
24+
const PHASE_EDIT = 1;
25+
const PHASE_EXPORTING = 2;
2226

2327
export default React.createClass({
2428
displayName: 'ExportE2eKeysDialog',
2529

30+
propTypes: {
31+
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
32+
onFinished: React.PropTypes.func.isRequired,
33+
},
34+
2635
getInitialState: function() {
2736
return {
28-
collectedPassword: false,
37+
phase: PHASE_EDIT,
38+
errStr: null,
2939
};
3040
},
3141

42+
componentWillMount: function() {
43+
this._unmounted = false;
44+
},
45+
46+
componentWillUnmount: function() {
47+
this._unmounted = true;
48+
},
49+
3250
_onPassphraseFormSubmit: function(ev) {
3351
ev.preventDefault();
34-
console.log(this.refs.passphrase1.value);
52+
53+
const passphrase = this.refs.passphrase1.value;
54+
if (passphrase !== this.refs.passphrase2.value) {
55+
this.setState({errStr: 'Passphrases must match'});
56+
return false;
57+
}
58+
if (!passphrase) {
59+
this.setState({errStr: 'Passphrase must not be empty'});
60+
return false;
61+
}
62+
63+
this._startExport(passphrase);
3564
return false;
3665
},
3766

38-
render: function() {
39-
let content;
40-
if (!this.state.collectedPassword) {
41-
content = (
42-
<div className="mx_Dialog_content">
43-
<p>
44-
This process will allow you to export the keys for messages
45-
you have received in encrypted rooms to a local file. You
46-
will then be able to import the file into another Matrix
47-
client in the future, so that client will also be able to
48-
decrypt these messages.
49-
</p>
50-
<p>
51-
The exported file will allow anyone who can read it to decrypt
52-
any encrypted messages that you can see, so you should be
53-
careful to keep it secure. To help with this, you should enter
54-
a passphrase below, which will be used to encrypt the exported
55-
data. It will only be possible to import the data by using the
56-
same passphrase.
57-
</p>
58-
<form onSubmit={this._onPassphraseFormSubmit}>
59-
<div className="mx_TextInputDialog_label">
60-
<label htmlFor="passphrase1">Enter passphrase</label>
61-
</div>
62-
<div>
63-
<input ref="passphrase1" id="passphrase1"
64-
className="mx_TextInputDialog_input"
65-
autoFocus={true} size="64" type="password"/>
66-
</div>
67-
<div className="mx_Dialog_buttons">
68-
<input className="mx_Dialog_primary" type="submit" value="Export" />
69-
</div>
70-
</form>
71-
</div>
67+
_startExport: function(passphrase) {
68+
// extra Promise.resolve() to turn synchronous exceptions into
69+
// asynchronous ones.
70+
Promise.resolve().then(() => {
71+
return this.props.matrixClient.exportRoomKeys();
72+
}).then((k) => {
73+
return MegolmExportEncryption.encryptMegolmKeyFile(
74+
JSON.stringify(k), passphrase
7275
);
73-
}
76+
}).then((f) => {
77+
const blob = new Blob([f], {
78+
type: 'text/plain;charset=us-ascii',
79+
});
80+
FileSaver.saveAs(blob, 'riot-keys.txt');
81+
this.props.onFinished(true);
82+
}).catch((e) => {
83+
if (this._unmounted) {
84+
return;
85+
}
86+
this.setState({
87+
errStr: e.message,
88+
phase: PHASE_EDIT,
89+
});
90+
});
91+
92+
this.setState({
93+
errStr: null,
94+
phase: PHASE_EXPORTING,
95+
});
96+
},
97+
98+
render: function() {
99+
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
100+
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
101+
102+
const disableForm = (this.state.phase === PHASE_EXPORTING);
74103

75104
return (
76-
<div className="mx_exportE2eKeysDialog">
77-
<div className="mx_Dialog_title">
78-
Export room keys
79-
</div>
80-
{content}
81-
</div>
105+
<BaseDialog className='mx_exportE2eKeysDialog'
106+
onFinished={this.props.onFinished}
107+
title="Export room keys"
108+
>
109+
<form onSubmit={this._onPassphraseFormSubmit}>
110+
<div className="mx_Dialog_content">
111+
<p>
112+
This process allows you to export the keys for messages
113+
you have received in encrypted rooms to a local file. You
114+
will then be able to import the file into another Matrix
115+
client in the future, so that client will also be able to
116+
decrypt these messages.
117+
</p>
118+
<p>
119+
The exported file will allow anyone who can read it to decrypt
120+
any encrypted messages that you can see, so you should be
121+
careful to keep it secure. To help with this, you should enter
122+
a passphrase below, which will be used to encrypt the exported
123+
data. It will only be possible to import the data by using the
124+
same passphrase.
125+
</p>
126+
<div className='error'>
127+
{this.state.errStr}
128+
</div>
129+
<div className='mx_E2eKeysDialog_inputTable'>
130+
<div className='mx_E2eKeysDialog_inputRow'>
131+
<div className='mx_E2eKeysDialog_inputLabel'>
132+
<label htmlFor='passphrase1'>
133+
Enter passphrase
134+
</label>
135+
</div>
136+
<div className='mx_E2eKeysDialog_inputCell'>
137+
<input ref='passphrase1' id='passphrase1'
138+
autoFocus={true} size='64' type='password'
139+
disabled={disableForm}
140+
/>
141+
</div>
142+
</div>
143+
<div className='mx_E2eKeysDialog_inputRow'>
144+
<div className='mx_E2eKeysDialog_inputLabel'>
145+
<label htmlFor='passphrase2'>
146+
Confirm passphrase
147+
</label>
148+
</div>
149+
<div className='mx_E2eKeysDialog_inputCell'>
150+
<input ref='passphrase2' id='passphrase2'
151+
size='64' type='password'
152+
disabled={disableForm}
153+
/>
154+
</div>
155+
</div>
156+
</div>
157+
</div>
158+
<div className='mx_Dialog_buttons'>
159+
<input className='mx_Dialog_primary' type='submit' value='Export'
160+
disabled={disableForm}
161+
/>
162+
<AccessibleButton element='button' onClick={this.props.onFinished}
163+
disabled={disableForm}>
164+
Cancel
165+
</AccessibleButton>
166+
</div>
167+
</form>
168+
</BaseDialog>
82169
);
83170
},
84171
});

0 commit comments

Comments
 (0)