mirror of https://github.com/vector-im/riot-web
Add advanced section
parent
8f967dbfcf
commit
a8f88e708a
|
@ -352,8 +352,10 @@
|
||||||
@import "./views/settings/_ThemeChoicePanel.pcss";
|
@import "./views/settings/_ThemeChoicePanel.pcss";
|
||||||
@import "./views/settings/_UpdateCheckButton.pcss";
|
@import "./views/settings/_UpdateCheckButton.pcss";
|
||||||
@import "./views/settings/_UserProfileSettings.pcss";
|
@import "./views/settings/_UserProfileSettings.pcss";
|
||||||
|
@import "./views/settings/encryption/_AdvancedPanel.pcss";
|
||||||
@import "./views/settings/encryption/_ChangeRecoveryKey.pcss";
|
@import "./views/settings/encryption/_ChangeRecoveryKey.pcss";
|
||||||
@import "./views/settings/encryption/_EncryptionCard.pcss";
|
@import "./views/settings/encryption/_EncryptionCard.pcss";
|
||||||
|
@import "./views/settings/encryption/_ResetIdentityPanel.pcss";
|
||||||
@import "./views/settings/tabs/_SettingsBanner.pcss";
|
@import "./views/settings/tabs/_SettingsBanner.pcss";
|
||||||
@import "./views/settings/tabs/_SettingsIndent.pcss";
|
@import "./views/settings/tabs/_SettingsIndent.pcss";
|
||||||
@import "./views/settings/tabs/_SettingsSection.pcss";
|
@import "./views/settings/tabs/_SettingsSection.pcss";
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_AdvancedPanel_Details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--cpd-space-6x);
|
||||||
|
width: 100%;
|
||||||
|
align-items: start;
|
||||||
|
|
||||||
|
.mx_AdvancedPanel_Details_content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--cpd-space-4x);
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
font: var(--cpd-font-body-lg-semibold);
|
||||||
|
padding-bottom: var(--cpd-space-2x);
|
||||||
|
border-bottom: 1px solid var(--cpd-color-gray-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
width: 50%;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> div:nth-child(odd) {
|
||||||
|
background-color: var(--cpd-color-gray-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AdvancedPanel_buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--cpd-space-4x);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_ResetIdentityPanel {
|
||||||
|
.mx_ResetIdentityPanel_content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--cpd-space-3x);
|
||||||
|
|
||||||
|
> ul {
|
||||||
|
margin: 0;
|
||||||
|
list-style-type: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--cpd-space-1x);
|
||||||
|
|
||||||
|
> li {
|
||||||
|
padding: var(--cpd-space-2x) var(--cpd-space-3x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
|
font: var(--cpd-font-body-md-medium);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ResetIdentityPanel_footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--cpd-space-4x);
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { JSX, lazy, MouseEventHandler } from "react";
|
||||||
|
import { Button, InlineSpinner } from "@vector-im/compound-web";
|
||||||
|
import DownloadIcon from "@vector-im/compound-design-tokens/assets/web/icons/download";
|
||||||
|
import ShareIcon from "@vector-im/compound-design-tokens/assets/web/icons/share";
|
||||||
|
|
||||||
|
import { _t } from "../../../../languageHandler";
|
||||||
|
import { SettingsSection } from "../shared/SettingsSection";
|
||||||
|
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
|
||||||
|
import { useAsyncMemo } from "../../../../hooks/useAsyncMemo";
|
||||||
|
import Modal from "../../../../Modal";
|
||||||
|
|
||||||
|
interface AdvancedPanelProps {
|
||||||
|
/**
|
||||||
|
* Callback for when the user clicks the button to reset their identity.
|
||||||
|
*/
|
||||||
|
onResetIdentityClick: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The advanced panel of the encryption settings.
|
||||||
|
*/
|
||||||
|
export function AdvancedPanel({ onResetIdentityClick }: AdvancedPanelProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<SettingsSection heading={_t("settings|encryption|advanced|title")} legacy={false}>
|
||||||
|
<EncryptionDetails onResetIdentityClick={onResetIdentityClick} />
|
||||||
|
</SettingsSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EncryptionDetails {
|
||||||
|
/**
|
||||||
|
* Callback for when the user clicks the button to reset their identity.
|
||||||
|
*/
|
||||||
|
onResetIdentityClick: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The encryption details section of the advanced panel.
|
||||||
|
*/
|
||||||
|
function EncryptionDetails({ onResetIdentityClick }: EncryptionDetails): JSX.Element {
|
||||||
|
const matrixClient = useMatrixClientContext();
|
||||||
|
// Null when the keys are not loaded yet
|
||||||
|
const keys = useAsyncMemo(
|
||||||
|
() => {
|
||||||
|
const crypto = matrixClient.getCrypto();
|
||||||
|
return crypto ? crypto.getOwnDeviceKeys() : Promise.resolve(null);
|
||||||
|
},
|
||||||
|
[matrixClient],
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_AdvancedPanel_Details">
|
||||||
|
<div className="mx_AdvancedPanel_Details_content">
|
||||||
|
<span>{_t("settings|encryption|advanced|details_title")}</span>
|
||||||
|
<div>
|
||||||
|
<span>{_t("settings|encryption|advanced|session_id")}</span>
|
||||||
|
<span>{matrixClient.deviceId}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>{_t("settings|encryption|advanced|session_key")}</span>
|
||||||
|
<span>{keys ? keys.ed25519 : <InlineSpinner />}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mx_AdvancedPanel_buttons">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
kind="secondary"
|
||||||
|
Icon={ShareIcon}
|
||||||
|
onClick={() =>
|
||||||
|
Modal.createDialog(
|
||||||
|
lazy(
|
||||||
|
() => import("../../../../async-components/views/dialogs/security/ExportE2eKeysDialog"),
|
||||||
|
),
|
||||||
|
{ matrixClient },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{_t("settings|encryption|advanced|export_keys")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
kind="secondary"
|
||||||
|
Icon={DownloadIcon}
|
||||||
|
onClick={() =>
|
||||||
|
Modal.createDialog(
|
||||||
|
lazy(
|
||||||
|
() => import("../../../../async-components/views/dialogs/security/ImportE2eKeysDialog"),
|
||||||
|
),
|
||||||
|
{ matrixClient },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{_t("settings|encryption|advanced|import_keys")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button size="sm" kind="tertiary" destructive={true} onClick={onResetIdentityClick}>
|
||||||
|
{_t("settings|encryption|advanced|reset_identity")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Breadcrumb, Button, VisualList, VisualListItem } from "@vector-im/compound-web";
|
||||||
|
import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check";
|
||||||
|
import InfoIcon from "@vector-im/compound-design-tokens/assets/web/icons/info";
|
||||||
|
import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error";
|
||||||
|
import React, { MouseEventHandler } from "react";
|
||||||
|
import { AuthDict, MatrixClient, UIAResponse } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { _t } from "../../../../languageHandler";
|
||||||
|
import { EncryptionCard } from "./EncryptionCard";
|
||||||
|
import { withSecretStorageKeyCache } from "../../../../SecurityManager";
|
||||||
|
import Modal from "../../../../Modal";
|
||||||
|
import InteractiveAuthDialog from "../../dialogs/InteractiveAuthDialog";
|
||||||
|
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
|
||||||
|
import { SSOAuthEntry } from "../../auth/InteractiveAuthEntryComponents";
|
||||||
|
|
||||||
|
interface ResetIdentityPanelProps {
|
||||||
|
/**
|
||||||
|
* Called when the identity is reset.
|
||||||
|
*/
|
||||||
|
onFinish: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
/**
|
||||||
|
* Called when the cancel button is clicked or when we go back in the breadcrumbs.
|
||||||
|
*/
|
||||||
|
onCancelClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The panel for resetting the identity of the current user.
|
||||||
|
*/
|
||||||
|
export function ResetIdentityPanel({ onCancelClick, onFinish }: ResetIdentityPanelProps): JSX.Element {
|
||||||
|
const matrixClient = useMatrixClientContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Breadcrumb
|
||||||
|
backLabel={_t("action|back")}
|
||||||
|
onBackClick={onCancelClick}
|
||||||
|
pages={[_t("settings|encryption|title"), _t("settings|encryption|advanced|breadcrumb_page")]}
|
||||||
|
onPageClick={onCancelClick}
|
||||||
|
/>
|
||||||
|
<EncryptionCard
|
||||||
|
Icon={ErrorIcon}
|
||||||
|
destructive={true}
|
||||||
|
title={_t("settings|encryption|advanced|breadcrumb_title")}
|
||||||
|
className="mx_ResetIdentityPanel"
|
||||||
|
>
|
||||||
|
<div className="mx_ResetIdentityPanel_content">
|
||||||
|
<VisualList>
|
||||||
|
<VisualListItem Icon={CheckIcon} success={true}>
|
||||||
|
{_t("settings|encryption|advanced|breadcrumb_first_description")}
|
||||||
|
</VisualListItem>
|
||||||
|
<VisualListItem Icon={InfoIcon}>
|
||||||
|
{_t("settings|encryption|advanced|breadcrumb_second_description")}
|
||||||
|
</VisualListItem>
|
||||||
|
<VisualListItem Icon={InfoIcon}>
|
||||||
|
{_t("settings|encryption|advanced|breadcrumb_third_description")}
|
||||||
|
</VisualListItem>
|
||||||
|
</VisualList>
|
||||||
|
<span>{_t("settings|encryption|advanced|breadcrumb_warning")}</span>
|
||||||
|
</div>
|
||||||
|
<div className="mx_ResetIdentityPanel_footer">
|
||||||
|
<Button
|
||||||
|
destructive={true}
|
||||||
|
onClick={async (evt) => {
|
||||||
|
await resetIdentity(matrixClient);
|
||||||
|
onFinish(evt);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{_t("action|continue")}
|
||||||
|
</Button>
|
||||||
|
<Button kind="tertiary" onClick={onCancelClick}>
|
||||||
|
{_t("action|cancel")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</EncryptionCard>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the identity of the current user.
|
||||||
|
*/
|
||||||
|
async function resetIdentity(matrixClient: MatrixClient): Promise<void> {
|
||||||
|
const crypto = matrixClient.getCrypto();
|
||||||
|
if (!crypto) return;
|
||||||
|
|
||||||
|
await withSecretStorageKeyCache(async () => {
|
||||||
|
await crypto.bootstrapCrossSigning({
|
||||||
|
authUploadDeviceSigningKeys: async (makeRequest): Promise<void> =>
|
||||||
|
uiAuthCallback(matrixClient, makeRequest),
|
||||||
|
setupNewCrossSigning: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await crypto.bootstrapSecretStorage({
|
||||||
|
setupNewKeyBackup: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the UIA flow for resetting the identity.
|
||||||
|
* @param matrixClient
|
||||||
|
* @param makeRequest
|
||||||
|
*/
|
||||||
|
async function uiAuthCallback(
|
||||||
|
matrixClient: MatrixClient,
|
||||||
|
makeRequest: (authData: AuthDict) => Promise<UIAResponse<void>>,
|
||||||
|
): Promise<void> {
|
||||||
|
const dialogAesthetics = {
|
||||||
|
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||||
|
title: _t("auth|uia|sso_title"),
|
||||||
|
body: _t("auth|uia|sso_preauth_body"),
|
||||||
|
continueText: _t("auth|sso"),
|
||||||
|
continueKind: "primary",
|
||||||
|
},
|
||||||
|
[SSOAuthEntry.PHASE_POSTAUTH]: {
|
||||||
|
title: _t("encryption|confirm_encryption_setup_title"),
|
||||||
|
body: _t("encryption|confirm_encryption_setup_body"),
|
||||||
|
continueText: _t("action|confirm"),
|
||||||
|
continueKind: "primary",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { finished } = Modal.createDialog(InteractiveAuthDialog, {
|
||||||
|
title: _t("encryption|bootstrap_title"),
|
||||||
|
matrixClient,
|
||||||
|
makeRequest,
|
||||||
|
aestheticsForStagePhases: {
|
||||||
|
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
|
||||||
|
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [confirmed] = await finished;
|
||||||
|
if (!confirmed) {
|
||||||
|
throw new Error("Cross-signing key upload auth canceled");
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { JSX, useCallback, useEffect, useState } from "react";
|
import React, { JSX, useCallback, useEffect, useState } from "react";
|
||||||
import { Button, InlineSpinner } from "@vector-im/compound-web";
|
import { Button, InlineSpinner, Separator } from "@vector-im/compound-web";
|
||||||
import ComputerIcon from "@vector-im/compound-design-tokens/assets/web/icons/computer";
|
import ComputerIcon from "@vector-im/compound-design-tokens/assets/web/icons/computer";
|
||||||
|
|
||||||
import SettingsTab from "../SettingsTab";
|
import SettingsTab from "../SettingsTab";
|
||||||
|
@ -18,6 +18,8 @@ import Modal from "../../../../../Modal";
|
||||||
import SetupEncryptionDialog from "../../../dialogs/security/SetupEncryptionDialog";
|
import SetupEncryptionDialog from "../../../dialogs/security/SetupEncryptionDialog";
|
||||||
import { SettingsSection } from "../../shared/SettingsSection";
|
import { SettingsSection } from "../../shared/SettingsSection";
|
||||||
import { SettingsSubheader } from "../../SettingsSubheader";
|
import { SettingsSubheader } from "../../SettingsSubheader";
|
||||||
|
import { AdvancedPanel } from "../../encryption/AdvancedPanel";
|
||||||
|
import { ResetIdentityPanel } from "../../encryption/ResetIdentityPanel";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The state in the encryption settings tab.
|
* The state in the encryption settings tab.
|
||||||
|
@ -29,8 +31,9 @@ import { SettingsSubheader } from "../../SettingsSubheader";
|
||||||
* This happens when the user has a key backup and the user clicks on "Change recovery key" button of the RecoveryPanel.
|
* This happens when the user has a key backup and the user clicks on "Change recovery key" button of the RecoveryPanel.
|
||||||
* - "set_recovery_key": The panel to show when the user is setting up their recovery key.
|
* - "set_recovery_key": The panel to show when the user is setting up their recovery key.
|
||||||
* This happens when the user doesn't have a key backup and the user clicks on "Set up recovery key" button of the RecoveryPanel.
|
* This happens when the user doesn't have a key backup and the user clicks on "Set up recovery key" button of the RecoveryPanel.
|
||||||
|
* - "reset_identity": The panel to show when the user is resetting their identity.
|
||||||
*/
|
*/
|
||||||
type State = "loading" | "main" | "set_up_encryption" | "change_recovery_key" | "set_recovery_key";
|
type State = "loading" | "main" | "set_up_encryption" | "change_recovery_key" | "set_recovery_key" | "reset_identity";
|
||||||
|
|
||||||
export function EncryptionUserSettingsTab(): JSX.Element {
|
export function EncryptionUserSettingsTab(): JSX.Element {
|
||||||
const [state, setState] = useState<State>("loading");
|
const [state, setState] = useState<State>("loading");
|
||||||
|
@ -46,11 +49,15 @@ export function EncryptionUserSettingsTab(): JSX.Element {
|
||||||
break;
|
break;
|
||||||
case "main":
|
case "main":
|
||||||
content = (
|
content = (
|
||||||
<RecoveryPanel
|
<>
|
||||||
onChangeRecoveryKeyClick={(setupNewKey) =>
|
<RecoveryPanel
|
||||||
setupNewKey ? setState("set_recovery_key") : setState("change_recovery_key")
|
onChangeRecoveryKeyClick={(setupNewKey) =>
|
||||||
}
|
setupNewKey ? setState("set_recovery_key") : setState("change_recovery_key")
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
|
<Separator kind="section" />
|
||||||
|
<AdvancedPanel onResetIdentityClick={() => setState("reset_identity")} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "change_recovery_key":
|
case "change_recovery_key":
|
||||||
|
@ -63,6 +70,9 @@ export function EncryptionUserSettingsTab(): JSX.Element {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case "reset_identity":
|
||||||
|
content = <ResetIdentityPanel onCancelClick={() => setState("main")} onFinish={() => setState("main")} />;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -2463,6 +2463,21 @@
|
||||||
"enable_markdown": "Enable Markdown",
|
"enable_markdown": "Enable Markdown",
|
||||||
"enable_markdown_description": "Start messages with <code>/plain</code> to send without markdown.",
|
"enable_markdown_description": "Start messages with <code>/plain</code> to send without markdown.",
|
||||||
"encryption": {
|
"encryption": {
|
||||||
|
"advanced": {
|
||||||
|
"breadcrumb_first_description": "Your account details, contacts, preferences, and chat list will be kept",
|
||||||
|
"breadcrumb_page": "Reset encryption",
|
||||||
|
"breadcrumb_second_description": "You will lose any message history that’s stored only on the server",
|
||||||
|
"breadcrumb_third_description": "You will need to verify all your existing devices and contacts again",
|
||||||
|
"breadcrumb_title": "Are you sure you want to reset your identity?",
|
||||||
|
"breadcrumb_warning": "Only do this if you believe your account has been compromised.",
|
||||||
|
"details_title": "Encryption details",
|
||||||
|
"export_keys": "Export keys",
|
||||||
|
"import_keys": "Import keys",
|
||||||
|
"reset_identity": "Reset cryptographic identity",
|
||||||
|
"session_id": "Session ID:",
|
||||||
|
"session_key": "Session key:",
|
||||||
|
"title": "Advanced"
|
||||||
|
},
|
||||||
"device_not_verified_button": "Verify this device",
|
"device_not_verified_button": "Verify this device",
|
||||||
"device_not_verified_description": "You need to verify this device in order to view your encryption settings.",
|
"device_not_verified_description": "You need to verify this device in order to view your encryption settings.",
|
||||||
"device_not_verified_title": "Device not verified",
|
"device_not_verified_title": "Device not verified",
|
||||||
|
|
Loading…
Reference in New Issue