diff --git a/res/css/views/settings/_EmailAddresses.scss b/res/css/views/settings/_EmailAddresses.scss index d7606ecea9..1c9ce724d1 100644 --- a/res/css/views/settings/_EmailAddresses.scss +++ b/res/css/views/settings/_EmailAddresses.scss @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/res/css/views/settings/_PhoneNumbers.scss b/res/css/views/settings/_PhoneNumbers.scss index 7aaef2a56b..507b07334e 100644 --- a/res/css/views/settings/_PhoneNumbers.scss +++ b/res/css/views/settings/_PhoneNumbers.scss @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -36,6 +37,15 @@ limitations under the License. margin-left: 5px; } +.mx_ExistingPhoneNumber_verification { + display: inline-flex; + align-items: center; + + .mx_Field { + margin: 0 0 0 1em; + } +} + .mx_PhoneNumbers_input { display: flex; align-items: center; diff --git a/src/components/views/settings/EmailAddresses.js b/src/components/views/settings/account/EmailAddresses.js similarity index 94% rename from src/components/views/settings/EmailAddresses.js rename to src/components/views/settings/account/EmailAddresses.js index 1bb41ae8b5..c13d2b4e0f 100644 --- a/src/components/views/settings/EmailAddresses.js +++ b/src/components/views/settings/account/EmailAddresses.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,14 +17,14 @@ 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"); +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. @@ -163,7 +164,7 @@ export default class EmailAddresses extends React.Component { const task = new AddThreepid(); this.setState({verifying: true, continueDisabled: true, addTask: task}); - task.addEmailAddress(email, true).then(() => { + task.addEmailAddress(email, false).then(() => { this.setState({continueDisabled: false}); }).catch((err) => { console.error("Unable to add email address " + email + " " + err); diff --git a/src/components/views/settings/PhoneNumbers.js b/src/components/views/settings/account/PhoneNumbers.js similarity index 94% rename from src/components/views/settings/PhoneNumbers.js rename to src/components/views/settings/account/PhoneNumbers.js index 4ebc2a20a6..236a4e7587 100644 --- a/src/components/views/settings/PhoneNumbers.js +++ b/src/components/views/settings/account/PhoneNumbers.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,14 +17,14 @@ 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 AddThreepid from "../../../AddThreepid"; -import CountryDropdown from "../auth/CountryDropdown"; -const sdk = require('../../../index'); -const Modal = require("../../../Modal"); +import {_t} from "../../../../languageHandler"; +import MatrixClientPeg from "../../../../MatrixClientPeg"; +import Field from "../../elements/Field"; +import AccessibleButton from "../../elements/AccessibleButton"; +import AddThreepid from "../../../../AddThreepid"; +import CountryDropdown from "../../auth/CountryDropdown"; +const sdk = require('../../../../index'); +const Modal = require("../../../../Modal"); /* TODO: Improve the UX for everything in here. @@ -160,7 +161,7 @@ export default class PhoneNumbers extends React.Component { const task = new AddThreepid(); this.setState({verifying: true, continueDisabled: true, addTask: task}); - task.addMsisdn(phoneCountry, phoneNumber, true).then((response) => { + task.addMsisdn(phoneCountry, phoneNumber, false).then((response) => { this.setState({continueDisabled: false, verifyMsisdn: response.msisdn}); }).catch((err) => { console.error("Unable to add phone number " + phoneNumber + " " + err); @@ -224,7 +225,7 @@ export default class PhoneNumbers extends React.Component {
{_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 })}
{this.state.verifyError}
diff --git a/src/components/views/settings/discovery/EmailAddresses.js b/src/components/views/settings/discovery/EmailAddresses.js new file mode 100644 index 0000000000..7862eda61e --- /dev/null +++ b/src/components/views/settings/discovery/EmailAddresses.js @@ -0,0 +1,248 @@ +/* +Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +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 sdk from '../../../../index'; +import Modal from '../../../../Modal'; +import IdentityAuthClient from '../../../../IdentityAuthClient'; +import AddThreepid from '../../../../AddThreepid'; + +/* +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. +*/ + +/* +TODO: Reduce all the copying between account vs. discovery components. +*/ + +export class EmailAddress extends React.Component { + static propTypes = { + email: PropTypes.object.isRequired, + }; + + constructor(props) { + super(props); + + const { bound } = props.email; + + this.state = { + verifying: false, + addTask: null, + continueDisabled: false, + bound, + }; + } + + async changeBinding({ bind, label, errorTitle }) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const { medium, address } = this.props.email; + + const task = new AddThreepid(); + this.setState({ + verifying: true, + continueDisabled: true, + addTask: task, + }); + + try { + // XXX: Unfortunately, at the moment we can't just bind via the HS + // in a single operation, at it will error saying the 3PID is in use + // even though it's in use by the current user. For the moment, we + // work around this by removing the 3PID from the HS and re-adding + // it with IS binding enabled. + // See https://github.com/matrix-org/matrix-doc/pull/2140/files#r311462052 + await MatrixClientPeg.get().deleteThreePid(medium, address); + await task.addEmailAddress(address, bind); + this.setState({ + continueDisabled: false, + bound: bind, + }); + } catch (err) { + console.error(`Unable to ${label} email address ${address} ${err}`); + this.setState({ + verifying: false, + continueDisabled: false, + addTask: null, + }); + Modal.createTrackedDialog(`Unable to ${label} email address`, '', ErrorDialog, { + title: errorTitle, + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + } + } + + onRevokeClick = (e) => { + e.stopPropagation(); + e.preventDefault(); + this.changeBinding({ + bind: false, + label: "revoke", + errorTitle: _t("Unable to revoke sharing for email address"), + }); + } + + onShareClick = (e) => { + e.stopPropagation(); + e.preventDefault(); + this.changeBinding({ + bind: true, + label: "share", + errorTitle: _t("Unable to share email address"), + }); + } + + onContinueClick = async (e) => { + e.stopPropagation(); + e.preventDefault(); + + this.setState({ continueDisabled: true }); + try { + await this.state.addTask.checkEmailLinkClicked(); + this.setState({ + addTask: null, + continueDisabled: false, + verifying: false, + }); + } 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 AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const { address } = this.props.email; + const { verifying, bound } = this.state; + + let status; + if (verifying) { + status = + {_t("Check your inbox, then click Continue")} + + {_t("Continue")} + + ; + } else if (bound) { + status = + {_t("Revoke")} + ; + } else { + status = + {_t("Share")} + ; + } + + return ( +
+ {address} + {status} +
+ ); + } +} + +export default class EmailAddresses extends React.Component { + constructor() { + super(); + + this.state = { + loaded: false, + emails: [], + }; + } + + async componentWillMount() { + const client = MatrixClientPeg.get(); + const userId = client.getUserId(); + + const { threepids } = await client.getThreePids(); + const emails = threepids.filter((a) => a.medium === 'email'); + + if (emails.length > 0) { + // TODO: Handle terms agreement + // See https://github.com/vector-im/riot-web/issues/10522 + const authClient = new IdentityAuthClient(); + const identityAccessToken = await authClient.getAccessToken(); + + // Restructure for lookup query + const query = emails.map(({ medium, address }) => [medium, address]); + const lookupResults = await client.bulkLookupThreePids(query, identityAccessToken); + + // Record which are already bound + for (const [medium, address, mxid] of lookupResults.threepids) { + if (medium !== "email" || mxid !== userId) { + continue; + } + const email = emails.find(e => e.address === address); + if (!email) continue; + email.bound = true; + } + } + + this.setState({ emails }); + } + + render() { + let content; + if (this.state.emails.length > 0) { + content = this.state.emails.map((e) => { + return ; + }); + } else { + content = + {_t("Discovery options will appear once you have added an email above.")} + ; + } + + return ( +
+ {content} +
+ ); + } +} diff --git a/src/components/views/settings/discovery/PhoneNumbers.js b/src/components/views/settings/discovery/PhoneNumbers.js new file mode 100644 index 0000000000..3930277aea --- /dev/null +++ b/src/components/views/settings/discovery/PhoneNumbers.js @@ -0,0 +1,267 @@ +/* +Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +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 sdk from '../../../../index'; +import Modal from '../../../../Modal'; +import IdentityAuthClient from '../../../../IdentityAuthClient'; +import AddThreepid from '../../../../AddThreepid'; + +/* +TODO: Improve the UX for everything in here. +This is a copy/paste of EmailAddresses, mostly. + */ + +// TODO: Combine EmailAddresses and PhoneNumbers to be 3pid agnostic + +export class PhoneNumber extends React.Component { + static propTypes = { + msisdn: PropTypes.object.isRequired, + }; + + constructor(props) { + super(props); + + const { bound } = props.msisdn; + + this.state = { + verifying: false, + verificationCode: "", + addTask: null, + continueDisabled: false, + bound, + }; + } + + async changeBinding({ bind, label, errorTitle }) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const { medium, address } = this.props.msisdn; + + const task = new AddThreepid(); + this.setState({ + verifying: true, + continueDisabled: true, + addTask: task, + }); + + try { + // XXX: Unfortunately, at the moment we can't just bind via the HS + // in a single operation, at it will error saying the 3PID is in use + // even though it's in use by the current user. For the moment, we + // work around this by removing the 3PID from the HS and re-adding + // it with IS binding enabled. + // See https://github.com/matrix-org/matrix-doc/pull/2140/files#r311462052 + await MatrixClientPeg.get().deleteThreePid(medium, address); + // XXX: Sydent will accept a number without country code if you add + // a leading plus sign to a number in E.164 format (which the 3PID + // address is), but this goes against the spec. + // See https://github.com/matrix-org/matrix-doc/issues/2222 + await task.addMsisdn(null, `+${address}`, bind); + this.setState({ + continueDisabled: false, + bound: bind, + }); + } catch (err) { + console.error(`Unable to ${label} phone number ${address} ${err}`); + this.setState({ + verifying: false, + continueDisabled: false, + addTask: null, + }); + Modal.createTrackedDialog(`Unable to ${label} phone number`, '', ErrorDialog, { + title: errorTitle, + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + } + } + + onRevokeClick = (e) => { + e.stopPropagation(); + e.preventDefault(); + this.changeBinding({ + bind: false, + label: "revoke", + errorTitle: _t("Unable to revoke sharing for phone number"), + }); + } + + onShareClick = (e) => { + e.stopPropagation(); + e.preventDefault(); + this.changeBinding({ + bind: true, + label: "share", + errorTitle: _t("Unable to share phone number"), + }); + } + + onVerificationCodeChange = (e) => { + this.setState({ + verificationCode: e.target.value, + }); + } + + onContinueClick = async (e) => { + e.stopPropagation(); + e.preventDefault(); + + this.setState({ continueDisabled: true }); + const token = this.state.verificationCode; + try { + await this.state.addTask.haveMsisdnToken(token); + this.setState({ + addTask: null, + continueDisabled: false, + verifying: false, + verifyError: null, + verificationCode: "", + }); + } catch (err) { + this.setState({ continueDisabled: false }); + if (err.errcode !== 'M_THREEPID_AUTH_FAILED') { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Unable to verify phone number: " + err); + Modal.createTrackedDialog('Unable to verify phone number', '', ErrorDialog, { + title: _t("Unable to verify phone number."), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + } else { + this.setState({verifyError: _t("Incorrect verification code")}); + } + } + } + + render() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const Field = sdk.getComponent('elements.Field'); + const { address } = this.props.msisdn; + const { verifying, bound } = this.state; + + let status; + if (verifying) { + status = + + {_t("Please enter verification code sent via text.")} +
+ {this.state.verifyError} +
+
+ + +
; + } else if (bound) { + status = + {_t("Revoke")} + ; + } else { + status = + {_t("Share")} + ; + } + + return ( +
+ +{address} + {status} +
+ ); + } +} + +export default class PhoneNumbers extends React.Component { + constructor() { + super(); + + this.state = { + loaded: false, + msisdns: [], + }; + } + + async componentWillMount() { + const client = MatrixClientPeg.get(); + const userId = client.getUserId(); + + const { threepids } = await client.getThreePids(); + const msisdns = threepids.filter((a) => a.medium === 'msisdn'); + + if (msisdns.length > 0) { + // TODO: Handle terms agreement + // See https://github.com/vector-im/riot-web/issues/10522 + const authClient = new IdentityAuthClient(); + const identityAccessToken = await authClient.getAccessToken(); + + // Restructure for lookup query + const query = msisdns.map(({ medium, address }) => [medium, address]); + const lookupResults = await client.bulkLookupThreePids(query, identityAccessToken); + + // Record which are already bound + for (const [medium, address, mxid] of lookupResults.threepids) { + if (medium !== "msisdn" || mxid !== userId) { + continue; + } + const msisdn = msisdns.find(e => e.address === address); + if (!msisdn) continue; + msisdn.bound = true; + } + } + + this.setState({ msisdns }); + } + + render() { + let content; + if (this.state.msisdns.length > 0) { + content = this.state.msisdns.map((e) => { + return ; + }); + } else { + content = + {_t("Discovery options will appear once you have added a phone number above.")} + ; + } + + return ( +
+ {content} +
+ ); + } +} diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 32dcf4721c..a9c010b6b4 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,8 +18,6 @@ limitations under the License. import React from 'react'; import {_t} from "../../../../../languageHandler"; import ProfileSettings from "../../ProfileSettings"; -import EmailAddresses from "../../EmailAddresses"; -import PhoneNumbers from "../../PhoneNumbers"; import Field from "../../../elements/Field"; import * as languageHandler from "../../../../../languageHandler"; import {SettingLevel} from "../../../../../settings/SettingsStore"; @@ -111,6 +110,9 @@ export default class GeneralUserSettingsTab extends React.Component { _renderAccountSection() { const ChangePassword = sdk.getComponent("views.settings.ChangePassword"); + const EmailAddresses = sdk.getComponent("views.settings.account.EmailAddresses"); + const PhoneNumbers = sdk.getComponent("views.settings.account.PhoneNumbers"); + const passwordChangeForm = ( + {_t("Email addresses")} + + + {_t("Phone numbers")} + +
+ ); + } + _renderManagementSection() { // TODO: Improve warning text for account deactivation return ( @@ -189,6 +206,9 @@ export default class GeneralUserSettingsTab extends React.Component { {this._renderAccountSection()} {this._renderLanguageSection()} {this._renderThemeSection()} +
{_t("Discovery")}
+ {this._renderDiscoverySection()} +
{_t("Deactivate account")}
{this._renderManagementSection()} ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8d43e7087c..8173051c30 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -474,18 +474,6 @@ "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.", - "Add": "Add", - "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", "No integrations server configured": "No integrations server configured", @@ -544,11 +532,6 @@ "Off": "Off", "On": "On", "Noisy": "Noisy", - "Unable to verify phone number.": "Unable to verify phone number.", - "Incorrect verification code": "Incorrect verification code", - "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains", - "Verification code": "Verification code", - "Phone Number": "Phone Number", "Profile picture": "Profile picture", "Upload profile picture": "Upload profile picture", "Upgrade to your own domain": "Upgrade to your own domain", @@ -571,6 +554,8 @@ "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", "Deactivate Account": "Deactivate Account", "General": "General", + "Discovery": "Discovery", + "Deactivate account": "Deactivate account", "Legal": "Legal", "Credits": "Credits", "For help with using Riot, click here.": "For help with using Riot, click here.", @@ -692,6 +677,33 @@ "Encrypted": "Encrypted", "Who can access this room?": "Who can access this room?", "Who can read history?": "Who can read history?", + "Unable to revoke sharing for email address": "Unable to revoke sharing for email address", + "Unable to share email address": "Unable to share email address", + "Unable to verify email address.": "Unable to verify email address.", + "Check your inbox, then click Continue": "Check your inbox, then click Continue", + "Revoke": "Revoke", + "Share": "Share", + "Discovery options will appear once you have added an email above.": "Discovery options will appear once you have added an email above.", + "Unable to revoke sharing for phone number": "Unable to revoke sharing for phone number", + "Unable to share phone number": "Unable to share phone number", + "Unable to verify phone number.": "Unable to verify phone number.", + "Incorrect verification code": "Incorrect verification code", + "Please enter verification code sent via text.": "Please enter verification code sent via text.", + "Verification code": "Verification code", + "Discovery options will appear once you have added a phone number above.": "Discovery options will appear once you have added a phone number above.", + "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", + "Add": "Add", + "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", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.", + "Phone Number": "Phone Number", "Cannot add any more widgets": "Cannot add any more widgets", "The maximum permitted number of widgets have already been added to this room.": "The maximum permitted number of widgets have already been added to this room.", "Add a widget": "Add a widget",