diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index a8e20f5ec14..96934d205e6 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -25,6 +25,9 @@ import emojione from 'emojione'; import classNames from 'classnames'; emojione.imagePathSVG = 'emojione/svg/'; +// Store PNG path for displaying many flags at once (for increased performance over SVG) +emojione.imagePathPNG = 'emojione/png/'; +// Use SVGs for emojis emojione.imageType = 'svg'; const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi"); @@ -64,16 +67,23 @@ export function unicodeToImage(str) { * emoji. * * @param alt {string} String to use for the image alt text + * @param useSvg {boolean} Whether to use SVG image src. If False, PNG will be used. * @param unicode {integer} One or more integers representing unicode characters * @returns A img node with the corresponding emoji */ -export function charactersToImageNode(alt, ...unicode) { +export function charactersToImageNode(alt, useSvg, ...unicode) { const fileName = unicode.map((u) => { return u.toString(16); }).join('-'); - return {alt}; + const path = useSvg ? emojione.imagePathSVG : emojione.imagePathPNG; + const fileType = useSvg ? 'svg' : 'png'; + return {alt}; } + export function stripParagraphs(html: string): string { const contentDiv = document.createElement('div'); contentDiv.innerHTML = html; diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 7e1a5f9d35c..315a0ea2425 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -17,13 +17,11 @@ limitations under the License. 'use strict'; -var React = require('react'); -var ReactDOM = require('react-dom'); -var sdk = require('../../../index'); -var Login = require("../../../Login"); -var PasswordLogin = require("../../views/login/PasswordLogin"); -var CasLogin = require("../../views/login/CasLogin"); -var ServerConfig = require("../../views/login/ServerConfig"); +import React from 'react'; +import ReactDOM from 'react-dom'; +import url from 'url'; +import sdk from '../../../index'; +import Login from '../../../Login'; /** * A wire component which glues together login UI components and Login logic @@ -67,6 +65,7 @@ module.exports = React.createClass({ username: "", phoneCountry: null, phoneNumber: "", + currentFlow: "m.login.password", }; }, @@ -129,23 +128,19 @@ module.exports = React.createClass({ this.setState({ phoneNumber: phoneNumber }); }, - onHsUrlChanged: function(newHsUrl) { + onServerConfigChange: function(config) { var self = this; - this.setState({ - enteredHomeserverUrl: newHsUrl, - errorText: null, // reset err messages - }, function() { - self._initLoginLogic(newHsUrl); - }); - }, - - onIsUrlChanged: function(newIsUrl) { - var self = this; - this.setState({ - enteredIdentityServerUrl: newIsUrl, + let newState = { errorText: null, // reset err messages - }, function() { - self._initLoginLogic(null, newIsUrl); + }; + if (config.hsUrl !== undefined) { + newState.enteredHomeserverUrl = config.hsUrl; + } + if (config.isUrl !== undefined) { + newState.enteredIdentityServerUrl = config.isUrl; + } + this.setState(newState, function() { + self._initLoginLogic(config.hsUrl || null, config.isUrl); }); }, @@ -161,25 +156,28 @@ module.exports = React.createClass({ }); this._loginLogic = loginLogic; + this.setState({ + enteredHomeserverUrl: hsUrl, + enteredIdentityServerUrl: isUrl, + busy: true, + loginIncorrect: false, + }); + loginLogic.getFlows().then(function(flows) { // old behaviour was to always use the first flow without presenting // options. This works in most cases (we don't have a UI for multiple // logins so let's skip that for now). loginLogic.chooseFlow(0); + self.setState({ + currentFlow: self._getCurrentFlowStep(), + }); }, function(err) { self._setStateFromError(err, false); }).finally(function() { self.setState({ - busy: false + busy: false, }); }); - - this.setState({ - enteredHomeserverUrl: hsUrl, - enteredIdentityServerUrl: isUrl, - busy: true, - loginIncorrect: false, - }); }, _getCurrentFlowStep: function() { @@ -231,6 +229,13 @@ module.exports = React.createClass({ componentForStep: function(step) { switch (step) { case 'm.login.password': + const PasswordLogin = sdk.getComponent('login.PasswordLogin'); + // HSs that are not matrix.org may not be configured to have their + // domain name === domain part. + let hsDomain = url.parse(this.state.enteredHomeserverUrl).hostname; + if (hsDomain !== 'matrix.org') { + hsDomain = null; + } return ( ); case 'm.login.cas': + const CasLogin = sdk.getComponent('login.CasLogin'); return ( ); @@ -262,10 +269,11 @@ module.exports = React.createClass({ }, render: function() { - var Loader = sdk.getComponent("elements.Spinner"); - var LoginHeader = sdk.getComponent("login.LoginHeader"); - var LoginFooter = sdk.getComponent("login.LoginFooter"); - var loader = this.state.busy ?
: null; + const Loader = sdk.getComponent("elements.Spinner"); + const LoginHeader = sdk.getComponent("login.LoginHeader"); + const LoginFooter = sdk.getComponent("login.LoginFooter"); + const ServerConfig = sdk.getComponent("login.ServerConfig"); + const loader = this.state.busy ?
: null; var loginAsGuestJsx; if (this.props.enableGuest) { @@ -291,15 +299,14 @@ module.exports = React.createClass({

Sign in { loader }

- { this.componentForStep(this._getCurrentFlowStep()) } + { this.componentForStep(this.state.currentFlow) }
{ this.state.errorText } diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js index 3b34d3cac19..a9ecf5b6690 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.js @@ -248,13 +248,10 @@ export default class Dropdown extends React.Component { ); }); - - if (!this.state.searchQuery) { - options.push( -
- Type to search... -
- ); + if (options.length === 0) { + return [
+ No results +
]; } return options; } @@ -267,16 +264,20 @@ export default class Dropdown extends React.Component { let menu; if (this.state.expanded) { - currentValue = ; + if (this.props.searchEnabled) { + currentValue = ; + } menu =
{this._getMenuOptions()}
; - } else { + } + + if (!currentValue) { const selectedChild = this.props.getShortOption ? this.props.getShortOption(this.props.value) : this.childrenByKey[this.props.value]; @@ -313,6 +314,7 @@ Dropdown.propTypes = { onOptionChange: React.PropTypes.func.isRequired, // Called when the value of the search field changes onSearchChange: React.PropTypes.func, + searchEnabled: React.PropTypes.bool, // Function that, given the key of an option, returns // a node representing that option to be displayed in the // box itself as the currently-selected option (ie. as diff --git a/src/components/views/login/CountryDropdown.js b/src/components/views/login/CountryDropdown.js index 9729c9e23f6..7f6b21650db 100644 --- a/src/components/views/login/CountryDropdown.js +++ b/src/components/views/login/CountryDropdown.js @@ -33,8 +33,6 @@ function countryMatchesSearchQuery(query, country) { return false; } -const MAX_DISPLAYED_ROWS = 2; - export default class CountryDropdown extends React.Component { constructor(props) { super(props); @@ -64,7 +62,7 @@ export default class CountryDropdown extends React.Component { // Unicode Regional Indicator Symbol letter 'A' const RIS_A = 0x1F1E6; const ASCII_A = 65; - return charactersToImageNode(iso2, + return charactersToImageNode(iso2, true, RIS_A + (iso2.charCodeAt(0) - ASCII_A), RIS_A + (iso2.charCodeAt(1) - ASCII_A), ); @@ -93,10 +91,6 @@ export default class CountryDropdown extends React.Component { displayedCountries = COUNTRIES; } - if (displayedCountries.length > MAX_DISPLAYED_ROWS) { - displayedCountries = displayedCountries.slice(0, MAX_DISPLAYED_ROWS); - } - const options = displayedCountries.map((country) => { return
{this._flagImgForIso2(country.iso2)} @@ -111,7 +105,7 @@ export default class CountryDropdown extends React.Component { return {options} diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 61cb3da6525..568461817c1 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -25,55 +25,49 @@ import {field_input_incorrect} from '../../../UiEffects'; /** * A pure UI component which displays a username/password form. */ -module.exports = React.createClass({displayName: 'PasswordLogin', - propTypes: { - onSubmit: React.PropTypes.func.isRequired, // fn(username, password) - onForgotPasswordClick: React.PropTypes.func, // fn() - initialUsername: React.PropTypes.string, - initialPhoneCountry: React.PropTypes.string, - initialPhoneNumber: React.PropTypes.string, - initialPassword: React.PropTypes.string, - onUsernameChanged: React.PropTypes.func, - onPhoneCountryChanged: React.PropTypes.func, - onPhoneNumberChanged: React.PropTypes.func, - onPasswordChanged: React.PropTypes.func, - loginIncorrect: React.PropTypes.bool, - }, - - getDefaultProps: function() { - return { - onUsernameChanged: function() {}, - onPasswordChanged: function() {}, - onPhoneCountryChanged: function() {}, - onPhoneNumberChanged: function() {}, - initialUsername: "", - initialPhoneCountry: "", - initialPhoneNumber: "", - initialPassword: "", - loginIncorrect: false, - }; - }, +class PasswordLogin extends React.Component { + static defaultProps = { + onUsernameChanged: function() {}, + onPasswordChanged: function() {}, + onPhoneCountryChanged: function() {}, + onPhoneNumberChanged: function() {}, + initialUsername: "", + initialPhoneCountry: "", + initialPhoneNumber: "", + initialPassword: "", + loginIncorrect: false, + hsDomain: "", + } - getInitialState: function() { - return { + constructor(props) { + super(props); + this.state = { username: this.props.initialUsername, password: this.props.initialPassword, phoneCountry: this.props.initialPhoneCountry, phoneNumber: this.props.initialPhoneNumber, + loginType: PasswordLogin.LOGIN_FIELD_MXID, }; - }, - componentWillMount: function() { + this.onSubmitForm = this.onSubmitForm.bind(this); + this.onUsernameChanged = this.onUsernameChanged.bind(this); + this.onLoginTypeChange = this.onLoginTypeChange.bind(this); + this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this); + this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this); + this.onPasswordChanged = this.onPasswordChanged.bind(this); + } + + componentWillMount() { this._passwordField = null; - }, + } - componentWillReceiveProps: function(nextProps) { + componentWillReceiveProps(nextProps) { if (!this.props.loginIncorrect && nextProps.loginIncorrect) { field_input_incorrect(this._passwordField); } - }, + } - onSubmitForm: function(ev) { + onSubmitForm(ev) { ev.preventDefault(); this.props.onSubmit( this.state.username, @@ -81,29 +75,99 @@ module.exports = React.createClass({displayName: 'PasswordLogin', this.state.phoneNumber, this.state.password, ); - }, + } - onUsernameChanged: function(ev) { + onUsernameChanged(ev) { this.setState({username: ev.target.value}); this.props.onUsernameChanged(ev.target.value); - }, + } + + onLoginTypeChange(loginType) { + this.setState({ + loginType: loginType, + username: "" // Reset because email and username use the same state + }); + } - onPhoneCountryChanged: function(country) { + onPhoneCountryChanged(country) { this.setState({phoneCountry: country}); this.props.onPhoneCountryChanged(country); - }, + } - onPhoneNumberChanged: function(ev) { + onPhoneNumberChanged(ev) { this.setState({phoneNumber: ev.target.value}); this.props.onPhoneNumberChanged(ev.target.value); - }, + } - onPasswordChanged: function(ev) { + onPasswordChanged(ev) { this.setState({password: ev.target.value}); this.props.onPasswordChanged(ev.target.value); - }, + } - render: function() { + renderLoginField(loginType) { + switch(loginType) { + case PasswordLogin.LOGIN_FIELD_EMAIL: + return ; + case PasswordLogin.LOGIN_FIELD_MXID: + const mxidInputClasses = classNames({ + "mx_Login_field": true, + "mx_Login_username": true, + "mx_Login_field_has_suffix": Boolean(this.props.hsDomain), + }); + let suffix = null; + if (this.props.hsDomain) { + suffix =
+ :{this.props.hsDomain} +
; + } + return
+
@
+ + {suffix} +
; + case PasswordLogin.LOGIN_FIELD_PHONE: + const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); + return
+ + +
; + } + } + + render() { var forgotPasswordJsx; if (this.props.onForgotPasswordClick) { @@ -119,29 +183,25 @@ module.exports = React.createClass({displayName: 'PasswordLogin', error: this.props.loginIncorrect, }); - const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); + const Dropdown = sdk.getComponent('elements.Dropdown'); + + const loginField = this.renderLoginField(this.state.loginType); + return (
- - or -
- - +
+ + + Matrix ID + Email Address + Phone +
-
+ {loginField} {this._passwordField = e;}} type="password" name="password" value={this.state.password} onChange={this.onPasswordChanged} @@ -153,4 +213,25 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
); } -}); +} + +PasswordLogin.LOGIN_FIELD_EMAIL = "login_field_email"; +PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid"; +PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone"; + +PasswordLogin.propTypes = { + onSubmit: React.PropTypes.func.isRequired, // fn(username, password) + onForgotPasswordClick: React.PropTypes.func, // fn() + initialUsername: React.PropTypes.string, + initialPhoneCountry: React.PropTypes.string, + initialPhoneNumber: React.PropTypes.string, + initialPassword: React.PropTypes.string, + onUsernameChanged: React.PropTypes.func, + onPhoneCountryChanged: React.PropTypes.func, + onPhoneNumberChanged: React.PropTypes.func, + onPasswordChanged: React.PropTypes.func, + loginIncorrect: React.PropTypes.bool, + hsDomain: React.PropTypes.string, +}; + +module.exports = PasswordLogin; diff --git a/src/components/views/login/ServerConfig.js b/src/components/views/login/ServerConfig.js index 4e6ed12f9ee..2853945425c 100644 --- a/src/components/views/login/ServerConfig.js +++ b/src/components/views/login/ServerConfig.js @@ -27,8 +27,7 @@ module.exports = React.createClass({ displayName: 'ServerConfig', propTypes: { - onHsUrlChanged: React.PropTypes.func, - onIsUrlChanged: React.PropTypes.func, + onServerConfigChange: React.PropTypes.func, // default URLs are defined in config.json (or the hardcoded defaults) // they are used if the user has not overridden them with a custom URL. @@ -50,8 +49,7 @@ module.exports = React.createClass({ getDefaultProps: function() { return { - onHsUrlChanged: function() {}, - onIsUrlChanged: function() {}, + onServerConfigChange: function() {}, customHsUrl: "", customIsUrl: "", withToggleButton: false, @@ -75,7 +73,10 @@ module.exports = React.createClass({ this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() { var hsUrl = this.state.hs_url.trim().replace(/\/$/, ""); if (hsUrl === "") hsUrl = this.props.defaultHsUrl; - this.props.onHsUrlChanged(hsUrl); + this.props.onServerConfigChange({ + hsUrl : this.state.hs_url, + isUrl : this.state.is_url, + }); }); }); }, @@ -85,7 +86,10 @@ module.exports = React.createClass({ this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() { var isUrl = this.state.is_url.trim().replace(/\/$/, ""); if (isUrl === "") isUrl = this.props.defaultIsUrl; - this.props.onIsUrlChanged(isUrl); + this.props.onServerConfigChange({ + hsUrl : this.state.hs_url, + isUrl : this.state.is_url, + }); }); }); }, @@ -102,12 +106,16 @@ module.exports = React.createClass({ configVisible: visible }); if (!visible) { - this.props.onHsUrlChanged(this.props.defaultHsUrl); - this.props.onIsUrlChanged(this.props.defaultIsUrl); + this.props.onServerConfigChange({ + hsUrl : this.props.defaultHsUrl, + isUrl : this.props.defaultIsUrl, + }); } else { - this.props.onHsUrlChanged(this.state.hs_url); - this.props.onIsUrlChanged(this.state.is_url); + this.props.onServerConfigChange({ + hsUrl : this.state.hs_url, + isUrl : this.state.is_url, + }); } },