2026-10-17 19:40:03 -04:00
|
|
|
|
import React, { forwardRef, useCallback, useEffect, useState } from 'react';
|
2025-02-10 16:49:47 +11:00
|
|
|
|
import { Dialog, Header, config, Box, Text, Button, Spinner, color } from 'folds';
|
2026-09-18 22:13:45 -04:00
|
|
|
|
import { useAtom } from 'jotai';
|
2025-02-10 16:49:47 +11:00
|
|
|
|
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
|
|
|
|
|
|
import { logoutClient } from '../../client/initMatrix';
|
2026-09-18 22:13:45 -04:00
|
|
|
|
import { callEmbedAtom } from '../state/callEmbed';
|
2026-09-19 01:04:26 -04:00
|
|
|
|
import { hangupCallAndWait } from '../plugins/call/hangup';
|
2025-02-10 16:49:47 +11:00
|
|
|
|
import { useMatrixClient } from '../hooks/useMatrixClient';
|
2026-06-18 14:51:28 -04:00
|
|
|
|
import { useModalStyle } from '../hooks/useModalStyle';
|
2025-02-10 16:49:47 +11:00
|
|
|
|
import { useCrossSigningActive } from '../hooks/useCrossSigning';
|
|
|
|
|
|
import { InfoCard } from './info-card';
|
2026-10-17 19:40:03 -04:00
|
|
|
|
import { ScheduledMessage } from '../state/scheduledMessages';
|
|
|
|
|
|
import { cancelScheduledMessage, fetchScheduledMessages } from '../utils/scheduledMessages';
|
2025-02-10 16:49:47 +11:00
|
|
|
|
import {
|
|
|
|
|
|
useDeviceVerificationStatus,
|
|
|
|
|
|
VerificationStatus,
|
|
|
|
|
|
} from '../hooks/useDeviceVerificationStatus';
|
|
|
|
|
|
|
2026-09-18 22:13:45 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* 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.
|
|
|
|
|
|
*/
|
2025-02-10 16:49:47 +11:00
|
|
|
|
type LogoutDialogProps = {
|
|
|
|
|
|
handleClose: () => void;
|
|
|
|
|
|
};
|
|
|
|
|
|
export const LogoutDialog = forwardRef<HTMLDivElement, LogoutDialogProps>(
|
|
|
|
|
|
({ handleClose }, ref) => {
|
|
|
|
|
|
const mx = useMatrixClient();
|
2026-06-18 14:51:28 -04:00
|
|
|
|
const modalStyle = useModalStyle(480);
|
2025-02-10 16:49:47 +11:00
|
|
|
|
const hasEncryptedRoom = !!mx.getRooms().find((room) => room.hasEncryptionStateEvent());
|
|
|
|
|
|
const crossSigningActive = useCrossSigningActive();
|
|
|
|
|
|
const verificationStatus = useDeviceVerificationStatus(
|
|
|
|
|
|
mx.getCrypto(),
|
|
|
|
|
|
mx.getSafeUserId(),
|
2026-05-21 23:30:50 -04:00
|
|
|
|
mx.getDeviceId() ?? undefined,
|
2025-02-10 16:49:47 +11:00
|
|
|
|
);
|
|
|
|
|
|
|
2026-09-18 22:13:45 -04:00
|
|
|
|
const [callEmbed, setCallEmbed] = useAtom(callEmbedAtom);
|
|
|
|
|
|
|
2026-10-17 19:40:03 -04:00
|
|
|
|
// [Gitea #322] Scheduled messages live on the server and still send after
|
|
|
|
|
|
// logout, where no client would show them. Say so, and offer to cancel.
|
|
|
|
|
|
const [scheduled, setScheduled] = useState<ScheduledMessage[]>([]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
let alive = true;
|
|
|
|
|
|
fetchScheduledMessages(mx)
|
|
|
|
|
|
.then((list) => alive && setScheduled(list))
|
|
|
|
|
|
.catch(() => undefined);
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
alive = false;
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [mx]);
|
|
|
|
|
|
|
|
|
|
|
|
const [logoutState, logout] = useAsyncCallback<void, Error, [boolean?]>(
|
|
|
|
|
|
useCallback(
|
|
|
|
|
|
async (cancelScheduled?: boolean) => {
|
|
|
|
|
|
if (cancelScheduled) {
|
|
|
|
|
|
await Promise.all(scheduled.map((m) => cancelScheduledMessage(mx, m.delayId)));
|
|
|
|
|
|
}
|
|
|
|
|
|
// [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 hangupCallAndWait(mx, callEmbed);
|
|
|
|
|
|
setCallEmbed(undefined);
|
|
|
|
|
|
}
|
|
|
|
|
|
await logoutClient(mx);
|
|
|
|
|
|
},
|
|
|
|
|
|
[mx, callEmbed, setCallEmbed, scheduled],
|
|
|
|
|
|
),
|
2025-02-10 16:49:47 +11:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const ongoingLogout = logoutState.status === AsyncStatus.Loading;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-09-24 13:06:10 -04:00
|
|
|
|
<Dialog
|
|
|
|
|
|
id="logoutdialog-dialog-1"
|
|
|
|
|
|
role="dialog"
|
|
|
|
|
|
aria-modal="true"
|
|
|
|
|
|
aria-labelledby="logoutdialog-dialog-1-title"
|
|
|
|
|
|
tabIndex={-1}
|
|
|
|
|
|
variant="Surface"
|
|
|
|
|
|
ref={ref}
|
|
|
|
|
|
style={modalStyle}
|
|
|
|
|
|
>
|
2025-02-10 16:49:47 +11:00
|
|
|
|
<Header
|
|
|
|
|
|
style={{
|
|
|
|
|
|
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
|
|
|
|
|
|
borderBottomWidth: config.borderWidth.B300,
|
|
|
|
|
|
}}
|
|
|
|
|
|
variant="Surface"
|
|
|
|
|
|
size="500"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Box grow="Yes">
|
2026-09-24 13:06:10 -04:00
|
|
|
|
<Text id="logoutdialog-dialog-1-title" as="h2" size="H4">
|
2026-05-21 20:49:33 -04:00
|
|
|
|
Logout
|
|
|
|
|
|
</Text>
|
2025-02-10 16:49:47 +11:00
|
|
|
|
</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."
|
|
|
|
|
|
/>
|
|
|
|
|
|
))}
|
2026-10-17 19:40:03 -04:00
|
|
|
|
{scheduled.length > 0 && (
|
|
|
|
|
|
<InfoCard
|
|
|
|
|
|
variant="Warning"
|
|
|
|
|
|
title={`${scheduled.length} scheduled message${scheduled.length === 1 ? '' : 's'}`}
|
|
|
|
|
|
description="Scheduled messages still send after you log out. Cancel them now if you don't want them sent."
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2025-02-10 16:49:47 +11:00
|
|
|
|
<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"
|
2026-10-17 19:40:03 -04:00
|
|
|
|
onClick={() => logout()}
|
2025-02-10 16:49:47 +11:00
|
|
|
|
disabled={ongoingLogout}
|
|
|
|
|
|
before={ongoingLogout && <Spinner variant="Critical" fill="Solid" size="200" />}
|
|
|
|
|
|
>
|
2026-10-17 19:40:03 -04:00
|
|
|
|
<Text size="B400">
|
|
|
|
|
|
{scheduled.length > 0 ? 'Logout and keep scheduled messages' : 'Logout'}
|
|
|
|
|
|
</Text>
|
2025-02-10 16:49:47 +11:00
|
|
|
|
</Button>
|
2026-10-17 19:40:03 -04:00
|
|
|
|
{scheduled.length > 0 && (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="Critical"
|
|
|
|
|
|
fill="Soft"
|
|
|
|
|
|
onClick={() => logout(true)}
|
|
|
|
|
|
disabled={ongoingLogout}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Text size="B400">Cancel scheduled messages and logout</Text>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
2025-02-10 16:49:47 +11:00
|
|
|
|
<Button variant="Secondary" fill="Soft" onClick={handleClose} disabled={ongoingLogout}>
|
|
|
|
|
|
<Text size="B400">Cancel</Text>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
);
|
2026-05-21 23:30:50 -04:00
|
|
|
|
},
|
2025-02-10 16:49:47 +11:00
|
|
|
|
);
|