diff --git a/.eslintrc.js b/.eslintrc.js index d5684e21a7..34d3af270c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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"], diff --git a/karma.conf.js b/karma.conf.js index 131a03ce79..6d3047bb3b 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -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, }); }; diff --git a/src/Markdown.js b/src/Markdown.js index 2f278183a3..d6dc979a5a 100644 --- a/src/Markdown.js +++ b/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 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; + } + } else { + return false; + } } - - 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; + 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; + 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() { - 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); } } diff --git a/src/Modal.js b/src/Modal.js index 89e8b1361c..b6cc46ed45 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -177,7 +177,7 @@ class ModalManager { var modal = this._modals[0]; var dialog = ( -
+
{modal.elem}
diff --git a/src/Resend.js b/src/Resend.js index 21da1c173b..ad0f58eb9b 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -34,7 +34,7 @@ module.exports = { Modal.createDialog(UnknownDeviceDialog, { devices: err.devices, room: MatrixClientPeg.get().getRoom(event.getRoomId()), - }); + }, "mx_Dialog_unknownDevice"); } dis.dispatch({ diff --git a/src/RtsClient.js b/src/RtsClient.js new file mode 100644 index 0000000000..ae62fb8b22 --- /dev/null +++ b/src/RtsClient.js @@ -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, + }, + } + ); + } +} diff --git a/src/Velociraptor.js b/src/Velociraptor.js index 006dbcb0ac..18c871a12d 100644 --- a/src/Velociraptor.js +++ b/src/Velociraptor.js @@ -62,11 +62,11 @@ module.exports = React.createClass({ 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); } + if (oldNode.style.visibility == 'hidden' && c.props.style.visibility == 'visible') { + oldNode.style.visibility = c.props.style.visibility; + } self.children[c.key] = old; } else { // new element. If we have a startStyle, use that as the style and go through diff --git a/src/WhoIsTyping.js b/src/WhoIsTyping.js index 96e76d618b..ecd7c495f9 100644 --- a/src/WhoIsTyping.js +++ b/src/WhoIsTyping.js @@ -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 = { diff --git a/src/async-components/views/dialogs/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/ExportE2eKeysDialog.js index 816b8eb73d..56b9d56cc9 100644 --- a/src/async-components/views/dialogs/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ExportE2eKeysDialog.js @@ -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({ - +
diff --git a/src/async-components/views/dialogs/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/ImportE2eKeysDialog.js index 586bd9b6cc..ddd13813e2 100644 --- a/src/async-components/views/dialogs/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ImportE2eKeysDialog.js @@ -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({ - +
diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index 5166619d48..fc4cbd9423 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -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" /> ); } diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index c00bd2c6db..44beb787c8 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -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 = ; break; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index cb61041d48..6a84fb940f 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -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} diff --git a/src/components/structures/NotificationPanel.js b/src/components/structures/NotificationPanel.js index 7d9e752657..16f9723c76 100644 --- a/src/components/structures/NotificationPanel.js +++ b/src/components/structures/NotificationPanel.js @@ -48,6 +48,7 @@ var NotificationPanel = React.createClass({ showUrlPreview = { false } opacity={ this.props.opacity } tileShape="notif" + empty="You have no visible notifications" /> ); } diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 3ba73bb181..5632b10c7f 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -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( - + +{othersCount} ); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 24c8ff53c0..38b3346e29 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -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 = ; } else if (!this.state.searchResults) { var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar'); - + isStatusAreaExpanded = this.state.statusBarVisible; statusBar = +
{ this.props.empty }
+ + ); + } + // 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. diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index bee600e7cd..ff19e7c239 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -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 ( +
+

Referral

+
+ Refer a friend to Riot: {href} +
+
+ ); + }, + _renderUserInterfaceSettings: function() { var client = MatrixClientPeg.get(); @@ -857,6 +881,8 @@ module.exports = React.createClass({ {accountJsx} + {this._renderReferral()} + {notification_area} {this._renderUserInterfaceSettings()} diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 90140b3280..0fc0cac527 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -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 = ; + break; + } registerStep = ( + 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 = ( ); @@ -367,7 +423,7 @@ module.exports = React.createClass({ return (
- + {this._getRegisterContentJsx()}
diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index c9c84aa1bf..65730be40b 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -145,27 +145,48 @@ module.exports = React.createClass({ if (imageUrl === this.state.defaultImageUrl) { const initialLetter = this._getInitialLetter(name); - return ( - - - - + const textNode = ( + ); + const imgNode = ( + + ); + if (onClick != null) { + return ( + + {textNode} + {imgNode} + + ); + } else { + return ( + + {textNode} + {imgNode} + + ); + } } if (onClick != null) { return ( - - - + ); } else { return ( diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index 61503196e5..ca3b07aa00 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -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"); diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index 178dd55657..b5501a5c15 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -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({ ); } else { - warning =

We strongly recommend you verify them before continuing.

; + warning = ( +
+

+ This means there is no guarantee that the devices belong + to a rightful user of the room. +

+

+ We recommend you go through the verification process + for each device before continuing, but you can resend + the message without verifying if you prefer. +

+
+ ); } const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); @@ -127,15 +140,16 @@ export default React.createClass({ onFinished={this.props.onFinished} title='Room contains unknown devices' > -
+

This room contains unknown devices which have not been verified.

{ warning } Unknown devices: + -
+