diff --git a/src/UiEffects.js b/src/UiEffects.js new file mode 100644 index 0000000000..76db0b7f12 --- /dev/null +++ b/src/UiEffects.js @@ -0,0 +1,27 @@ +/* +Copyright 2016 OpenMarket 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. +*/ + +/** + * Functions for applying common thematic effects to UI elements. + * Ideally this would be themeable. + */ + +import Velocity from 'velocity-vector'; +import 'velocity-vector/velocity.ui'; + +export function field_input_incorrect(element) { + Velocity(element, "callout.shake", 300); +} diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index a73ad30f87..74edc4ecd9 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -53,6 +53,7 @@ module.exports = React.createClass({displayName: 'Login', return { busy: false, errorText: null, + loginIncorrect: false, enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl, enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl, @@ -68,13 +69,15 @@ module.exports = React.createClass({displayName: 'Login', onPasswordLogin: function(username, password) { var self = this; self.setState({ - busy: true + busy: true, + errorText: null, + loginIncorrect: false, }); this._loginLogic.loginViaPassword(username, password).then(function(data) { self.props.onLoggedIn(data); }, function(error) { - self._setErrorTextFromError(error); + self._setStateFromError(error, true); }).finally(function() { self.setState({ busy: false @@ -100,7 +103,7 @@ module.exports = React.createClass({displayName: 'Login', this.setState({ enteredIdentityServerUrl: newIsUrl }, function() { - self._initLoginLogic(null, newIsUrl); + self._initLoginLogic(null, newIsUrl); }); }, @@ -120,7 +123,7 @@ module.exports = React.createClass({displayName: 'Login', // logins so let's skip that for now). loginLogic.chooseFlow(0); }, function(err) { - self._setErrorTextFromError(err); + self._setStateFromError(err, false); }).finally(function() { self.setState({ busy: false @@ -131,7 +134,8 @@ module.exports = React.createClass({displayName: 'Login', enteredHomeserverUrl: hsUrl, enteredIdentityServerUrl: isUrl, busy: true, - errorText: null // reset err messages + errorText: null, // reset err messages + loginIncorrect: false, }); }, @@ -139,25 +143,30 @@ module.exports = React.createClass({displayName: 'Login', return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null }, - _setErrorTextFromError: function(err) { + _setStateFromError: function(err, isLoginAttempt) { + this.setState({ + errorText: this._errorTextFromError(err), + // https://matrix.org/jira/browse/SYN-744 + loginIncorrect: isLoginAttempt && (err.httpStatus == 401 || err.httpStatus == 403) + }); + }, + + _errorTextFromError(err) { if (err.friendlyText) { - this.setState({ - errorText: err.friendlyText - }); - return; + return err.friendlyText; } - var errCode = err.errcode; + let errCode = err.errcode; if (!errCode && err.httpStatus) { errCode = "HTTP " + err.httpStatus; } - var errorText = "Error: Problem communicating with the given homeserver " + + let errorText = "Error: Problem communicating with the given homeserver " + (errCode ? "(" + errCode + ")" : "") if (err.cors === 'rejected') { if (window.location.protocol === 'https:' && - (this.state.enteredHomeserverUrl.startsWith("http:") || + (this.state.enteredHomeserverUrl.startsWith("http:") || !this.state.enteredHomeserverUrl.startsWith("http"))) { errorText = @@ -173,9 +182,7 @@ module.exports = React.createClass({displayName: 'Login', } } - this.setState({ - errorText: errorText - }); + return errorText; }, componentForStep: function(step) { @@ -186,7 +193,9 @@ module.exports = React.createClass({displayName: 'Login', onSubmit={this.onPasswordLogin} initialUsername={this.state.username} onUsernameChanged={this.onUsernameChanged} - onForgotPasswordClick={this.props.onForgotPasswordClick} /> + onForgotPasswordClick={this.props.onForgotPasswordClick} + loginIncorrect={this.state.loginIncorrect} + /> ); case 'm.login.cas': return ( @@ -221,7 +230,7 @@ module.exports = React.createClass({displayName: 'Login', var returnToAppJsx; if (this.props.onCancelClick) { - returnToAppJsx = + returnToAppJsx = Return to app diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index c5474f60c1..1a40ddde6b 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -14,8 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -var React = require('react'); -var ReactDOM = require('react-dom'); +import React from 'react'; +import ReactDOM from 'react-dom'; +import classNames from 'classnames'; +import {field_input_incorrect} from '../../../UiEffects'; + /** * A pure UI component which displays a username/password form. @@ -28,6 +31,7 @@ module.exports = React.createClass({displayName: 'PasswordLogin', initialPassword: React.PropTypes.string, onUsernameChanged: React.PropTypes.func, onPasswordChanged: React.PropTypes.func, + loginIncorrect: React.PropTypes.bool, }, getDefaultProps: function() { @@ -36,6 +40,7 @@ module.exports = React.createClass({displayName: 'PasswordLogin', onPasswordChanged: function() {}, initialUsername: "", initialPassword: "", + loginIncorrect: false, }; }, @@ -46,6 +51,16 @@ module.exports = React.createClass({displayName: 'PasswordLogin', }; }, + componentWillMount: function() { + this._passwordField = null; + }, + + componentWillReceiveProps: function(nextProps) { + if (!this.props.loginIncorrect && nextProps.loginIncorrect) { + field_input_incorrect(this._passwordField); + } + }, + onSubmitForm: function(ev) { ev.preventDefault(); this.props.onSubmit(this.state.username, this.state.password); @@ -72,14 +87,19 @@ module.exports = React.createClass({displayName: 'PasswordLogin', ); } + const pwFieldClass = classNames({ + mx_Login_field: true, + error: this.props.loginIncorrect, + }); + return (
-
- {this._passwordField = e;}} type="password" value={this.state.password} onChange={this.onPasswordChanged} placeholder="Password" />
@@ -89,4 +109,4 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
); } -}); \ No newline at end of file +}); diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 39c1acc625..33809fbfd6 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -17,8 +17,7 @@ limitations under the License. 'use strict'; var React = require('react'); -var Velocity = require('velocity-vector'); -require('velocity-vector/velocity.ui'); +var UiEffects = require('../../../UiEffects'); var sdk = require('../../../index'); var Email = require('../../../email'); var Modal = require("../../../Modal"); @@ -117,7 +116,7 @@ module.exports = React.createClass({ promise.finally(function() { ev.target.disabled = false; }); - } + } }, /** @@ -196,7 +195,7 @@ module.exports = React.createClass({ fieldValid[field_id] = val; this.setState({fieldValid: fieldValid}); if (!val) { - Velocity(this.fieldElementById(field_id), "callout.shake", 300); + UiEffects.field_input_incorrect(this.fieldElementById(field_id)); this.props.onError(error_code); } }, @@ -214,12 +213,13 @@ module.exports = React.createClass({ } }, - _styleField: function(field_id, baseStyle) { - var style = baseStyle || {}; + _classForField: function(field_id, baseClass) { + let cls = baseClass || ''; if (this.state.fieldValid[field_id] === false) { - style['borderColor'] = 'red'; + if (cls) cls += ' '; + cls += 'error'; } - return style; + return cls; }, render: function() { @@ -227,10 +227,10 @@ module.exports = React.createClass({ var emailSection, registerButton; if (this.props.showEmail) { emailSection = ( - ); } @@ -250,22 +250,22 @@ module.exports = React.createClass({ {emailSection}
-
{ this.props.guestUsername ?
Setting a user name will create a fresh account
: null } -
-