From df9cac8f5c2b4aaa89783b2ad7ed9292560bb68b Mon Sep 17 00:00:00 2001 From: heaven Date: Mon, 15 Jun 2026 02:57:32 +0300 Subject: [PATCH] refactor(settings): replace the shared LogoutDialog with a self-contained localised logout-dialog card --- src/app/components/LogoutDialog.tsx | 91 ---------- .../logout-dialog/LogoutDialog.css.ts | 79 +++++++++ .../components/logout-dialog/LogoutDialog.tsx | 164 ++++++++++++++++++ src/app/components/logout-dialog/index.ts | 1 + 4 files changed, 244 insertions(+), 91 deletions(-) delete mode 100644 src/app/components/LogoutDialog.tsx create mode 100644 src/app/components/logout-dialog/LogoutDialog.css.ts create mode 100644 src/app/components/logout-dialog/LogoutDialog.tsx create mode 100644 src/app/components/logout-dialog/index.ts diff --git a/src/app/components/LogoutDialog.tsx b/src/app/components/LogoutDialog.tsx deleted file mode 100644 index 1d0d1ce9..00000000 --- a/src/app/components/LogoutDialog.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { forwardRef, useCallback } from 'react'; -import { Dialog, Header, config, Box, Text, Button, Spinner, color } from 'folds'; -import { useTranslation } from 'react-i18next'; -import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback'; -import { logoutClient } from '../../client/initMatrix'; -import { useMatrixClient } from '../hooks/useMatrixClient'; -import { useCrossSigningActive } from '../hooks/useCrossSigning'; -import { InfoCard } from './info-card'; -import { - useDeviceVerificationStatus, - VerificationStatus, -} from '../hooks/useDeviceVerificationStatus'; - -type LogoutDialogProps = { - handleClose: () => void; -}; -export const LogoutDialog = forwardRef( - ({ handleClose }, ref) => { - const { t } = useTranslation(); - const mx = useMatrixClient(); - const hasEncryptedRoom = !!mx.getRooms().find((room) => room.hasEncryptionStateEvent()); - const crossSigningActive = useCrossSigningActive(); - const verificationStatus = useDeviceVerificationStatus( - mx.getCrypto(), - mx.getSafeUserId(), - mx.getDeviceId() ?? undefined - ); - - const [logoutState, logout] = useAsyncCallback( - useCallback(async () => { - await logoutClient(mx); - }, [mx]) - ); - - const ongoingLogout = logoutState.status === AsyncStatus.Loading; - - return ( - -
- - {t('Settings.logout')} - -
- - {hasEncryptedRoom && - (crossSigningActive ? ( - verificationStatus === VerificationStatus.Unverified && ( - - ) - ) : ( - - ))} - {t('Settings.logout_confirm')} - {logoutState.status === AsyncStatus.Error && ( - - {t('Settings.logout_failed', { message: logoutState.error.message })} - - )} - - - - - -
- ); - } -); diff --git a/src/app/components/logout-dialog/LogoutDialog.css.ts b/src/app/components/logout-dialog/LogoutDialog.css.ts new file mode 100644 index 00000000..71066f00 --- /dev/null +++ b/src/app/components/logout-dialog/LogoutDialog.css.ts @@ -0,0 +1,79 @@ +import { style } from '@vanilla-extract/css'; +import { color, config, toRem } from 'folds'; + +// Logout confirmation — same self-styled Vojo card vocabulary as the AI +// privacy notice (AiChatPrivacy.css.ts): own surface/radius/border/shadow, +// branded badge header with NO bottom-border bar (the border bar is what made +// the old folds Dialog read as a generic cinny dialog). Narrower than the +// privacy card — it's a yes/no confirm, not a reading surface. +export const DialogCard = style({ + width: '90vw', + maxWidth: toRem(400), + display: 'flex', + flexDirection: 'column', + overflow: 'hidden', + backgroundColor: color.Surface.Container, + borderRadius: config.radii.R400, + border: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`, + boxShadow: '0 20px 60px rgba(0, 0, 0, 0.5)', +}); + +export const Header = style({ + flexShrink: 0, + display: 'flex', + alignItems: 'flex-start', + gap: config.space.S300, + padding: `${config.space.S400} ${config.space.S400} ${config.space.S300}`, +}); + +// Rounded-square badge in the critical tone — the destructive twin of the +// privacy card's fleet shield. +export const HeaderBadge = style({ + flexShrink: 0, + width: toRem(40), + height: toRem(40), + borderRadius: config.radii.R400, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: color.Critical.Container, + color: color.Critical.Main, +}); + +export const HeaderText = style({ + flexGrow: 1, + minWidth: 0, + display: 'flex', + flexDirection: 'column', + gap: toRem(2), + paddingTop: toRem(2), +}); + +export const HeaderTitle = style({ + fontSize: toRem(18), + fontWeight: 700, + lineHeight: toRem(22), + color: color.Surface.OnContainer, +}); + +export const HeaderSubtitle = style({ + fontSize: toRem(13), + lineHeight: toRem(17), + color: color.SurfaceVariant.OnContainer, + opacity: 0.7, +}); + +export const Body = style({ + display: 'flex', + flexDirection: 'column', + gap: config.space.S400, + padding: `0 ${config.space.S400} ${config.space.S400}`, +}); + +// Telegram-style horizontal confirm row: Cancel first, the destructive +// action trailing at the end. +export const ButtonRow = style({ + display: 'flex', + justifyContent: 'flex-end', + gap: config.space.S200, +}); diff --git a/src/app/components/logout-dialog/LogoutDialog.tsx b/src/app/components/logout-dialog/LogoutDialog.tsx new file mode 100644 index 00000000..76ef26b4 --- /dev/null +++ b/src/app/components/logout-dialog/LogoutDialog.tsx @@ -0,0 +1,164 @@ +import React, { useCallback, useRef } from 'react'; +import FocusTrap from 'focus-trap-react'; +import { + Box, + Button, + Icon, + IconButton, + Icons, + Overlay, + OverlayBackdrop, + OverlayCenter, + Spinner, + Text, + color, +} from 'folds'; +import { useTranslation } from 'react-i18next'; +import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; +import { logoutClient } from '../../../client/initMatrix'; +import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { useCrossSigningActive } from '../../hooks/useCrossSigning'; +import { InfoCard } from '../info-card'; +import { + useDeviceVerificationStatus, + VerificationStatus, +} from '../../hooks/useDeviceVerificationStatus'; +import { stopPropagation } from '../../utils/keyboard'; +import * as css from './LogoutDialog.css'; + +type LogoutDialogProps = { + handleClose: () => void; +}; + +// Logout confirmation — self-contained Vojo card (owns its Overlay + +// FocusTrap, callers just conditionally render it). Same chrome as the AI +// privacy notice: badge header without a border bar, horizontal confirm row. +// The encrypted-rooms key-loss warnings (InfoCard) stay — they are the only +// thing standing between an unverified device and losing message history. +export function LogoutDialog({ handleClose }: LogoutDialogProps) { + const { t } = useTranslation(); + const mx = useMatrixClient(); + const hasEncryptedRoom = !!mx.getRooms().find((room) => room.hasEncryptionStateEvent()); + const crossSigningActive = useCrossSigningActive(); + const verificationStatus = useDeviceVerificationStatus( + mx.getCrypto(), + mx.getSafeUserId(), + mx.getDeviceId() ?? undefined + ); + + const [logoutState, logout] = useAsyncCallback( + useCallback(async () => { + await logoutClient(mx); + }, [mx]) + ); + + const ongoingLogout = logoutState.status === AsyncStatus.Loading; + // focus-trap-react copies focusTrapOptions ONCE at mount — plain values + // here would freeze at `ongoingLogout = false` and let a backdrop click / + // Esc dismiss the dialog mid-logout (hiding a potential failure while the + // session is still alive). Function options are called at event time, so + // they read the live state through this ref. + const ongoingLogoutRef = useRef(ongoingLogout); + ongoingLogoutRef.current = ongoingLogout; + + return ( + }> + + { + if (!ongoingLogoutRef.current) handleClose(); + }, + clickOutsideDeactivates: () => !ongoingLogoutRef.current, + escapeDeactivates: (evt) => { + // Mid-logout the Esc must still be CONSUMED: just returning + // false would let the keydown bubble on to the surface + // hosting the dialog (the mobile settings sheet's window + // listener, route-level handlers) and tear it down — the + // exact dismissal this ref-guard exists to prevent. + if (ongoingLogoutRef.current) { + evt.stopPropagation(); + return false; + } + return stopPropagation(evt); + }, + }} + > +
+
+ +
+ {t('Settings.logout')} + {t('Settings.logout_confirm')} +
+ + + +
+ +
+ {hasEncryptedRoom && + (crossSigningActive ? ( + verificationStatus === VerificationStatus.Unverified && ( + + ) + ) : ( + + ))} + {logoutState.status === AsyncStatus.Error && ( + + {t('Settings.logout_failed', { message: logoutState.error.message })} + + )} + + + + +
+
+
+
+
+ ); +} diff --git a/src/app/components/logout-dialog/index.ts b/src/app/components/logout-dialog/index.ts new file mode 100644 index 00000000..364465fa --- /dev/null +++ b/src/app/components/logout-dialog/index.ts @@ -0,0 +1 @@ +export * from './LogoutDialog';