merge
This commit is contained in:
commit
be41462f3a
35 changed files with 621 additions and 275 deletions
|
@ -53,7 +53,13 @@ module.exports = {
|
||||||
* things that are errors in the js-sdk config that the current
|
* things that are errors in the js-sdk config that the current
|
||||||
* code does not adhere to, turned down to warn
|
* code does not adhere to, turned down to warn
|
||||||
*/
|
*/
|
||||||
"max-len": ["warn"],
|
"max-len": ["warn", {
|
||||||
|
// apparently people believe the length limit shouldn't apply
|
||||||
|
// to JSX.
|
||||||
|
ignorePattern: '^\\s*<',
|
||||||
|
ignoreComments: true,
|
||||||
|
code: 90,
|
||||||
|
}],
|
||||||
"valid-jsdoc": ["warn"],
|
"valid-jsdoc": ["warn"],
|
||||||
"new-cap": ["warn"],
|
"new-cap": ["warn"],
|
||||||
"key-spacing": ["warn"],
|
"key-spacing": ["warn"],
|
||||||
|
|
|
@ -165,6 +165,14 @@ module.exports = function (config) {
|
||||||
},
|
},
|
||||||
devtool: 'inline-source-map',
|
devtool: 'inline-source-map',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
webpackMiddleware: {
|
||||||
|
stats: {
|
||||||
|
// don't fill the console up with a mahoosive list of modules
|
||||||
|
chunks: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
browserNoActivityTimeout: 15000,
|
browserNoActivityTimeout: 15000,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
155
src/Markdown.js
155
src/Markdown.js
|
@ -15,110 +15,143 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import commonmark from 'commonmark';
|
import commonmark from 'commonmark';
|
||||||
|
import escape from 'lodash/escape';
|
||||||
|
|
||||||
|
const ALLOWED_HTML_TAGS = ['del'];
|
||||||
|
|
||||||
|
// These types of node are definitely text
|
||||||
|
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
|
||||||
|
|
||||||
|
function is_allowed_html_tag(node) {
|
||||||
|
// Regex won't work for tags with attrs, but we only
|
||||||
|
// allow <del> anyway.
|
||||||
|
const matches = /^<\/?(.*)>$/.exec(node.literal);
|
||||||
|
if (matches && matches.length == 2) {
|
||||||
|
const tag = matches[1];
|
||||||
|
return ALLOWED_HTML_TAGS.indexOf(tag) > -1;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function html_if_tag_allowed(node) {
|
||||||
|
if (is_allowed_html_tag(node)) {
|
||||||
|
this.lit(node.literal);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
this.lit(escape(node.literal));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns true if the parse output containing the node
|
||||||
|
* comprises multiple block level elements (ie. lines),
|
||||||
|
* or false if it is only a single line.
|
||||||
|
*/
|
||||||
|
function is_multi_line(node) {
|
||||||
|
var par = node;
|
||||||
|
while (par.parent) {
|
||||||
|
par = par.parent;
|
||||||
|
}
|
||||||
|
return par.firstChild != par.lastChild;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that wraps marked, adding the ability to see whether
|
* Class that wraps commonmark, adding the ability to see whether
|
||||||
* a given message actually uses any markdown syntax or whether
|
* a given message actually uses any markdown syntax or whether
|
||||||
* it's plain text.
|
* it's plain text.
|
||||||
*/
|
*/
|
||||||
export default class Markdown {
|
export default class Markdown {
|
||||||
constructor(input) {
|
constructor(input) {
|
||||||
this.input = input;
|
this.input = input;
|
||||||
this.parser = new commonmark.Parser();
|
|
||||||
this.renderer = new commonmark.HtmlRenderer({safe: false});
|
const parser = new commonmark.Parser();
|
||||||
|
this.parsed = parser.parse(this.input);
|
||||||
}
|
}
|
||||||
|
|
||||||
isPlainText() {
|
isPlainText() {
|
||||||
// we determine if the message requires markdown by
|
const walker = this.parsed.walker();
|
||||||
// running the parser on the tokens with a dummy
|
|
||||||
// rendered and seeing if any of the renderer's
|
|
||||||
// functions are called other than those noted below.
|
|
||||||
// In case you were wondering, no we can't just examine
|
|
||||||
// the tokens because the tokens we have are only the
|
|
||||||
// output of the *first* tokenizer: any line-based
|
|
||||||
// markdown is processed by marked within Parser by
|
|
||||||
// the 'inline lexer'...
|
|
||||||
let is_plain = true;
|
|
||||||
|
|
||||||
function setNotPlain() {
|
let ev;
|
||||||
is_plain = false;
|
while ( (ev = walker.next()) ) {
|
||||||
|
const node = ev.node;
|
||||||
|
if (TEXT_NODES.indexOf(node.type) > -1) {
|
||||||
|
// definitely text
|
||||||
|
continue;
|
||||||
|
} else if (node.type == 'html_inline' || node.type == 'html_block') {
|
||||||
|
// if it's an allowed html tag, we need to render it and therefore
|
||||||
|
// we will need to use HTML. If it's not allowed, it's not HTML since
|
||||||
|
// we'll just be treating it as text.
|
||||||
|
if (is_allowed_html_tag(node)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
const dummy_renderer = new commonmark.HtmlRenderer();
|
|
||||||
for (const k of Object.keys(commonmark.HtmlRenderer.prototype)) {
|
|
||||||
dummy_renderer[k] = setNotPlain;
|
|
||||||
}
|
|
||||||
// text and paragraph are just text
|
|
||||||
dummy_renderer.text = function(t) { return t; };
|
|
||||||
dummy_renderer.softbreak = function(t) { return t; };
|
|
||||||
dummy_renderer.paragraph = function(t) { return t; };
|
|
||||||
|
|
||||||
const dummy_parser = new commonmark.Parser();
|
|
||||||
dummy_renderer.render(dummy_parser.parse(this.input));
|
|
||||||
|
|
||||||
return is_plain;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toHTML() {
|
toHTML() {
|
||||||
const real_paragraph = this.renderer.paragraph;
|
const renderer = new commonmark.HtmlRenderer({safe: false});
|
||||||
|
const real_paragraph = renderer.paragraph;
|
||||||
|
|
||||||
this.renderer.paragraph = function(node, entering) {
|
renderer.paragraph = function(node, entering) {
|
||||||
// If there is only one top level node, just return the
|
// If there is only one top level node, just return the
|
||||||
// bare text: it's a single line of text and so should be
|
// bare text: it's a single line of text and so should be
|
||||||
// 'inline', rather than unnecessarily wrapped in its own
|
// 'inline', rather than unnecessarily wrapped in its own
|
||||||
// p tag. If, however, we have multiple nodes, each gets
|
// p tag. If, however, we have multiple nodes, each gets
|
||||||
// its own p tag to keep them as separate paragraphs.
|
// its own p tag to keep them as separate paragraphs.
|
||||||
var par = node;
|
if (is_multi_line(node)) {
|
||||||
while (par.parent) {
|
|
||||||
par = par.parent;
|
|
||||||
}
|
|
||||||
if (par.firstChild != par.lastChild) {
|
|
||||||
real_paragraph.call(this, node, entering);
|
real_paragraph.call(this, node, entering);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var parsed = this.parser.parse(this.input);
|
renderer.html_inline = html_if_tag_allowed;
|
||||||
var rendered = this.renderer.render(parsed);
|
renderer.html_block = function(node) {
|
||||||
|
// as with `paragraph`, we only insert line breaks
|
||||||
|
// if there are multiple lines in the markdown.
|
||||||
|
const isMultiLine = is_multi_line(node);
|
||||||
|
|
||||||
this.renderer.paragraph = real_paragraph;
|
if (isMultiLine) this.cr();
|
||||||
|
html_if_tag_allowed.call(this, node);
|
||||||
|
if (isMultiLine) this.cr();
|
||||||
|
}
|
||||||
|
|
||||||
return rendered;
|
return renderer.render(this.parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Render the markdown message to plain text. That is, essentially
|
||||||
|
* just remove any backslashes escaping what would otherwise be
|
||||||
|
* markdown syntax
|
||||||
|
* (to fix https://github.com/vector-im/riot-web/issues/2870)
|
||||||
|
*/
|
||||||
toPlaintext() {
|
toPlaintext() {
|
||||||
const real_paragraph = this.renderer.paragraph;
|
const renderer = new commonmark.HtmlRenderer({safe: false});
|
||||||
|
const real_paragraph = renderer.paragraph;
|
||||||
|
|
||||||
// The default `out` function only sends the input through an XML
|
// The default `out` function only sends the input through an XML
|
||||||
// escaping function, which causes messages to be entity encoded,
|
// escaping function, which causes messages to be entity encoded,
|
||||||
// which we don't want in this case.
|
// which we don't want in this case.
|
||||||
this.renderer.out = function(s) {
|
renderer.out = function(s) {
|
||||||
// The `lit` function adds a string literal to the output buffer.
|
// The `lit` function adds a string literal to the output buffer.
|
||||||
this.lit(s);
|
this.lit(s);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.renderer.paragraph = function(node, entering) {
|
renderer.paragraph = function(node, entering) {
|
||||||
// If there is only one top level node, just return the
|
// as with toHTML, only append lines to paragraphs if there are
|
||||||
// bare text: it's a single line of text and so should be
|
// multiple paragraphs
|
||||||
// 'inline', rather than unnecessarily wrapped in its own
|
if (is_multi_line(node)) {
|
||||||
// p tag. If, however, we have multiple nodes, each gets
|
if (!entering && node.next) {
|
||||||
// its own p tag to keep them as separate paragraphs.
|
|
||||||
var par = node;
|
|
||||||
while (par.parent) {
|
|
||||||
node = par;
|
|
||||||
par = par.parent;
|
|
||||||
}
|
|
||||||
if (node != par.lastChild) {
|
|
||||||
if (!entering) {
|
|
||||||
this.lit('\n\n');
|
this.lit('\n\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
renderer.html_block = function(node) {
|
||||||
|
this.lit(node.literal);
|
||||||
|
if (is_multi_line(node) && node.next) this.lit('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
var parsed = this.parser.parse(this.input);
|
return renderer.render(this.parsed);
|
||||||
var rendered = this.renderer.render(parsed);
|
|
||||||
|
|
||||||
this.renderer.paragraph = real_paragraph;
|
|
||||||
|
|
||||||
return rendered;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -177,7 +177,7 @@ class ModalManager {
|
||||||
|
|
||||||
var modal = this._modals[0];
|
var modal = this._modals[0];
|
||||||
var dialog = (
|
var dialog = (
|
||||||
<div className={"mx_Dialog_wrapper " + modal.className}>
|
<div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '') }>
|
||||||
<div className="mx_Dialog">
|
<div className="mx_Dialog">
|
||||||
{modal.elem}
|
{modal.elem}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -34,7 +34,7 @@ module.exports = {
|
||||||
Modal.createDialog(UnknownDeviceDialog, {
|
Modal.createDialog(UnknownDeviceDialog, {
|
||||||
devices: err.devices,
|
devices: err.devices,
|
||||||
room: MatrixClientPeg.get().getRoom(event.getRoomId()),
|
room: MatrixClientPeg.get().getRoom(event.getRoomId()),
|
||||||
});
|
}, "mx_Dialog_unknownDevice");
|
||||||
}
|
}
|
||||||
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
|
|
80
src/RtsClient.js
Normal file
80
src/RtsClient.js
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import 'whatwg-fetch';
|
||||||
|
|
||||||
|
function checkStatus(response) {
|
||||||
|
if (!response.ok) {
|
||||||
|
return response.text().then((text) => {
|
||||||
|
throw new Error(text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJson(response) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeQueryParams(params) {
|
||||||
|
return '?' + Object.keys(params).map((k) => {
|
||||||
|
return k + '=' + encodeURIComponent(params[k]);
|
||||||
|
}).join('&');
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = (url, opts) => {
|
||||||
|
if (opts && opts.qs) {
|
||||||
|
url += encodeQueryParams(opts.qs);
|
||||||
|
delete opts.qs;
|
||||||
|
}
|
||||||
|
if (opts && opts.body) {
|
||||||
|
if (!opts.headers) {
|
||||||
|
opts.headers = {};
|
||||||
|
}
|
||||||
|
opts.body = JSON.stringify(opts.body);
|
||||||
|
opts.headers['Content-Type'] = 'application/json';
|
||||||
|
}
|
||||||
|
return fetch(url, opts)
|
||||||
|
.then(checkStatus)
|
||||||
|
.then(parseJson);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default class RtsClient {
|
||||||
|
constructor(url) {
|
||||||
|
this._url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTeamsConfig() {
|
||||||
|
return request(this._url + '/teams');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track a referral with the Riot Team Server. This should be called once a referred
|
||||||
|
* user has been successfully registered.
|
||||||
|
* @param {string} referrer the user ID of one who referred the user to Riot.
|
||||||
|
* @param {string} userId the user ID of the user being referred.
|
||||||
|
* @param {string} userEmail the email address linked to `userId`.
|
||||||
|
* @returns {Promise} a promise that resolves to { team_token: 'sometoken' } upon
|
||||||
|
* success.
|
||||||
|
*/
|
||||||
|
trackReferral(referrer, userId, userEmail) {
|
||||||
|
return request(this._url + '/register',
|
||||||
|
{
|
||||||
|
body: {
|
||||||
|
referrer: referrer,
|
||||||
|
user_id: userId,
|
||||||
|
user_email: userEmail,
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTeam(teamToken) {
|
||||||
|
return request(this._url + '/teamConfiguration',
|
||||||
|
{
|
||||||
|
qs: {
|
||||||
|
team_token: teamToken,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -62,11 +62,11 @@ module.exports = React.createClass({
|
||||||
oldNode.style.visibility = c.props.style.visibility;
|
oldNode.style.visibility = c.props.style.visibility;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (oldNode.style.visibility == 'hidden' && c.props.style.visibility == 'visible') {
|
|
||||||
oldNode.style.visibility = c.props.style.visibility;
|
|
||||||
}
|
|
||||||
//console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
|
//console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
|
||||||
}
|
}
|
||||||
|
if (oldNode.style.visibility == 'hidden' && c.props.style.visibility == 'visible') {
|
||||||
|
oldNode.style.visibility = c.props.style.visibility;
|
||||||
|
}
|
||||||
self.children[c.key] = old;
|
self.children[c.key] = old;
|
||||||
} else {
|
} else {
|
||||||
// new element. If we have a startStyle, use that as the style and go through
|
// new element. If we have a startStyle, use that as the style and go through
|
||||||
|
|
|
@ -1,3 +1,19 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
@ -71,7 +71,7 @@ export default React.createClass({
|
||||||
return this.props.matrixClient.exportRoomKeys();
|
return this.props.matrixClient.exportRoomKeys();
|
||||||
}).then((k) => {
|
}).then((k) => {
|
||||||
return MegolmExportEncryption.encryptMegolmKeyFile(
|
return MegolmExportEncryption.encryptMegolmKeyFile(
|
||||||
JSON.stringify(k), passphrase
|
JSON.stringify(k), passphrase,
|
||||||
);
|
);
|
||||||
}).then((f) => {
|
}).then((f) => {
|
||||||
const blob = new Blob([f], {
|
const blob = new Blob([f], {
|
||||||
|
@ -95,9 +95,14 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onCancelClick: function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.props.onFinished(false);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
|
|
||||||
|
|
||||||
const disableForm = (this.state.phase === PHASE_EXPORTING);
|
const disableForm = (this.state.phase === PHASE_EXPORTING);
|
||||||
|
|
||||||
|
@ -159,10 +164,9 @@ export default React.createClass({
|
||||||
<input className='mx_Dialog_primary' type='submit' value='Export'
|
<input className='mx_Dialog_primary' type='submit' value='Export'
|
||||||
disabled={disableForm}
|
disabled={disableForm}
|
||||||
/>
|
/>
|
||||||
<AccessibleButton element='button' onClick={this.props.onFinished}
|
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||||
disabled={disableForm}>
|
|
||||||
Cancel
|
Cancel
|
||||||
</AccessibleButton>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
|
@ -80,7 +80,7 @@ export default React.createClass({
|
||||||
|
|
||||||
return readFileAsArrayBuffer(file).then((arrayBuffer) => {
|
return readFileAsArrayBuffer(file).then((arrayBuffer) => {
|
||||||
return MegolmExportEncryption.decryptMegolmKeyFile(
|
return MegolmExportEncryption.decryptMegolmKeyFile(
|
||||||
arrayBuffer, passphrase
|
arrayBuffer, passphrase,
|
||||||
);
|
);
|
||||||
}).then((keys) => {
|
}).then((keys) => {
|
||||||
return this.props.matrixClient.importRoomKeys(JSON.parse(keys));
|
return this.props.matrixClient.importRoomKeys(JSON.parse(keys));
|
||||||
|
@ -98,9 +98,14 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onCancelClick: function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.props.onFinished(false);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
|
|
||||||
|
|
||||||
const disableForm = (this.state.phase !== PHASE_EDIT);
|
const disableForm = (this.state.phase !== PHASE_EDIT);
|
||||||
|
|
||||||
|
@ -158,10 +163,9 @@ export default React.createClass({
|
||||||
<input className='mx_Dialog_primary' type='submit' value='Import'
|
<input className='mx_Dialog_primary' type='submit' value='Import'
|
||||||
disabled={!this.state.enableSubmit || disableForm}
|
disabled={!this.state.enableSubmit || disableForm}
|
||||||
/>
|
/>
|
||||||
<AccessibleButton element='button' onClick={this.props.onFinished}
|
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||||
disabled={disableForm}>
|
|
||||||
Cancel
|
Cancel
|
||||||
</AccessibleButton>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
|
@ -105,6 +105,7 @@ var FilePanel = React.createClass({
|
||||||
showUrlPreview = { false }
|
showUrlPreview = { false }
|
||||||
tileShape="file_grid"
|
tileShape="file_grid"
|
||||||
opacity={ this.props.opacity }
|
opacity={ this.props.opacity }
|
||||||
|
empty="There are no visible files in this room"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,6 +171,7 @@ export default React.createClass({
|
||||||
brand={this.props.config.brand}
|
brand={this.props.config.brand}
|
||||||
collapsedRhs={this.props.collapse_rhs}
|
collapsedRhs={this.props.collapse_rhs}
|
||||||
enableLabs={this.props.config.enableLabs}
|
enableLabs={this.props.config.enableLabs}
|
||||||
|
referralBaseUrl={this.props.config.referralBaseUrl}
|
||||||
/>;
|
/>;
|
||||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
|
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -1055,12 +1055,13 @@ module.exports = React.createClass({
|
||||||
sessionId={this.state.register_session_id}
|
sessionId={this.state.register_session_id}
|
||||||
idSid={this.state.register_id_sid}
|
idSid={this.state.register_id_sid}
|
||||||
email={this.props.startingFragmentQueryParams.email}
|
email={this.props.startingFragmentQueryParams.email}
|
||||||
|
referrer={this.props.startingFragmentQueryParams.referrer}
|
||||||
username={this.state.upgradeUsername}
|
username={this.state.upgradeUsername}
|
||||||
guestAccessToken={this.state.guestAccessToken}
|
guestAccessToken={this.state.guestAccessToken}
|
||||||
defaultHsUrl={this.getDefaultHsUrl()}
|
defaultHsUrl={this.getDefaultHsUrl()}
|
||||||
defaultIsUrl={this.getDefaultIsUrl()}
|
defaultIsUrl={this.getDefaultIsUrl()}
|
||||||
brand={this.props.config.brand}
|
brand={this.props.config.brand}
|
||||||
teamsConfig={this.props.config.teamsConfig}
|
teamServerConfig={this.props.config.teamServerConfig}
|
||||||
customHsUrl={this.getCurrentHsUrl()}
|
customHsUrl={this.getCurrentHsUrl()}
|
||||||
customIsUrl={this.getCurrentIsUrl()}
|
customIsUrl={this.getCurrentIsUrl()}
|
||||||
registrationUrl={this.props.registrationUrl}
|
registrationUrl={this.props.registrationUrl}
|
||||||
|
|
|
@ -48,6 +48,7 @@ var NotificationPanel = React.createClass({
|
||||||
showUrlPreview = { false }
|
showUrlPreview = { false }
|
||||||
opacity={ this.props.opacity }
|
opacity={ this.props.opacity }
|
||||||
tileShape="notif"
|
tileShape="notif"
|
||||||
|
empty="You have no visible notifications"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,6 +74,7 @@ module.exports = React.createClass({
|
||||||
// callback for when the status bar can be hidden from view, as it is
|
// callback for when the status bar can be hidden from view, as it is
|
||||||
// not displaying anything
|
// not displaying anything
|
||||||
onHidden: React.PropTypes.func,
|
onHidden: React.PropTypes.func,
|
||||||
|
|
||||||
// callback for when the status bar is displaying something and should
|
// callback for when the status bar is displaying something and should
|
||||||
// be visible
|
// be visible
|
||||||
onVisible: React.PropTypes.func,
|
onVisible: React.PropTypes.func,
|
||||||
|
@ -113,7 +114,9 @@ module.exports = React.createClass({
|
||||||
clearTimeout(this.hideDebouncer);
|
clearTimeout(this.hideDebouncer);
|
||||||
}
|
}
|
||||||
this.hideDebouncer = setTimeout(() => {
|
this.hideDebouncer = setTimeout(() => {
|
||||||
this.props.onHidden();
|
// temporarily stop hiding the statusbar as per
|
||||||
|
// https://github.com/vector-im/riot-web/issues/1991#issuecomment-276953915
|
||||||
|
// this.props.onHidden();
|
||||||
}, HIDE_DEBOUNCE_MS);
|
}, HIDE_DEBOUNCE_MS);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -238,7 +241,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
if (othersCount > 0) {
|
if (othersCount > 0) {
|
||||||
avatars.push(
|
avatars.push(
|
||||||
<span className="mx_RoomStatusBar_typingIndicatorRemaining">
|
<span className="mx_RoomStatusBar_typingIndicatorRemaining" key="others">
|
||||||
+{othersCount}
|
+{othersCount}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1332,12 +1332,14 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onStatusBarVisible: function() {
|
onStatusBarVisible: function() {
|
||||||
|
if (this.unmounted) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
statusBarVisible: true,
|
statusBarVisible: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onStatusBarHidden: function() {
|
onStatusBarHidden: function() {
|
||||||
|
if (this.unmounted) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
statusBarVisible: false,
|
statusBarVisible: false,
|
||||||
});
|
});
|
||||||
|
@ -1507,13 +1509,14 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
|
|
||||||
var statusBar;
|
var statusBar;
|
||||||
|
let isStatusAreaExpanded = true;
|
||||||
|
|
||||||
if (ContentMessages.getCurrentUploads().length > 0) {
|
if (ContentMessages.getCurrentUploads().length > 0) {
|
||||||
var UploadBar = sdk.getComponent('structures.UploadBar');
|
var UploadBar = sdk.getComponent('structures.UploadBar');
|
||||||
statusBar = <UploadBar room={this.state.room} />;
|
statusBar = <UploadBar room={this.state.room} />;
|
||||||
} else if (!this.state.searchResults) {
|
} else if (!this.state.searchResults) {
|
||||||
var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
|
var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
|
||||||
|
isStatusAreaExpanded = this.state.statusBarVisible;
|
||||||
statusBar = <RoomStatusBar
|
statusBar = <RoomStatusBar
|
||||||
room={this.state.room}
|
room={this.state.room}
|
||||||
tabComplete={this.tabComplete}
|
tabComplete={this.tabComplete}
|
||||||
|
@ -1683,7 +1686,7 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let statusBarAreaClass = "mx_RoomView_statusArea mx_fadable";
|
let statusBarAreaClass = "mx_RoomView_statusArea mx_fadable";
|
||||||
if (this.state.statusBarVisible) {
|
if (isStatusAreaExpanded) {
|
||||||
statusBarAreaClass += " mx_RoomView_statusArea_expanded";
|
statusBarAreaClass += " mx_RoomView_statusArea_expanded";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ var DEBUG_SCROLL = false;
|
||||||
|
|
||||||
// The amount of extra scroll distance to allow prior to unfilling.
|
// The amount of extra scroll distance to allow prior to unfilling.
|
||||||
// See _getExcessHeight.
|
// See _getExcessHeight.
|
||||||
const UNPAGINATION_PADDING = 1500;
|
const UNPAGINATION_PADDING = 3000;
|
||||||
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
|
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
|
||||||
// many scroll events causing many unfilling requests.
|
// many scroll events causing many unfilling requests.
|
||||||
const UNFILL_REQUEST_DEBOUNCE_MS = 200;
|
const UNFILL_REQUEST_DEBOUNCE_MS = 200;
|
||||||
|
@ -570,7 +570,7 @@ module.exports = React.createClass({
|
||||||
var boundingRect = node.getBoundingClientRect();
|
var boundingRect = node.getBoundingClientRect();
|
||||||
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
|
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
|
||||||
|
|
||||||
debuglog("Scrolling to token '" + node.dataset.scrollToken + "'+" +
|
debuglog("ScrollPanel: scrolling to token '" + node.dataset.scrollToken + "'+" +
|
||||||
pixelOffset + " (delta: "+scrollDelta+")");
|
pixelOffset + " (delta: "+scrollDelta+")");
|
||||||
|
|
||||||
if(scrollDelta != 0) {
|
if(scrollDelta != 0) {
|
||||||
|
@ -582,7 +582,7 @@ module.exports = React.createClass({
|
||||||
_saveScrollState: function() {
|
_saveScrollState: function() {
|
||||||
if (this.props.stickyBottom && this.isAtBottom()) {
|
if (this.props.stickyBottom && this.isAtBottom()) {
|
||||||
this.scrollState = { stuckAtBottom: true };
|
this.scrollState = { stuckAtBottom: true };
|
||||||
debuglog("Saved scroll state", this.scrollState);
|
debuglog("ScrollPanel: Saved scroll state", this.scrollState);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -601,12 +601,12 @@ module.exports = React.createClass({
|
||||||
trackedScrollToken: node.dataset.scrollToken,
|
trackedScrollToken: node.dataset.scrollToken,
|
||||||
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
|
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
|
||||||
};
|
};
|
||||||
debuglog("Saved scroll state", this.scrollState);
|
debuglog("ScrollPanel: saved scroll state", this.scrollState);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debuglog("Unable to save scroll state: found no children in the viewport");
|
debuglog("ScrollPanel: unable to save scroll state: found no children in the viewport");
|
||||||
},
|
},
|
||||||
|
|
||||||
_restoreSavedScrollState: function() {
|
_restoreSavedScrollState: function() {
|
||||||
|
@ -640,7 +640,7 @@ module.exports = React.createClass({
|
||||||
this._lastSetScroll = scrollNode.scrollTop;
|
this._lastSetScroll = scrollNode.scrollTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
debuglog("Set scrollTop:", scrollNode.scrollTop,
|
debuglog("ScrollPanel: set scrollTop:", scrollNode.scrollTop,
|
||||||
"requested:", scrollTop,
|
"requested:", scrollTop,
|
||||||
"_lastSetScroll:", this._lastSetScroll);
|
"_lastSetScroll:", this._lastSetScroll);
|
||||||
},
|
},
|
||||||
|
|
|
@ -96,6 +96,9 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
// shape property to be passed to EventTiles
|
// shape property to be passed to EventTiles
|
||||||
tileShape: React.PropTypes.string,
|
tileShape: React.PropTypes.string,
|
||||||
|
|
||||||
|
// placeholder text to use if the timeline is empty
|
||||||
|
empty: React.PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
statics: {
|
statics: {
|
||||||
|
@ -990,6 +993,14 @@ var TimelinePanel = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.events.length == 0) {
|
||||||
|
return (
|
||||||
|
<div className={ this.props.className + " mx_RoomView_messageListWrapper" }>
|
||||||
|
<div className="mx_RoomView_empty">{ this.props.empty }</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// give the messagepanel a stickybottom if we're at the end of the
|
// give the messagepanel a stickybottom if we're at the end of the
|
||||||
// live timeline, so that the arrival of new events triggers a
|
// live timeline, so that the arrival of new events triggers a
|
||||||
// scroll.
|
// scroll.
|
||||||
|
|
|
@ -104,6 +104,9 @@ module.exports = React.createClass({
|
||||||
// True to show the 'labs' section of experimental features
|
// True to show the 'labs' section of experimental features
|
||||||
enableLabs: React.PropTypes.bool,
|
enableLabs: React.PropTypes.bool,
|
||||||
|
|
||||||
|
// The base URL to use in the referral link. Defaults to window.location.origin.
|
||||||
|
referralBaseUrl: React.PropTypes.string,
|
||||||
|
|
||||||
// true if RightPanel is collapsed
|
// true if RightPanel is collapsed
|
||||||
collapsedRhs: React.PropTypes.bool,
|
collapsedRhs: React.PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
@ -458,6 +461,27 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_renderReferral: function() {
|
||||||
|
const teamToken = window.localStorage.getItem('mx_team_token');
|
||||||
|
if (!teamToken) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof teamToken !== 'string') {
|
||||||
|
console.warn('Team token not a string');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const href = (this.props.referralBaseUrl || window.location.origin) +
|
||||||
|
`/#/register?referrer=${this._me}&team_token=${teamToken}`;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3>Referral</h3>
|
||||||
|
<div className="mx_UserSettings_section">
|
||||||
|
Refer a friend to Riot: <a href={href}>{href}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
_renderUserInterfaceSettings: function() {
|
_renderUserInterfaceSettings: function() {
|
||||||
var client = MatrixClientPeg.get();
|
var client = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
@ -857,6 +881,8 @@ module.exports = React.createClass({
|
||||||
{accountJsx}
|
{accountJsx}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{this._renderReferral()}
|
||||||
|
|
||||||
{notification_area}
|
{notification_area}
|
||||||
|
|
||||||
{this._renderUserInterfaceSettings()}
|
{this._renderUserInterfaceSettings()}
|
||||||
|
|
|
@ -25,6 +25,7 @@ var ServerConfig = require("../../views/login/ServerConfig");
|
||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
var RegistrationForm = require("../../views/login/RegistrationForm");
|
var RegistrationForm = require("../../views/login/RegistrationForm");
|
||||||
var CaptchaForm = require("../../views/login/CaptchaForm");
|
var CaptchaForm = require("../../views/login/CaptchaForm");
|
||||||
|
var RtsClient = require("../../../RtsClient");
|
||||||
|
|
||||||
var MIN_PASSWORD_LENGTH = 6;
|
var MIN_PASSWORD_LENGTH = 6;
|
||||||
|
|
||||||
|
@ -47,23 +48,16 @@ module.exports = React.createClass({
|
||||||
defaultIsUrl: React.PropTypes.string,
|
defaultIsUrl: React.PropTypes.string,
|
||||||
brand: React.PropTypes.string,
|
brand: React.PropTypes.string,
|
||||||
email: React.PropTypes.string,
|
email: React.PropTypes.string,
|
||||||
|
referrer: React.PropTypes.string,
|
||||||
username: React.PropTypes.string,
|
username: React.PropTypes.string,
|
||||||
guestAccessToken: React.PropTypes.string,
|
guestAccessToken: React.PropTypes.string,
|
||||||
teamsConfig: React.PropTypes.shape({
|
teamServerConfig: React.PropTypes.shape({
|
||||||
// Email address to request new teams
|
// Email address to request new teams
|
||||||
supportEmail: React.PropTypes.string,
|
supportEmail: React.PropTypes.string.isRequired,
|
||||||
teams: React.PropTypes.arrayOf(React.PropTypes.shape({
|
// URL of the riot-team-server to get team configurations and track referrals
|
||||||
// The displayed name of the team
|
teamServerURL: React.PropTypes.string.isRequired,
|
||||||
"name": React.PropTypes.string,
|
|
||||||
// The suffix with which every team email address ends
|
|
||||||
"emailSuffix": React.PropTypes.string,
|
|
||||||
// The rooms to use during auto-join
|
|
||||||
"rooms": React.PropTypes.arrayOf(React.PropTypes.shape({
|
|
||||||
"id": React.PropTypes.string,
|
|
||||||
"autoJoin": React.PropTypes.bool,
|
|
||||||
})),
|
|
||||||
})).required,
|
|
||||||
}),
|
}),
|
||||||
|
teamSelected: React.PropTypes.object,
|
||||||
|
|
||||||
defaultDeviceDisplayName: React.PropTypes.string,
|
defaultDeviceDisplayName: React.PropTypes.string,
|
||||||
|
|
||||||
|
@ -75,6 +69,7 @@ module.exports = React.createClass({
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
busy: false,
|
busy: false,
|
||||||
|
teamServerBusy: false,
|
||||||
errorText: null,
|
errorText: null,
|
||||||
// We remember the values entered by the user because
|
// We remember the values entered by the user because
|
||||||
// the registration form will be unmounted during the
|
// the registration form will be unmounted during the
|
||||||
|
@ -90,6 +85,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
this._unmounted = false;
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
// attach this to the instance rather than this.state since it isn't UI
|
// attach this to the instance rather than this.state since it isn't UI
|
||||||
this.registerLogic = new Signup.Register(
|
this.registerLogic = new Signup.Register(
|
||||||
|
@ -103,10 +99,40 @@ module.exports = React.createClass({
|
||||||
this.registerLogic.setIdSid(this.props.idSid);
|
this.registerLogic.setIdSid(this.props.idSid);
|
||||||
this.registerLogic.setGuestAccessToken(this.props.guestAccessToken);
|
this.registerLogic.setGuestAccessToken(this.props.guestAccessToken);
|
||||||
this.registerLogic.recheckState();
|
this.registerLogic.recheckState();
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.props.teamServerConfig &&
|
||||||
|
this.props.teamServerConfig.teamServerURL &&
|
||||||
|
!this._rtsClient
|
||||||
|
) {
|
||||||
|
this._rtsClient = new RtsClient(this.props.teamServerConfig.teamServerURL);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
teamServerBusy: true,
|
||||||
|
});
|
||||||
|
// GET team configurations including domains, names and icons
|
||||||
|
this._rtsClient.getTeamsConfig().then((data) => {
|
||||||
|
const teamsConfig = {
|
||||||
|
teams: data,
|
||||||
|
supportEmail: this.props.teamServerConfig.supportEmail,
|
||||||
|
};
|
||||||
|
console.log('Setting teams config to ', teamsConfig);
|
||||||
|
this.setState({
|
||||||
|
teamsConfig: teamsConfig,
|
||||||
|
teamServerBusy: false,
|
||||||
|
});
|
||||||
|
}, (err) => {
|
||||||
|
console.error('Error retrieving config for teams', err);
|
||||||
|
this.setState({
|
||||||
|
teamServerBusy: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
|
this._unmounted = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
|
@ -184,24 +210,41 @@ module.exports = React.createClass({
|
||||||
accessToken: response.access_token
|
accessToken: response.access_token
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-join rooms
|
if (
|
||||||
if (self.props.teamsConfig && self.props.teamsConfig.teams) {
|
self._rtsClient &&
|
||||||
for (let i = 0; i < self.props.teamsConfig.teams.length; i++) {
|
self.props.referrer &&
|
||||||
let team = self.props.teamsConfig.teams[i];
|
self.state.teamSelected
|
||||||
if (self.state.formVals.email.endsWith(team.emailSuffix)) {
|
) {
|
||||||
console.log("User successfully registered with team " + team.name);
|
// Track referral, get team_token in order to retrieve team config
|
||||||
|
self._rtsClient.trackReferral(
|
||||||
|
self.props.referrer,
|
||||||
|
response.user_id,
|
||||||
|
self.state.formVals.email
|
||||||
|
).then((data) => {
|
||||||
|
const teamToken = data.team_token;
|
||||||
|
// Store for use /w welcome pages
|
||||||
|
window.localStorage.setItem('mx_team_token', teamToken);
|
||||||
|
|
||||||
|
self._rtsClient.getTeam(teamToken).then((team) => {
|
||||||
|
console.log(
|
||||||
|
`User successfully registered with team ${team.name}`
|
||||||
|
);
|
||||||
if (!team.rooms) {
|
if (!team.rooms) {
|
||||||
break;
|
return;
|
||||||
}
|
}
|
||||||
|
// Auto-join rooms
|
||||||
team.rooms.forEach((room) => {
|
team.rooms.forEach((room) => {
|
||||||
if (room.autoJoin) {
|
if (room.auto_join && room.room_id) {
|
||||||
console.log("Auto-joining " + room.id);
|
console.log(`Auto-joining ${room.room_id}`);
|
||||||
MatrixClientPeg.get().joinRoom(room.id);
|
MatrixClientPeg.get().joinRoom(room.room_id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
}, (err) => {
|
||||||
}
|
console.error('Error getting team config', err);
|
||||||
}
|
});
|
||||||
|
}, (err) => {
|
||||||
|
console.error('Error tracking referral', err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.props.brand) {
|
if (self.props.brand) {
|
||||||
|
@ -273,7 +316,15 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onTeamSelected: function(teamSelected) {
|
||||||
|
if (!this._unmounted) {
|
||||||
|
this.setState({ teamSelected });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
_getRegisterContentJsx: function() {
|
_getRegisterContentJsx: function() {
|
||||||
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
|
||||||
var currStep = this.registerLogic.getStep();
|
var currStep = this.registerLogic.getStep();
|
||||||
var registerStep;
|
var registerStep;
|
||||||
switch (currStep) {
|
switch (currStep) {
|
||||||
|
@ -283,17 +334,23 @@ module.exports = React.createClass({
|
||||||
case "Register.STEP_m.login.dummy":
|
case "Register.STEP_m.login.dummy":
|
||||||
// NB. Our 'username' prop is specifically for upgrading
|
// NB. Our 'username' prop is specifically for upgrading
|
||||||
// a guest account
|
// a guest account
|
||||||
|
if (this.state.teamServerBusy) {
|
||||||
|
registerStep = <Spinner />;
|
||||||
|
break;
|
||||||
|
}
|
||||||
registerStep = (
|
registerStep = (
|
||||||
<RegistrationForm
|
<RegistrationForm
|
||||||
showEmail={true}
|
showEmail={true}
|
||||||
defaultUsername={this.state.formVals.username}
|
defaultUsername={this.state.formVals.username}
|
||||||
defaultEmail={this.state.formVals.email}
|
defaultEmail={this.state.formVals.email}
|
||||||
defaultPassword={this.state.formVals.password}
|
defaultPassword={this.state.formVals.password}
|
||||||
teamsConfig={this.props.teamsConfig}
|
teamsConfig={this.state.teamsConfig}
|
||||||
guestUsername={this.props.username}
|
guestUsername={this.props.username}
|
||||||
minPasswordLength={MIN_PASSWORD_LENGTH}
|
minPasswordLength={MIN_PASSWORD_LENGTH}
|
||||||
onError={this.onFormValidationFailed}
|
onError={this.onFormValidationFailed}
|
||||||
onRegisterClick={this.onFormSubmit} />
|
onRegisterClick={this.onFormSubmit}
|
||||||
|
onTeamSelected={this.onTeamSelected}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "Register.STEP_m.login.email.identity":
|
case "Register.STEP_m.login.email.identity":
|
||||||
|
@ -322,7 +379,6 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
var busySpinner;
|
var busySpinner;
|
||||||
if (this.state.busy) {
|
if (this.state.busy) {
|
||||||
var Spinner = sdk.getComponent("elements.Spinner");
|
|
||||||
busySpinner = (
|
busySpinner = (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
);
|
);
|
||||||
|
@ -367,7 +423,7 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className="mx_Login">
|
<div className="mx_Login">
|
||||||
<div className="mx_Login_box">
|
<div className="mx_Login_box">
|
||||||
<LoginHeader />
|
<LoginHeader icon={this.state.teamSelected ? this.state.teamSelected.icon : null}/>
|
||||||
{this._getRegisterContentJsx()}
|
{this._getRegisterContentJsx()}
|
||||||
<LoginFooter />
|
<LoginFooter />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -145,27 +145,48 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
if (imageUrl === this.state.defaultImageUrl) {
|
if (imageUrl === this.state.defaultImageUrl) {
|
||||||
const initialLetter = this._getInitialLetter(name);
|
const initialLetter = this._getInitialLetter(name);
|
||||||
return (
|
const textNode = (
|
||||||
<span className="mx_BaseAvatar" {...otherProps}>
|
<EmojiText className="mx_BaseAvatar_initial" aria-hidden="true"
|
||||||
<EmojiText className="mx_BaseAvatar_initial" aria-hidden="true"
|
style={{ fontSize: (width * 0.65) + "px",
|
||||||
style={{ fontSize: (width * 0.65) + "px",
|
width: width + "px",
|
||||||
width: width + "px",
|
lineHeight: height + "px" }}
|
||||||
lineHeight: height + "px" }}>{initialLetter}</EmojiText>
|
>
|
||||||
<img className="mx_BaseAvatar_image" src={imageUrl}
|
{initialLetter}
|
||||||
alt="" title={title} onError={this.onError}
|
</EmojiText>
|
||||||
width={width} height={height} />
|
|
||||||
</span>
|
|
||||||
);
|
);
|
||||||
|
const imgNode = (
|
||||||
|
<img className="mx_BaseAvatar_image" src={imageUrl}
|
||||||
|
alt="" title={title} onError={this.onError}
|
||||||
|
width={width} height={height} />
|
||||||
|
);
|
||||||
|
if (onClick != null) {
|
||||||
|
return (
|
||||||
|
<AccessibleButton element='span' className="mx_BaseAvatar"
|
||||||
|
onClick={onClick} {...otherProps}
|
||||||
|
>
|
||||||
|
{textNode}
|
||||||
|
{imgNode}
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<span className="mx_BaseAvatar" {...otherProps}>
|
||||||
|
{textNode}
|
||||||
|
{imgNode}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (onClick != null) {
|
if (onClick != null) {
|
||||||
return (
|
return (
|
||||||
<AccessibleButton className="mx_BaseAvatar" onClick={onClick}>
|
<AccessibleButton className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||||
<img className="mx_BaseAvatar_image" src={imageUrl}
|
element='img'
|
||||||
onError={this.onError}
|
src={imageUrl}
|
||||||
width={width} height={height}
|
onClick={onClick}
|
||||||
title={title} alt=""
|
onError={this.onError}
|
||||||
{...otherProps} />
|
width={width} height={height}
|
||||||
</AccessibleButton>
|
title={title} alt=""
|
||||||
|
{...otherProps} />
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -14,17 +14,18 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var React = require("react");
|
import React from 'react';
|
||||||
var classNames = require('classnames');
|
import classNames from 'classnames';
|
||||||
var sdk = require("../../../index");
|
import sdk from '../../../index';
|
||||||
var Invite = require("../../../Invite");
|
import { getAddressType, inviteMultipleToRoom } from '../../../Invite';
|
||||||
var createRoom = require("../../../createRoom");
|
import createRoom from '../../../createRoom';
|
||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
var DMRoomMap = require('../../../utils/DMRoomMap');
|
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||||
var rate_limited_func = require("../../../ratelimitedfunc");
|
import rate_limited_func from '../../../ratelimitedfunc';
|
||||||
var dis = require("../../../dispatcher");
|
import dis from '../../../dispatcher';
|
||||||
var Modal = require('../../../Modal');
|
import Modal from '../../../Modal';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
import q from 'q';
|
||||||
|
|
||||||
const TRUNCATE_QUERY_LIST = 40;
|
const TRUNCATE_QUERY_LIST = 40;
|
||||||
|
|
||||||
|
@ -186,13 +187,17 @@ module.exports = React.createClass({
|
||||||
// If the query isn't a user we know about, but is a
|
// If the query isn't a user we know about, but is a
|
||||||
// valid address, add an entry for that
|
// valid address, add an entry for that
|
||||||
if (queryList.length == 0) {
|
if (queryList.length == 0) {
|
||||||
const addrType = Invite.getAddressType(query);
|
const addrType = getAddressType(query);
|
||||||
if (addrType !== null) {
|
if (addrType !== null) {
|
||||||
queryList.push({
|
queryList[0] = {
|
||||||
addressType: addrType,
|
addressType: addrType,
|
||||||
address: query,
|
address: query,
|
||||||
isKnown: false,
|
isKnown: false,
|
||||||
});
|
};
|
||||||
|
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||||
|
if (addrType == 'email') {
|
||||||
|
this._lookupThreepid(addrType, query).done();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -212,6 +217,7 @@ module.exports = React.createClass({
|
||||||
inviteList: inviteList,
|
inviteList: inviteList,
|
||||||
queryList: [],
|
queryList: [],
|
||||||
});
|
});
|
||||||
|
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -229,6 +235,7 @@ module.exports = React.createClass({
|
||||||
inviteList: inviteList,
|
inviteList: inviteList,
|
||||||
queryList: [],
|
queryList: [],
|
||||||
});
|
});
|
||||||
|
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||||
},
|
},
|
||||||
|
|
||||||
_getDirectMessageRoom: function(addr) {
|
_getDirectMessageRoom: function(addr) {
|
||||||
|
@ -266,7 +273,7 @@ module.exports = React.createClass({
|
||||||
if (this.props.roomId) {
|
if (this.props.roomId) {
|
||||||
// Invite new user to a room
|
// Invite new user to a room
|
||||||
var self = this;
|
var self = this;
|
||||||
Invite.inviteMultipleToRoom(this.props.roomId, addrTexts)
|
inviteMultipleToRoom(this.props.roomId, addrTexts)
|
||||||
.then(function(addrs) {
|
.then(function(addrs) {
|
||||||
var room = MatrixClientPeg.get().getRoom(self.props.roomId);
|
var room = MatrixClientPeg.get().getRoom(self.props.roomId);
|
||||||
return self._showAnyInviteErrors(addrs, room);
|
return self._showAnyInviteErrors(addrs, room);
|
||||||
|
@ -300,7 +307,7 @@ module.exports = React.createClass({
|
||||||
var room;
|
var room;
|
||||||
createRoom().then(function(roomId) {
|
createRoom().then(function(roomId) {
|
||||||
room = MatrixClientPeg.get().getRoom(roomId);
|
room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
return Invite.inviteMultipleToRoom(roomId, addrTexts);
|
return inviteMultipleToRoom(roomId, addrTexts);
|
||||||
})
|
})
|
||||||
.then(function(addrs) {
|
.then(function(addrs) {
|
||||||
return self._showAnyInviteErrors(addrs, room);
|
return self._showAnyInviteErrors(addrs, room);
|
||||||
|
@ -380,7 +387,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_isDmChat: function(addrs) {
|
_isDmChat: function(addrs) {
|
||||||
if (addrs.length === 1 && Invite.getAddressType(addrs[0]) === "mx" && !this.props.roomId) {
|
if (addrs.length === 1 && getAddressType(addrs[0]) === "mx" && !this.props.roomId) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
|
@ -408,7 +415,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_addInputToList: function() {
|
_addInputToList: function() {
|
||||||
const addressText = this.refs.textinput.value.trim();
|
const addressText = this.refs.textinput.value.trim();
|
||||||
const addrType = Invite.getAddressType(addressText);
|
const addrType = getAddressType(addressText);
|
||||||
const addrObj = {
|
const addrObj = {
|
||||||
addressType: addrType,
|
addressType: addrType,
|
||||||
address: addressText,
|
address: addressText,
|
||||||
|
@ -432,9 +439,45 @@ module.exports = React.createClass({
|
||||||
inviteList: inviteList,
|
inviteList: inviteList,
|
||||||
queryList: [],
|
queryList: [],
|
||||||
});
|
});
|
||||||
|
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||||
return inviteList;
|
return inviteList;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_lookupThreepid: function(medium, address) {
|
||||||
|
let cancelled = false;
|
||||||
|
// Note that we can't safely remove this after we're done
|
||||||
|
// because we don't know that it's the same one, so we just
|
||||||
|
// leave it: it's replacing the old one each time so it's
|
||||||
|
// not like they leak.
|
||||||
|
this._cancelThreepidLookup = function() {
|
||||||
|
cancelled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait a bit to let the user finish typing
|
||||||
|
return q.delay(500).then(() => {
|
||||||
|
if (cancelled) return null;
|
||||||
|
return MatrixClientPeg.get().lookupThreePid(medium, address);
|
||||||
|
}).then((res) => {
|
||||||
|
if (res === null || !res.mxid) return null;
|
||||||
|
if (cancelled) return null;
|
||||||
|
|
||||||
|
return MatrixClientPeg.get().getProfileInfo(res.mxid);
|
||||||
|
}).then((res) => {
|
||||||
|
if (res === null) return null;
|
||||||
|
if (cancelled) return null;
|
||||||
|
this.setState({
|
||||||
|
queryList: [{
|
||||||
|
// an InviteAddressType
|
||||||
|
addressType: medium,
|
||||||
|
address: address,
|
||||||
|
displayName: res.displayname,
|
||||||
|
avatarMxc: res.avatar_url,
|
||||||
|
isKnown: true,
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||||
|
|
||||||
function DeviceListEntry(props) {
|
function DeviceListEntry(props) {
|
||||||
const {userId, device} = props;
|
const {userId, device} = props;
|
||||||
|
@ -118,7 +119,19 @@ export default React.createClass({
|
||||||
</h4>
|
</h4>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
warning = <h4>We strongly recommend you verify them before continuing.</h4>;
|
warning = (
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
This means there is no guarantee that the devices belong
|
||||||
|
to a rightful user of the room.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We recommend you go through the verification process
|
||||||
|
for each device before continuing, but you can resend
|
||||||
|
the message without verifying if you prefer.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
@ -127,15 +140,16 @@ export default React.createClass({
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title='Room contains unknown devices'
|
title='Room contains unknown devices'
|
||||||
>
|
>
|
||||||
<div className="mx_Dialog_content">
|
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
|
||||||
<h4>
|
<h4>
|
||||||
This room contains unknown devices which have not been
|
This room contains unknown devices which have not been
|
||||||
verified.
|
verified.
|
||||||
</h4>
|
</h4>
|
||||||
{ warning }
|
{ warning }
|
||||||
Unknown devices:
|
Unknown devices:
|
||||||
|
|
||||||
<UnknownDeviceList devices={this.props.devices} />
|
<UnknownDeviceList devices={this.props.devices} />
|
||||||
</div>
|
</GeminiScrollbar>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button className="mx_Dialog_primary" autoFocus={ true }
|
<button className="mx_Dialog_primary" autoFocus={ true }
|
||||||
onClick={ this.props.onFinished } >
|
onClick={ this.props.onFinished } >
|
||||||
|
|
|
@ -94,14 +94,14 @@ export default React.createClass({
|
||||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
|
||||||
|
const nameClasses = classNames({
|
||||||
|
"mx_AddressTile_name": true,
|
||||||
|
"mx_AddressTile_justified": this.props.justified,
|
||||||
|
});
|
||||||
|
|
||||||
let info;
|
let info;
|
||||||
let error = false;
|
let error = false;
|
||||||
if (address.addressType === "mx" && address.isKnown) {
|
if (address.addressType === "mx" && address.isKnown) {
|
||||||
const nameClasses = classNames({
|
|
||||||
"mx_AddressTile_name": true,
|
|
||||||
"mx_AddressTile_justified": this.props.justified,
|
|
||||||
});
|
|
||||||
|
|
||||||
const idClasses = classNames({
|
const idClasses = classNames({
|
||||||
"mx_AddressTile_id": true,
|
"mx_AddressTile_id": true,
|
||||||
"mx_AddressTile_justified": this.props.justified,
|
"mx_AddressTile_justified": this.props.justified,
|
||||||
|
@ -123,13 +123,21 @@ export default React.createClass({
|
||||||
<div className={unknownMxClasses}>{ this.props.address.address }</div>
|
<div className={unknownMxClasses}>{ this.props.address.address }</div>
|
||||||
);
|
);
|
||||||
} else if (address.addressType === "email") {
|
} else if (address.addressType === "email") {
|
||||||
var emailClasses = classNames({
|
const emailClasses = classNames({
|
||||||
"mx_AddressTile_email": true,
|
"mx_AddressTile_email": true,
|
||||||
"mx_AddressTile_justified": this.props.justified,
|
"mx_AddressTile_justified": this.props.justified,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let nameNode = null;
|
||||||
|
if (address.displayName) {
|
||||||
|
nameNode = <div className={nameClasses}>{ address.displayName }</div>
|
||||||
|
}
|
||||||
|
|
||||||
info = (
|
info = (
|
||||||
<div className={emailClasses}>{ address.address }</div>
|
<div className="mx_AddressTile_mx">
|
||||||
|
<div className={emailClasses}>{ address.address }</div>
|
||||||
|
{nameNode}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
error = true;
|
error = true;
|
||||||
|
|
|
@ -44,8 +44,8 @@ module.exports = React.createClass({
|
||||||
teams: React.PropTypes.arrayOf(React.PropTypes.shape({
|
teams: React.PropTypes.arrayOf(React.PropTypes.shape({
|
||||||
// The displayed name of the team
|
// The displayed name of the team
|
||||||
"name": React.PropTypes.string,
|
"name": React.PropTypes.string,
|
||||||
// The suffix with which every team email address ends
|
// The domain of team email addresses
|
||||||
"emailSuffix": React.PropTypes.string,
|
"domain": React.PropTypes.string,
|
||||||
})).required,
|
})).required,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
@ -117,9 +117,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_doSubmit: function() {
|
_doSubmit: function() {
|
||||||
let email = this.refs.email.value.trim();
|
let email = this.refs.email.value.trim();
|
||||||
if (this.state.selectedTeam) {
|
|
||||||
email += "@" + this.state.selectedTeam.emailSuffix;
|
|
||||||
}
|
|
||||||
var promise = this.props.onRegisterClick({
|
var promise = this.props.onRegisterClick({
|
||||||
username: this.refs.username.value.trim() || this.props.guestUsername,
|
username: this.refs.username.value.trim() || this.props.guestUsername,
|
||||||
password: this.refs.password.value.trim(),
|
password: this.refs.password.value.trim(),
|
||||||
|
@ -134,25 +131,6 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onSelectTeam: function(teamIndex) {
|
|
||||||
let team = this._getSelectedTeam(teamIndex);
|
|
||||||
if (team) {
|
|
||||||
this.refs.email.value = this.refs.email.value.split("@")[0];
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
selectedTeam: team,
|
|
||||||
showSupportEmail: teamIndex === "other",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_getSelectedTeam: function(teamIndex) {
|
|
||||||
if (this.props.teamsConfig &&
|
|
||||||
this.props.teamsConfig.teams[teamIndex]) {
|
|
||||||
return this.props.teamsConfig.teams[teamIndex];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if all fields were valid last time
|
* Returns true if all fields were valid last time
|
||||||
* they were validated.
|
* they were validated.
|
||||||
|
@ -167,20 +145,36 @@ module.exports = React.createClass({
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_isUniEmail: function(email) {
|
||||||
|
return email.endsWith('.ac.uk') || email.endsWith('.edu');
|
||||||
|
},
|
||||||
|
|
||||||
validateField: function(field_id) {
|
validateField: function(field_id) {
|
||||||
var pwd1 = this.refs.password.value.trim();
|
var pwd1 = this.refs.password.value.trim();
|
||||||
var pwd2 = this.refs.passwordConfirm.value.trim();
|
var pwd2 = this.refs.passwordConfirm.value.trim();
|
||||||
|
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case FIELD_EMAIL:
|
case FIELD_EMAIL:
|
||||||
let email = this.refs.email.value;
|
const email = this.refs.email.value;
|
||||||
if (this.props.teamsConfig) {
|
if (this.props.teamsConfig && this._isUniEmail(email)) {
|
||||||
let team = this.state.selectedTeam;
|
const matchingTeam = this.props.teamsConfig.teams.find(
|
||||||
if (team) {
|
(team) => {
|
||||||
email = email + "@" + team.emailSuffix;
|
return email.split('@').pop() === team.domain;
|
||||||
}
|
}
|
||||||
|
) || null;
|
||||||
|
this.setState({
|
||||||
|
selectedTeam: matchingTeam,
|
||||||
|
showSupportEmail: !matchingTeam,
|
||||||
|
});
|
||||||
|
this.props.onTeamSelected(matchingTeam);
|
||||||
|
} else {
|
||||||
|
this.props.onTeamSelected(null);
|
||||||
|
this.setState({
|
||||||
|
selectedTeam: null,
|
||||||
|
showSupportEmail: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let valid = email === '' || Email.looksValid(email);
|
const valid = email === '' || Email.looksValid(email);
|
||||||
this.markFieldValid(field_id, valid, "RegistrationForm.ERR_EMAIL_INVALID");
|
this.markFieldValid(field_id, valid, "RegistrationForm.ERR_EMAIL_INVALID");
|
||||||
break;
|
break;
|
||||||
case FIELD_USERNAME:
|
case FIELD_USERNAME:
|
||||||
|
@ -260,61 +254,35 @@ module.exports = React.createClass({
|
||||||
return cls;
|
return cls;
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderEmailInputSuffix: function() {
|
|
||||||
let suffix = null;
|
|
||||||
if (!this.state.selectedTeam) {
|
|
||||||
return suffix;
|
|
||||||
}
|
|
||||||
let team = this.state.selectedTeam;
|
|
||||||
if (team) {
|
|
||||||
suffix = "@" + team.emailSuffix;
|
|
||||||
}
|
|
||||||
return suffix;
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
var emailSection, teamSection, teamAdditionSupport, registerButton;
|
var emailSection, belowEmailSection, registerButton;
|
||||||
if (this.props.showEmail) {
|
if (this.props.showEmail) {
|
||||||
let emailSuffix = this._renderEmailInputSuffix();
|
|
||||||
emailSection = (
|
emailSection = (
|
||||||
<div>
|
<input type="text" ref="email"
|
||||||
<input type="text" ref="email"
|
autoFocus={true} placeholder="Email address (optional)"
|
||||||
autoFocus={true} placeholder="Email address (optional)"
|
defaultValue={this.props.defaultEmail}
|
||||||
defaultValue={this.props.defaultEmail}
|
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
|
||||||
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
|
onBlur={function() {self.validateField(FIELD_EMAIL);}}
|
||||||
onBlur={function() {self.validateField(FIELD_EMAIL);}}
|
value={self.state.email}/>
|
||||||
value={self.state.email}/>
|
|
||||||
{emailSuffix ? <input className="mx_Login_field" value={emailSuffix} disabled/> : null }
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
if (this.props.teamsConfig) {
|
if (this.props.teamsConfig) {
|
||||||
teamSection = (
|
|
||||||
<select
|
|
||||||
defaultValue="-1"
|
|
||||||
className="mx_Login_field"
|
|
||||||
onBlur={function() {self.validateField(FIELD_EMAIL);}}
|
|
||||||
onChange={function(ev) {self.onSelectTeam(ev.target.value);}}
|
|
||||||
>
|
|
||||||
<option key="-1" value="-1">No team</option>
|
|
||||||
{this.props.teamsConfig.teams.map((t, index) => {
|
|
||||||
return (
|
|
||||||
<option key={index} value={index}>
|
|
||||||
{t.name}
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<option key="-2" value="other">Other</option>
|
|
||||||
</select>
|
|
||||||
);
|
|
||||||
if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) {
|
if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) {
|
||||||
teamAdditionSupport = (
|
belowEmailSection = (
|
||||||
<span>
|
<p className="mx_Login_support">
|
||||||
If your team is not listed, email
|
Sorry, but your university is not registered with us just yet.
|
||||||
|
Email us on
|
||||||
<a href={"mailto:" + this.props.teamsConfig.supportEmail}>
|
<a href={"mailto:" + this.props.teamsConfig.supportEmail}>
|
||||||
{this.props.teamsConfig.supportEmail}
|
{this.props.teamsConfig.supportEmail}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
to get your university signed up. Or continue to register with Riot to enjoy our open source platform.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
} else if (this.state.selectedTeam) {
|
||||||
|
belowEmailSection = (
|
||||||
|
<p className="mx_Login_support">
|
||||||
|
You are registering with {this.state.selectedTeam.name}
|
||||||
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -333,11 +301,8 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<form onSubmit={this.onSubmit}>
|
<form onSubmit={this.onSubmit}>
|
||||||
{teamSection}
|
|
||||||
{teamAdditionSupport}
|
|
||||||
<br />
|
|
||||||
{emailSection}
|
{emailSection}
|
||||||
<br />
|
{belowEmailSection}
|
||||||
<input type="text" ref="username"
|
<input type="text" ref="username"
|
||||||
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
|
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
|
||||||
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
|
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
|
||||||
|
|
|
@ -35,7 +35,7 @@ export function onSendMessageFailed(err, room) {
|
||||||
Modal.createDialog(UnknownDeviceDialog, {
|
Modal.createDialog(UnknownDeviceDialog, {
|
||||||
devices: err.devices,
|
devices: err.devices,
|
||||||
room: room,
|
room: room,
|
||||||
});
|
}, "mx_Dialog_unknownDevice");
|
||||||
}
|
}
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'message_send_failed',
|
action: 'message_send_failed',
|
||||||
|
|
|
@ -170,15 +170,15 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
let title;
|
let title;
|
||||||
if (this.props.timestamp) {
|
if (this.props.timestamp) {
|
||||||
let suffix = " (" + this.props.member.userId + ")";
|
const prefix = "Seen by " + this.props.member.userId + " at ";
|
||||||
let ts = new Date(this.props.timestamp);
|
let ts = new Date(this.props.timestamp);
|
||||||
if (this.props.showFullTimestamp) {
|
if (this.props.showFullTimestamp) {
|
||||||
// "15/12/2016, 7:05:45 PM (@alice:matrix.org)"
|
// "15/12/2016, 7:05:45 PM (@alice:matrix.org)"
|
||||||
title = ts.toLocaleString() + suffix;
|
title = prefix + ts.toLocaleString();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// "7:05:45 PM (@alice:matrix.org)"
|
// "7:05:45 PM (@alice:matrix.org)"
|
||||||
title = ts.toLocaleTimeString() + suffix;
|
title = prefix + ts.toLocaleTimeString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,9 +192,9 @@ module.exports = React.createClass({
|
||||||
width={14} height={14} resizeMethod="crop"
|
width={14} height={14} resizeMethod="crop"
|
||||||
style={style}
|
style={style}
|
||||||
title={title}
|
title={title}
|
||||||
|
onClick={this.props.onClick}
|
||||||
/>
|
/>
|
||||||
</Velociraptor>
|
</Velociraptor>
|
||||||
);
|
);
|
||||||
/* onClick={this.props.onClick} */
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -301,8 +301,8 @@ module.exports = React.createClass({
|
||||||
var rightPanel_buttons;
|
var rightPanel_buttons;
|
||||||
if (this.props.collapsedRhs) {
|
if (this.props.collapsedRhs) {
|
||||||
rightPanel_buttons =
|
rightPanel_buttons =
|
||||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title="<">
|
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title="Show panel">
|
||||||
<TintableSvg src="img/minimise.svg" width="10" height="16"/>
|
<TintableSvg src="img/maximise.svg" width="10" height="16"/>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -146,7 +146,7 @@ module.exports = React.createClass({
|
||||||
<div>
|
<div>
|
||||||
<div className="mx_RoomPreviewBar_join_text">
|
<div className="mx_RoomPreviewBar_join_text">
|
||||||
You are trying to access { name }.<br/>
|
You are trying to access { name }.<br/>
|
||||||
Would you like to <a onClick={ this.props.onJoinClick }>join</a> in order to participate in the discussion?
|
<a onClick={ this.props.onJoinClick }><b>Click here</b></a> to join the discussion!
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -50,7 +50,7 @@ export function decryptMegolmKeyFile(data, password) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ciphertextLength = body.length-(1+16+16+4+32);
|
const ciphertextLength = body.length-(1+16+16+4+32);
|
||||||
if (body.length < 0) {
|
if (ciphertextLength < 0) {
|
||||||
throw new Error('Invalid file: too short');
|
throw new Error('Invalid file: too short');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,19 +102,19 @@ export function decryptMegolmKeyFile(data, password) {
|
||||||
*/
|
*/
|
||||||
export function encryptMegolmKeyFile(data, password, options) {
|
export function encryptMegolmKeyFile(data, password, options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
const kdf_rounds = options.kdf_rounds || 100000;
|
const kdf_rounds = options.kdf_rounds || 500000;
|
||||||
|
|
||||||
const salt = new Uint8Array(16);
|
const salt = new Uint8Array(16);
|
||||||
window.crypto.getRandomValues(salt);
|
window.crypto.getRandomValues(salt);
|
||||||
|
|
||||||
// clear bit 63 of the salt to stop us hitting the 64-bit counter boundary
|
|
||||||
// (which would mean we wouldn't be able to decrypt on Android). The loss
|
|
||||||
// of a single bit of salt is a price we have to pay.
|
|
||||||
salt[9] &= 0x7f;
|
|
||||||
|
|
||||||
const iv = new Uint8Array(16);
|
const iv = new Uint8Array(16);
|
||||||
window.crypto.getRandomValues(iv);
|
window.crypto.getRandomValues(iv);
|
||||||
|
|
||||||
|
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
|
||||||
|
// (which would mean we wouldn't be able to decrypt on Android). The loss
|
||||||
|
// of a single bit of iv is a price we have to pay.
|
||||||
|
iv[9] &= 0x7f;
|
||||||
|
|
||||||
return deriveKeys(salt, kdf_rounds, password).then((keys) => {
|
return deriveKeys(salt, kdf_rounds, password).then((keys) => {
|
||||||
const [aes_key, hmac_key] = keys;
|
const [aes_key, hmac_key] = keys;
|
||||||
|
|
||||||
|
@ -164,6 +164,7 @@ export function encryptMegolmKeyFile(data, password, options) {
|
||||||
* @return {Promise<[CryptoKey, CryptoKey]>} promise for [aes key, hmac key]
|
* @return {Promise<[CryptoKey, CryptoKey]>} promise for [aes key, hmac key]
|
||||||
*/
|
*/
|
||||||
function deriveKeys(salt, iterations, password) {
|
function deriveKeys(salt, iterations, password) {
|
||||||
|
const start = new Date();
|
||||||
return subtleCrypto.importKey(
|
return subtleCrypto.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
new TextEncoder().encode(password),
|
new TextEncoder().encode(password),
|
||||||
|
@ -182,6 +183,9 @@ function deriveKeys(salt, iterations, password) {
|
||||||
512
|
512
|
||||||
);
|
);
|
||||||
}).then((keybits) => {
|
}).then((keybits) => {
|
||||||
|
const now = new Date();
|
||||||
|
console.log("E2e import/export: deriveKeys took " + (now - start) + "ms");
|
||||||
|
|
||||||
const aes_key = keybits.slice(0, 32);
|
const aes_key = keybits.slice(0, 32);
|
||||||
const hmac_key = keybits.slice(32);
|
const hmac_key = keybits.slice(32);
|
||||||
|
|
||||||
|
|
|
@ -42,17 +42,12 @@ describe('RoomView', function () {
|
||||||
it('resolves a room alias to a room id', function (done) {
|
it('resolves a room alias to a room id', function (done) {
|
||||||
peg.get().getRoomIdForAlias.returns(q({room_id: "!randomcharacters:aser.ver"}));
|
peg.get().getRoomIdForAlias.returns(q({room_id: "!randomcharacters:aser.ver"}));
|
||||||
|
|
||||||
var onRoomIdResolved = sinon.spy();
|
function onRoomIdResolved(room_id) {
|
||||||
|
expect(room_id).toEqual("!randomcharacters:aser.ver");
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
ReactDOM.render(<RoomView roomAddress="#alias:ser.ver" onRoomIdResolved={onRoomIdResolved} />, parentDiv);
|
ReactDOM.render(<RoomView roomAddress="#alias:ser.ver" onRoomIdResolved={onRoomIdResolved} />, parentDiv);
|
||||||
|
|
||||||
process.nextTick(function() {
|
|
||||||
// These expect()s don't read very well and don't give very good failure
|
|
||||||
// messages, but expect's toHaveBeenCalled only takes an expect spy object,
|
|
||||||
// not a sinon spy object.
|
|
||||||
expect(onRoomIdResolved.called).toExist();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('joins by alias if given an alias', function (done) {
|
it('joins by alias if given an alias', function (done) {
|
||||||
|
|
|
@ -73,6 +73,7 @@ var Tester = React.createClass({
|
||||||
|
|
||||||
/* returns a promise which will resolve when the fill happens */
|
/* returns a promise which will resolve when the fill happens */
|
||||||
awaitFill: function(dir) {
|
awaitFill: function(dir) {
|
||||||
|
console.log("ScrollPanel Tester: awaiting " + dir + " fill");
|
||||||
var defer = q.defer();
|
var defer = q.defer();
|
||||||
this._fillDefers[dir] = defer;
|
this._fillDefers[dir] = defer;
|
||||||
return defer.promise;
|
return defer.promise;
|
||||||
|
@ -80,7 +81,7 @@ var Tester = React.createClass({
|
||||||
|
|
||||||
_onScroll: function(ev) {
|
_onScroll: function(ev) {
|
||||||
var st = ev.target.scrollTop;
|
var st = ev.target.scrollTop;
|
||||||
console.log("Scroll event; scrollTop: " + st);
|
console.log("ScrollPanel Tester: scroll event; scrollTop: " + st);
|
||||||
this.lastScrollEvent = st;
|
this.lastScrollEvent = st;
|
||||||
|
|
||||||
var d = this._scrollDefer;
|
var d = this._scrollDefer;
|
||||||
|
@ -159,10 +160,29 @@ describe('ScrollPanel', function() {
|
||||||
scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass(
|
scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass(
|
||||||
tester, "gm-scroll-view");
|
tester, "gm-scroll-view");
|
||||||
|
|
||||||
// wait for a browser tick to let the initial paginates complete
|
// we need to make sure we don't call done() until q has finished
|
||||||
setTimeout(function() {
|
// running the completion handlers from the fill requests. We can't
|
||||||
done();
|
// just use .done(), because that will end up ahead of those handlers
|
||||||
}, 0);
|
// in the queue. We can't use window.setTimeout(0), because that also might
|
||||||
|
// run ahead of those handlers.
|
||||||
|
const sp = tester.scrollPanel();
|
||||||
|
let retriesRemaining = 1;
|
||||||
|
const awaitReady = function() {
|
||||||
|
return q().then(() => {
|
||||||
|
if (sp._pendingFillRequests.b === false &&
|
||||||
|
sp._pendingFillRequests.f === false
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retriesRemaining == 0) {
|
||||||
|
throw new Error("fillRequests did not complete");
|
||||||
|
}
|
||||||
|
retriesRemaining--;
|
||||||
|
return awaitReady();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
awaitReady().done(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function() {
|
afterEach(function() {
|
||||||
|
|
|
@ -99,7 +99,11 @@ describe('TimelinePanel', function() {
|
||||||
// the document so that we can interact with it properly.
|
// the document so that we can interact with it properly.
|
||||||
parentDiv = document.createElement('div');
|
parentDiv = document.createElement('div');
|
||||||
parentDiv.style.width = '800px';
|
parentDiv.style.width = '800px';
|
||||||
parentDiv.style.height = '600px';
|
|
||||||
|
// This has to be slightly carefully chosen. We expect to have to do
|
||||||
|
// exactly one pagination to fill it.
|
||||||
|
parentDiv.style.height = '500px';
|
||||||
|
|
||||||
parentDiv.style.overflow = 'hidden';
|
parentDiv.style.overflow = 'hidden';
|
||||||
document.body.appendChild(parentDiv);
|
document.body.appendChild(parentDiv);
|
||||||
});
|
});
|
||||||
|
@ -235,7 +239,7 @@ describe('TimelinePanel', function() {
|
||||||
expect(client.paginateEventTimeline.callCount).toEqual(0);
|
expect(client.paginateEventTimeline.callCount).toEqual(0);
|
||||||
done();
|
done();
|
||||||
}, 0);
|
}, 0);
|
||||||
}, 0);
|
}, 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should let you scroll down to the bottom after you've scrolled up", function(done) {
|
it("should let you scroll down to the bottom after you've scrolled up", function(done) {
|
||||||
|
|
|
@ -14,7 +14,15 @@ var MatrixEvent = jssdk.MatrixEvent;
|
||||||
*/
|
*/
|
||||||
export function beforeEach(context) {
|
export function beforeEach(context) {
|
||||||
var desc = context.currentTest.fullTitle();
|
var desc = context.currentTest.fullTitle();
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
|
// this puts a mark in the chrome devtools timeline, which can help
|
||||||
|
// figure out what's been going on.
|
||||||
|
if (console.timeStamp) {
|
||||||
|
console.timeStamp(desc);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(desc);
|
console.log(desc);
|
||||||
console.log(new Array(1 + desc.length).join("="));
|
console.log(new Array(1 + desc.length).join("="));
|
||||||
};
|
};
|
||||||
|
|
|
@ -75,6 +75,16 @@ describe('MegolmExportEncryption', function() {
|
||||||
.toThrow('Trailer line not found');
|
.toThrow('Trailer line not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle a too-short body', function() {
|
||||||
|
const input=stringToArray(`-----BEGIN MEGOLM SESSION DATA-----
|
||||||
|
AXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx
|
||||||
|
cissyYBxjsfsAn
|
||||||
|
-----END MEGOLM SESSION DATA-----
|
||||||
|
`);
|
||||||
|
expect(()=>{MegolmExportEncryption.decryptMegolmKeyFile(input, '')})
|
||||||
|
.toThrow('Invalid file: too short');
|
||||||
|
});
|
||||||
|
|
||||||
it('should decrypt a range of inputs', function(done) {
|
it('should decrypt a range of inputs', function(done) {
|
||||||
function next(i) {
|
function next(i) {
|
||||||
if (i >= TEST_VECTORS.length) {
|
if (i >= TEST_VECTORS.length) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue