diff --git a/res/css/_common.pcss b/res/css/_common.pcss index 74328af39b2..9964ec8e508 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -596,7 +596,9 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button), + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):not( + .mx_EncryptionUserSettingsTab button + ), .mx_Dialog input[type="submit"], .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton), .mx_Dialog_buttons input[type="submit"] { @@ -616,8 +618,8 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not( - .mx_ShareDialog button + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):not( + .mx_EncryptionUserSettingsTab button ):last-child { margin-right: 0px; } @@ -625,7 +627,9 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):focus, + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):not( + .mx_EncryptionUserSettingsTab button + ):focus, .mx_Dialog input[type="submit"]:focus, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus, .mx_Dialog_buttons input[type="submit"]:focus { @@ -637,7 +641,9 @@ legend { .mx_Dialog_buttons button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button), + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):not( + .mx_EncryptionUserSettingsTab button + ), .mx_Dialog_buttons input[type="submit"].mx_Dialog_primary { color: var(--cpd-color-text-on-solid-primary); background-color: var(--cpd-color-bg-action-primary-rest); @@ -650,7 +656,7 @@ legend { .mx_Dialog_buttons button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button):not( .mx_ThemeChoicePanel_CustomTheme button - ):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button), + ):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):not(.mx_EncryptionUserSettingsTab button), .mx_Dialog_buttons input[type="submit"].danger { background-color: var(--cpd-color-bg-critical-primary); border: solid 1px var(--cpd-color-bg-critical-primary); @@ -666,7 +672,9 @@ legend { .mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( .mx_UserProfileSettings button - ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):disabled, + ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):not( + .mx_EncryptionUserSettingsTab button + ):disabled, .mx_Dialog input[type="submit"]:disabled, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled, .mx_Dialog_buttons input[type="submit"]:disabled { diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 7426f407990..b424c1e3c1a 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -351,6 +351,9 @@ @import "./views/settings/_ThemeChoicePanel.pcss"; @import "./views/settings/_UpdateCheckButton.pcss"; @import "./views/settings/_UserProfileSettings.pcss"; +@import "./views/settings/encryption/_ChangeRecoveryKey.pcss"; +@import "./views/settings/encryption/_RecoveryPanel.pcss"; +@import "./views/settings/encryption/_EncryptionCard.pcss"; @import "./views/settings/tabs/_SettingsBanner.pcss"; @import "./views/settings/tabs/_SettingsIndent.pcss"; @import "./views/settings/tabs/_SettingsSection.pcss"; diff --git a/res/css/views/settings/encryption/_ChangeRecoveryKey.pcss b/res/css/views/settings/encryption/_ChangeRecoveryKey.pcss new file mode 100644 index 00000000000..2d8a8299f6c --- /dev/null +++ b/res/css/views/settings/encryption/_ChangeRecoveryKey.pcss @@ -0,0 +1,63 @@ +/* + * 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_ChangeRecoveryKey_header { + display: flex; + flex-direction: column; + gap: var(--cpd-space-4x); + align-items: center; + + > h2 { + margin: 0; + } + + > span { + color: var(--cpd-color-text-secondary); + text-align: center; + } +} + +.mx_ChangeRecoveryKey_content { + display: grid; + grid-template: + "header button" auto + "content button" auto / 1fr 36px; + column-gap: var(--cpd-space-3x); + row-gap: var(--cpd-space-1x); + align-items: center; + + > span { + grid-area: header; + } + + > div { + grid-area: content; + display: flex; + flex-direction: column; + gap: var(--cpd-space-2x); + color: var(--cpd-color-text-secondary); + + > code { + background-color: var(--cpd-color-bg-subtle-secondary); + border-radius: var(--cpd-space-2x); + padding: var(--cpd-space-3x) var(--cpd-space-4x); + } + } + + > button { + margin: 0 var(--cpd-space-1x); + color: var(--cpd-color-bg-subtle-secondary); + grid-area: button; + } +} + +.mx_ChangeRecoveryKey_action { + display: flex; + flex-direction: column; + gap: var(--cpd-space-4x); + justify-content: center; +} diff --git a/res/css/views/settings/encryption/_EncryptionCard.pcss b/res/css/views/settings/encryption/_EncryptionCard.pcss new file mode 100644 index 00000000000..d017f74e688 --- /dev/null +++ b/res/css/views/settings/encryption/_EncryptionCard.pcss @@ -0,0 +1,17 @@ +/* + * 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_EncryptionCard { + display: flex; + flex-direction: column; + gap: var(--cpd-space-8x); + padding: var(--cpd-space-10x); + border-radius: var(--cpd-space-4x); + /* From figma */ + box-shadow: 0 1.2px 2.4px 0 rgba(27, 29, 34, 0.15); + border: 1px solid var(--cpd-color-gray-400); +} diff --git a/res/css/views/settings/encryption/_RecoveryPanel.pcss b/res/css/views/settings/encryption/_RecoveryPanel.pcss new file mode 100644 index 00000000000..0ecc51187d4 --- /dev/null +++ b/res/css/views/settings/encryption/_RecoveryPanel.pcss @@ -0,0 +1,22 @@ +/* + * 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_RecoveryPanel { + .mx_RecoveryPanel_Subheader { + display: flex; + flex-direction: column; + gap: var(--cpd-space-2x); + + > span { + display: flex; + align-items: center; + gap: var(--cpd-space-2x); + font: var(--cpd-font-body-sm-medium); + color: var(--cpd-color-text-success-primary); + } + } +} diff --git a/src/components/views/settings/encryption/ChangeRecoveryKey.tsx b/src/components/views/settings/encryption/ChangeRecoveryKey.tsx new file mode 100644 index 00000000000..afe6f415006 --- /dev/null +++ b/src/components/views/settings/encryption/ChangeRecoveryKey.tsx @@ -0,0 +1,60 @@ +/* + * 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 } from "react"; +import { Breadcrumb, Heading, BigIcon, IconButton, Text, Button } from "@vector-im/compound-web"; +import KeyIcon from "@vector-im/compound-design-tokens/assets/web/icons/key-solid"; +import CopyIcon from "@vector-im/compound-design-tokens/assets/web/icons/copy"; + +import { _t } from "../../../../languageHandler.tsx"; +import { EncryptionCard } from "./EncryptionCard.tsx"; + +interface ChangeRecoveryKeyProps { + onBackClick: () => void; +} + +export function ChangeRecoveryKey({ onBackClick }: ChangeRecoveryKeyProps): JSX.Element { + return ( + <> + + +
+ + + + + {_t("settings|encryption|recovery|change_recovery_key_title")} + + {_t("settings|encryption|recovery|change_recovery_key_description")} +
+
+ + {_t("settings|encryption|recovery|change_recovery_key_content_title")} + +
+ Text text text text text text text text text text text text text text text + + {_t("settings|encryption|recovery|change_recovery_key_content_description")} + +
+ + + +
+
+ + +
+
+ + ); +} diff --git a/src/components/views/settings/encryption/EncryptionCard.tsx b/src/components/views/settings/encryption/EncryptionCard.tsx new file mode 100644 index 00000000000..819cfbc7ae9 --- /dev/null +++ b/src/components/views/settings/encryption/EncryptionCard.tsx @@ -0,0 +1,15 @@ +/* + * 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, PropsWithChildren } from "react"; + +/** + * A styled card for encryption settings. + */ +export function EncryptionCard({ children }: PropsWithChildren): JSX.Element { + return
{children}
; +} diff --git a/src/components/views/settings/encryption/RecoveryPanel.tsx b/src/components/views/settings/encryption/RecoveryPanel.tsx new file mode 100644 index 00000000000..652220ea4a3 --- /dev/null +++ b/src/components/views/settings/encryption/RecoveryPanel.tsx @@ -0,0 +1,89 @@ +/* + * 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, MouseEventHandler, useEffect, useState } from "react"; +import { Button, InlineSpinner } from "@vector-im/compound-web"; +import KeyIcon from "@vector-im/compound-design-tokens/assets/web/icons/key"; +import CheckCircleIcon from "@vector-im/compound-design-tokens/assets/web/icons/check-circle-solid"; + +import { SettingsSection } from "../shared/SettingsSection"; +import { _t } from "../../../../languageHandler"; +import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; +import { SettingsHeader } from "../SettingsHeader"; + +interface RecoveryPanelProps { + onChangingRecoveryKeyClick: MouseEventHandler; +} + +/** + * This component allows the user to set up or change their recovery key. + */ +export function RecoveryPanel({ onChangingRecoveryKeyClick }: RecoveryPanelProps): JSX.Element { + const [hasRecoveryKey, setHasRecoveryKey] = useState(null); + const isLoading = hasRecoveryKey === null; + const matrixClient = useMatrixClientContext(); + + useEffect(() => { + const getRecoveryKey = async (): Promise => + setHasRecoveryKey(Boolean(await matrixClient.getCrypto()?.getKeyBackupInfo())); + getRecoveryKey(); + }, [matrixClient]); + + return ( + + } + subHeading={} + className="mx_RecoveryPanel" + > + {isLoading && } + {!isLoading && ( + <> + {hasRecoveryKey ? ( + + ) : ( + + )} + + )} + + ); +} + +/** + * The subheader for the recovery panel. + */ +interface SubheaderProps { + /** + * Whether the user has a recovery key. + * If null, the recovery key is still fetching. + */ + hasRecoveryKey: boolean | null; +} + +function Subheader({ hasRecoveryKey }: SubheaderProps): JSX.Element { + if (!hasRecoveryKey) return <>{_t("settings|encryption|recovery|description")}; + + return ( +
+ {_t("settings|encryption|recovery|description")} + + + {_t("settings|encryption|recovery|key_active")} + +
+ ); +} diff --git a/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx b/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx index 7964f2641ae..dc9bb293126 100644 --- a/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @@ -5,10 +5,23 @@ * Please see LICENSE files in the repository root for full details. */ -import React, { JSX } from "react"; +import React, { JSX, useState } from "react"; import SettingsTab from "../SettingsTab"; +import { RecoveryPanel } from "../../encryption/RecoveryPanel"; +import { ChangeRecoveryKey } from "../../encryption/ChangeRecoveryKey.tsx"; + +type Panel = "main" | "change_recovery_key" | "set_recovery_key"; export function EncryptionUserSettingsTab(): JSX.Element { - return ; + const [panel, setPanel] = useState("main"); + const displayChangeRecoveryKey = panel === "change_recovery_key"; + const displayMain = panel === "main"; + + return ( + + {displayChangeRecoveryKey && setPanel("main")} />} + {displayMain && setPanel("change_recovery_key")} />} + + ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ffdbddb6df8..46a9c338eba 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2464,6 +2464,17 @@ "enable_markdown_description": "Start messages with /plain to send without markdown.", "encryption": { "dialog_title": "Settings: Encryption", + "recovery": { + "change_recovery_key": "Change recovery key", + "change_recovery_key_content_description": "Do not share this with anyone!", + "change_recovery_key_content_title": "Recovery key", + "change_recovery_key_description": "Get a new recovery key if you've lost your existing one. After changing your recovery key, your old one will no longer work.", + "change_recovery_key_title": "Change recovery key?", + "description": "Recover your cryptographic identity and message history with a recovery key if you’ve lost all your existing devices.", + "key_active": "Recovery key active", + "set_up_recovery": "Set up recovery", + "title": "Recovery" + }, "title": "Encryption" }, "general": {