diff --git a/res/css/_components.scss b/res/css/_components.scss index ee34eeb524..89573ee865 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -127,6 +127,7 @@ @import "./views/rooms/_TopUnreadMessagesBar.scss"; @import "./views/rooms/_WhoIsTypingTile.scss"; @import "./views/settings/_DevicesPanel.scss"; +@import "./views/settings/_EmailAddresses.scss"; @import "./views/settings/_IntegrationsManager.scss"; @import "./views/settings/_KeyBackupPanel.scss"; @import "./views/settings/_Notifications.scss"; diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index 23445f5f6f..27c3a152ba 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -42,3 +42,35 @@ limitations under the License. color: $button-primary-disabled-fg-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; +} \ No newline at end of file diff --git a/res/css/views/settings/_EmailAddresses.scss b/res/css/views/settings/_EmailAddresses.scss new file mode 100644 index 0000000000..1b61d2a9b5 --- /dev/null +++ b/res/css/views/settings/_EmailAddresses.scss @@ -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); +} diff --git a/res/css/views/settings/tabs/_GeneralSettingsTab.scss b/res/css/views/settings/tabs/_GeneralSettingsTab.scss index d1a31e37f8..2cfaebfb4d 100644 --- a/res/css/views/settings/tabs/_GeneralSettingsTab.scss +++ b/res/css/views/settings/tabs/_GeneralSettingsTab.scss @@ -30,4 +30,8 @@ limitations under the License. .mx_GeneralSettingsTab_changePassword .mx_Field:first-child { margin-top: 0; +} + +.mx_GeneralSettingsTab_accountSection > .mx_EmailAddresses { + margin-right: 100px; // Align with the other fields on the page } \ No newline at end of file diff --git a/res/themes/dharma/css/_dharma.scss b/res/themes/dharma/css/_dharma.scss index f54c058220..03d59cfc9d 100644 --- a/res/themes/dharma/css/_dharma.scss +++ b/res/themes/dharma/css/_dharma.scss @@ -211,6 +211,10 @@ $button-primary-fg-color: #ffffff; $button-primary-bg-color: #7ac9a1; $button-primary-disabled-fg-color: #ffffff; $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? $progressbar-color: #000; diff --git a/res/themes/light/css/_base.scss b/res/themes/light/css/_base.scss index fce4e93112..d9d9bff2d3 100644 --- a/res/themes/light/css/_base.scss +++ b/res/themes/light/css/_base.scss @@ -207,6 +207,10 @@ $button-primary-fg-color: #ffffff; $button-primary-bg-color: #7ac9a1; $button-primary-disabled-fg-color: #ffffff; $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? $progressbar-color: #000; diff --git a/src/AddThreepid.js b/src/AddThreepid.js index 337e38d867..8de7e9e21c 100644 --- a/src/AddThreepid.js +++ b/src/AddThreepid.js @@ -26,7 +26,7 @@ import { _t } from './languageHandler'; * the client owns the given email address, which is then passed to the * add threepid API on the homeserver. */ -class AddThreepid { +export default class AddThreepid { constructor() { this.clientSecret = MatrixClientPeg.get().generateClientSecret(); } @@ -124,5 +124,3 @@ class AddThreepid { }); } } - -module.exports = AddThreepid; diff --git a/src/components/views/elements/Field.js b/src/components/views/elements/Field.js index bbbd1e6bf7..42521120b9 100644 --- a/src/components/views/elements/Field.js +++ b/src/components/views/elements/Field.js @@ -38,6 +38,13 @@ export default class Field extends React.PureComponent { 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() { const extraProps = Object.assign({}, this.props); diff --git a/src/components/views/settings/EmailAddresses.js b/src/components/views/settings/EmailAddresses.js new file mode 100644 index 0000000000..24d1547fd7 --- /dev/null +++ b/src/components/views/settings/EmailAddresses.js @@ -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 ( +
+ + {_t("Are you sure?")} + + + {_t("Yes")} + + + {_t("No")} + +
+ ) + } + + return ( +
+ {_t("Remove")} + {this.props.email.address} +
+ ); + } +} + +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 ; + }); + + let addButton = ( + + {_t("Add")} + + ); + if (this.state.verifying) { + addButton = ( +
+
{_t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.")}
+ + {_t("Continue")} + +
+ ); + } + + return ( +
+ {existingEmailElements} +
+ + {addButton} + +
+ ); + } +} diff --git a/src/components/views/settings/tabs/GeneralSettingsTab.js b/src/components/views/settings/tabs/GeneralSettingsTab.js index e2d3a924d3..0b6a88d652 100644 --- a/src/components/views/settings/tabs/GeneralSettingsTab.js +++ b/src/components/views/settings/tabs/GeneralSettingsTab.js @@ -22,6 +22,7 @@ import PropTypes from "prop-types"; import {MatrixClient} from "matrix-js-sdk"; import { DragDropContext } from 'react-beautiful-dnd'; import ProfileSettings from "../ProfileSettings"; +import EmailAddresses from "../EmailAddresses"; const sdk = require('../../../../index'); const Modal = require("../../../../Modal"); @@ -95,12 +96,15 @@ export default class GeneralSettingsTab extends React.Component { ); return ( -
+
{_t("Account")}

{_t("Set a new account password...")}

{passwordChangeForm} + + {_t("Email addresses")} +
); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dfbe9cc6a3..34b1e201c0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -352,6 +352,17 @@ "Last seen": "Last seen", "Select devices": "Select devices", "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", "Enable Notifications": "Enable Notifications", "Delete Backup": "Delete Backup", @@ -413,6 +424,7 @@ "Flair": "Flair", "Account": "Account", "Set a new account password...": "Set a new account password...", + "Email addresses": "Email addresses", "Language and region": "Language and region", "Theme": "Theme", "Account management": "Account management", @@ -464,7 +476,6 @@ "Failed to toggle moderator status": "Failed to toggle moderator status", "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.", - "Are you sure?": "Are you sure?", "No devices with registered encryption keys": "No devices with registered encryption keys", "Devices": "Devices", "Unignore": "Unignore", @@ -737,7 +748,6 @@ "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?", "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 '%(roomName)s' from %(groupId)s": "Failed to remove '%(roomName)s' from %(groupId)s", "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.", "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.", - "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", "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", "This will allow you to reset your password and receive notifications.": "This will allow you to reset your password and receive notifications.", "Skip": "Skip", @@ -1253,7 +1259,6 @@ "Server may be unavailable or overloaded": "Server may be unavailable or overloaded", "Remove Contact Information?": "Remove Contact Information?", "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:", "Interface Language": "Interface Language", "User Interface": "User Interface",