Merge pull request #2780 from jryans/auth-validation

Initial portions of support for Field validation
This commit is contained in:
J. Ryan Stinnett 2019-03-14 10:19:21 +00:00 committed by GitHub
commit 45063cad4a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 262 additions and 100 deletions

View file

@ -95,6 +95,7 @@
@import "./views/elements/_SyntaxHighlight.scss"; @import "./views/elements/_SyntaxHighlight.scss";
@import "./views/elements/_ToggleSwitch.scss"; @import "./views/elements/_ToggleSwitch.scss";
@import "./views/elements/_ToolTipButton.scss"; @import "./views/elements/_ToolTipButton.scss";
@import "./views/elements/_Tooltip.scss";
@import "./views/globals/_MatrixToolbar.scss"; @import "./views/globals/_MatrixToolbar.scss";
@import "./views/groups/_GroupPublicityToggle.scss"; @import "./views/groups/_GroupPublicityToggle.scss";
@import "./views/groups/_GroupRoomList.scss"; @import "./views/groups/_GroupRoomList.scss";
@ -137,7 +138,6 @@
@import "./views/rooms/_RoomPreviewBar.scss"; @import "./views/rooms/_RoomPreviewBar.scss";
@import "./views/rooms/_RoomRecoveryReminder.scss"; @import "./views/rooms/_RoomRecoveryReminder.scss";
@import "./views/rooms/_RoomTile.scss"; @import "./views/rooms/_RoomTile.scss";
@import "./views/rooms/_RoomTooltip.scss";
@import "./views/rooms/_RoomUpgradeWarningBar.scss"; @import "./views/rooms/_RoomUpgradeWarningBar.scss";
@import "./views/rooms/_SearchBar.scss"; @import "./views/rooms/_SearchBar.scss";
@import "./views/rooms/_SearchableEntityList.scss"; @import "./views/rooms/_SearchableEntityList.scss";

View file

@ -141,6 +141,39 @@ limitations under the License.
color: $greyed-fg-color; color: $greyed-fg-color;
} }
.mx_Field_valid {
&.mx_Field,
&.mx_Field:focus-within {
border-color: $input-valid-border-color;
}
&.mx_Field label,
&.mx_Field:focus-within label {
color: $input-valid-border-color;
}
}
.mx_Field_invalid {
&.mx_Field,
&.mx_Field:focus-within {
border-color: $input-invalid-border-color;
}
&.mx_Field label,
&.mx_Field:focus-within label {
color: $input-invalid-border-color;
}
}
.mx_Field_tooltip {
margin-top: -12px;
margin-left: 4px;
}
.mx_Field_tooltip.mx_Field_valid {
animation: mx_fadeout 1s 2s forwards;
}
// Customise other components when placed inside a Field // Customise other components when placed inside a Field
.mx_Field .mx_Dropdown_input { .mx_Field .mx_Dropdown_input {

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,41 +15,55 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_RoomTooltip_chevron { @keyframes mx_fadein {
position: absolute; from { opacity: 0; }
left: -8px; to { opacity: 1; }
top: 4px;
width: 0;
height: 0;
border-top: 8px solid transparent;
border-right: 8px solid $menu-bg-color;
border-bottom: 8px solid transparent;
} }
.mx_RoomTooltip_chevron:after { @keyframes mx_fadeout {
content:''; from { opacity: 1; }
to { opacity: 0; }
}
.mx_Tooltip_chevron {
position: absolute;
left: -7px;
top: 10px;
width: 0; width: 0;
height: 0; height: 0;
border-top: 7px solid transparent; border-top: 7px solid transparent;
border-right: 7px solid $primary-bg-color; border-right: 7px solid $menu-border-color;
border-bottom: 7px solid transparent; border-bottom: 7px solid transparent;
position:absolute; }
top: -7px;
.mx_Tooltip_chevron:after {
content:'';
width: 0;
height: 0;
border-top: 6px solid transparent;
border-right: 6px solid $menu-bg-color;
border-bottom: 6px solid transparent;
position: absolute;
top: -6px;
left: 1px; left: 1px;
} }
.mx_RoomTooltip { .mx_Tooltip {
display: none; display: none;
animation: mx_fadein 0.2s;
position: fixed; position: fixed;
border-radius: 5px; border: 1px solid $menu-border-color;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.21); border-radius: 4px;
background-color: $primary-bg-color; box-shadow: 4px 4px 12px 0 rgba(118, 131, 156, 0.6);
background-color: $menu-bg-color;
z-index: 2000; z-index: 2000;
padding: 5px; padding: 10px;
pointer-events: none; pointer-events: none;
line-height: 14px; line-height: 14px;
font-size: 13px; font-size: 12px;
font-weight: 600;
color: $primary-fg-color; color: $primary-fg-color;
max-width: 600px; max-width: 200px;
word-break: break-word;
margin-right: 50px; margin-right: 50px;
} }

View file

@ -53,6 +53,7 @@ $input-lighter-bg-color: #f2f5f8;
$input-lighter-fg-color: $input-darker-fg-color; $input-lighter-fg-color: $input-darker-fg-color;
$input-focused-border-color: #238cf5; $input-focused-border-color: #238cf5;
$input-valid-border-color: $accent-color; $input-valid-border-color: $accent-color;
$input-invalid-border-color: $warning-color;
$field-focused-label-bg-color: $bg-color; $field-focused-label-bg-color: $bg-color;

View file

@ -79,6 +79,7 @@ $input-lighter-bg-color: #f2f5f8;
$input-lighter-fg-color: $input-darker-fg-color; $input-lighter-fg-color: $input-darker-fg-color;
$input-focused-border-color: #238cf5; $input-focused-border-color: #238cf5;
$input-valid-border-color: $accent-color; $input-valid-border-color: $accent-color;
$input-invalid-border-color: $warning-color;
$field-focused-label-bg-color: #ffffff; $field-focused-label-bg-color: #ffffff;
@ -95,7 +96,7 @@ $input-fg-color: rgba(74, 74, 74, 0.9);
$scrollbar-thumb-color: rgba(0, 0, 0, 0.2); $scrollbar-thumb-color: rgba(0, 0, 0, 0.2);
$scrollbar-track-color: transparent; $scrollbar-track-color: transparent;
// context menus // context menus
$menu-border-color: #ebedf8; $menu-border-color: #e7e7e7;
$menu-bg-color: #fff; $menu-bg-color: #fff;
$menu-box-shadow-color: rgba(118, 131, 156, 0.6); $menu-box-shadow-color: rgba(118, 131, 156, 0.6);
$menu-selected-color: #f5f8fa; $menu-selected-color: #f5f8fa;

View file

@ -145,8 +145,8 @@ module.exports = React.createClass({
// Get the label/tooltip to show // Get the label/tooltip to show
getLabel: function(label, show) { getLabel: function(label, show) {
if (show) { if (show) {
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip"); const Tooltip = sdk.getComponent("elements.Tooltip");
return <RoomTooltip className="mx_BottomLeftMenu_tooltip" label={label} />; return <Tooltip className="mx_BottomLeftMenu_tooltip" label={label} />;
} }
}, },

View file

@ -70,11 +70,6 @@ class PasswordLogin extends React.Component {
this.isLoginEmpty = this.isLoginEmpty.bind(this); this.isLoginEmpty = this.isLoginEmpty.bind(this);
} }
componentWillMount() {
this._passwordField = null;
this._loginField = null;
}
onForgotPasswordClick(ev) { onForgotPasswordClick(ev) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
@ -180,7 +175,6 @@ class PasswordLogin extends React.Component {
return <Field return <Field
className={classNames(classes)} className={classNames(classes)}
id="mx_PasswordLogin_email" id="mx_PasswordLogin_email"
ref={(e) => { this._loginField = e; }}
name="username" // make it a little easier for browser's remember-password name="username" // make it a little easier for browser's remember-password
key="email_input" key="email_input"
type="text" type="text"
@ -196,7 +190,6 @@ class PasswordLogin extends React.Component {
return <Field return <Field
className={classNames(classes)} className={classNames(classes)}
id="mx_PasswordLogin_username" id="mx_PasswordLogin_username"
ref={(e) => { this._loginField = e; }}
name="username" // make it a little easier for browser's remember-password name="username" // make it a little easier for browser's remember-password
key="username_input" key="username_input"
type="text" type="text"
@ -223,7 +216,6 @@ class PasswordLogin extends React.Component {
return <Field return <Field
className={classNames(classes)} className={classNames(classes)}
id="mx_PasswordLogin_phoneNumber" id="mx_PasswordLogin_phoneNumber"
ref={(e) => { this._loginField = e; }}
name="phoneNumber" name="phoneNumber"
key="phone_input" key="phone_input"
type="text" type="text"
@ -344,7 +336,6 @@ class PasswordLogin extends React.Component {
<Field <Field
className={pwFieldClass} className={pwFieldClass}
id="mx_PasswordLogin_password" id="mx_PasswordLogin_password"
ref={(e) => { this._passwordField = e; }}
type="password" type="password"
name="password" name="password"
label={_t('Password')} label={_t('Password')}

View file

@ -69,8 +69,8 @@ export default React.createClass({
let tooltip; let tooltip;
if (this.state.showTooltip) { if (this.state.showTooltip) {
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip"); const Tooltip = sdk.getComponent("elements.Tooltip");
tooltip = <RoomTooltip className="mx_RoleButton_tooltip" label={this.props.label} />; tooltip = <Tooltip className="mx_RoleButton_tooltip" label={this.props.label} />;
} }
const icon = this.props.iconPath ? const icon = this.props.iconPath ?

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import sdk from '../../../index';
export default class Field extends React.PureComponent { export default class Field extends React.PureComponent {
static propTypes = { static propTypes = {
@ -31,25 +32,43 @@ export default class Field extends React.PureComponent {
label: PropTypes.string, label: PropTypes.string,
// The field's placeholder string. Defaults to the label. // The field's placeholder string. Defaults to the label.
placeholder: PropTypes.string, placeholder: PropTypes.string,
// The field's value.
// This is a controlled component, so the value is required.
value: PropTypes.string.isRequired,
// Optional component to include inside the field before the input. // Optional component to include inside the field before the input.
prefix: PropTypes.node, prefix: PropTypes.node,
// The callback called whenever the contents of the field
// changes. Returns an object with `valid` boolean field
// and a `feedback` react component field to provide feedback
// to the user.
onValidate: PropTypes.func,
// All other props pass through to the <input>. // All other props pass through to the <input>.
}; };
get value() { constructor() {
if (!this.refs.fieldInput) return null; super();
return this.refs.fieldInput.value; this.state = {
valid: undefined,
feedback: undefined,
};
} }
set value(newValue) { onChange = (ev) => {
if (!this.refs.fieldInput) { if (this.props.onValidate) {
throw new Error("No field input reference"); const result = this.props.onValidate(ev.target.value);
this.setState({
valid: result.valid,
feedback: result.feedback,
});
} }
this.refs.fieldInput.value = newValue; // Parent component may have supplied its own `onChange` as well
} if (this.props.onChange) {
this.props.onChange(ev);
}
};
render() { render() {
const { element, prefix, children, ...inputProps } = this.props; const { element, prefix, onValidate, children, ...inputProps } = this.props;
const inputElement = element || "input"; const inputElement = element || "input";
@ -58,6 +77,8 @@ export default class Field extends React.PureComponent {
inputProps.ref = "fieldInput"; inputProps.ref = "fieldInput";
inputProps.placeholder = inputProps.placeholder || inputProps.label; inputProps.placeholder = inputProps.placeholder || inputProps.label;
inputProps.onChange = this.onChange;
const fieldInput = React.createElement(inputElement, inputProps, children); const fieldInput = React.createElement(inputElement, inputProps, children);
let prefixContainer = null; let prefixContainer = null;
@ -65,17 +86,37 @@ export default class Field extends React.PureComponent {
prefixContainer = <span className="mx_Field_prefix">{prefix}</span>; prefixContainer = <span className="mx_Field_prefix">{prefix}</span>;
} }
const classes = classNames("mx_Field", `mx_Field_${inputElement}`, { let validClass;
if (onValidate) {
validClass = classNames({
mx_Field_valid: this.state.valid === true,
mx_Field_invalid: this.state.valid === false,
});
}
const fieldClasses = classNames("mx_Field", `mx_Field_${inputElement}`, {
// If we have a prefix element, leave the label always at the top left and // If we have a prefix element, leave the label always at the top left and
// don't animate it, as it looks a bit clunky and would add complexity to do // don't animate it, as it looks a bit clunky and would add complexity to do
// properly. // properly.
mx_Field_labelAlwaysTopLeft: prefix, mx_Field_labelAlwaysTopLeft: prefix,
[validClass]: true,
}); });
return <div className={classes}> // handle displaying feedback on validity
const Tooltip = sdk.getComponent("elements.Tooltip");
let feedback;
if (this.state.feedback) {
feedback = <Tooltip
tooltipClassName="mx_Field_tooltip"
label={this.state.feedback}
/>;
}
return <div className={fieldClasses}>
{prefixContainer} {prefixContainer}
{fieldInput} {fieldInput}
<label htmlFor={this.props.id}>{this.props.label}</label> <label htmlFor={this.props.id}>{this.props.label}</label>
{feedback}
</div>; </div>;
} }
} }

View file

@ -156,7 +156,7 @@ export default React.createClass({
render: function() { render: function() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip'); const Tooltip = sdk.getComponent('elements.Tooltip');
const profile = this.state.profile || {}; const profile = this.state.profile || {};
const name = profile.name || this.props.tag; const name = profile.name || this.props.tag;
const avatarHeight = 40; const avatarHeight = 40;
@ -181,7 +181,7 @@ export default React.createClass({
} }
const tip = this.state.hover ? const tip = this.state.hover ?
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> : <Tooltip className="mx_TagTile_tooltip" label={name} /> :
<div />; <div />;
const contextButton = this.state.hover || this.state.menuDisplayed ? const contextButton = this.state.hover || this.state.menuDisplayed ?
<div className="mx_TagTile_context_button" onClick={this.onContextButtonClick}> <div className="mx_TagTile_context_button" onClick={this.onContextButtonClick}>

View file

@ -39,8 +39,8 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip"); const Tooltip = sdk.getComponent("elements.Tooltip");
const tip = this.state.hover ? <RoomTooltip const tip = this.state.hover ? <Tooltip
className="mx_ToolTipButton_container" className="mx_ToolTipButton_container"
tooltipClassName="mx_ToolTipButton_helpText" tooltipClassName="mx_ToolTipButton_helpText"
label={this.props.helpText} label={this.props.helpText}

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -23,22 +24,21 @@ import classNames from 'classnames';
const MIN_TOOLTIP_HEIGHT = 25; const MIN_TOOLTIP_HEIGHT = 25;
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'RoomTooltip', displayName: 'Tooltip',
propTypes: { propTypes: {
// Class applied to the element used to position the tooltip // Class applied to the element used to position the tooltip
className: React.PropTypes.string.isRequired, className: React.PropTypes.string,
// Class applied to the tooltip itself // Class applied to the tooltip itself
tooltipClassName: React.PropTypes.string, tooltipClassName: React.PropTypes.string,
// The tooltip is derived from either the room name or a label // the react element to put into the tooltip
room: React.PropTypes.object,
label: React.PropTypes.node, label: React.PropTypes.node,
}, },
// Create a wrapper for the tooltip outside the parent and attach it to the body element // Create a wrapper for the tooltip outside the parent and attach it to the body element
componentDidMount: function() { componentDidMount: function() {
this.tooltipContainer = document.createElement("div"); this.tooltipContainer = document.createElement("div");
this.tooltipContainer.className = "mx_RoomTileTooltip_wrapper"; this.tooltipContainer.className = "mx_Tooltip_wrapper";
document.body.appendChild(this.tooltipContainer); document.body.appendChild(this.tooltipContainer);
window.addEventListener('scroll', this._renderTooltip, true); window.addEventListener('scroll', this._renderTooltip, true);
@ -85,11 +85,11 @@ module.exports = React.createClass({
style = this._updatePosition(style); style = this._updatePosition(style);
style.display = "block"; style.display = "block";
const tooltipClasses = classNames("mx_RoomTooltip", this.props.tooltipClassName); const tooltipClasses = classNames("mx_Tooltip", this.props.tooltipClassName);
const tooltip = ( const tooltip = (
<div className={tooltipClasses} style={style}> <div className={tooltipClasses} style={style}>
<div className="mx_RoomTooltip_chevron" /> <div className="mx_Tooltip_chevron" />
{ this.props.label } { this.props.label }
</div> </div>
); );

View file

@ -143,8 +143,8 @@ export default React.createClass({
let tooltip; let tooltip;
if (this.props.collapsed && this.state.hover) { if (this.props.collapsed && this.state.hover) {
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip"); const Tooltip = sdk.getComponent("elements.Tooltip");
tooltip = <RoomTooltip className="mx_RoomTile_tooltip" label={groupName} dir="auto" />; tooltip = <Tooltip className="mx_RoomTile_tooltip" label={groupName} dir="auto" />;
} }
const classes = classNames('mx_RoomTile mx_RoomTile_highlight', { const classes = classNames('mx_RoomTile mx_RoomTile_highlight', {

View file

@ -44,9 +44,9 @@ export default class MStickerBody extends MImageBody {
if (!content || !content.body || !content.info || !content.info.w) return null; if (!content || !content.body || !content.info || !content.info.w) return null;
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip'); const Tooltip = sdk.getComponent('elements.Tooltip');
return <div style={{left: content.info.w + 'px'}} className="mx_MStickerBody_tooltip"> return <div style={{left: content.info.w + 'px'}} className="mx_MStickerBody_tooltip">
<RoomTooltip label={content.body} /> <Tooltip label={content.body} />
</div>; </div>;
} }

View file

@ -364,8 +364,8 @@ module.exports = React.createClass({
label = <EmojiText element="div" title={name} className={nameClasses} dir="auto">{ name }</EmojiText>; label = <EmojiText element="div" title={name} className={nameClasses} dir="auto">{ name }</EmojiText>;
} }
} else if (this.state.hover) { } else if (this.state.hover) {
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip"); const Tooltip = sdk.getComponent("elements.Tooltip");
tooltip = <RoomTooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />; tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />;
} }
//var incomingCallBox; //var incomingCallBox;

View file

@ -32,6 +32,7 @@ import sessionStore from '../../../stores/SessionStore';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'ChangePassword', displayName: 'ChangePassword',
propTypes: { propTypes: {
onFinished: PropTypes.func, onFinished: PropTypes.func,
onError: PropTypes.func, onError: PropTypes.func,
@ -73,6 +74,9 @@ module.exports = React.createClass({
return { return {
phase: this.Phases.Edit, phase: this.Phases.Edit,
cachedPassword: null, cachedPassword: null,
oldPassword: "",
newPassword: "",
newPasswordConfirm: "",
}; };
}, },
@ -165,6 +169,9 @@ module.exports = React.createClass({
}).finally(() => { }).finally(() => {
this.setState({ this.setState({
phase: this.Phases.Edit, phase: this.Phases.Edit,
oldPassword: "",
newPassword: "",
newPasswordConfirm: "",
}); });
}).done(); }).done();
}, },
@ -192,11 +199,29 @@ module.exports = React.createClass({
); );
}, },
onChangeOldPassword(ev) {
this.setState({
oldPassword: ev.target.value,
});
},
onChangeNewPassword(ev) {
this.setState({
newPassword: ev.target.value,
});
},
onChangeNewPasswordConfirm(ev) {
this.setState({
newPasswordConfirm: ev.target.value,
});
},
onClickChange: function(ev) { onClickChange: function(ev) {
ev.preventDefault(); ev.preventDefault();
const oldPassword = this.state.cachedPassword || this.refs.old_input.value; const oldPassword = this.state.cachedPassword || this.state.oldPassword;
const newPassword = this.refs.new_input.value; const newPassword = this.state.newPassword;
const confirmPassword = this.refs.confirm_input.value; const confirmPassword = this.state.newPasswordConfirm;
const err = this.props.onCheckPassword( const err = this.props.onCheckPassword(
oldPassword, newPassword, confirmPassword, oldPassword, newPassword, confirmPassword,
); );
@ -217,7 +242,12 @@ module.exports = React.createClass({
if (!this.state.cachedPassword) { if (!this.state.cachedPassword) {
currentPassword = ( currentPassword = (
<div className={rowClassName}> <div className={rowClassName}>
<Field id="passwordold" type="password" ref="old_input" label={_t('Current password')} /> <Field id="mx_ChangePassword_oldPassword"
type="password"
label={_t('Current password')}
value={this.state.oldPassword}
onChange={this.onChangeOldPassword}
/>
</div> </div>
); );
} }
@ -230,11 +260,21 @@ module.exports = React.createClass({
<form className={this.props.className} onSubmit={this.onClickChange}> <form className={this.props.className} onSubmit={this.onClickChange}>
{ currentPassword } { currentPassword }
<div className={rowClassName}> <div className={rowClassName}>
<Field id="password1" type="password" ref="new_input" label={passwordLabel} <Field id="mx_ChangePassword_newPassword"
autoFocus={this.props.autoFocusNewPasswordInput} /> type="password"
label={passwordLabel}
value={this.state.newPassword}
autoFocus={this.props.autoFocusNewPasswordInput}
onChange={this.onChangeNewPassword}
/>
</div> </div>
<div className={rowClassName}> <div className={rowClassName}>
<Field id="password2" type="password" ref="confirm_input" label={_t("Confirm password")} /> <Field id="mx_ChangePassword_newPasswordConfirm"
type="password"
label={_t("Confirm password")}
value={this.state.newPasswordConfirm}
onChange={this.onChangeNewPasswordConfirm}
/>
</div> </div>
<AccessibleButton className={buttonClassName} kind={this.props.buttonKind} onClick={this.onClickChange}> <AccessibleButton className={buttonClassName} kind={this.props.buttonKind} onClick={this.onClickChange}>
{ _t('Change Password') } { _t('Change Password') }

View file

@ -119,6 +119,7 @@ export default class EmailAddresses extends React.Component {
verifying: false, verifying: false,
addTask: null, addTask: null,
continueDisabled: false, continueDisabled: false,
newEmailAddress: "",
}; };
} }
@ -134,14 +135,20 @@ export default class EmailAddresses extends React.Component {
this.setState({emails: this.state.emails.filter((e) => e !== address)}); this.setState({emails: this.state.emails.filter((e) => e !== address)});
}; };
_onChangeNewEmailAddress = (e) => {
this.setState({
newEmailAddress: e.target.value,
});
};
_onAddClick = (e) => { _onAddClick = (e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
if (!this.refs.newEmailAddress) return; if (!this.state.newEmailAddress) return;
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const email = this.refs.newEmailAddress.value; const email = this.state.newEmailAddress;
// TODO: Inline field validation // TODO: Inline field validation
if (!Email.looksValid(email)) { if (!Email.looksValid(email)) {
@ -173,14 +180,14 @@ export default class EmailAddresses extends React.Component {
this.setState({continueDisabled: true}); this.setState({continueDisabled: true});
this.state.addTask.checkEmailLinkClicked().then(() => { this.state.addTask.checkEmailLinkClicked().then(() => {
const email = this.refs.newEmailAddress.value; const email = this.state.newEmailAddress;
this.setState({ this.setState({
emails: [...this.state.emails, {address: email, medium: "email"}], emails: [...this.state.emails, {address: email, medium: "email"}],
addTask: null, addTask: null,
continueDisabled: false, continueDisabled: false,
verifying: false, verifying: false,
newEmailAddress: "",
}); });
this.refs.newEmailAddress.value = "";
}).catch((err) => { }).catch((err) => {
this.setState({continueDisabled: false}); this.setState({continueDisabled: false});
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') { if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
@ -221,8 +228,14 @@ export default class EmailAddresses extends React.Component {
{existingEmailElements} {existingEmailElements}
<form onSubmit={this._onAddClick} autoComplete={false} <form onSubmit={this._onAddClick} autoComplete={false}
noValidate={true} className="mx_EmailAddresses_new"> noValidate={true} className="mx_EmailAddresses_new">
<Field id="newEmailAddress" ref="newEmailAddress" label={_t("Email Address")} <Field id="mx_EmailAddressses_newEmailAddress"
type="text" autoComplete="off" disabled={this.state.verifying} /> type="text"
label={_t("Email Address")}
autoComplete="off"
disabled={this.state.verifying}
value={this.state.newEmailAddress}
onChange={this._onChangeNewEmailAddress}
/>
{addButton} {addButton}
</form> </form>
</div> </div>

View file

@ -117,6 +117,8 @@ export default class PhoneNumbers extends React.Component {
addTask: null, addTask: null,
continueDisabled: false, continueDisabled: false,
phoneCountry: "", phoneCountry: "",
newPhoneNumber: "",
newPhoneNumberCode: "",
}; };
} }
@ -132,14 +134,26 @@ export default class PhoneNumbers extends React.Component {
this.setState({msisdns: this.state.msisdns.filter((e) => e !== address)}); this.setState({msisdns: this.state.msisdns.filter((e) => e !== address)});
}; };
_onChangeNewPhoneNumber = (e) => {
this.setState({
newPhoneNumber: e.target.value,
});
};
_onChangeNewPhoneNumberCode = (e) => {
this.setState({
newPhoneNumberCode: e.target.value,
});
};
_onAddClick = (e) => { _onAddClick = (e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
if (!this.refs.newPhoneNumber) return; if (!this.state.newPhoneNumber) return;
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const phoneNumber = this.refs.newPhoneNumber.value; const phoneNumber = this.state.newPhoneNumber;
const phoneCountry = this.state.phoneCountry; const phoneCountry = this.state.phoneCountry;
const task = new AddThreepid(); const task = new AddThreepid();
@ -162,7 +176,7 @@ export default class PhoneNumbers extends React.Component {
e.preventDefault(); e.preventDefault();
this.setState({continueDisabled: true}); this.setState({continueDisabled: true});
const token = this.refs.newPhoneNumberCode.value; const token = this.state.newPhoneNumberCode;
this.state.addTask.haveMsisdnToken(token).then(() => { this.state.addTask.haveMsisdnToken(token).then(() => {
this.setState({ this.setState({
msisdns: [...this.state.msisdns, {address: this.state.verifyMsisdn, medium: "msisdn"}], msisdns: [...this.state.msisdns, {address: this.state.verifyMsisdn, medium: "msisdn"}],
@ -171,8 +185,9 @@ export default class PhoneNumbers extends React.Component {
verifying: false, verifying: false,
verifyMsisdn: "", verifyMsisdn: "",
verifyError: null, verifyError: null,
newPhoneNumber: "",
newPhoneNumberCode: "",
}); });
this.refs.newPhoneNumber.value = "";
}).catch((err) => { }).catch((err) => {
this.setState({continueDisabled: false}); this.setState({continueDisabled: false});
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') { if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
@ -205,22 +220,28 @@ export default class PhoneNumbers extends React.Component {
if (this.state.verifying) { if (this.state.verifying) {
const msisdn = this.state.verifyMsisdn; const msisdn = this.state.verifyMsisdn;
addVerifySection = ( addVerifySection = (
<div> <div>
<div> <div>
{_t("A text message has been sent to +%(msisdn)s. " + {_t("A text message has been sent to +%(msisdn)s. " +
"Please enter the verification code it contains", {msisdn: msisdn})} "Please enter the verification code it contains", { msisdn: msisdn })}
<br /> <br />
{this.state.verifyError} {this.state.verifyError}
</div> </div>
<form onSubmit={this._onContinueClick} autoComplete={false} noValidate={true}> <form onSubmit={this._onContinueClick} autoComplete={false} noValidate={true}>
<Field id="newPhoneNumberCode" ref="newPhoneNumberCode" label={_t("Verification code")} <Field id="mx_PhoneNumbers_newPhoneNumberCode"
type="text" autoComplete="off" disabled={this.state.continueDisabled} /> type="text"
<AccessibleButton onClick={this._onContinueClick} kind="primary" label={_t("Verification code")}
disabled={this.state.continueDisabled}> autoComplete="off"
{_t("Continue")} disabled={this.state.continueDisabled}
</AccessibleButton> value={this.state.newPhoneNumberCode}
</form> onChange={this._onChangeNewPhoneNumberCode}
</div> />
<AccessibleButton onClick={this._onContinueClick} kind="primary"
disabled={this.state.continueDisabled}>
{_t("Continue")}
</AccessibleButton>
</form>
</div>
); );
} }
@ -238,9 +259,15 @@ export default class PhoneNumbers extends React.Component {
<form onSubmit={this._onAddClick} autoComplete={false} <form onSubmit={this._onAddClick} autoComplete={false}
noValidate={true} className="mx_PhoneNumbers_new"> noValidate={true} className="mx_PhoneNumbers_new">
<div className="mx_PhoneNumbers_input"> <div className="mx_PhoneNumbers_input">
<Field id="newPhoneNumber" ref="newPhoneNumber" label={_t("Phone Number")} <Field id="mx_PhoneNumbers_newPhoneNumber"
type="text" autoComplete="off" disabled={this.state.verifying} type="text"
prefix={phoneCountry} /> label={_t("Phone Number")}
autoComplete="off"
disabled={this.state.verifying}
prefix={phoneCountry}
value={this.state.newPhoneNumber}
onChange={this._onChangeNewPhoneNumber}
/>
</div> </div>
{addVerifySection} {addVerifySection}
</form> </form>