Files
cinny/src/app/components/LogoutDialog.tsx
T

133 lines
4.8 KiB
TypeScript
Raw Normal View History

import React, { forwardRef, useCallback } from 'react';
import { Dialog, Header, config, Box, Text, Button, Spinner, color } from 'folds';
import { useAtom } from 'jotai';
import { MatrixClient } from 'matrix-js-sdk';
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
import { logoutClient } from '../../client/initMatrix';
import { callEmbedAtom } from '../state/callEmbed';
import { CallEmbed } from '../plugins/call';
import { useMatrixClient } from '../hooks/useMatrixClient';
import { useModalStyle } from '../hooks/useModalStyle';
import { useCrossSigningActive } from '../hooks/useCrossSigning';
import { InfoCard } from './info-card';
import {
useDeviceVerificationStatus,
VerificationStatus,
} from '../hooks/useDeviceVerificationStatus';
/**
* Ask Element Call to hang up and wait (bounded) until our own MatrixRTC
* membership has actually been removed from the room, so the leave reaches
* the homeserver before the client is stopped and the token is revoked.
*/
const hangupAndWait = async (mx: MatrixClient, embed: CallEmbed): Promise<void> => {
const myUserId = mx.getUserId();
const myDeviceId = mx.getDeviceId();
const stillIn = (): boolean =>
mx.matrixRTC
.getRoomSession(embed.room)
.memberships.some((m) => m.sender === myUserId && m.deviceId === myDeviceId);
try {
await embed.hangup();
} catch {
// widget already gone — fall through to the wait/timeout
}
const deadline = Date.now() + 4000;
while (stillIn() && Date.now() < deadline) {
// eslint-disable-next-line no-await-in-loop
await new Promise((r) => {
setTimeout(r, 150);
});
}
};
type LogoutDialogProps = {
handleClose: () => void;
};
export const LogoutDialog = forwardRef<HTMLDivElement, LogoutDialogProps>(
({ handleClose }, ref) => {
const mx = useMatrixClient();
const modalStyle = useModalStyle(480);
const hasEncryptedRoom = !!mx.getRooms().find((room) => room.hasEncryptionStateEvent());
const crossSigningActive = useCrossSigningActive();
const verificationStatus = useDeviceVerificationStatus(
mx.getCrypto(),
mx.getSafeUserId(),
mx.getDeviceId() ?? undefined,
);
const [callEmbed, setCallEmbed] = useAtom(callEmbedAtom);
const [logoutState, logout] = useAsyncCallback<void, Error, []>(
useCallback(async () => {
// [Gitea #29] Logging out mid-call must hang up first, or the MatrixRTC
// membership (expires: 4 h) stays behind as a ghost participant and
// everyone else sees you "in call" until it times out.
if (callEmbed && callEmbed.joined && !callEmbed.disposed) {
await hangupAndWait(mx, callEmbed);
setCallEmbed(undefined);
}
await logoutClient(mx);
}, [mx, callEmbed, setCallEmbed]),
);
const ongoingLogout = logoutState.status === AsyncStatus.Loading;
return (
<Dialog variant="Surface" ref={ref} style={modalStyle}>
<Header
style={{
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
borderBottomWidth: config.borderWidth.B300,
}}
variant="Surface"
size="500"
>
<Box grow="Yes">
<Text as="h2" size="H4">
Logout
</Text>
</Box>
</Header>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
{hasEncryptedRoom &&
(crossSigningActive ? (
verificationStatus === VerificationStatus.Unverified && (
<InfoCard
variant="Critical"
title="Unverified Device"
description="Verify your device before logging out to save your encrypted messages."
/>
)
) : (
<InfoCard
variant="Critical"
title="Alert"
description="Enable device verification or export your encrypted data from settings to avoid losing access to your messages."
/>
))}
<Text priority="400">You’re about to log out. Are you sure?</Text>
{logoutState.status === AsyncStatus.Error && (
<Text style={{ color: color.Critical.Main }} size="T300">
Failed to logout! {logoutState.error.message}
</Text>
)}
<Box direction="Column" gap="200">
<Button
variant="Critical"
onClick={logout}
disabled={ongoingLogout}
before={ongoingLogout && <Spinner variant="Critical" fill="Solid" size="200" />}
>
<Text size="B400">Logout</Text>
</Button>
<Button variant="Secondary" fill="Soft" onClick={handleClose} disabled={ongoingLogout}>
<Text size="B400">Cancel</Text>
</Button>
</Box>
</Box>
</Dialog>
);
},
);