Merge branch 'develop' into hs/media-previews-server-config

This commit is contained in:
Will Hunt
2025-04-08 15:52:04 +01:00
committed by GitHub
9 changed files with 308 additions and 33 deletions

View File

@@ -45,17 +45,16 @@ import { type NonEmptyArray } from "../../../@types/common";
import { SDKContext, type SdkContextClass } from "../../../contexts/SDKContext";
import { useSettingValue } from "../../../hooks/useSettings";
import { ToastContext, useActiveToast } from "../../../contexts/ToastContext";
import { EncryptionUserSettingsTab } from "../settings/tabs/user/EncryptionUserSettingsTab";
import { EncryptionUserSettingsTab, type State } from "../settings/tabs/user/EncryptionUserSettingsTab";
interface IProps {
initialTabId?: UserTab;
showMsc4108QrCode?: boolean;
/**
* If `true`, the flow for a user to reset their encryption will be shown. In this case, `initialTabId` must be `UserTab.Encryption`.
*
* If false or undefined, show the tab as normal.
/*
* The initial state of the Encryption tab.
* If undefined, the default state is used ("loading").
*/
showResetIdentity?: boolean;
initialEncryptionState?: State;
sdkContext: SdkContextClass;
onFinished(): void;
}
@@ -99,7 +98,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element {
const mjolnirEnabled = useSettingValue("feature_mjolnir");
// store these props in state as changing tabs back and forth should clear them
const [showMsc4108QrCode, setShowMsc4108QrCode] = useState(props.showMsc4108QrCode);
const [showResetIdentity, setShowResetIdentity] = useState(props.showResetIdentity);
const [initialEncryptionState, setInitialEncryptionState] = useState(props.initialEncryptionState);
const getTabs = (): NonEmptyArray<Tab<UserTab>> => {
const tabs: Tab<UserTab>[] = [];
@@ -195,7 +194,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element {
UserTab.Encryption,
_td("settings|encryption|title"),
<KeyIcon />,
<EncryptionUserSettingsTab initialState={showResetIdentity ? "reset_identity_forgot" : undefined} />,
<EncryptionUserSettingsTab initialState={initialEncryptionState} />,
"UserSettingsEncryption",
),
);
@@ -234,7 +233,7 @@ export default function UserSettingsDialog(props: IProps): JSX.Element {
_setActiveTabId(tabId);
// Clear these so switching away from the tab and back to it will not show the QR code again
setShowMsc4108QrCode(false);
setShowResetIdentity(false);
setInitialEncryptionState(undefined);
};
const [activeToast, toastRack] = useActiveToast();

View File

@@ -29,17 +29,23 @@ interface ResetIdentityPanelProps {
onCancelClick: () => void;
/**
* The variant of the panel to show. We show more warnings in the 'compromised' variant (no use in showing a user this
* warning if they have to reset because they no longer have their key)
*
* "compromised" is shown when the user chooses 'reset' explicitly in settings, usually because they believe their
* identity has been compromised.
*
* "forgot" is shown when the user has just forgotten their passphrase.
* The variant of the panel to show. We show more warnings in the 'compromised' variant (no use in showing a user
* this warning if they have to reset because they no longer have their key)
*/
variant: "compromised" | "forgot";
variant: ResetIdentityPanelVariant;
}
/**
* "compromised" is shown when the user chooses 'reset' explicitly in settings, usually because they believe their
* identity has been compromised.
*
* "sync_failed" is shown when the user tried to recover their identity but the process failed, probably because
* the required information is missing from recovery.
*
* "forgot" is shown when the user has just forgotten their passphrase.
*/
export type ResetIdentityPanelVariant = "compromised" | "forgot" | "sync_failed";
/**
* The panel for resetting the identity of the current user.
*/
@@ -58,15 +64,7 @@ export function ResetIdentityPanel({ onCancelClick, onFinish, variant }: ResetId
pages={[_t("settings|encryption|title"), _t("settings|encryption|advanced|breadcrumb_page")]}
onPageClick={onCancelClick}
/>
<EncryptionCard
Icon={ErrorIcon}
destructive={true}
title={
variant === "forgot"
? _t("settings|encryption|advanced|breadcrumb_title_forgot")
: _t("settings|encryption|advanced|breadcrumb_title")
}
>
<EncryptionCard Icon={ErrorIcon} destructive={true} title={titleForVariant(variant)}>
<EncryptionCardEmphasisedContent>
<VisualList>
<VisualListItem Icon={CheckIcon} success={true}>
@@ -117,3 +115,16 @@ export function ResetIdentityPanel({ onCancelClick, onFinish, variant }: ResetId
</>
);
}
function titleForVariant(variant: ResetIdentityPanelVariant): string {
switch (variant) {
case "compromised":
return _t("settings|encryption|advanced|breadcrumb_title");
case "sync_failed":
return _t("settings|encryption|advanced|breadcrumb_title_sync_failed");
default:
case "forgot":
return _t("settings|encryption|advanced|breadcrumb_title_forgot");
}
}

View File

@@ -20,7 +20,7 @@ import SetupEncryptionDialog from "../../../dialogs/security/SetupEncryptionDial
import { SettingsSection } from "../../shared/SettingsSection";
import { SettingsSubheader } from "../../SettingsSubheader";
import { AdvancedPanel } from "../../encryption/AdvancedPanel";
import { ResetIdentityPanel } from "../../encryption/ResetIdentityPanel";
import { ResetIdentityPanel, type ResetIdentityPanelVariant } from "../../encryption/ResetIdentityPanel";
import { RecoveryPanelOutOfSync } from "../../encryption/RecoveryPanelOutOfSync";
import { useTypedEventEmitter } from "../../../../../hooks/useEventEmitter";
import { KeyStoragePanel } from "../../encryption/KeyStoragePanel";
@@ -39,6 +39,7 @@ import { DeleteKeyStoragePanel } from "../../encryption/DeleteKeyStoragePanel";
* This happens when the user doesn't have a key a recovery key and the user clicks on "Set up recovery key" button of the RecoveryPanel.
* - "reset_identity_compromised": The panel to show when the user is resetting their identity, in the case where their key is compromised.
* - "reset_identity_forgot": The panel to show when the user is resetting their identity, in the case where they forgot their recovery key.
* - "reset_identity_sync_failed": The panel to show when the user us resetting their identity, in the case where recovery failed.
* - "secrets_not_cached": The secrets are not cached locally. This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets.
* If the "set_up_encryption" and "secrets_not_cached" conditions are both filled, "set_up_encryption" prevails.
* - "key_storage_delete": The confirmation page asking if the user really wants to turn off key storage.
@@ -52,6 +53,7 @@ export type State =
| "set_recovery_key"
| "reset_identity_compromised"
| "reset_identity_forgot"
| "reset_identity_sync_failed"
| "secrets_not_cached"
| "key_storage_delete";
@@ -120,9 +122,10 @@ export function EncryptionUserSettingsTab({ initialState = "loading" }: Props):
break;
case "reset_identity_compromised":
case "reset_identity_forgot":
case "reset_identity_sync_failed":
content = (
<ResetIdentityPanel
variant={state === "reset_identity_compromised" ? "compromised" : "forgot"}
variant={findResetVariant(state)}
onCancelClick={checkEncryptionState}
onFinish={checkEncryptionState}
/>
@@ -140,6 +143,23 @@ export function EncryptionUserSettingsTab({ initialState = "loading" }: Props):
);
}
/**
* Given what state we want the tab to be in, what variant of the
* ResetIdentityPanel do we need?
*/
function findResetVariant(state: State): ResetIdentityPanelVariant {
switch (state) {
case "reset_identity_compromised":
return "compromised";
case "reset_identity_sync_failed":
return "sync_failed";
default:
case "reset_identity_forgot":
return "forgot";
}
}
/**
* Hook to check if the user needs:
* - to go through the SetupEncryption flow.

View File

@@ -2529,6 +2529,7 @@
"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_title_forgot": "Forgot your recovery key? Youll need to reset your identity.",
"breadcrumb_title_sync_failed": "Failed to sync key storage. You need to reset your identity.",
"breadcrumb_warning": "Only do this if you believe your account has been compromised.",
"details_title": "Encryption details",
"do_not_close_warning": "Do not close this window until the reset is finished",

View File

@@ -14,7 +14,7 @@ import Modal from "../Modal";
import { _t } from "../languageHandler";
import DeviceListener from "../DeviceListener";
import SetupEncryptionDialog from "../components/views/dialogs/security/SetupEncryptionDialog";
import { accessSecretStorage } from "../SecurityManager";
import { AccessCancelledError, accessSecretStorage } from "../SecurityManager";
import ToastStore from "../stores/ToastStore";
import GenericToast from "../components/views/toasts/GenericToast";
import { ModuleRunner } from "../modules/ModuleRunner";
@@ -153,6 +153,8 @@ export const showToast = (kind: Kind): void => {
);
try {
await accessSecretStorage();
} catch (error) {
onAccessSecretStorageFailed(error as Error);
} finally {
modal.close();
}
@@ -165,7 +167,7 @@ export const showToast = (kind: Kind): void => {
const payload: OpenToTabPayload = {
action: Action.ViewUserSettings,
initialTabId: UserTab.Encryption,
props: { showResetIdentity: true },
props: { initialEncryptionState: "reset_identity_forgot" },
};
defaultDispatcher.dispatch(payload);
} else {
@@ -173,6 +175,27 @@ export const showToast = (kind: Kind): void => {
}
};
/**
* We tried to accessSecretStorage, which triggered us to ask for the
* recovery key, but this failed. If the user just gave up, that is fine,
* but if not, that means downloading encryption info from 4S did not fix
* the problem we identified. Presumably, something is wrong with what
* they have in 4S: we tell them to reset their identity.
*/
const onAccessSecretStorageFailed = (error: Error): void => {
if (error instanceof AccessCancelledError) {
// The user cancelled the dialog - just allow it to close
} else {
// A real error happened - jump to the reset identity tab
const payload: OpenToTabPayload = {
action: Action.ViewUserSettings,
initialTabId: UserTab.Encryption,
props: { initialEncryptionState: "reset_identity_sync_failed" },
};
defaultDispatcher.dispatch(payload);
}
};
ToastStore.sharedInstance().addOrReplaceToast({
key: TOAST_KEY,
title: getTitle(kind),