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
|
||||
* 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"],
|
||||
"new-cap": ["warn"],
|
||||
"key-spacing": ["warn"],
|
||||
|
|
|
@ -165,6 +165,14 @@ module.exports = function (config) {
|
|||
},
|
||||
devtool: 'inline-source-map',
|
||||
},
|
||||
|
||||
webpackMiddleware: {
|
||||
stats: {
|
||||
// don't fill the console up with a mahoosive list of modules
|
||||
chunks: false,
|
||||
},
|
||||
},
|
||||
|
||||
browserNoActivityTimeout: 15000,
|
||||
});
|
||||
};
|
||||
|
|
155
src/Markdown.js
155
src/Markdown.js
|
@ -15,110 +15,143 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
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
|
||||
* it's plain text.
|
||||
*/
|
||||
export default class Markdown {
|
||||
constructor(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() {
|
||||
// we determine if the message requires markdown by
|
||||
// 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;
|
||||
const walker = this.parsed.walker();
|
||||
|
||||
function setNotPlain() {
|
||||
is_plain = false;
|
||||
let ev;
|
||||
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;
|
||||
}
|
||||
|
||||
const dummy_renderer = new commonmark.HtmlRenderer();
|
||||
for (const k of Object.keys(commonmark.HtmlRenderer.prototype)) {
|
||||
dummy_renderer[k] = setNotPlain;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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
|
||||
// bare text: it's a single line of text and so should be
|
||||
// 'inline', rather than unnecessarily wrapped in its own
|
||||
// p tag. If, however, we have multiple nodes, each gets
|
||||
// its own p tag to keep them as separate paragraphs.
|
||||
var par = node;
|
||||
while (par.parent) {
|
||||
par = par.parent;
|
||||
}
|
||||
if (par.firstChild != par.lastChild) {
|
||||
if (is_multi_line(node)) {
|
||||
real_paragraph.call(this, node, entering);
|
||||
}
|
||||
};
|
||||
|
||||
var parsed = this.parser.parse(this.input);
|
||||
var rendered = this.renderer.render(parsed);
|
||||
renderer.html_inline = html_if_tag_allowed;
|
||||
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;
|
||||
|
||||
return rendered;
|
||||
if (isMultiLine) this.cr();
|
||||
html_if_tag_allowed.call(this, node);
|
||||
if (isMultiLine) this.cr();
|
||||
}
|
||||
|
||||
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() {
|
||||
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
|
||||
// escaping function, which causes messages to be entity encoded,
|
||||
// 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.
|
||||
this.lit(s);
|
||||
};
|
||||
|
||||
this.renderer.paragraph = function(node, entering) {
|
||||
// If there is only one top level node, just return the
|
||||
// bare text: it's a single line of text and so should be
|
||||
// 'inline', rather than unnecessarily wrapped in its own
|
||||
// p tag. If, however, we have multiple nodes, each gets
|
||||
// its own p tag to keep them as separate paragraphs.
|
||||
var par = node;
|
||||
while (par.parent) {
|
||||
node = par;
|
||||
par = par.parent;
|
||||
}
|
||||
if (node != par.lastChild) {
|
||||
if (!entering) {
|
||||
renderer.paragraph = function(node, entering) {
|
||||
// as with toHTML, only append lines to paragraphs if there are
|
||||
// multiple paragraphs
|
||||
if (is_multi_line(node)) {
|
||||
if (!entering && node.next) {
|
||||
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);
|
||||
var rendered = this.renderer.render(parsed);
|
||||
|
||||
this.renderer.paragraph = real_paragraph;
|
||||
|
||||
return rendered;
|
||||
return renderer.render(this.parsed);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -177,7 +177,7 @@ class ModalManager {
|
|||
|
||||
var modal = this._modals[0];
|
||||
var dialog = (
|
||||
<div className={"mx_Dialog_wrapper " + modal.className}>
|
||||
<div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '') }>
|
||||
<div className="mx_Dialog">
|
||||
{modal.elem}
|
||||
</div>
|
||||
|
|
|
@ -34,7 +34,7 @@ module.exports = {
|
|||
Modal.createDialog(UnknownDeviceDialog, {
|
||||
devices: err.devices,
|
||||
room: MatrixClientPeg.get().getRoom(event.getRoomId()),
|
||||
});
|
||||
}, "mx_Dialog_unknownDevice");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
//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;
|
||||
}
|
||||
//console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
|
||||
}
|
||||
self.children[c.key] = old;
|
||||
} else {
|
||||
// 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");
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -71,7 +71,7 @@ export default React.createClass({
|
|||
return this.props.matrixClient.exportRoomKeys();
|
||||
}).then((k) => {
|
||||
return MegolmExportEncryption.encryptMegolmKeyFile(
|
||||
JSON.stringify(k), passphrase
|
||||
JSON.stringify(k), passphrase,
|
||||
);
|
||||
}).then((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() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
|
||||
|
||||
const disableForm = (this.state.phase === PHASE_EXPORTING);
|
||||
|
||||
|
@ -159,10 +164,9 @@ export default React.createClass({
|
|||
<input className='mx_Dialog_primary' type='submit' value='Export'
|
||||
disabled={disableForm}
|
||||
/>
|
||||
<AccessibleButton element='button' onClick={this.props.onFinished}
|
||||
disabled={disableForm}>
|
||||
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||
Cancel
|
||||
</AccessibleButton>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -80,7 +80,7 @@ export default React.createClass({
|
|||
|
||||
return readFileAsArrayBuffer(file).then((arrayBuffer) => {
|
||||
return MegolmExportEncryption.decryptMegolmKeyFile(
|
||||
arrayBuffer, passphrase
|
||||
arrayBuffer, passphrase,
|
||||
);
|
||||
}).then((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() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
|
||||
|
||||
const disableForm = (this.state.phase !== PHASE_EDIT);
|
||||
|
||||
|
@ -158,10 +163,9 @@ export default React.createClass({
|
|||
<input className='mx_Dialog_primary' type='submit' value='Import'
|
||||
disabled={!this.state.enableSubmit || disableForm}
|
||||
/>
|
||||
<AccessibleButton element='button' onClick={this.props.onFinished}
|
||||
disabled={disableForm}>
|
||||
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||
Cancel
|
||||
</AccessibleButton>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
|
|
|
@ -105,6 +105,7 @@ var FilePanel = React.createClass({
|
|||
showUrlPreview = { false }
|
||||
tileShape="file_grid"
|
||||
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}
|
||||
collapsedRhs={this.props.collapse_rhs}
|
||||
enableLabs={this.props.config.enableLabs}
|
||||
referralBaseUrl={this.props.config.referralBaseUrl}
|
||||
/>;
|
||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.sideOpacity}/>;
|
||||
break;
|
||||
|
|
|
@ -1055,12 +1055,13 @@ module.exports = React.createClass({
|
|||
sessionId={this.state.register_session_id}
|
||||
idSid={this.state.register_id_sid}
|
||||
email={this.props.startingFragmentQueryParams.email}
|
||||
referrer={this.props.startingFragmentQueryParams.referrer}
|
||||
username={this.state.upgradeUsername}
|
||||
guestAccessToken={this.state.guestAccessToken}
|
||||
defaultHsUrl={this.getDefaultHsUrl()}
|
||||
defaultIsUrl={this.getDefaultIsUrl()}
|
||||
brand={this.props.config.brand}
|
||||
teamsConfig={this.props.config.teamsConfig}
|
||||
teamServerConfig={this.props.config.teamServerConfig}
|
||||
customHsUrl={this.getCurrentHsUrl()}
|
||||
customIsUrl={this.getCurrentIsUrl()}
|
||||
registrationUrl={this.props.registrationUrl}
|
||||
|
|
|
@ -48,6 +48,7 @@ var NotificationPanel = React.createClass({
|
|||
showUrlPreview = { false }
|
||||
opacity={ this.props.opacity }
|
||||
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
|
||||
// not displaying anything
|
||||
onHidden: React.PropTypes.func,
|
||||
|
||||
// callback for when the status bar is displaying something and should
|
||||
// be visible
|
||||
onVisible: React.PropTypes.func,
|
||||
|
@ -113,7 +114,9 @@ module.exports = React.createClass({
|
|||
clearTimeout(this.hideDebouncer);
|
||||
}
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
@ -238,7 +241,7 @@ module.exports = React.createClass({
|
|||
|
||||
if (othersCount > 0) {
|
||||
avatars.push(
|
||||
<span className="mx_RoomStatusBar_typingIndicatorRemaining">
|
||||
<span className="mx_RoomStatusBar_typingIndicatorRemaining" key="others">
|
||||
+{othersCount}
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -1332,12 +1332,14 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onStatusBarVisible: function() {
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
statusBarVisible: true,
|
||||
});
|
||||
},
|
||||
|
||||
onStatusBarHidden: function() {
|
||||
if (this.unmounted) return;
|
||||
this.setState({
|
||||
statusBarVisible: false,
|
||||
});
|
||||
|
@ -1507,13 +1509,14 @@ module.exports = React.createClass({
|
|||
});
|
||||
|
||||
var statusBar;
|
||||
let isStatusAreaExpanded = true;
|
||||
|
||||
if (ContentMessages.getCurrentUploads().length > 0) {
|
||||
var UploadBar = sdk.getComponent('structures.UploadBar');
|
||||
statusBar = <UploadBar room={this.state.room} />;
|
||||
} else if (!this.state.searchResults) {
|
||||
var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
|
||||
|
||||
isStatusAreaExpanded = this.state.statusBarVisible;
|
||||
statusBar = <RoomStatusBar
|
||||
room={this.state.room}
|
||||
tabComplete={this.tabComplete}
|
||||
|
@ -1683,7 +1686,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
let statusBarAreaClass = "mx_RoomView_statusArea mx_fadable";
|
||||
if (this.state.statusBarVisible) {
|
||||
if (isStatusAreaExpanded) {
|
||||
statusBarAreaClass += " mx_RoomView_statusArea_expanded";
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ var DEBUG_SCROLL = false;
|
|||
|
||||
// The amount of extra scroll distance to allow prior to unfilling.
|
||||
// See _getExcessHeight.
|
||||
const UNPAGINATION_PADDING = 1500;
|
||||
const UNPAGINATION_PADDING = 3000;
|
||||
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
|
||||
// many scroll events causing many unfilling requests.
|
||||
const UNFILL_REQUEST_DEBOUNCE_MS = 200;
|
||||
|
@ -570,7 +570,7 @@ module.exports = React.createClass({
|
|||
var boundingRect = node.getBoundingClientRect();
|
||||
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+")");
|
||||
|
||||
if(scrollDelta != 0) {
|
||||
|
@ -582,7 +582,7 @@ module.exports = React.createClass({
|
|||
_saveScrollState: function() {
|
||||
if (this.props.stickyBottom && this.isAtBottom()) {
|
||||
this.scrollState = { stuckAtBottom: true };
|
||||
debuglog("Saved scroll state", this.scrollState);
|
||||
debuglog("ScrollPanel: Saved scroll state", this.scrollState);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -601,12 +601,12 @@ module.exports = React.createClass({
|
|||
trackedScrollToken: node.dataset.scrollToken,
|
||||
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
|
||||
};
|
||||
debuglog("Saved scroll state", this.scrollState);
|
||||
debuglog("ScrollPanel: saved scroll state", this.scrollState);
|
||||
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() {
|
||||
|
@ -640,7 +640,7 @@ module.exports = React.createClass({
|
|||
this._lastSetScroll = scrollNode.scrollTop;
|
||||
}
|
||||
|
||||
debuglog("Set scrollTop:", scrollNode.scrollTop,
|
||||
debuglog("ScrollPanel: set scrollTop:", scrollNode.scrollTop,
|
||||
"requested:", scrollTop,
|
||||
"_lastSetScroll:", this._lastSetScroll);
|
||||
},
|
||||
|
|
|
@ -96,6 +96,9 @@ var TimelinePanel = React.createClass({
|
|||
|
||||
// shape property to be passed to EventTiles
|
||||
tileShape: React.PropTypes.string,
|
||||
|
||||
// placeholder text to use if the timeline is empty
|
||||
empty: React.PropTypes.string,
|
||||
},
|
||||
|
||||
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
|
||||
// live timeline, so that the arrival of new events triggers a
|
||||
// scroll.
|
||||
|
|
|
@ -104,6 +104,9 @@ module.exports = React.createClass({
|
|||
// True to show the 'labs' section of experimental features
|
||||
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
|
||||
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() {
|
||||
var client = MatrixClientPeg.get();
|
||||
|
||||
|
@ -857,6 +881,8 @@ module.exports = React.createClass({
|
|||
{accountJsx}
|
||||
</div>
|
||||
|
||||
{this._renderReferral()}
|
||||
|
||||
{notification_area}
|
||||
|
||||
{this._renderUserInterfaceSettings()}
|
||||
|
|
|
@ -25,6 +25,7 @@ var ServerConfig = require("../../views/login/ServerConfig");
|
|||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var RegistrationForm = require("../../views/login/RegistrationForm");
|
||||
var CaptchaForm = require("../../views/login/CaptchaForm");
|
||||
var RtsClient = require("../../../RtsClient");
|
||||
|
||||
var MIN_PASSWORD_LENGTH = 6;
|
||||
|
||||
|
@ -47,23 +48,16 @@ module.exports = React.createClass({
|
|||
defaultIsUrl: React.PropTypes.string,
|
||||
brand: React.PropTypes.string,
|
||||
email: React.PropTypes.string,
|
||||
referrer: React.PropTypes.string,
|
||||
username: React.PropTypes.string,
|
||||
guestAccessToken: React.PropTypes.string,
|
||||
teamsConfig: React.PropTypes.shape({
|
||||
teamServerConfig: React.PropTypes.shape({
|
||||
// Email address to request new teams
|
||||
supportEmail: React.PropTypes.string,
|
||||
teams: React.PropTypes.arrayOf(React.PropTypes.shape({
|
||||
// The displayed name of the team
|
||||
"name": React.PropTypes.string,
|
||||
// The suffix with which every team email address ends
|
||||
"emailSuffix": React.PropTypes.string,
|
||||
// The rooms to use during auto-join
|
||||
"rooms": React.PropTypes.arrayOf(React.PropTypes.shape({
|
||||
"id": React.PropTypes.string,
|
||||
"autoJoin": React.PropTypes.bool,
|
||||
})),
|
||||
})).required,
|
||||
supportEmail: React.PropTypes.string.isRequired,
|
||||
// URL of the riot-team-server to get team configurations and track referrals
|
||||
teamServerURL: React.PropTypes.string.isRequired,
|
||||
}),
|
||||
teamSelected: React.PropTypes.object,
|
||||
|
||||
defaultDeviceDisplayName: React.PropTypes.string,
|
||||
|
||||
|
@ -75,6 +69,7 @@ module.exports = React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
busy: false,
|
||||
teamServerBusy: false,
|
||||
errorText: null,
|
||||
// We remember the values entered by the user because
|
||||
// the registration form will be unmounted during the
|
||||
|
@ -90,6 +85,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
// attach this to the instance rather than this.state since it isn't UI
|
||||
this.registerLogic = new Signup.Register(
|
||||
|
@ -103,10 +99,40 @@ module.exports = React.createClass({
|
|||
this.registerLogic.setIdSid(this.props.idSid);
|
||||
this.registerLogic.setGuestAccessToken(this.props.guestAccessToken);
|
||||
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() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
this._unmounted = true;
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -184,24 +210,41 @@ module.exports = React.createClass({
|
|||
accessToken: response.access_token
|
||||
});
|
||||
|
||||
// Auto-join rooms
|
||||
if (self.props.teamsConfig && self.props.teamsConfig.teams) {
|
||||
for (let i = 0; i < self.props.teamsConfig.teams.length; i++) {
|
||||
let team = self.props.teamsConfig.teams[i];
|
||||
if (self.state.formVals.email.endsWith(team.emailSuffix)) {
|
||||
console.log("User successfully registered with team " + team.name);
|
||||
if (
|
||||
self._rtsClient &&
|
||||
self.props.referrer &&
|
||||
self.state.teamSelected
|
||||
) {
|
||||
// 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) {
|
||||
break;
|
||||
return;
|
||||
}
|
||||
// Auto-join rooms
|
||||
team.rooms.forEach((room) => {
|
||||
if (room.autoJoin) {
|
||||
console.log("Auto-joining " + room.id);
|
||||
MatrixClientPeg.get().joinRoom(room.id);
|
||||
if (room.auto_join && room.room_id) {
|
||||
console.log(`Auto-joining ${room.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) {
|
||||
|
@ -273,7 +316,15 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onTeamSelected: function(teamSelected) {
|
||||
if (!this._unmounted) {
|
||||
this.setState({ teamSelected });
|
||||
}
|
||||
},
|
||||
|
||||
_getRegisterContentJsx: function() {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
||||
var currStep = this.registerLogic.getStep();
|
||||
var registerStep;
|
||||
switch (currStep) {
|
||||
|
@ -283,17 +334,23 @@ module.exports = React.createClass({
|
|||
case "Register.STEP_m.login.dummy":
|
||||
// NB. Our 'username' prop is specifically for upgrading
|
||||
// a guest account
|
||||
if (this.state.teamServerBusy) {
|
||||
registerStep = <Spinner />;
|
||||
break;
|
||||
}
|
||||
registerStep = (
|
||||
<RegistrationForm
|
||||
showEmail={true}
|
||||
defaultUsername={this.state.formVals.username}
|
||||
defaultEmail={this.state.formVals.email}
|
||||
defaultPassword={this.state.formVals.password}
|
||||
teamsConfig={this.props.teamsConfig}
|
||||
teamsConfig={this.state.teamsConfig}
|
||||
guestUsername={this.props.username}
|
||||
minPasswordLength={MIN_PASSWORD_LENGTH}
|
||||
onError={this.onFormValidationFailed}
|
||||
onRegisterClick={this.onFormSubmit} />
|
||||
onRegisterClick={this.onFormSubmit}
|
||||
onTeamSelected={this.onTeamSelected}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "Register.STEP_m.login.email.identity":
|
||||
|
@ -322,7 +379,6 @@ module.exports = React.createClass({
|
|||
}
|
||||
var busySpinner;
|
||||
if (this.state.busy) {
|
||||
var Spinner = sdk.getComponent("elements.Spinner");
|
||||
busySpinner = (
|
||||
<Spinner />
|
||||
);
|
||||
|
@ -367,7 +423,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_Login">
|
||||
<div className="mx_Login_box">
|
||||
<LoginHeader />
|
||||
<LoginHeader icon={this.state.teamSelected ? this.state.teamSelected.icon : null}/>
|
||||
{this._getRegisterContentJsx()}
|
||||
<LoginFooter />
|
||||
</div>
|
||||
|
|
|
@ -145,27 +145,48 @@ module.exports = React.createClass({
|
|||
|
||||
if (imageUrl === this.state.defaultImageUrl) {
|
||||
const initialLetter = this._getInitialLetter(name);
|
||||
return (
|
||||
<span className="mx_BaseAvatar" {...otherProps}>
|
||||
const textNode = (
|
||||
<EmojiText className="mx_BaseAvatar_initial" aria-hidden="true"
|
||||
style={{ fontSize: (width * 0.65) + "px",
|
||||
width: width + "px",
|
||||
lineHeight: height + "px" }}>{initialLetter}</EmojiText>
|
||||
lineHeight: height + "px" }}
|
||||
>
|
||||
{initialLetter}
|
||||
</EmojiText>
|
||||
);
|
||||
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) {
|
||||
return (
|
||||
<AccessibleButton className="mx_BaseAvatar" onClick={onClick}>
|
||||
<img className="mx_BaseAvatar_image" src={imageUrl}
|
||||
<AccessibleButton className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
element='img'
|
||||
src={imageUrl}
|
||||
onClick={onClick}
|
||||
onError={this.onError}
|
||||
width={width} height={height}
|
||||
title={title} alt=""
|
||||
{...otherProps} />
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
|
|
|
@ -14,17 +14,18 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require("react");
|
||||
var classNames = require('classnames');
|
||||
var sdk = require("../../../index");
|
||||
var Invite = require("../../../Invite");
|
||||
var createRoom = require("../../../createRoom");
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var DMRoomMap = require('../../../utils/DMRoomMap');
|
||||
var rate_limited_func = require("../../../ratelimitedfunc");
|
||||
var dis = require("../../../dispatcher");
|
||||
var Modal = require('../../../Modal');
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import sdk from '../../../index';
|
||||
import { getAddressType, inviteMultipleToRoom } from '../../../Invite';
|
||||
import createRoom from '../../../createRoom';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import rate_limited_func from '../../../ratelimitedfunc';
|
||||
import dis from '../../../dispatcher';
|
||||
import Modal from '../../../Modal';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import q from 'q';
|
||||
|
||||
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
|
||||
// valid address, add an entry for that
|
||||
if (queryList.length == 0) {
|
||||
const addrType = Invite.getAddressType(query);
|
||||
const addrType = getAddressType(query);
|
||||
if (addrType !== null) {
|
||||
queryList.push({
|
||||
queryList[0] = {
|
||||
addressType: addrType,
|
||||
address: query,
|
||||
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,
|
||||
queryList: [],
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -229,6 +235,7 @@ module.exports = React.createClass({
|
|||
inviteList: inviteList,
|
||||
queryList: [],
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
},
|
||||
|
||||
_getDirectMessageRoom: function(addr) {
|
||||
|
@ -266,7 +273,7 @@ module.exports = React.createClass({
|
|||
if (this.props.roomId) {
|
||||
// Invite new user to a room
|
||||
var self = this;
|
||||
Invite.inviteMultipleToRoom(this.props.roomId, addrTexts)
|
||||
inviteMultipleToRoom(this.props.roomId, addrTexts)
|
||||
.then(function(addrs) {
|
||||
var room = MatrixClientPeg.get().getRoom(self.props.roomId);
|
||||
return self._showAnyInviteErrors(addrs, room);
|
||||
|
@ -300,7 +307,7 @@ module.exports = React.createClass({
|
|||
var room;
|
||||
createRoom().then(function(roomId) {
|
||||
room = MatrixClientPeg.get().getRoom(roomId);
|
||||
return Invite.inviteMultipleToRoom(roomId, addrTexts);
|
||||
return inviteMultipleToRoom(roomId, addrTexts);
|
||||
})
|
||||
.then(function(addrs) {
|
||||
return self._showAnyInviteErrors(addrs, room);
|
||||
|
@ -380,7 +387,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_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;
|
||||
} else {
|
||||
return false;
|
||||
|
@ -408,7 +415,7 @@ module.exports = React.createClass({
|
|||
|
||||
_addInputToList: function() {
|
||||
const addressText = this.refs.textinput.value.trim();
|
||||
const addrType = Invite.getAddressType(addressText);
|
||||
const addrType = getAddressType(addressText);
|
||||
const addrObj = {
|
||||
addressType: addrType,
|
||||
address: addressText,
|
||||
|
@ -432,9 +439,45 @@ module.exports = React.createClass({
|
|||
inviteList: inviteList,
|
||||
queryList: [],
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
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() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
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");
|
||||
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 sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
|
||||
function DeviceListEntry(props) {
|
||||
const {userId, device} = props;
|
||||
|
@ -118,7 +119,19 @@ export default React.createClass({
|
|||
</h4>
|
||||
);
|
||||
} 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');
|
||||
|
@ -127,15 +140,16 @@ export default React.createClass({
|
|||
onFinished={this.props.onFinished}
|
||||
title='Room contains unknown devices'
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
|
||||
<h4>
|
||||
This room contains unknown devices which have not been
|
||||
verified.
|
||||
</h4>
|
||||
{ warning }
|
||||
Unknown devices:
|
||||
|
||||
<UnknownDeviceList devices={this.props.devices} />
|
||||
</div>
|
||||
</GeminiScrollbar>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" autoFocus={ true }
|
||||
onClick={ this.props.onFinished } >
|
||||
|
|
|
@ -94,14 +94,14 @@ export default React.createClass({
|
|||
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,
|
||||
});
|
||||
|
||||
let info;
|
||||
let error = false;
|
||||
if (address.addressType === "mx" && address.isKnown) {
|
||||
const idClasses = classNames({
|
||||
"mx_AddressTile_id": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
|
@ -123,13 +123,21 @@ export default React.createClass({
|
|||
<div className={unknownMxClasses}>{ this.props.address.address }</div>
|
||||
);
|
||||
} else if (address.addressType === "email") {
|
||||
var emailClasses = classNames({
|
||||
const emailClasses = classNames({
|
||||
"mx_AddressTile_email": true,
|
||||
"mx_AddressTile_justified": this.props.justified,
|
||||
});
|
||||
|
||||
let nameNode = null;
|
||||
if (address.displayName) {
|
||||
nameNode = <div className={nameClasses}>{ address.displayName }</div>
|
||||
}
|
||||
|
||||
info = (
|
||||
<div className="mx_AddressTile_mx">
|
||||
<div className={emailClasses}>{ address.address }</div>
|
||||
{nameNode}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
error = true;
|
||||
|
|
|
@ -44,8 +44,8 @@ module.exports = React.createClass({
|
|||
teams: React.PropTypes.arrayOf(React.PropTypes.shape({
|
||||
// The displayed name of the team
|
||||
"name": React.PropTypes.string,
|
||||
// The suffix with which every team email address ends
|
||||
"emailSuffix": React.PropTypes.string,
|
||||
// The domain of team email addresses
|
||||
"domain": React.PropTypes.string,
|
||||
})).required,
|
||||
}),
|
||||
|
||||
|
@ -117,9 +117,6 @@ module.exports = React.createClass({
|
|||
|
||||
_doSubmit: function() {
|
||||
let email = this.refs.email.value.trim();
|
||||
if (this.state.selectedTeam) {
|
||||
email += "@" + this.state.selectedTeam.emailSuffix;
|
||||
}
|
||||
var promise = this.props.onRegisterClick({
|
||||
username: this.refs.username.value.trim() || this.props.guestUsername,
|
||||
password: this.refs.password.value.trim(),
|
||||
|
@ -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
|
||||
* they were validated.
|
||||
|
@ -167,20 +145,36 @@ module.exports = React.createClass({
|
|||
return true;
|
||||
},
|
||||
|
||||
_isUniEmail: function(email) {
|
||||
return email.endsWith('.ac.uk') || email.endsWith('.edu');
|
||||
},
|
||||
|
||||
validateField: function(field_id) {
|
||||
var pwd1 = this.refs.password.value.trim();
|
||||
var pwd2 = this.refs.passwordConfirm.value.trim();
|
||||
|
||||
switch (field_id) {
|
||||
case FIELD_EMAIL:
|
||||
let email = this.refs.email.value;
|
||||
if (this.props.teamsConfig) {
|
||||
let team = this.state.selectedTeam;
|
||||
if (team) {
|
||||
email = email + "@" + team.emailSuffix;
|
||||
const email = this.refs.email.value;
|
||||
if (this.props.teamsConfig && this._isUniEmail(email)) {
|
||||
const matchingTeam = this.props.teamsConfig.teams.find(
|
||||
(team) => {
|
||||
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");
|
||||
break;
|
||||
case FIELD_USERNAME:
|
||||
|
@ -260,61 +254,35 @@ module.exports = React.createClass({
|
|||
return cls;
|
||||
},
|
||||
|
||||
_renderEmailInputSuffix: function() {
|
||||
let suffix = null;
|
||||
if (!this.state.selectedTeam) {
|
||||
return suffix;
|
||||
}
|
||||
let team = this.state.selectedTeam;
|
||||
if (team) {
|
||||
suffix = "@" + team.emailSuffix;
|
||||
}
|
||||
return suffix;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
var emailSection, teamSection, teamAdditionSupport, registerButton;
|
||||
var emailSection, belowEmailSection, registerButton;
|
||||
if (this.props.showEmail) {
|
||||
let emailSuffix = this._renderEmailInputSuffix();
|
||||
emailSection = (
|
||||
<div>
|
||||
<input type="text" ref="email"
|
||||
autoFocus={true} placeholder="Email address (optional)"
|
||||
defaultValue={this.props.defaultEmail}
|
||||
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
|
||||
onBlur={function() {self.validateField(FIELD_EMAIL);}}
|
||||
value={self.state.email}/>
|
||||
{emailSuffix ? <input className="mx_Login_field" value={emailSuffix} disabled/> : null }
|
||||
</div>
|
||||
);
|
||||
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) {
|
||||
teamAdditionSupport = (
|
||||
<span>
|
||||
If your team is not listed, email
|
||||
belowEmailSection = (
|
||||
<p className="mx_Login_support">
|
||||
Sorry, but your university is not registered with us just yet.
|
||||
Email us on
|
||||
<a href={"mailto:" + this.props.teamsConfig.supportEmail}>
|
||||
{this.props.teamsConfig.supportEmail}
|
||||
</a>
|
||||
</span>
|
||||
</a>
|
||||
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 (
|
||||
<div>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
{teamSection}
|
||||
{teamAdditionSupport}
|
||||
<br />
|
||||
{emailSection}
|
||||
<br />
|
||||
{belowEmailSection}
|
||||
<input type="text" ref="username"
|
||||
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
|
||||
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
|
||||
|
|
|
@ -35,7 +35,7 @@ export function onSendMessageFailed(err, room) {
|
|||
Modal.createDialog(UnknownDeviceDialog, {
|
||||
devices: err.devices,
|
||||
room: room,
|
||||
});
|
||||
}, "mx_Dialog_unknownDevice");
|
||||
}
|
||||
dis.dispatch({
|
||||
action: 'message_send_failed',
|
||||
|
|
|
@ -170,15 +170,15 @@ module.exports = React.createClass({
|
|||
|
||||
let title;
|
||||
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);
|
||||
if (this.props.showFullTimestamp) {
|
||||
// "15/12/2016, 7:05:45 PM (@alice:matrix.org)"
|
||||
title = ts.toLocaleString() + suffix;
|
||||
title = prefix + ts.toLocaleString();
|
||||
}
|
||||
else {
|
||||
// "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"
|
||||
style={style}
|
||||
title={title}
|
||||
onClick={this.props.onClick}
|
||||
/>
|
||||
</Velociraptor>
|
||||
);
|
||||
/* onClick={this.props.onClick} */
|
||||
},
|
||||
});
|
||||
|
|
|
@ -301,8 +301,8 @@ module.exports = React.createClass({
|
|||
var rightPanel_buttons;
|
||||
if (this.props.collapsedRhs) {
|
||||
rightPanel_buttons =
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title="<">
|
||||
<TintableSvg src="img/minimise.svg" width="10" height="16"/>
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title="Show panel">
|
||||
<TintableSvg src="img/maximise.svg" width="10" height="16"/>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
|
|
|
@ -146,7 +146,7 @@ module.exports = React.createClass({
|
|||
<div>
|
||||
<div className="mx_RoomPreviewBar_join_text">
|
||||
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>
|
||||
);
|
||||
|
|
|
@ -50,7 +50,7 @@ export function decryptMegolmKeyFile(data, password) {
|
|||
}
|
||||
|
||||
const ciphertextLength = body.length-(1+16+16+4+32);
|
||||
if (body.length < 0) {
|
||||
if (ciphertextLength < 0) {
|
||||
throw new Error('Invalid file: too short');
|
||||
}
|
||||
|
||||
|
@ -102,19 +102,19 @@ export function decryptMegolmKeyFile(data, password) {
|
|||
*/
|
||||
export function encryptMegolmKeyFile(data, password, options) {
|
||||
options = options || {};
|
||||
const kdf_rounds = options.kdf_rounds || 100000;
|
||||
const kdf_rounds = options.kdf_rounds || 500000;
|
||||
|
||||
const salt = new Uint8Array(16);
|
||||
window.crypto.getRandomValues(salt);
|
||||
|
||||
// clear bit 63 of the salt to stop us hitting the 64-bit counter boundary
|
||||
// (which would mean we wouldn't be able to decrypt on Android). The loss
|
||||
// of a single bit of salt is a price we have to pay.
|
||||
salt[9] &= 0x7f;
|
||||
|
||||
const iv = new Uint8Array(16);
|
||||
window.crypto.getRandomValues(iv);
|
||||
|
||||
// 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) => {
|
||||
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]
|
||||
*/
|
||||
function deriveKeys(salt, iterations, password) {
|
||||
const start = new Date();
|
||||
return subtleCrypto.importKey(
|
||||
'raw',
|
||||
new TextEncoder().encode(password),
|
||||
|
@ -182,6 +183,9 @@ function deriveKeys(salt, iterations, password) {
|
|||
512
|
||||
);
|
||||
}).then((keybits) => {
|
||||
const now = new Date();
|
||||
console.log("E2e import/export: deriveKeys took " + (now - start) + "ms");
|
||||
|
||||
const aes_key = keybits.slice(0, 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) {
|
||||
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);
|
||||
|
||||
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) {
|
||||
|
|
|
@ -73,6 +73,7 @@ var Tester = React.createClass({
|
|||
|
||||
/* returns a promise which will resolve when the fill happens */
|
||||
awaitFill: function(dir) {
|
||||
console.log("ScrollPanel Tester: awaiting " + dir + " fill");
|
||||
var defer = q.defer();
|
||||
this._fillDefers[dir] = defer;
|
||||
return defer.promise;
|
||||
|
@ -80,7 +81,7 @@ var Tester = React.createClass({
|
|||
|
||||
_onScroll: function(ev) {
|
||||
var st = ev.target.scrollTop;
|
||||
console.log("Scroll event; scrollTop: " + st);
|
||||
console.log("ScrollPanel Tester: scroll event; scrollTop: " + st);
|
||||
this.lastScrollEvent = st;
|
||||
|
||||
var d = this._scrollDefer;
|
||||
|
@ -159,10 +160,29 @@ describe('ScrollPanel', function() {
|
|||
scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass(
|
||||
tester, "gm-scroll-view");
|
||||
|
||||
// wait for a browser tick to let the initial paginates complete
|
||||
setTimeout(function() {
|
||||
done();
|
||||
}, 0);
|
||||
// we need to make sure we don't call done() until q has finished
|
||||
// running the completion handlers from the fill requests. We can't
|
||||
// just use .done(), because that will end up ahead of those handlers
|
||||
// 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() {
|
||||
|
|
|
@ -99,7 +99,11 @@ describe('TimelinePanel', function() {
|
|||
// the document so that we can interact with it properly.
|
||||
parentDiv = document.createElement('div');
|
||||
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';
|
||||
document.body.appendChild(parentDiv);
|
||||
});
|
||||
|
@ -235,7 +239,7 @@ describe('TimelinePanel', function() {
|
|||
expect(client.paginateEventTimeline.callCount).toEqual(0);
|
||||
done();
|
||||
}, 0);
|
||||
}, 0);
|
||||
}, 10);
|
||||
});
|
||||
|
||||
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) {
|
||||
var desc = context.currentTest.fullTitle();
|
||||
|
||||
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(new Array(1 + desc.length).join("="));
|
||||
};
|
||||
|
|
|
@ -75,6 +75,16 @@ describe('MegolmExportEncryption', function() {
|
|||
.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) {
|
||||
function next(i) {
|
||||
if (i >= TEST_VECTORS.length) {
|
||||
|
|
Loading…
Reference in a new issue