diff --git a/res/css/views/settings/_PhoneNumbers.scss b/res/css/views/settings/_PhoneNumbers.scss
index d88ed176aa..507b07334e 100644
--- a/res/css/views/settings/_PhoneNumbers.scss
+++ b/res/css/views/settings/_PhoneNumbers.scss
@@ -37,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/account/PhoneNumbers.js b/src/components/views/settings/account/PhoneNumbers.js
index cabe4aef86..d892f17ff8 100644
--- a/src/components/views/settings/account/PhoneNumbers.js
+++ b/src/components/views/settings/account/PhoneNumbers.js
@@ -225,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 8283d26ef1..fc1a9b8c4a 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -164,6 +164,21 @@ export default class GeneralUserSettingsTab extends React.Component {
);
}
+ _renderDiscoverySection() {
+ const EmailAddresses = sdk.getComponent("views.settings.discovery.EmailAddresses");
+ const PhoneNumbers = sdk.getComponent("views.settings.discovery.PhoneNumbers");
+
+ return (
+
+
{_t("Email addresses")}
+
+
+
{_t("Phone numbers")}
+
+
+ );
+ }
+
_renderManagementSection() {
// TODO: Improve warning text for account deactivation
return (
@@ -187,6 +202,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 9ad20bf56c..34f11bf2cf 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -470,18 +470,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",
@@ -541,11 +529,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",
"