Bring over email address management

This commit is contained in:
Travis Ralston 2019-01-22 15:18:14 -07:00
parent fa1ce61a06
commit aa7afe819f
11 changed files with 342 additions and 11 deletions

View file

@ -127,6 +127,7 @@
@import "./views/rooms/_TopUnreadMessagesBar.scss"; @import "./views/rooms/_TopUnreadMessagesBar.scss";
@import "./views/rooms/_WhoIsTypingTile.scss"; @import "./views/rooms/_WhoIsTypingTile.scss";
@import "./views/settings/_DevicesPanel.scss"; @import "./views/settings/_DevicesPanel.scss";
@import "./views/settings/_EmailAddresses.scss";
@import "./views/settings/_IntegrationsManager.scss"; @import "./views/settings/_IntegrationsManager.scss";
@import "./views/settings/_KeyBackupPanel.scss"; @import "./views/settings/_KeyBackupPanel.scss";
@import "./views/settings/_Notifications.scss"; @import "./views/settings/_Notifications.scss";

View file

@ -42,3 +42,35 @@ limitations under the License.
color: $button-primary-disabled-fg-color; color: $button-primary-disabled-fg-color;
background-color: $button-primary-disabled-bg-color; background-color: $button-primary-disabled-bg-color;
} }
.mx_AccessibleButton_kind_primary_sm {
padding: 5px 12px !important;
color: $button-primary-fg-color;
background-color: $button-primary-bg-color;
}
.mx_AccessibleButton_kind_primary_sm.mx_AccessibleButton_disabled {
color: $button-primary-disabled-fg-color;
background-color: $button-primary-disabled-bg-color;
}
.mx_AccessibleButton_kind_danger {
color: $button-danger-fg-color;
background-color: $button-danger-bg-color;
}
.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled {
color: $button-danger-disabled-fg-color;
background-color: $button-danger-disabled-bg-color;
}
.mx_AccessibleButton_kind_danger_sm {
padding: 5px 12px !important;
color: $button-danger-fg-color;
background-color: $button-danger-bg-color;
}
.mx_AccessibleButton_kind_danger_sm.mx_AccessibleButton_disabled {
color: $button-danger-disabled-fg-color;
background-color: $button-danger-disabled-bg-color;
}

View file

@ -0,0 +1,41 @@
/*
Copyright 2019 New Vector 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.
*/
.mx_ExistingEmailAddress {
margin-bottom: 5px;
}
.mx_ExistingEmailAddress_delete {
margin-right: 5px;
cursor: pointer;
vertical-align: middle;
}
.mx_ExistingEmailAddress_email {
vertical-align: middle;
}
.mx_ExistingEmailAddress_promptText {
margin-right: 10px;
}
.mx_ExistingEmailAddress_confirmBtn {
margin-right: 5px;
}
.mx_EmailAddresses_new .mx_Field input {
width: calc(100% - 20px);
}

View file

@ -30,4 +30,8 @@ limitations under the License.
.mx_GeneralSettingsTab_changePassword .mx_Field:first-child { .mx_GeneralSettingsTab_changePassword .mx_Field:first-child {
margin-top: 0; margin-top: 0;
}
.mx_GeneralSettingsTab_accountSection > .mx_EmailAddresses {
margin-right: 100px; // Align with the other fields on the page
} }

View file

@ -211,6 +211,10 @@ $button-primary-fg-color: #ffffff;
$button-primary-bg-color: #7ac9a1; $button-primary-bg-color: #7ac9a1;
$button-primary-disabled-fg-color: #ffffff; $button-primary-disabled-fg-color: #ffffff;
$button-primary-disabled-bg-color: #bce4d0; $button-primary-disabled-bg-color: #bce4d0;
$button-danger-fg-color: #ffffff;
$button-danger-bg-color: #f56679;
$button-danger-disabled-fg-color: #ffffff;
$button-danger-disabled-bg-color: #f5b6bb; // TODO: Verify color
// unused? // unused?
$progressbar-color: #000; $progressbar-color: #000;

View file

@ -207,6 +207,10 @@ $button-primary-fg-color: #ffffff;
$button-primary-bg-color: #7ac9a1; $button-primary-bg-color: #7ac9a1;
$button-primary-disabled-fg-color: #ffffff; $button-primary-disabled-fg-color: #ffffff;
$button-primary-disabled-bg-color: #bce4d0; $button-primary-disabled-bg-color: #bce4d0;
$button-danger-fg-color: #ffffff;
$button-danger-bg-color: #f56679;
$button-danger-disabled-fg-color: #ffffff;
$button-danger-disabled-bg-color: #f5b6bb; // TODO: Verify color
// unused? // unused?
$progressbar-color: #000; $progressbar-color: #000;

View file

@ -26,7 +26,7 @@ import { _t } from './languageHandler';
* the client owns the given email address, which is then passed to the * the client owns the given email address, which is then passed to the
* add threepid API on the homeserver. * add threepid API on the homeserver.
*/ */
class AddThreepid { export default class AddThreepid {
constructor() { constructor() {
this.clientSecret = MatrixClientPeg.get().generateClientSecret(); this.clientSecret = MatrixClientPeg.get().generateClientSecret();
} }
@ -124,5 +124,3 @@ class AddThreepid {
}); });
} }
} }
module.exports = AddThreepid;

View file

@ -38,6 +38,13 @@ export default class Field extends React.PureComponent {
return this.refs.fieldInput.value; return this.refs.fieldInput.value;
} }
set value(newValue) {
if (!this.refs.fieldInput) {
throw new Error("No field input reference");
}
this.refs.fieldInput.value = newValue;
}
render() { render() {
const extraProps = Object.assign({}, this.props); const extraProps = Object.assign({}, this.props);

View file

@ -0,0 +1,231 @@
/*
Copyright 2019 New Vector 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.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import MatrixClientPeg from "../../../MatrixClientPeg";
import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton";
import * as Email from "../../../email";
import AddThreepid from "../../../AddThreepid";
const sdk = require('../../../index');
const Modal = require("../../../Modal");
/*
TODO: Improve the UX for everything in here.
It's very much placeholder, but it gets the job done. The old way of handling
email addresses in user settings was to use dialogs to communicate state, however
due to our dialog system overriding dialogs (causing unmounts) this creates problems
for a sane UX. For instance, the user could easily end up entering an email address
and receive a dialog to verify the address, which then causes the component here
to forget what it was doing and ultimately fail. Dialogs are still used in some
places to communicate errors - these should be replaced with inline validation when
that is available.
*/
export class ExistingEmailAddress extends React.Component {
static propTypes = {
email: PropTypes.object.isRequired,
onRemoved: PropTypes.func.isRequired,
};
constructor() {
super();
this.state = {
verifyRemove: false,
};
}
_onRemove = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({verifyRemove: true});
};
_onDontRemove = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({verifyRemove: false});
};
_onActuallyRemove = (e) => {
e.stopPropagation();
e.preventDefault();
MatrixClientPeg.get().deleteThreePid(this.props.email.medium, this.props.email.address).then(() => {
return this.props.onRemoved(this.props.email);
}).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to remove contact information: " + err);
Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
title: _t("Unable to remove contact information"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
};
render() {
if (this.state.verifyRemove) {
return (
<div className="mx_ExistingEmailAddress">
<span className="mx_ExistingEmailAddress_promptText">
{_t("Are you sure?")}
</span>
<AccessibleButton onClick={this._onActuallyRemove} kind="primary_sm"
className="mx_ExistingEmailAddress_confirmBtn">
{_t("Yes")}
</AccessibleButton>
<AccessibleButton onClick={this._onDontRemove} kind="danger_sm"
className="mx_ExistingEmailAddress_confirmBtn">
{_t("No")}
</AccessibleButton>
</div>
)
}
return (
<div className="mx_ExistingEmailAddress">
<img src={require("../../../../res/img/feather-icons/cancel.svg")} width={14} height={14}
onClick={this._onRemove} className="mx_ExistingEmailAddress_delete" alt={_t("Remove")} />
<span className="mx_ExistingEmailAddress_email">{this.props.email.address}</span>
</div>
);
}
}
export default class EmailAddresses extends React.Component {
constructor() {
super();
this.state = {
emails: [],
verifying: false,
addTask: null,
continueDisabled: false,
};
}
componentWillMount(): void {
const client = MatrixClientPeg.get();
client.getThreePids().then((addresses) => {
this.setState({emails: addresses.threepids.filter((a) => a.medium === 'email')});
});
}
_onRemoved = (address) => {
this.setState({emails: this.state.emails.filter((e) => e !== address)});
};
_onAddClick = (e) => {
e.stopPropagation();
e.preventDefault();
if (!this.refs.newEmailAddress) return;
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const email = this.refs.newEmailAddress.value;
// TODO: Inline field validation
if (!Email.looksValid(email)) {
Modal.createTrackedDialog('Invalid email address', '', ErrorDialog, {
title: _t("Invalid Email Address"),
description: _t("This doesn't appear to be a valid email address"),
});
return;
}
const task = new AddThreepid();
this.setState({verifying: true, continueDisabled: true, addTask: task});
task.addEmailAddress(email, true).then(() => {
this.setState({continueDisabled: false});
}).catch((err) => {
console.error("Unable to add email address " + email + " " + err);
this.setState({verifying: false, continueDisabled: false, addTask: null});
Modal.createTrackedDialog('Unable to add email address', '', ErrorDialog, {
title: _t("Unable to add email address"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
};
_onContinueClick = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({continueDisabled: true});
this.state.addTask.checkEmailLinkClicked().then(() => {
const email = this.refs.newEmailAddress.value;
this.setState({
emails: [...this.state.emails, {address: email, medium: "email"}],
addTask: null,
continueDisabled: false,
verifying: false,
});
this.refs.newEmailAddress.value = "";
}).catch((err) => {
this.setState({continueDisabled: false});
if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to verify email address: " + err);
Modal.createTrackedDialog('Unable to verify email address', '', ErrorDialog, {
title: _t("Unable to verify email address."),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
}
});
};
render() {
const existingEmailElements = this.state.emails.map((e) => {
return <ExistingEmailAddress email={e} onRemoved={this._onRemoved} key={e.address}/>;
});
let addButton = (
<AccessibleButton onClick={this._onAddClick} kind="primary">
{_t("Add")}
</AccessibleButton>
);
if (this.state.verifying) {
addButton = (
<div>
<div>{_t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.")}</div>
<AccessibleButton onClick={this._onContinueClick} kind="primary"
disabled={this.state.continueDisabled}>
{_t("Continue")}
</AccessibleButton>
</div>
);
}
return (
<div className="mx_EmailAddresses">
{existingEmailElements}
<form onSubmit={this._onAddClick} autoComplete={false}
noValidate={true} className="mx_EmailAddresses_new">
<Field id="newEmailAddress" ref="newEmailAddress" label={_t("Email Address")}
type="text" autoComplete="off" disabled={this.state.verifying}/>
{addButton}
</form>
</div>
);
}
}

View file

@ -22,6 +22,7 @@ import PropTypes from "prop-types";
import {MatrixClient} from "matrix-js-sdk"; import {MatrixClient} from "matrix-js-sdk";
import { DragDropContext } from 'react-beautiful-dnd'; import { DragDropContext } from 'react-beautiful-dnd';
import ProfileSettings from "../ProfileSettings"; import ProfileSettings from "../ProfileSettings";
import EmailAddresses from "../EmailAddresses";
const sdk = require('../../../../index'); const sdk = require('../../../../index');
const Modal = require("../../../../Modal"); const Modal = require("../../../../Modal");
@ -95,12 +96,15 @@ export default class GeneralSettingsTab extends React.Component {
); );
return ( return (
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section mx_GeneralSettingsTab_accountSection">
<span className="mx_SettingsTab_subheading">{_t("Account")}</span> <span className="mx_SettingsTab_subheading">{_t("Account")}</span>
<p className="mx_SettingsTab_subsectionText"> <p className="mx_SettingsTab_subsectionText">
{_t("Set a new account password...")} {_t("Set a new account password...")}
</p> </p>
{passwordChangeForm} {passwordChangeForm}
<span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span>
<EmailAddresses />
</div> </div>
); );
} }

View file

@ -352,6 +352,17 @@
"Last seen": "Last seen", "Last seen": "Last seen",
"Select devices": "Select devices", "Select devices": "Select devices",
"Failed to set display name": "Failed to set display name", "Failed to set display name": "Failed to set display name",
"Unable to remove contact information": "Unable to remove contact information",
"Are you sure?": "Are you sure?",
"Yes": "Yes",
"No": "No",
"Remove": "Remove",
"Invalid Email Address": "Invalid Email Address",
"This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address",
"Unable to add email address": "Unable to add email address",
"Unable to verify email address.": "Unable to verify email address.",
"We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.",
"Email Address": "Email Address",
"Disable Notifications": "Disable Notifications", "Disable Notifications": "Disable Notifications",
"Enable Notifications": "Enable Notifications", "Enable Notifications": "Enable Notifications",
"Delete Backup": "Delete Backup", "Delete Backup": "Delete Backup",
@ -413,6 +424,7 @@
"Flair": "Flair", "Flair": "Flair",
"Account": "Account", "Account": "Account",
"Set a new account password...": "Set a new account password...", "Set a new account password...": "Set a new account password...",
"Email addresses": "Email addresses",
"Language and region": "Language and region", "Language and region": "Language and region",
"Theme": "Theme", "Theme": "Theme",
"Account management": "Account management", "Account management": "Account management",
@ -464,7 +476,6 @@
"Failed to toggle moderator status": "Failed to toggle moderator status", "Failed to toggle moderator status": "Failed to toggle moderator status",
"Failed to change power level": "Failed to change power level", "Failed to change power level": "Failed to change power level",
"You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.", "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.",
"Are you sure?": "Are you sure?",
"No devices with registered encryption keys": "No devices with registered encryption keys", "No devices with registered encryption keys": "No devices with registered encryption keys",
"Devices": "Devices", "Devices": "Devices",
"Unignore": "Unignore", "Unignore": "Unignore",
@ -737,7 +748,6 @@
"Flair will not appear": "Flair will not appear", "Flair will not appear": "Flair will not appear",
"Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?", "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?",
"Removing a room from the community will also remove it from the community page.": "Removing a room from the community will also remove it from the community page.", "Removing a room from the community will also remove it from the community page.": "Removing a room from the community will also remove it from the community page.",
"Remove": "Remove",
"Failed to remove room from community": "Failed to remove room from community", "Failed to remove room from community": "Failed to remove room from community",
"Failed to remove '%(roomName)s' from %(groupId)s": "Failed to remove '%(roomName)s' from %(groupId)s", "Failed to remove '%(roomName)s' from %(groupId)s": "Failed to remove '%(roomName)s' from %(groupId)s",
"Something went wrong!": "Something went wrong!", "Something went wrong!": "Something went wrong!",
@ -982,12 +992,8 @@
"We encountered an error trying to restore your previous session.": "We encountered an error trying to restore your previous session.", "We encountered an error trying to restore your previous session.": "We encountered an error trying to restore your previous session.",
"If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.", "If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.",
"Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.", "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.",
"Invalid Email Address": "Invalid Email Address",
"This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address",
"Verification Pending": "Verification Pending", "Verification Pending": "Verification Pending",
"Please check your email and click on the link it contains. Once this is done, click continue.": "Please check your email and click on the link it contains. Once this is done, click continue.", "Please check your email and click on the link it contains. Once this is done, click continue.": "Please check your email and click on the link it contains. Once this is done, click continue.",
"Unable to add email address": "Unable to add email address",
"Unable to verify email address.": "Unable to verify email address.",
"Email address": "Email address", "Email address": "Email address",
"This will allow you to reset your password and receive notifications.": "This will allow you to reset your password and receive notifications.", "This will allow you to reset your password and receive notifications.": "This will allow you to reset your password and receive notifications.",
"Skip": "Skip", "Skip": "Skip",
@ -1253,7 +1259,6 @@
"Server may be unavailable or overloaded": "Server may be unavailable or overloaded", "Server may be unavailable or overloaded": "Server may be unavailable or overloaded",
"Remove Contact Information?": "Remove Contact Information?", "Remove Contact Information?": "Remove Contact Information?",
"Remove %(threePid)s?": "Remove %(threePid)s?", "Remove %(threePid)s?": "Remove %(threePid)s?",
"Unable to remove contact information": "Unable to remove contact information",
"Refer a friend to Riot:": "Refer a friend to Riot:", "Refer a friend to Riot:": "Refer a friend to Riot:",
"Interface Language": "Interface Language", "Interface Language": "Interface Language",
"User Interface": "User Interface", "User Interface": "User Interface",