Split out email & phone number settings to separate components & move discovery to privacy tab (#12670)

* WIP update of threepid settings section

* Remove email / phone number section from original place

and don't show the new one if 3pids are disabled

* Update snapshots

* Pull identity server / 3pid binding settings out to separate component

and put it in the security & privacy section which is its new home

* Update snapshot

* Move relevant part of test & update screenshots / snapshots

* Remove unnecessary dependency

* Add test for discovery settings

* Add spacing in terms agreement
This commit is contained in:
David Baker 2024-06-26 14:04:19 +01:00 committed by GitHub
parent 72475240ec
commit ea0baee101
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 454 additions and 269 deletions

View file

@ -17,10 +17,9 @@ limitations under the License.
*/
import React, { ReactNode } from "react";
import { SERVICE_TYPES, HTTPError, IThreepid, ThreepidMedium } from "matrix-js-sdk/src/matrix";
import { HTTPError } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { Icon as WarningIcon } from "../../../../../../res/img/feather-customised/warning-triangle.svg";
import { UserFriendlyError, _t } from "../../../../../languageHandler";
import UserProfileSettings from "../../UserProfileSettings";
import * as languageHandler from "../../../../../languageHandler";
@ -31,22 +30,10 @@ import AccessibleButton from "../../../elements/AccessibleButton";
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
import PlatformPeg from "../../../../../PlatformPeg";
import Modal from "../../../../../Modal";
import dis from "../../../../../dispatcher/dispatcher";
import { Service, ServicePolicyPair, startTermsFlow } from "../../../../../Terms";
import IdentityAuthClient from "../../../../../IdentityAuthClient";
import { abbreviateUrl } from "../../../../../utils/UrlUtils";
import { getThreepidsWithBindStatus } from "../../../../../boundThreepids";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import { UIFeature } from "../../../../../settings/UIFeature";
import { ActionPayload } from "../../../../../dispatcher/payloads";
import ErrorDialog, { extractErrorMessageFromError } from "../../../dialogs/ErrorDialog";
import AccountPhoneNumbers from "../../account/PhoneNumbers";
import AccountEmailAddresses from "../../account/EmailAddresses";
import DiscoveryEmailAddresses from "../../discovery/EmailAddresses";
import DiscoveryPhoneNumbers from "../../discovery/PhoneNumbers";
import ChangePassword from "../../ChangePassword";
import InlineTermsAgreement from "../../../terms/InlineTermsAgreement";
import SetIdServer from "../../SetIdServer";
import SetIntegrationManager from "../../SetIntegrationManager";
import ToggleSwitch from "../../../elements/ToggleSwitch";
import { IS_MAC } from "../../../../../Keyboard";
@ -54,10 +41,8 @@ import SettingsTab from "../SettingsTab";
import { SettingsSection } from "../../shared/SettingsSection";
import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection";
import { SettingsSubsectionHeading } from "../../shared/SettingsSubsectionHeading";
import Heading from "../../../typography/Heading";
import InlineSpinner from "../../../elements/InlineSpinner";
import { ThirdPartyIdentifier } from "../../../../../AddThreepid";
import { SDKContext } from "../../../../../contexts/SDKContext";
import UserPersonalInfoSettings from "../../UserPersonalInfoSettings";
interface IProps {
closeSettingsFn: () => void;
@ -67,25 +52,6 @@ interface IState {
language: string;
spellCheckEnabled?: boolean;
spellCheckLanguages: string[];
haveIdServer: boolean;
idServerHasUnsignedTerms: boolean;
requiredPolicyInfo:
| {
// This object is passed along to a component for handling
hasTerms: false;
policiesAndServices: null; // From the startTermsFlow callback
agreedUrls: null; // From the startTermsFlow callback
resolve: null; // Promise resolve function for startTermsFlow callback
}
| {
hasTerms: boolean;
policiesAndServices: ServicePolicyPair[];
agreedUrls: string[];
resolve: (values: string[]) => void;
};
emails: ThirdPartyIdentifier[];
msisdns: ThirdPartyIdentifier[];
loading3pids: boolean; // whether or not the emails and msisdns have been loaded
canChangePassword: boolean;
idServerName?: string;
externalAccountManagementUrl?: string;
@ -96,38 +62,19 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
public static contextType = SDKContext;
public context!: React.ContextType<typeof SDKContext>;
private readonly dispatcherRef: string;
public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) {
super(props);
this.context = context;
const cli = this.context.client!;
this.state = {
language: languageHandler.getCurrentLanguage(),
spellCheckEnabled: false,
spellCheckLanguages: [],
haveIdServer: Boolean(cli.getIdentityServerUrl()),
idServerHasUnsignedTerms: false,
requiredPolicyInfo: {
// This object is passed along to a component for handling
hasTerms: false,
policiesAndServices: null, // From the startTermsFlow callback
agreedUrls: null, // From the startTermsFlow callback
resolve: null, // Promise resolve function for startTermsFlow callback
},
emails: [],
msisdns: [],
loading3pids: true, // whether or not the emails and msisdns have been loaded
canChangePassword: false,
canMake3pidChanges: false,
};
this.dispatcherRef = dis.register(this.onAction);
this.getCapabilities();
this.getThreepidState();
}
public async componentDidMount(): Promise<void> {
@ -145,25 +92,6 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
}
}
public componentWillUnmount(): void {
dis.unregister(this.dispatcherRef);
}
private onAction = (payload: ActionPayload): void => {
if (payload.action === "id_server_changed") {
this.setState({ haveIdServer: Boolean(this.context.client!.getIdentityServerUrl()) });
this.getThreepidState();
}
};
private onEmailsChange = (emails: ThirdPartyIdentifier[]): void => {
this.setState({ emails });
};
private onMsisdnsChange = (msisdns: ThirdPartyIdentifier[]): void => {
this.setState({ msisdns });
};
private async getCapabilities(): Promise<void> {
const cli = this.context.client!;
@ -185,73 +113,6 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
this.setState({ canChangePassword, externalAccountManagementUrl, canMake3pidChanges });
}
private async getThreepidState(): Promise<void> {
const cli = this.context.client!;
// Check to see if terms need accepting
this.checkTerms();
// Need to get 3PIDs generally for Account section and possibly also for
// Discovery (assuming we have an IS and terms are agreed).
let threepids: IThreepid[] = [];
try {
threepids = await getThreepidsWithBindStatus(cli);
} catch (e) {
const idServerUrl = cli.getIdentityServerUrl();
logger.warn(
`Unable to reach identity server at ${idServerUrl} to check ` + `for 3PIDs bindings in Settings`,
);
logger.warn(e);
}
this.setState({
emails: threepids.filter((a) => a.medium === ThreepidMedium.Email),
msisdns: threepids.filter((a) => a.medium === ThreepidMedium.Phone),
loading3pids: false,
});
}
private async checkTerms(): Promise<void> {
// By starting the terms flow we get the logic for checking which terms the user has signed
// for free. So we might as well use that for our own purposes.
const idServerUrl = this.context.client!.getIdentityServerUrl();
if (!this.state.haveIdServer || !idServerUrl) {
this.setState({ idServerHasUnsignedTerms: false });
return;
}
const authClient = new IdentityAuthClient();
try {
const idAccessToken = await authClient.getAccessToken({ check: false });
await startTermsFlow(
this.context.client!,
[new Service(SERVICE_TYPES.IS, idServerUrl, idAccessToken!)],
(policiesAndServices, agreedUrls, extraClassNames) => {
return new Promise((resolve, reject) => {
this.setState({
idServerName: abbreviateUrl(idServerUrl),
requiredPolicyInfo: {
hasTerms: true,
policiesAndServices,
agreedUrls,
resolve,
},
});
});
},
);
// User accepted all terms
this.setState({
requiredPolicyInfo: {
...this.state.requiredPolicyInfo, // set first so we can override
hasTerms: false,
},
});
} catch (e) {
logger.warn(`Unable to reach identity server at ${idServerUrl} to check ` + `for terms in Settings`);
logger.warn(e);
}
}
private onLanguageChange = (newLanguage: string): void => {
if (this.state.language === newLanguage) return;
@ -324,48 +185,6 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
};
private renderAccountSection(): JSX.Element {
let threepidSection: ReactNode = null;
if (SettingsStore.getValue(UIFeature.ThirdPartyID)) {
const emails = this.state.loading3pids ? (
<InlineSpinner />
) : (
<AccountEmailAddresses
emails={this.state.emails}
onEmailsChange={this.onEmailsChange}
disabled={!this.state.canMake3pidChanges}
/>
);
const msisdns = this.state.loading3pids ? (
<InlineSpinner />
) : (
<AccountPhoneNumbers
msisdns={this.state.msisdns}
onMsisdnsChange={this.onMsisdnsChange}
disabled={!this.state.canMake3pidChanges}
/>
);
threepidSection = (
<>
<SettingsSubsection
heading={_t("settings|general|emails_heading")}
stretchContent
data-testid="mx_AccountEmailAddresses"
>
{emails}
</SettingsSubsection>
<SettingsSubsection
heading={_t("settings|general|msisdns_heading")}
stretchContent
data-testid="mx_AccountPhoneNumbers"
>
{msisdns}
</SettingsSubsection>
</>
);
}
let passwordChangeSection: ReactNode = null;
if (this.state.canChangePassword) {
passwordChangeSection = (
@ -419,7 +238,6 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
{externalAccountManagement}
{passwordChangeSection}
</SettingsSubsection>
{threepidSection}
</>
);
}
@ -455,51 +273,6 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
);
}
private renderDiscoverySection(): JSX.Element {
if (this.state.requiredPolicyInfo.hasTerms) {
const intro = (
<SettingsSubsectionText>
{_t("settings|general|discovery_needs_terms", { serverName: this.state.idServerName })}
</SettingsSubsectionText>
);
return (
<>
<InlineTermsAgreement
policiesAndServicePairs={this.state.requiredPolicyInfo.policiesAndServices}
agreedUrls={this.state.requiredPolicyInfo.agreedUrls}
onFinished={this.state.requiredPolicyInfo.resolve}
introElement={intro}
/>
{/* has its own heading as it includes the current identity server */}
<SetIdServer missingTerms={true} />
</>
);
}
const threepidSection = this.state.haveIdServer ? (
<>
<DiscoveryEmailAddresses
emails={this.state.emails}
isLoading={this.state.loading3pids}
disabled={!this.state.canMake3pidChanges}
/>
<DiscoveryPhoneNumbers
msisdns={this.state.msisdns}
isLoading={this.state.loading3pids}
disabled={!this.state.canMake3pidChanges}
/>
</>
) : null;
return (
<>
{threepidSection}
{/* has its own heading as it includes the current identity server */}
<SetIdServer missingTerms={false} />
</>
);
}
private renderManagementSection(): JSX.Element {
// TODO: Improve warning text for account deactivation
return (
@ -533,40 +306,15 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
accountManagementSection = this.renderManagementSection();
}
let discoverySection;
if (SettingsStore.getValue(UIFeature.IdentityServer)) {
const discoWarning = this.state.requiredPolicyInfo.hasTerms ? (
<WarningIcon
className="mx_GeneralUserSettingsTab_warningIcon"
width="18"
height="18"
// override icon default values
aria-hidden={false}
aria-label={_t("common|warning")}
/>
) : null;
const heading = (
<Heading size="2">
{discoWarning}
{_t("settings|general|discovery_section")}
</Heading>
);
discoverySection = (
<SettingsSection heading={heading} data-testid="discoverySection">
{this.renderDiscoverySection()}
</SettingsSection>
);
}
return (
<SettingsTab data-testid="mx_GeneralUserSettingsTab">
<SettingsSection>
<UserProfileSettings />
<UserPersonalInfoSettings canMake3pidChanges={this.state.canMake3pidChanges} />
{this.renderAccountSection()}
{this.renderLanguageSection()}
{supportsMultiLanguageSpellCheck ? this.renderSpellCheckSection() : null}
</SettingsSection>
{discoverySection}
{this.renderIntegrationManagerSection()}
{accountManagementSection}
</SettingsTab>