Use semantic headings in user settings Security (#10774)

* split SettingsSection out of SettingsTab, replace usage

* correct copyright

* use semantic headings in GeneralRoomSettingsTab

* use SettingsTab and SettingsSubsection in room settings

* fix VoipRoomSettingsTab

* use SettingsSection components in space settings

* settingssubsection text component

* use semantic headings in HelpUserSetttings tab

* use ExternalLink components for external links

* test

* strict

* lint

* semantic heading in labs settings

* semantic headings in keyboard settings tab

* semantic heading in preferencesusersettingstab

* tidying

* use new settings components in eventindexpanel

* findByTestId

* prettier

* semantic headings and style refresh for crypto settings

* e2e panel

* test cross signing panel

* strict

* more strict

* tweak

* test eventindexpanel

* strict fixes
This commit is contained in:
Kerry 2023-05-19 10:32:10 +12:00 committed by GitHub
parent 6c262fff6b
commit d9a61c093c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 721 additions and 303 deletions

View file

@ -28,6 +28,7 @@ import ConfirmDestroyCrossSigningDialog from "../dialogs/security/ConfirmDestroy
import SetupEncryptionDialog from "../dialogs/security/SetupEncryptionDialog";
import { accessSecretStorage } from "../../../SecurityManager";
import AccessibleButton from "../elements/AccessibleButton";
import { SettingsSubsectionText } from "./shared/SettingsSubsection";
interface IState {
error?: Error;
@ -178,22 +179,38 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
if (homeserverSupportsCrossSigning === undefined) {
summarisedStatus = <Spinner />;
} else if (!homeserverSupportsCrossSigning) {
summarisedStatus = <p>{_t("Your homeserver does not support cross-signing.")}</p>;
summarisedStatus = (
<SettingsSubsectionText data-testid="summarised-status">
{_t("Your homeserver does not support cross-signing.")}
</SettingsSubsectionText>
);
} else if (crossSigningReady && crossSigningPrivateKeysInStorage) {
summarisedStatus = <p> {_t("Cross-signing is ready for use.")}</p>;
summarisedStatus = (
<SettingsSubsectionText data-testid="summarised-status">
{_t("Cross-signing is ready for use.")}
</SettingsSubsectionText>
);
} else if (crossSigningReady && !crossSigningPrivateKeysInStorage) {
summarisedStatus = <p> {_t("Cross-signing is ready but keys are not backed up.")}</p>;
summarisedStatus = (
<SettingsSubsectionText data-testid="summarised-status">
{_t("Cross-signing is ready but keys are not backed up.")}
</SettingsSubsectionText>
);
} else if (crossSigningPrivateKeysInStorage) {
summarisedStatus = (
<p>
<SettingsSubsectionText data-testid="summarised-status">
{_t(
"Your account has a cross-signing identity in secret storage, " +
"but it is not yet trusted by this session.",
)}
</p>
</SettingsSubsectionText>
);
} else {
summarisedStatus = <p>{_t("Cross-signing is not set up.")}</p>;
summarisedStatus = (
<SettingsSubsectionText data-testid="summarised-status">
{_t("Cross-signing is not set up.")}
</SettingsSubsectionText>
);
}
const keysExistAnywhere =
@ -238,7 +255,7 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
}
return (
<div>
<>
{summarisedStatus}
<details>
<summary>{_t("Advanced")}</summary>
@ -275,7 +292,7 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
</details>
{errorSection}
{actionRow}
</div>
</>
);
}
}

View file

@ -26,6 +26,7 @@ import * as FormattingUtils from "../../../utils/FormattingUtils";
import SettingsStore from "../../../settings/SettingsStore";
import SettingsFlag from "../elements/SettingsFlag";
import { SettingLevel } from "../../../settings/SettingLevel";
import SettingsSubsection, { SettingsSubsectionText } from "./shared/SettingsSubsection";
interface IProps {}
@ -72,27 +73,28 @@ export default class CryptographyPanel extends React.Component<IProps, IState> {
}
return (
<div className="mx_SettingsTab_section mx_CryptographyPanel">
<span className="mx_SettingsTab_subheading">{_t("Cryptography")}</span>
<table className="mx_SettingsTab_subsectionText mx_CryptographyPanel_sessionInfo">
<tr>
<th scope="row">{_t("Session ID:")}</th>
<td>
<code>{deviceId}</code>
</td>
</tr>
<tr>
<th scope="row">{_t("Session key:")}</th>
<td>
<code>
<b>{identityKey}</b>
</code>
</td>
</tr>
</table>
<SettingsSubsection heading={_t("Cryptography")}>
<SettingsSubsectionText>
<table className="mx_CryptographyPanel_sessionInfo">
<tr>
<th scope="row">{_t("Session ID:")}</th>
<td>
<code>{deviceId}</code>
</td>
</tr>
<tr>
<th scope="row">{_t("Session key:")}</th>
<td>
<code>
<b>{identityKey}</b>
</code>
</td>
</tr>
</table>
</SettingsSubsectionText>
{importExportButtons}
{noSendUnverifiedSetting}
</div>
</SettingsSubsection>
);
}

View file

@ -20,21 +20,20 @@ import { _t } from "../../../languageHandler";
import { SettingLevel } from "../../../settings/SettingLevel";
import SettingsStore from "../../../settings/SettingsStore";
import SettingsFlag from "../elements/SettingsFlag";
import SettingsSubsection, { SettingsSubsectionText } from "./shared/SettingsSubsection";
const SETTING_MANUALLY_VERIFY_ALL_SESSIONS = "e2ee.manuallyVerifyAllSessions";
const E2eAdvancedPanel: React.FC = () => {
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Encryption")}</span>
<SettingsSubsection heading={_t("Encryption")}>
<SettingsFlag name={SETTING_MANUALLY_VERIFY_ALL_SESSIONS} level={SettingLevel.DEVICE} />
<div className="mx_SettingsTab_subsectionText">
<SettingsSubsectionText>
{_t(
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.",
)}
</div>
</div>
</SettingsSubsectionText>
</SettingsSubsection>
);
};

View file

@ -27,6 +27,7 @@ import { SettingLevel } from "../../../settings/SettingLevel";
import SeshatResetDialog from "../dialogs/SeshatResetDialog";
import InlineSpinner from "../elements/InlineSpinner";
import ExternalLink from "../elements/ExternalLink";
import { SettingsSubsectionText } from "./shared/SettingsSubsection";
interface IState {
enabling: boolean;
@ -145,8 +146,8 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
if (EventIndexPeg.get() !== null) {
eventIndexingSettings = (
<div>
<div className="mx_SettingsTab_subsectionText">
<>
<SettingsSubsectionText>
{_t(
"Securely cache encrypted messages locally for them " +
"to appear in search results, using %(size)s to store messages from %(rooms)s rooms.",
@ -158,27 +159,25 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
rooms: formatCountLong(this.state.roomCount),
},
)}
</div>
<div>
<AccessibleButton kind="primary" onClick={this.onManage}>
{_t("Manage")}
</AccessibleButton>
</div>
</div>
</SettingsSubsectionText>
<AccessibleButton kind="primary" onClick={this.onManage}>
{_t("Manage")}
</AccessibleButton>
</>
);
} else if (!this.state.eventIndexingEnabled && EventIndexPeg.supportIsInstalled()) {
eventIndexingSettings = (
<div>
<div className="mx_SettingsTab_subsectionText">
<>
<SettingsSubsectionText>
{_t("Securely cache encrypted messages locally for them to appear in search results.")}
</div>
</SettingsSubsectionText>
<div>
<AccessibleButton kind="primary" disabled={this.state.enabling} onClick={this.onEnable}>
{_t("Enable")}
</AccessibleButton>
{this.state.enabling ? <InlineSpinner /> : <div />}
</div>
</div>
</>
);
} else if (EventIndexPeg.platformHasSupport() && !EventIndexPeg.supportIsInstalled()) {
const nativeLink =
@ -187,7 +186,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
"adding-seshat-for-search-in-e2e-encrypted-rooms";
eventIndexingSettings = (
<div className="mx_SettingsTab_subsectionText">
<SettingsSubsectionText>
{_t(
"%(brand)s is missing some components required for securely " +
"caching encrypted messages locally. If you'd like to " +
@ -204,11 +203,11 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
),
},
)}
</div>
</SettingsSubsectionText>
);
} else if (!EventIndexPeg.platformHasSupport()) {
eventIndexingSettings = (
<div className="mx_SettingsTab_subsectionText">
<SettingsSubsectionText>
{_t(
"%(brand)s can't securely cache encrypted messages locally " +
"while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> " +
@ -228,24 +227,28 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
),
},
)}
</div>
</SettingsSubsectionText>
);
} else {
eventIndexingSettings = (
<div className="mx_SettingsTab_subsectionText">
<p>{this.state.enabling ? <InlineSpinner /> : _t("Message search initialisation failed")}</p>
<>
<SettingsSubsectionText>
{this.state.enabling ? <InlineSpinner /> : _t("Message search initialisation failed")}
</SettingsSubsectionText>
{EventIndexPeg.error && (
<details>
<summary>{_t("Advanced")}</summary>
<code>{EventIndexPeg.error.message}</code>
<p>
<AccessibleButton key="delete" kind="danger" onClick={this.confirmEventStoreReset}>
{_t("Reset")}
</AccessibleButton>
</p>
</details>
<SettingsSubsectionText>
<details>
<summary>{_t("Advanced")}</summary>
<code>{EventIndexPeg.error.message}</code>
<p>
<AccessibleButton key="delete" kind="danger" onClick={this.confirmEventStoreReset}>
{_t("Reset")}
</AccessibleButton>
</p>
</details>
</SettingsSubsectionText>
)}
</div>
</>
);
}

View file

@ -31,6 +31,7 @@ import AccessibleButton from "../elements/AccessibleButton";
import QuestionDialog from "../dialogs/QuestionDialog";
import RestoreKeyBackupDialog from "../dialogs/security/RestoreKeyBackupDialog";
import { accessSecretStorage } from "../../../SecurityManager";
import { SettingsSubsectionText } from "./shared/SettingsSubsection";
interface IState {
loading: boolean;
@ -247,7 +248,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
} else {
statusDescription = (
<>
<p>
<SettingsSubsectionText>
{_t(
"This session is <b>not backing up your keys</b>, " +
"but you do have an existing backup you can restore from " +
@ -255,13 +256,13 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
{},
{ b: (sub) => <b>{sub}</b> },
)}
</p>
<p>
</SettingsSubsectionText>
<SettingsSubsectionText>
{_t(
"Connect this session to key backup before signing out to avoid " +
"losing any keys that may only be on this session.",
)}
</p>
</SettingsSubsectionText>
</>
);
restoreButtonCaption = _t("Connect this session to Key Backup");
@ -425,14 +426,16 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
} else {
statusDescription = (
<>
<p>
<SettingsSubsectionText>
{_t(
"Your keys are <b>not being backed up from this session</b>.",
{},
{ b: (sub) => <b>{sub}</b> },
)}
</p>
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
</SettingsSubsectionText>
<SettingsSubsectionText>
{_t("Back up your keys before signing out to avoid losing them.")}
</SettingsSubsectionText>
</>
);
actions.push(
@ -466,14 +469,14 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
}
return (
<div>
<p>
<>
<SettingsSubsectionText>
{_t(
"Back up your encryption keys with your account data in case you " +
"lose access to your sessions. Your keys will be secured with a " +
"unique Security Key.",
)}
</p>
</SettingsSubsectionText>
{statusDescription}
<details>
<summary>{_t("Advanced")}</summary>
@ -502,7 +505,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
{extraDetails}
</details>
{actionRow}
</div>
</>
);
}
}

View file

@ -477,10 +477,6 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
{historySection}
</SettingsSection>
</SettingsTab>
// <div className="mx_SettingsTab mx_SecurityRoomSettingsTab">
// <div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
// </div>
);
}
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 - 2022 The Matrix.org Foundation C.I.C.
Copyright 2019 - 2023 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.
@ -41,6 +41,9 @@ import { privateShouldBeEncrypted } from "../../../../../utils/rooms";
import LoginWithQR, { Mode } from "../../../auth/LoginWithQR";
import LoginWithQRSection from "../../devices/LoginWithQRSection";
import type { IServerVersions } from "matrix-js-sdk/src/matrix";
import SettingsTab from "../SettingsTab";
import { SettingsSection } from "../../shared/SettingsSection";
import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection";
interface IIgnoredUserProps {
userId: string;
@ -245,10 +248,9 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
});
return (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Ignored users")}</span>
<div className="mx_SettingsTab_subsectionText">{userIds}</div>
</div>
<SettingsSubsection heading={_t("Ignored users")}>
<SettingsSubsectionText>{userIds}</SettingsSubsectionText>
</SettingsSubsection>
);
}
@ -260,24 +262,25 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
}
return (
<div className="mx_SettingsTab_section mx_SecurityUserSettingsTab_bulkOptions">
<span className="mx_SettingsTab_subheading">{_t("Bulk options")}</span>
<AccessibleButton
onClick={this.onAcceptAllInvitesClicked}
kind="primary"
disabled={this.state.managingInvites}
>
{_t("Accept all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size })}
</AccessibleButton>
<AccessibleButton
onClick={this.onRejectAllInvitesClicked}
kind="danger"
disabled={this.state.managingInvites}
>
{_t("Reject all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size })}
</AccessibleButton>
{this.state.managingInvites ? <InlineSpinner /> : <div />}
</div>
<SettingsSubsection heading={_t("Bulk options")}>
<div className="mx_SecurityUserSettingsTab_bulkOptions">
<AccessibleButton
onClick={this.onAcceptAllInvitesClicked}
kind="primary"
disabled={this.state.managingInvites}
>
{_t("Accept all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size })}
</AccessibleButton>
<AccessibleButton
onClick={this.onRejectAllInvitesClicked}
kind="danger"
disabled={this.state.managingInvites}
>
{_t("Reject all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size })}
</AccessibleButton>
{this.state.managingInvites ? <InlineSpinner /> : <div />}
</div>
</SettingsSubsection>
);
}
@ -291,19 +294,15 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
public render(): React.ReactNode {
const secureBackup = (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Secure Backup")}</span>
<div className="mx_SettingsTab_subsectionText">
<SecureBackupPanel />
</div>
</div>
<SettingsSubsection heading={_t("Secure Backup")}>
<SecureBackupPanel />
</SettingsSubsection>
);
const eventIndex = (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Message search")}</span>
<SettingsSubsection heading={_t("Message search")}>
<EventIndexPanel />
</div>
</SettingsSubsection>
);
// XXX: There's no such panel in the current cross-signing designs, but
@ -311,12 +310,9 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
// in having advanced details here once all flows are implemented, we
// can remove this.
const crossSigning = (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Cross-signing")}</span>
<div className="mx_SettingsTab_subsectionText">
<CrossSigningPanel />
</div>
</div>
<SettingsSubsection heading={_t("Cross-signing")}>
<CrossSigningPanel />
</SettingsSubsection>
);
let warning;
@ -340,28 +336,24 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
});
};
privacySection = (
<React.Fragment>
<div className="mx_SettingsTab_heading">{_t("Privacy")}</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Analytics")}</span>
<div className="mx_SettingsTab_subsectionText">
<p>
{_t(
"Share anonymous data to help us identify issues. Nothing personal. " +
"No third parties.",
)}
</p>
<AccessibleButton kind="link" onClick={onClickAnalyticsLearnMore}>
{_t("Learn more")}
</AccessibleButton>
</div>
<SettingsSection heading={_t("Privacy")}>
<SettingsSubsection
heading={_t("Analytics")}
description={_t(
"Share anonymous data to help us identify issues. Nothing personal. No third parties.",
)}
>
<AccessibleButton kind="link" onClick={onClickAnalyticsLearnMore}>
{_t("Learn more")}
</AccessibleButton>
{PosthogAnalytics.instance.isEnabled() && (
<SettingsFlag name="pseudonymousAnalyticsOptIn" level={SettingLevel.ACCOUNT} />
)}
<span className="mx_SettingsTab_subheading">{_t("Sessions")}</span>
</SettingsSubsection>
<SettingsSubsection heading={_t("Sessions")}>
<SettingsFlag name="deviceClientInformationOptIn" level={SettingLevel.ACCOUNT} />
</div>
</React.Fragment>
</SettingsSubsection>
</SettingsSection>
);
}
@ -373,67 +365,60 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
// only show the section if there's something to show
if (ignoreUsersPanel || invitesPanel || e2ePanel) {
advancedSection = (
<>
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
<div className="mx_SettingsTab_section">
{ignoreUsersPanel}
{invitesPanel}
{e2ePanel}
</div>
</>
<SettingsSection heading={_t("Advanced")}>
{ignoreUsersPanel}
{invitesPanel}
{e2ePanel}
</SettingsSection>
);
}
}
const useNewSessionManager = SettingsStore.getValue("feature_new_device_manager");
const devicesSection = useNewSessionManager ? null : (
<>
<div className="mx_SettingsTab_heading">{_t("Where you're signed in")}</div>
<div className="mx_SettingsTab_section" data-testid="devices-section">
<span className="mx_SettingsTab_subsectionText">
{_t(
"Manage your signed-in devices below. " +
"A device's name is visible to people you communicate with.",
)}
</span>
<DevicesPanel />
</div>
<SettingsSection heading={_t("Where you're signed in")} data-testid="devices-section">
<SettingsSubsectionText>
{_t(
"Manage your signed-in devices below. " +
"A device's name is visible to people you communicate with.",
)}
</SettingsSubsectionText>
<DevicesPanel />
<LoginWithQRSection
onShowQr={this.onShowQRClicked}
versions={this.state.versions}
capabilities={this.state.capabilities}
/>
</>
</SettingsSection>
);
const client = MatrixClientPeg.get();
if (this.state.showLoginWithQR) {
return (
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
<SettingsTab>
<LoginWithQR
onFinished={this.onLoginWithQRFinished}
mode={this.state.showLoginWithQR}
client={client}
/>
</div>
</SettingsTab>
);
}
return (
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
<SettingsTab>
{warning}
{devicesSection}
<div className="mx_SettingsTab_heading">{_t("Encryption")}</div>
<div className="mx_SettingsTab_section">
<SettingsSection heading={_t("Encryption")}>
{secureBackup}
{eventIndex}
{crossSigning}
<CryptographyPanel />
</div>
</SettingsSection>
{privacySection}
{advancedSection}
</div>
</SettingsTab>
);
}
}

View file

@ -259,6 +259,7 @@ const SessionManagerTab: React.FC = () => {
`from any session that you don't recognize or use anymore.`,
)}
data-testid="other-sessions-section"
stretchContent
>
<FilteredDeviceList
devices={otherDevices}