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:
parent
6c262fff6b
commit
d9a61c093c
16 changed files with 721 additions and 303 deletions
|
@ -32,13 +32,9 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CryptographyPanel_importExportButtons .mx_AccessibleButton {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CryptographyPanel_importExportButtons {
|
.mx_CryptographyPanel_importExportButtons {
|
||||||
margin-bottom: 15px;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-flow: wrap;
|
flex-flow: wrap;
|
||||||
row-gap: 10px;
|
row-gap: $spacing-8;
|
||||||
|
column-gap: $spacing-8;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_SettingsTab {
|
.mx_SettingsTab {
|
||||||
--SettingsTab_section-margin-bottom-preferences-labs: 30px;
|
|
||||||
--SettingsTab_heading_nth_child-margin-top: 30px; /* TODO: Use a spacing variable */
|
--SettingsTab_heading_nth_child-margin-top: 30px; /* TODO: Use a spacing variable */
|
||||||
--SettingsTab_fullWidthField-margin-inline-end: 100px;
|
--SettingsTab_fullWidthField-margin-inline-end: 100px;
|
||||||
--SettingsTab_tooltip-max-width: 120px; /* So it fits in the space provided by the page */
|
--SettingsTab_tooltip-max-width: 120px; /* So it fits in the space provided by the page */
|
||||||
|
|
|
@ -14,43 +14,36 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_SecurityUserSettingsTab_bulkOptions .mx_AccessibleButton {
|
.mx_SecurityUserSettingsTab_bulkOptions {
|
||||||
margin-right: 10px;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
column-gap: $spacing-8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SecurityUserSettingsTab_ignoredUser {
|
.mx_SecurityUserSettingsTab_ignoredUser {
|
||||||
margin-bottom: 5px;
|
margin-bottom: $spacing-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SecurityUserSettingsTab_ignoredUser .mx_AccessibleButton {
|
.mx_SecurityUserSettingsTab_ignoredUser .mx_AccessibleButton {
|
||||||
margin-right: 10px;
|
margin-right: $spacing-8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SecurityUserSettingsTab {
|
.mx_SecurityUserSettingsTab_warning {
|
||||||
.mx_SettingsTab_section {
|
color: $alert;
|
||||||
.mx_AccessibleButton_kind_link {
|
position: relative;
|
||||||
font-size: inherit;
|
padding-left: 40px;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SecurityUserSettingsTab_warning {
|
&::before {
|
||||||
color: $alert;
|
mask-repeat: no-repeat;
|
||||||
position: relative;
|
mask-position: 0 center;
|
||||||
padding-left: 40px;
|
mask-size: $font-24px;
|
||||||
margin-top: 30px;
|
position: absolute;
|
||||||
|
width: $font-24px;
|
||||||
&::before {
|
height: $font-24px;
|
||||||
mask-repeat: no-repeat;
|
content: "";
|
||||||
mask-position: 0 center;
|
top: 0;
|
||||||
mask-size: $font-24px;
|
left: 0;
|
||||||
position: absolute;
|
background-color: $alert;
|
||||||
width: $font-24px;
|
mask-image: url("$(res)/img/feather-customised/alert-triangle.svg");
|
||||||
height: $font-24px;
|
|
||||||
content: "";
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
background-color: $alert;
|
|
||||||
mask-image: url("$(res)/img/feather-customised/alert-triangle.svg");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ import ConfirmDestroyCrossSigningDialog from "../dialogs/security/ConfirmDestroy
|
||||||
import SetupEncryptionDialog from "../dialogs/security/SetupEncryptionDialog";
|
import SetupEncryptionDialog from "../dialogs/security/SetupEncryptionDialog";
|
||||||
import { accessSecretStorage } from "../../../SecurityManager";
|
import { accessSecretStorage } from "../../../SecurityManager";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
import { SettingsSubsectionText } from "./shared/SettingsSubsection";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
error?: Error;
|
error?: Error;
|
||||||
|
@ -178,22 +179,38 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
|
||||||
if (homeserverSupportsCrossSigning === undefined) {
|
if (homeserverSupportsCrossSigning === undefined) {
|
||||||
summarisedStatus = <Spinner />;
|
summarisedStatus = <Spinner />;
|
||||||
} else if (!homeserverSupportsCrossSigning) {
|
} 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) {
|
} 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) {
|
} 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) {
|
} else if (crossSigningPrivateKeysInStorage) {
|
||||||
summarisedStatus = (
|
summarisedStatus = (
|
||||||
<p>
|
<SettingsSubsectionText data-testid="summarised-status">
|
||||||
{_t(
|
{_t(
|
||||||
"Your account has a cross-signing identity in secret storage, " +
|
"Your account has a cross-signing identity in secret storage, " +
|
||||||
"but it is not yet trusted by this session.",
|
"but it is not yet trusted by this session.",
|
||||||
)}
|
)}
|
||||||
</p>
|
</SettingsSubsectionText>
|
||||||
);
|
);
|
||||||
} else {
|
} 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 =
|
const keysExistAnywhere =
|
||||||
|
@ -238,7 +255,7 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
{summarisedStatus}
|
{summarisedStatus}
|
||||||
<details>
|
<details>
|
||||||
<summary>{_t("Advanced")}</summary>
|
<summary>{_t("Advanced")}</summary>
|
||||||
|
@ -275,7 +292,7 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> {
|
||||||
</details>
|
</details>
|
||||||
{errorSection}
|
{errorSection}
|
||||||
{actionRow}
|
{actionRow}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import * as FormattingUtils from "../../../utils/FormattingUtils";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import SettingsFlag from "../elements/SettingsFlag";
|
import SettingsFlag from "../elements/SettingsFlag";
|
||||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
|
import SettingsSubsection, { SettingsSubsectionText } from "./shared/SettingsSubsection";
|
||||||
|
|
||||||
interface IProps {}
|
interface IProps {}
|
||||||
|
|
||||||
|
@ -72,27 +73,28 @@ export default class CryptographyPanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab_section mx_CryptographyPanel">
|
<SettingsSubsection heading={_t("Cryptography")}>
|
||||||
<span className="mx_SettingsTab_subheading">{_t("Cryptography")}</span>
|
<SettingsSubsectionText>
|
||||||
<table className="mx_SettingsTab_subsectionText mx_CryptographyPanel_sessionInfo">
|
<table className="mx_CryptographyPanel_sessionInfo">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{_t("Session ID:")}</th>
|
<th scope="row">{_t("Session ID:")}</th>
|
||||||
<td>
|
<td>
|
||||||
<code>{deviceId}</code>
|
<code>{deviceId}</code>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{_t("Session key:")}</th>
|
<th scope="row">{_t("Session key:")}</th>
|
||||||
<td>
|
<td>
|
||||||
<code>
|
<code>
|
||||||
<b>{identityKey}</b>
|
<b>{identityKey}</b>
|
||||||
</code>
|
</code>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
</SettingsSubsectionText>
|
||||||
{importExportButtons}
|
{importExportButtons}
|
||||||
{noSendUnverifiedSetting}
|
{noSendUnverifiedSetting}
|
||||||
</div>
|
</SettingsSubsection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,21 +20,20 @@ import { _t } from "../../../languageHandler";
|
||||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import SettingsFlag from "../elements/SettingsFlag";
|
import SettingsFlag from "../elements/SettingsFlag";
|
||||||
|
import SettingsSubsection, { SettingsSubsectionText } from "./shared/SettingsSubsection";
|
||||||
|
|
||||||
const SETTING_MANUALLY_VERIFY_ALL_SESSIONS = "e2ee.manuallyVerifyAllSessions";
|
const SETTING_MANUALLY_VERIFY_ALL_SESSIONS = "e2ee.manuallyVerifyAllSessions";
|
||||||
|
|
||||||
const E2eAdvancedPanel: React.FC = () => {
|
const E2eAdvancedPanel: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab_section">
|
<SettingsSubsection heading={_t("Encryption")}>
|
||||||
<span className="mx_SettingsTab_subheading">{_t("Encryption")}</span>
|
|
||||||
|
|
||||||
<SettingsFlag name={SETTING_MANUALLY_VERIFY_ALL_SESSIONS} level={SettingLevel.DEVICE} />
|
<SettingsFlag name={SETTING_MANUALLY_VERIFY_ALL_SESSIONS} level={SettingLevel.DEVICE} />
|
||||||
<div className="mx_SettingsTab_subsectionText">
|
<SettingsSubsectionText>
|
||||||
{_t(
|
{_t(
|
||||||
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.",
|
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.",
|
||||||
)}
|
)}
|
||||||
</div>
|
</SettingsSubsectionText>
|
||||||
</div>
|
</SettingsSubsection>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
import SeshatResetDialog from "../dialogs/SeshatResetDialog";
|
import SeshatResetDialog from "../dialogs/SeshatResetDialog";
|
||||||
import InlineSpinner from "../elements/InlineSpinner";
|
import InlineSpinner from "../elements/InlineSpinner";
|
||||||
import ExternalLink from "../elements/ExternalLink";
|
import ExternalLink from "../elements/ExternalLink";
|
||||||
|
import { SettingsSubsectionText } from "./shared/SettingsSubsection";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
enabling: boolean;
|
enabling: boolean;
|
||||||
|
@ -145,8 +146,8 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
|
||||||
|
|
||||||
if (EventIndexPeg.get() !== null) {
|
if (EventIndexPeg.get() !== null) {
|
||||||
eventIndexingSettings = (
|
eventIndexingSettings = (
|
||||||
<div>
|
<>
|
||||||
<div className="mx_SettingsTab_subsectionText">
|
<SettingsSubsectionText>
|
||||||
{_t(
|
{_t(
|
||||||
"Securely cache encrypted messages locally for them " +
|
"Securely cache encrypted messages locally for them " +
|
||||||
"to appear in search results, using %(size)s to store messages from %(rooms)s rooms.",
|
"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),
|
rooms: formatCountLong(this.state.roomCount),
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
</div>
|
</SettingsSubsectionText>
|
||||||
<div>
|
<AccessibleButton kind="primary" onClick={this.onManage}>
|
||||||
<AccessibleButton kind="primary" onClick={this.onManage}>
|
{_t("Manage")}
|
||||||
{_t("Manage")}
|
</AccessibleButton>
|
||||||
</AccessibleButton>
|
</>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
} else if (!this.state.eventIndexingEnabled && EventIndexPeg.supportIsInstalled()) {
|
} else if (!this.state.eventIndexingEnabled && EventIndexPeg.supportIsInstalled()) {
|
||||||
eventIndexingSettings = (
|
eventIndexingSettings = (
|
||||||
<div>
|
<>
|
||||||
<div className="mx_SettingsTab_subsectionText">
|
<SettingsSubsectionText>
|
||||||
{_t("Securely cache encrypted messages locally for them to appear in search results.")}
|
{_t("Securely cache encrypted messages locally for them to appear in search results.")}
|
||||||
</div>
|
</SettingsSubsectionText>
|
||||||
<div>
|
<div>
|
||||||
<AccessibleButton kind="primary" disabled={this.state.enabling} onClick={this.onEnable}>
|
<AccessibleButton kind="primary" disabled={this.state.enabling} onClick={this.onEnable}>
|
||||||
{_t("Enable")}
|
{_t("Enable")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
{this.state.enabling ? <InlineSpinner /> : <div />}
|
{this.state.enabling ? <InlineSpinner /> : <div />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
} else if (EventIndexPeg.platformHasSupport() && !EventIndexPeg.supportIsInstalled()) {
|
} else if (EventIndexPeg.platformHasSupport() && !EventIndexPeg.supportIsInstalled()) {
|
||||||
const nativeLink =
|
const nativeLink =
|
||||||
|
@ -187,7 +186,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
|
||||||
"adding-seshat-for-search-in-e2e-encrypted-rooms";
|
"adding-seshat-for-search-in-e2e-encrypted-rooms";
|
||||||
|
|
||||||
eventIndexingSettings = (
|
eventIndexingSettings = (
|
||||||
<div className="mx_SettingsTab_subsectionText">
|
<SettingsSubsectionText>
|
||||||
{_t(
|
{_t(
|
||||||
"%(brand)s is missing some components required for securely " +
|
"%(brand)s is missing some components required for securely " +
|
||||||
"caching encrypted messages locally. If you'd like to " +
|
"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()) {
|
} else if (!EventIndexPeg.platformHasSupport()) {
|
||||||
eventIndexingSettings = (
|
eventIndexingSettings = (
|
||||||
<div className="mx_SettingsTab_subsectionText">
|
<SettingsSubsectionText>
|
||||||
{_t(
|
{_t(
|
||||||
"%(brand)s can't securely cache encrypted messages locally " +
|
"%(brand)s can't securely cache encrypted messages locally " +
|
||||||
"while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> " +
|
"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 {
|
} else {
|
||||||
eventIndexingSettings = (
|
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 && (
|
{EventIndexPeg.error && (
|
||||||
<details>
|
<SettingsSubsectionText>
|
||||||
<summary>{_t("Advanced")}</summary>
|
<details>
|
||||||
<code>{EventIndexPeg.error.message}</code>
|
<summary>{_t("Advanced")}</summary>
|
||||||
<p>
|
<code>{EventIndexPeg.error.message}</code>
|
||||||
<AccessibleButton key="delete" kind="danger" onClick={this.confirmEventStoreReset}>
|
<p>
|
||||||
{_t("Reset")}
|
<AccessibleButton key="delete" kind="danger" onClick={this.confirmEventStoreReset}>
|
||||||
</AccessibleButton>
|
{_t("Reset")}
|
||||||
</p>
|
</AccessibleButton>
|
||||||
</details>
|
</p>
|
||||||
|
</details>
|
||||||
|
</SettingsSubsectionText>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||||
import RestoreKeyBackupDialog from "../dialogs/security/RestoreKeyBackupDialog";
|
import RestoreKeyBackupDialog from "../dialogs/security/RestoreKeyBackupDialog";
|
||||||
import { accessSecretStorage } from "../../../SecurityManager";
|
import { accessSecretStorage } from "../../../SecurityManager";
|
||||||
|
import { SettingsSubsectionText } from "./shared/SettingsSubsection";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
@ -247,7 +248,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
||||||
} else {
|
} else {
|
||||||
statusDescription = (
|
statusDescription = (
|
||||||
<>
|
<>
|
||||||
<p>
|
<SettingsSubsectionText>
|
||||||
{_t(
|
{_t(
|
||||||
"This session is <b>not backing up your keys</b>, " +
|
"This session is <b>not backing up your keys</b>, " +
|
||||||
"but you do have an existing backup you can restore from " +
|
"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> },
|
{ b: (sub) => <b>{sub}</b> },
|
||||||
)}
|
)}
|
||||||
</p>
|
</SettingsSubsectionText>
|
||||||
<p>
|
<SettingsSubsectionText>
|
||||||
{_t(
|
{_t(
|
||||||
"Connect this session to key backup before signing out to avoid " +
|
"Connect this session to key backup before signing out to avoid " +
|
||||||
"losing any keys that may only be on this session.",
|
"losing any keys that may only be on this session.",
|
||||||
)}
|
)}
|
||||||
</p>
|
</SettingsSubsectionText>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
restoreButtonCaption = _t("Connect this session to Key Backup");
|
restoreButtonCaption = _t("Connect this session to Key Backup");
|
||||||
|
@ -425,14 +426,16 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
||||||
} else {
|
} else {
|
||||||
statusDescription = (
|
statusDescription = (
|
||||||
<>
|
<>
|
||||||
<p>
|
<SettingsSubsectionText>
|
||||||
{_t(
|
{_t(
|
||||||
"Your keys are <b>not being backed up from this session</b>.",
|
"Your keys are <b>not being backed up from this session</b>.",
|
||||||
{},
|
{},
|
||||||
{ b: (sub) => <b>{sub}</b> },
|
{ b: (sub) => <b>{sub}</b> },
|
||||||
)}
|
)}
|
||||||
</p>
|
</SettingsSubsectionText>
|
||||||
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
|
<SettingsSubsectionText>
|
||||||
|
{_t("Back up your keys before signing out to avoid losing them.")}
|
||||||
|
</SettingsSubsectionText>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
actions.push(
|
actions.push(
|
||||||
|
@ -466,14 +469,14 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<p>
|
<SettingsSubsectionText>
|
||||||
{_t(
|
{_t(
|
||||||
"Back up your encryption keys with your account data in case you " +
|
"Back up your encryption keys with your account data in case you " +
|
||||||
"lose access to your sessions. Your keys will be secured with a " +
|
"lose access to your sessions. Your keys will be secured with a " +
|
||||||
"unique Security Key.",
|
"unique Security Key.",
|
||||||
)}
|
)}
|
||||||
</p>
|
</SettingsSubsectionText>
|
||||||
{statusDescription}
|
{statusDescription}
|
||||||
<details>
|
<details>
|
||||||
<summary>{_t("Advanced")}</summary>
|
<summary>{_t("Advanced")}</summary>
|
||||||
|
@ -502,7 +505,7 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
|
||||||
{extraDetails}
|
{extraDetails}
|
||||||
</details>
|
</details>
|
||||||
{actionRow}
|
{actionRow}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -477,10 +477,6 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
||||||
{historySection}
|
{historySection}
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
</SettingsTab>
|
</SettingsTab>
|
||||||
// <div className="mx_SettingsTab mx_SecurityRoomSettingsTab">
|
|
||||||
// <div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
|
|
||||||
|
|
||||||
// </div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 LoginWithQR, { Mode } from "../../../auth/LoginWithQR";
|
||||||
import LoginWithQRSection from "../../devices/LoginWithQRSection";
|
import LoginWithQRSection from "../../devices/LoginWithQRSection";
|
||||||
import type { IServerVersions } from "matrix-js-sdk/src/matrix";
|
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 {
|
interface IIgnoredUserProps {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
@ -245,10 +248,9 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab_section">
|
<SettingsSubsection heading={_t("Ignored users")}>
|
||||||
<span className="mx_SettingsTab_subheading">{_t("Ignored users")}</span>
|
<SettingsSubsectionText>{userIds}</SettingsSubsectionText>
|
||||||
<div className="mx_SettingsTab_subsectionText">{userIds}</div>
|
</SettingsSubsection>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -260,24 +262,25 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab_section mx_SecurityUserSettingsTab_bulkOptions">
|
<SettingsSubsection heading={_t("Bulk options")}>
|
||||||
<span className="mx_SettingsTab_subheading">{_t("Bulk options")}</span>
|
<div className="mx_SecurityUserSettingsTab_bulkOptions">
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this.onAcceptAllInvitesClicked}
|
onClick={this.onAcceptAllInvitesClicked}
|
||||||
kind="primary"
|
kind="primary"
|
||||||
disabled={this.state.managingInvites}
|
disabled={this.state.managingInvites}
|
||||||
>
|
>
|
||||||
{_t("Accept all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size })}
|
{_t("Accept all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size })}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this.onRejectAllInvitesClicked}
|
onClick={this.onRejectAllInvitesClicked}
|
||||||
kind="danger"
|
kind="danger"
|
||||||
disabled={this.state.managingInvites}
|
disabled={this.state.managingInvites}
|
||||||
>
|
>
|
||||||
{_t("Reject all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size })}
|
{_t("Reject all %(invitedRooms)s invites", { invitedRooms: invitedRoomIds.size })}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
{this.state.managingInvites ? <InlineSpinner /> : <div />}
|
{this.state.managingInvites ? <InlineSpinner /> : <div />}
|
||||||
</div>
|
</div>
|
||||||
|
</SettingsSubsection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -291,19 +294,15 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
const secureBackup = (
|
const secureBackup = (
|
||||||
<div className="mx_SettingsTab_section">
|
<SettingsSubsection heading={_t("Secure Backup")}>
|
||||||
<span className="mx_SettingsTab_subheading">{_t("Secure Backup")}</span>
|
<SecureBackupPanel />
|
||||||
<div className="mx_SettingsTab_subsectionText">
|
</SettingsSubsection>
|
||||||
<SecureBackupPanel />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const eventIndex = (
|
const eventIndex = (
|
||||||
<div className="mx_SettingsTab_section">
|
<SettingsSubsection heading={_t("Message search")}>
|
||||||
<span className="mx_SettingsTab_subheading">{_t("Message search")}</span>
|
|
||||||
<EventIndexPanel />
|
<EventIndexPanel />
|
||||||
</div>
|
</SettingsSubsection>
|
||||||
);
|
);
|
||||||
|
|
||||||
// XXX: There's no such panel in the current cross-signing designs, but
|
// 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
|
// in having advanced details here once all flows are implemented, we
|
||||||
// can remove this.
|
// can remove this.
|
||||||
const crossSigning = (
|
const crossSigning = (
|
||||||
<div className="mx_SettingsTab_section">
|
<SettingsSubsection heading={_t("Cross-signing")}>
|
||||||
<span className="mx_SettingsTab_subheading">{_t("Cross-signing")}</span>
|
<CrossSigningPanel />
|
||||||
<div className="mx_SettingsTab_subsectionText">
|
</SettingsSubsection>
|
||||||
<CrossSigningPanel />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let warning;
|
let warning;
|
||||||
|
@ -340,28 +336,24 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
privacySection = (
|
privacySection = (
|
||||||
<React.Fragment>
|
<SettingsSection heading={_t("Privacy")}>
|
||||||
<div className="mx_SettingsTab_heading">{_t("Privacy")}</div>
|
<SettingsSubsection
|
||||||
<div className="mx_SettingsTab_section">
|
heading={_t("Analytics")}
|
||||||
<span className="mx_SettingsTab_subheading">{_t("Analytics")}</span>
|
description={_t(
|
||||||
<div className="mx_SettingsTab_subsectionText">
|
"Share anonymous data to help us identify issues. Nothing personal. No third parties.",
|
||||||
<p>
|
)}
|
||||||
{_t(
|
>
|
||||||
"Share anonymous data to help us identify issues. Nothing personal. " +
|
<AccessibleButton kind="link" onClick={onClickAnalyticsLearnMore}>
|
||||||
"No third parties.",
|
{_t("Learn more")}
|
||||||
)}
|
</AccessibleButton>
|
||||||
</p>
|
|
||||||
<AccessibleButton kind="link" onClick={onClickAnalyticsLearnMore}>
|
|
||||||
{_t("Learn more")}
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
|
||||||
{PosthogAnalytics.instance.isEnabled() && (
|
{PosthogAnalytics.instance.isEnabled() && (
|
||||||
<SettingsFlag name="pseudonymousAnalyticsOptIn" level={SettingLevel.ACCOUNT} />
|
<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} />
|
<SettingsFlag name="deviceClientInformationOptIn" level={SettingLevel.ACCOUNT} />
|
||||||
</div>
|
</SettingsSubsection>
|
||||||
</React.Fragment>
|
</SettingsSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -373,67 +365,60 @@ export default class SecurityUserSettingsTab extends React.Component<IProps, ISt
|
||||||
// only show the section if there's something to show
|
// only show the section if there's something to show
|
||||||
if (ignoreUsersPanel || invitesPanel || e2ePanel) {
|
if (ignoreUsersPanel || invitesPanel || e2ePanel) {
|
||||||
advancedSection = (
|
advancedSection = (
|
||||||
<>
|
<SettingsSection heading={_t("Advanced")}>
|
||||||
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
|
{ignoreUsersPanel}
|
||||||
<div className="mx_SettingsTab_section">
|
{invitesPanel}
|
||||||
{ignoreUsersPanel}
|
{e2ePanel}
|
||||||
{invitesPanel}
|
</SettingsSection>
|
||||||
{e2ePanel}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const useNewSessionManager = SettingsStore.getValue("feature_new_device_manager");
|
const useNewSessionManager = SettingsStore.getValue("feature_new_device_manager");
|
||||||
const devicesSection = useNewSessionManager ? null : (
|
const devicesSection = useNewSessionManager ? null : (
|
||||||
<>
|
<SettingsSection heading={_t("Where you're signed in")} data-testid="devices-section">
|
||||||
<div className="mx_SettingsTab_heading">{_t("Where you're signed in")}</div>
|
<SettingsSubsectionText>
|
||||||
<div className="mx_SettingsTab_section" data-testid="devices-section">
|
{_t(
|
||||||
<span className="mx_SettingsTab_subsectionText">
|
"Manage your signed-in devices below. " +
|
||||||
{_t(
|
"A device's name is visible to people you communicate with.",
|
||||||
"Manage your signed-in devices below. " +
|
)}
|
||||||
"A device's name is visible to people you communicate with.",
|
</SettingsSubsectionText>
|
||||||
)}
|
<DevicesPanel />
|
||||||
</span>
|
|
||||||
<DevicesPanel />
|
|
||||||
</div>
|
|
||||||
<LoginWithQRSection
|
<LoginWithQRSection
|
||||||
onShowQr={this.onShowQRClicked}
|
onShowQr={this.onShowQRClicked}
|
||||||
versions={this.state.versions}
|
versions={this.state.versions}
|
||||||
capabilities={this.state.capabilities}
|
capabilities={this.state.capabilities}
|
||||||
/>
|
/>
|
||||||
</>
|
</SettingsSection>
|
||||||
);
|
);
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
if (this.state.showLoginWithQR) {
|
if (this.state.showLoginWithQR) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
|
<SettingsTab>
|
||||||
<LoginWithQR
|
<LoginWithQR
|
||||||
onFinished={this.onLoginWithQRFinished}
|
onFinished={this.onLoginWithQRFinished}
|
||||||
mode={this.state.showLoginWithQR}
|
mode={this.state.showLoginWithQR}
|
||||||
client={client}
|
client={client}
|
||||||
/>
|
/>
|
||||||
</div>
|
</SettingsTab>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
|
<SettingsTab>
|
||||||
{warning}
|
{warning}
|
||||||
{devicesSection}
|
{devicesSection}
|
||||||
<div className="mx_SettingsTab_heading">{_t("Encryption")}</div>
|
<SettingsSection heading={_t("Encryption")}>
|
||||||
<div className="mx_SettingsTab_section">
|
|
||||||
{secureBackup}
|
{secureBackup}
|
||||||
{eventIndex}
|
{eventIndex}
|
||||||
{crossSigning}
|
{crossSigning}
|
||||||
<CryptographyPanel />
|
<CryptographyPanel />
|
||||||
</div>
|
</SettingsSection>
|
||||||
{privacySection}
|
{privacySection}
|
||||||
{advancedSection}
|
{advancedSection}
|
||||||
</div>
|
</SettingsTab>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -259,6 +259,7 @@ const SessionManagerTab: React.FC = () => {
|
||||||
`from any session that you don't recognize or use anymore.`,
|
`from any session that you don't recognize or use anymore.`,
|
||||||
)}
|
)}
|
||||||
data-testid="other-sessions-section"
|
data-testid="other-sessions-section"
|
||||||
|
stretchContent
|
||||||
>
|
>
|
||||||
<FilteredDeviceList
|
<FilteredDeviceList
|
||||||
devices={otherDevices}
|
devices={otherDevices}
|
||||||
|
|
113
test/components/views/settings/CrossSigningPanel-test.tsx
Normal file
113
test/components/views/settings/CrossSigningPanel-test.tsx
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
/*
|
||||||
|
Copyright 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.
|
||||||
|
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 { render, screen } from "@testing-library/react";
|
||||||
|
import { mocked } from "jest-mock";
|
||||||
|
|
||||||
|
import CrossSigningPanel from "../../../../src/components/views/settings/CrossSigningPanel";
|
||||||
|
import {
|
||||||
|
flushPromises,
|
||||||
|
getMockClientWithEventEmitter,
|
||||||
|
mockClientMethodsCrypto,
|
||||||
|
mockClientMethodsUser,
|
||||||
|
} from "../../../test-utils";
|
||||||
|
|
||||||
|
describe("<CrossSigningPanel />", () => {
|
||||||
|
const userId = "@alice:server.org";
|
||||||
|
const mockClient = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsUser(userId),
|
||||||
|
...mockClientMethodsCrypto(),
|
||||||
|
doesServerSupportUnstableFeature: jest.fn(),
|
||||||
|
});
|
||||||
|
const getComponent = () => render(<CrossSigningPanel />);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockClient.doesServerSupportUnstableFeature.mockResolvedValue(true);
|
||||||
|
mockClient.isCrossSigningReady.mockResolvedValue(false);
|
||||||
|
mocked(mockClient.crypto!.crossSigningInfo).isStoredInSecretStorage.mockClear().mockResolvedValue(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render a spinner while loading", () => {
|
||||||
|
getComponent();
|
||||||
|
|
||||||
|
expect(screen.getByRole("progressbar")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render when homeserver does not support cross-signing", async () => {
|
||||||
|
mockClient.doesServerSupportUnstableFeature.mockResolvedValue(false);
|
||||||
|
|
||||||
|
getComponent();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(screen.getByText("Your homeserver does not support cross-signing.")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when cross signing is ready", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockClient.isCrossSigningReady.mockResolvedValue(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render when keys are not backed up", async () => {
|
||||||
|
getComponent();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(screen.getByTestId("summarised-status").innerHTML).toEqual(
|
||||||
|
"⚠️ Cross-signing is ready but keys are not backed up.",
|
||||||
|
);
|
||||||
|
expect(screen.getByText("Cross-signing private keys:").parentElement!).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render when keys are backed up", async () => {
|
||||||
|
mocked(mockClient.crypto!.crossSigningInfo).isStoredInSecretStorage.mockResolvedValue({ test: {} });
|
||||||
|
getComponent();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(screen.getByTestId("summarised-status").innerHTML).toEqual("✅ Cross-signing is ready for use.");
|
||||||
|
expect(screen.getByText("Cross-signing private keys:").parentElement!).toMatchSnapshot();
|
||||||
|
expect(mockClient.crypto!.crossSigningInfo.isStoredInSecretStorage).toHaveBeenCalledWith(
|
||||||
|
mockClient.crypto!.secretStorage,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when cross signing is not ready", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockClient.isCrossSigningReady.mockResolvedValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render when keys are not backed up", async () => {
|
||||||
|
getComponent();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(screen.getByTestId("summarised-status").innerHTML).toEqual("Cross-signing is not set up.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render when keys are backed up", async () => {
|
||||||
|
mocked(mockClient.crypto!.crossSigningInfo).isStoredInSecretStorage.mockResolvedValue({ test: {} });
|
||||||
|
getComponent();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(screen.getByTestId("summarised-status").innerHTML).toEqual(
|
||||||
|
"Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.",
|
||||||
|
);
|
||||||
|
expect(screen.getByText("Cross-signing private keys:").parentElement!).toMatchSnapshot();
|
||||||
|
expect(mockClient.crypto!.crossSigningInfo.isStoredInSecretStorage).toHaveBeenCalledWith(
|
||||||
|
mockClient.crypto!.secretStorage,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
201
test/components/views/settings/EventIndexPanel-test.tsx
Normal file
201
test/components/views/settings/EventIndexPanel-test.tsx
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
/*
|
||||||
|
Copyright 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.
|
||||||
|
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 { fireEvent, render, screen, within } from "@testing-library/react";
|
||||||
|
import { defer, IDeferred } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
|
import EventIndexPanel from "../../../../src/components/views/settings/EventIndexPanel";
|
||||||
|
import EventIndexPeg from "../../../../src/indexing/EventIndexPeg";
|
||||||
|
import EventIndex from "../../../../src/indexing/EventIndex";
|
||||||
|
import { clearAllModals, flushPromises, getMockClientWithEventEmitter } from "../../../test-utils";
|
||||||
|
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||||
|
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
||||||
|
|
||||||
|
describe("<EventIndexPanel />", () => {
|
||||||
|
getMockClientWithEventEmitter({
|
||||||
|
getRooms: jest.fn().mockReturnValue([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getComponent = () => render(<EventIndexPanel />);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(EventIndexPeg, "get").mockRestore();
|
||||||
|
jest.spyOn(EventIndexPeg, "platformHasSupport").mockReturnValue(false);
|
||||||
|
jest.spyOn(EventIndexPeg, "supportIsInstalled").mockReturnValue(false);
|
||||||
|
jest.spyOn(EventIndexPeg, "initEventIndex").mockClear().mockResolvedValue(true);
|
||||||
|
jest.spyOn(EventIndexPeg, "deleteEventIndex").mockClear();
|
||||||
|
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(false);
|
||||||
|
jest.spyOn(SettingsStore, "setValue").mockClear();
|
||||||
|
|
||||||
|
// @ts-ignore private property
|
||||||
|
EventIndexPeg.error = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await clearAllModals();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when event index is initialised", () => {
|
||||||
|
it("renders event index information", () => {
|
||||||
|
jest.spyOn(EventIndexPeg, "get").mockReturnValue(new EventIndex());
|
||||||
|
|
||||||
|
const { container } = getComponent();
|
||||||
|
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens event index management dialog", async () => {
|
||||||
|
jest.spyOn(EventIndexPeg, "get").mockReturnValue(new EventIndex());
|
||||||
|
getComponent();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Manage"));
|
||||||
|
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
expect(within(dialog).getByText("Message search")).toBeInTheDocument();
|
||||||
|
|
||||||
|
// close the modal
|
||||||
|
fireEvent.click(within(dialog).getByText("Done"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when event indexing is fully supported and enabled but not initialised", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(EventIndexPeg, "supportIsInstalled").mockReturnValue(true);
|
||||||
|
jest.spyOn(EventIndexPeg, "platformHasSupport").mockReturnValue(true);
|
||||||
|
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(true);
|
||||||
|
|
||||||
|
// @ts-ignore private property
|
||||||
|
EventIndexPeg.error = { message: "Test error message" };
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays an error when no event index is found and enabling not in progress", () => {
|
||||||
|
getComponent();
|
||||||
|
|
||||||
|
expect(screen.getByText("Message search initialisation failed")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays an error from the event index", () => {
|
||||||
|
getComponent();
|
||||||
|
|
||||||
|
expect(screen.getByText("Test error message")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("asks for confirmation when resetting seshat", async () => {
|
||||||
|
getComponent();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Reset"));
|
||||||
|
|
||||||
|
// wait for reset modal to open
|
||||||
|
await screen.findByText("Reset event store?");
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
|
||||||
|
expect(within(dialog).getByText("Reset event store?")).toBeInTheDocument();
|
||||||
|
fireEvent.click(within(dialog).getByText("Cancel"));
|
||||||
|
|
||||||
|
// didn't reset
|
||||||
|
expect(SettingsStore.setValue).not.toHaveBeenCalled();
|
||||||
|
expect(EventIndexPeg.deleteEventIndex).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resets seshat", async () => {
|
||||||
|
getComponent();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Reset"));
|
||||||
|
|
||||||
|
// wait for reset modal to open
|
||||||
|
await screen.findByText("Reset event store?");
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
|
||||||
|
fireEvent.click(within(dialog).getByText("Reset event store"));
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(SettingsStore.setValue).toHaveBeenCalledWith(
|
||||||
|
"enableEventIndexing",
|
||||||
|
null,
|
||||||
|
SettingLevel.DEVICE,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(EventIndexPeg.deleteEventIndex).toHaveBeenCalled();
|
||||||
|
|
||||||
|
await clearAllModals();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when event indexing is supported but not enabled", () => {
|
||||||
|
it("renders enable text", () => {
|
||||||
|
jest.spyOn(EventIndexPeg, "supportIsInstalled").mockReturnValue(true);
|
||||||
|
|
||||||
|
getComponent();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText("Securely cache encrypted messages locally for them to appear in search results."),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it("enables event indexing on enable button click", async () => {
|
||||||
|
jest.spyOn(EventIndexPeg, "supportIsInstalled").mockReturnValue(true);
|
||||||
|
let deferredInitEventIndex: IDeferred<boolean> | undefined;
|
||||||
|
jest.spyOn(EventIndexPeg, "initEventIndex").mockImplementation(() => {
|
||||||
|
deferredInitEventIndex = defer<boolean>();
|
||||||
|
return deferredInitEventIndex.promise;
|
||||||
|
});
|
||||||
|
|
||||||
|
getComponent();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Enable"));
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
// spinner shown while enabling
|
||||||
|
expect(screen.getByLabelText("Loading…")).toBeInTheDocument();
|
||||||
|
|
||||||
|
// add an event indx to the peg and resolve the init promise
|
||||||
|
jest.spyOn(EventIndexPeg, "get").mockReturnValue(new EventIndex());
|
||||||
|
expect(EventIndexPeg.initEventIndex).toHaveBeenCalled();
|
||||||
|
deferredInitEventIndex!.resolve(true);
|
||||||
|
await flushPromises();
|
||||||
|
expect(SettingsStore.setValue).toHaveBeenCalledWith("enableEventIndexing", null, SettingLevel.DEVICE, true);
|
||||||
|
|
||||||
|
// message for enabled event index
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
"Securely cache encrypted messages locally for them to appear in search results, using 0 Bytes to store messages from 0 rooms.",
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when event indexing is supported but not installed", () => {
|
||||||
|
it("renders link to install seshat", () => {
|
||||||
|
jest.spyOn(EventIndexPeg, "supportIsInstalled").mockReturnValue(false);
|
||||||
|
jest.spyOn(EventIndexPeg, "platformHasSupport").mockReturnValue(true);
|
||||||
|
|
||||||
|
const { container } = getComponent();
|
||||||
|
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when event indexing is not supported", () => {
|
||||||
|
it("renders link to download a desktop client", () => {
|
||||||
|
jest.spyOn(EventIndexPeg, "platformHasSupport").mockReturnValue(false);
|
||||||
|
|
||||||
|
const { container } = getComponent();
|
||||||
|
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,40 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<CrossSigningPanel /> when cross signing is not ready should render when keys are backed up 1`] = `
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="row"
|
||||||
|
>
|
||||||
|
Cross-signing private keys:
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
in secret storage
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<CrossSigningPanel /> when cross signing is ready should render when keys are backed up 1`] = `
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="row"
|
||||||
|
>
|
||||||
|
Cross-signing private keys:
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
in secret storage
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<CrossSigningPanel /> when cross signing is ready should render when keys are not backed up 1`] = `
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="row"
|
||||||
|
>
|
||||||
|
Cross-signing private keys:
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
not found in storage
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
|
@ -0,0 +1,66 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<EventIndexPanel /> when event index is initialised renders event index information 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsSubsection_text"
|
||||||
|
>
|
||||||
|
Securely cache encrypted messages locally for them to appear in search results, using 0 Bytes to store messages from 0 rooms.
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Manage
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<EventIndexPanel /> when event indexing is not supported renders link to download a desktop client 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsSubsection_text"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Element can't securely cache encrypted messages locally while running in a web browser. Use
|
||||||
|
<a
|
||||||
|
class="mx_ExternalLink"
|
||||||
|
href="https://element.io/get-started"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Element Desktop
|
||||||
|
<i
|
||||||
|
class="mx_ExternalLink_icon"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
for encrypted messages to appear in search results.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<EventIndexPanel /> when event indexing is supported but not installed renders link to install seshat 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsSubsection_text"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Element is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom Element Desktop with
|
||||||
|
<a
|
||||||
|
class="mx_ExternalLink"
|
||||||
|
href="https://github.com/vector-im/element-desktop/blob/develop/docs/native-node-modules.md#adding-seshat-for-search-in-e2e-encrypted-rooms"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
search components added
|
||||||
|
<i
|
||||||
|
class="mx_ExternalLink_icon"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -2,114 +2,118 @@
|
||||||
|
|
||||||
exports[`<SecureBackupPanel /> suggests connecting session to key backup when backup exists 1`] = `
|
exports[`<SecureBackupPanel /> suggests connecting session to key backup when backup exists 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div
|
||||||
<p>
|
class="mx_SettingsSubsection_text"
|
||||||
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>
|
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>
|
</div>
|
||||||
<span>
|
<div
|
||||||
This session is
|
class="mx_SettingsSubsection_text"
|
||||||
<b>
|
>
|
||||||
not backing up your keys
|
<span>
|
||||||
</b>
|
This session is
|
||||||
, but you do have an existing backup you can restore from and add to going forward.
|
<b>
|
||||||
</span>
|
not backing up your keys
|
||||||
</p>
|
</b>
|
||||||
<p>
|
, but you do have an existing backup you can restore from and add to going forward.
|
||||||
Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.
|
</span>
|
||||||
</p>
|
</div>
|
||||||
<details>
|
<div
|
||||||
<summary>
|
class="mx_SettingsSubsection_text"
|
||||||
Advanced
|
>
|
||||||
</summary>
|
Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.
|
||||||
<table
|
</div>
|
||||||
class="mx_SecureBackupPanel_statusList"
|
<details>
|
||||||
>
|
<summary>
|
||||||
<tr>
|
Advanced
|
||||||
<th
|
</summary>
|
||||||
scope="row"
|
<table
|
||||||
>
|
class="mx_SecureBackupPanel_statusList"
|
||||||
Backup key stored:
|
|
||||||
</th>
|
|
||||||
<td>
|
|
||||||
not stored
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th
|
|
||||||
scope="row"
|
|
||||||
>
|
|
||||||
Backup key cached:
|
|
||||||
</th>
|
|
||||||
<td>
|
|
||||||
not found locally
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th
|
|
||||||
scope="row"
|
|
||||||
>
|
|
||||||
Secret storage public key:
|
|
||||||
</th>
|
|
||||||
<td>
|
|
||||||
not found
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th
|
|
||||||
scope="row"
|
|
||||||
>
|
|
||||||
Secret storage:
|
|
||||||
</th>
|
|
||||||
<td>
|
|
||||||
not ready
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th
|
|
||||||
scope="row"
|
|
||||||
>
|
|
||||||
Backup version:
|
|
||||||
</th>
|
|
||||||
<td>
|
|
||||||
1
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th
|
|
||||||
scope="row"
|
|
||||||
>
|
|
||||||
Algorithm:
|
|
||||||
</th>
|
|
||||||
<td>
|
|
||||||
test
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
Backup is not signed by any of your sessions
|
|
||||||
</div>
|
|
||||||
<div />
|
|
||||||
</details>
|
|
||||||
<div
|
|
||||||
class="mx_SecureBackupPanel_buttonRow"
|
|
||||||
>
|
>
|
||||||
<div
|
<tr>
|
||||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
<th
|
||||||
role="button"
|
scope="row"
|
||||||
tabindex="0"
|
>
|
||||||
>
|
Backup key stored:
|
||||||
Connect this session to Key Backup
|
</th>
|
||||||
</div>
|
<td>
|
||||||
<div
|
not stored
|
||||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger"
|
</td>
|
||||||
role="button"
|
</tr>
|
||||||
tabindex="0"
|
<tr>
|
||||||
>
|
<th
|
||||||
Delete Backup
|
scope="row"
|
||||||
</div>
|
>
|
||||||
|
Backup key cached:
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
not found locally
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="row"
|
||||||
|
>
|
||||||
|
Secret storage public key:
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
not found
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="row"
|
||||||
|
>
|
||||||
|
Secret storage:
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
not ready
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="row"
|
||||||
|
>
|
||||||
|
Backup version:
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
1
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="row"
|
||||||
|
>
|
||||||
|
Algorithm:
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
test
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
Backup is not signed by any of your sessions
|
||||||
|
</div>
|
||||||
|
<div />
|
||||||
|
</details>
|
||||||
|
<div
|
||||||
|
class="mx_SecureBackupPanel_buttonRow"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Connect this session to Key Backup
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Delete Backup
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue