import React, { useCallback, useEffect, useState } from 'react'; import { Box, Button, Checkbox, color, config, Dialog, Header, Icon, IconButton, Icons, Input, Overlay, OverlayBackdrop, OverlayCenter, Spinner, Text, } from 'folds'; import FocusTrap from 'focus-trap-react'; import { useAtomValue, useSetAtom, useStore } from 'jotai'; import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCardStyle } from '../../room-settings/styles.css'; import { SettingTile } from '../../../components/setting-tile'; import { useRoom } from '../../../hooks/useRoom'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useModalStyle } from '../../../hooks/useModalStyle'; import { stopPropagation } from '../../../utils/keyboard'; import { toastQueueAtom } from '../../../state/toast'; import { cancelRedactJob, clearRedactJob, hydrateRedactJob, redactJobsAtom, runRedactJob, } from '../../../state/redactOwnMessagesJob'; import { collectOwnEventIds } from '../../../utils/redactOwnMessages'; const TYPE_TO_CONFIRM_ABOVE = 50; type ConfirmDialogProps = { onStart: (leaveAfter: boolean) => void; requestClose: () => void; }; /** * Scan first (live count), then confirm with the number — typing the room name * for > 50 — and an optional "leave the room afterwards". */ function ConfirmDialog({ onStart, requestClose }: ConfirmDialogProps) { const mx = useMatrixClient(); const room = useRoom(); const modalStyle = useModalStyle(480); const [count, setCount] = useState(null); const [scanError, setScanError] = useState(); const [typed, setTyped] = useState(''); const [leaveAfter, setLeaveAfter] = useState(false); useEffect(() => { const ctrl = new AbortController(); setCount(null); collectOwnEventIds(mx, room.roomId, mx.getSafeUserId(), (n) => setCount(n), ctrl.signal) .then((ids) => setCount(ids.length)) .catch((e) => { if (!ctrl.signal.aborted) setScanError(e instanceof Error ? e.message : String(e)); }); return () => ctrl.abort(); }, [mx, room.roomId]); const scanning = count === null && !scanError; const needsTyping = (count ?? 0) > TYPE_TO_CONFIRM_ABOVE; const roomName = room.name ?? room.roomId; const canStart = !scanning && !scanError && (count ?? 0) > 0 && (!needsTyping || typed.trim() === roomName); return ( }>
Delete all your messages
{scanning && ( Counting your messages in {roomName}…{' '} {count ? `${Number(count).toLocaleString()} so far` : ''} )} {scanError && ( Couldn't count your messages: {scanError} )} {!scanning && !scanError && ( <> Redact {(count ?? 0).toLocaleString()} messages you sent in{' '} {roomName}? This cannot be undone. Redactions are visible to others as “message deleted”, and files you uploaded stay on the server until an admin purges them. {needsTyping && ( Type the room name to confirm setTyped(e.currentTarget.value)} placeholder={roomName} aria-label="Room name confirmation" /> )} setLeaveAfter((v) => !v)} aria-label="Leave the room afterwards" /> Leave the room afterwards )}
); } /** * [Gitea #169] Room Settings tile: "Your messages in this room" with a * Delete all… flow. The job runs outside React (closing settings is fine), * shows its progress here, persists so a reload can resume, and toasts when done. */ export function DeleteMyMessages() { const mx = useMatrixClient(); const room = useRoom(); const store = useStore(); const setToast = useSetAtom(toastQueueAtom); const jobs = useAtomValue(redactJobsAtom); const job = jobs[room.roomId]; const [confirm, setConfirm] = useState(false); // A reload mid-job: offer to resume. useEffect(() => { if (!jobs[room.roomId]) hydrateRedactJob(store, room.roomId); }, [store, room.roomId, jobs]); const start = useCallback( (leaveAfter: boolean, resume?: boolean) => { setConfirm(false); const resumeState = resume ? jobs[room.roomId] : undefined; const name = room.name ?? room.roomId; runRedactJob( store, mx, room.roomId, { leaveAfter, onDone: (s) => setToast({ id: `redact-done-${room.roomId}`, displayName: 'Lotus Chat', body: `Deleted ${s.redacted.toLocaleString()} of your messages in ${name}${s.skipped ? ` (${s.skipped} already gone)` : ''}.`, roomName: name, roomId: room.roomId, }), }, resumeState, ); }, [store, mx, room, jobs, setToast], ); const running = job && (job.phase === 'collecting' || job.phase === 'redacting'); const total = job ? job.redacted + job.skipped + job.pending.length : 0; const done = job ? job.redacted + job.skipped : 0; let status: React.ReactNode = 'Redact everything you sent here — no moderator needed.'; if (job?.phase === 'collecting') status = `Counting… ${job.found.toLocaleString()} found`; else if (job?.phase === 'redacting') status = `Deleting… ${done.toLocaleString()} / ${total.toLocaleString()}`; else if (job?.phase === 'error') status = `Stopped: ${job.error}. ${job.pending.length} left — you can resume.`; else if (job?.phase === 'cancelled' && job.pending.length > 0) status = `Cancelled with ${job.pending.length.toLocaleString()} left — you can resume.`; else if (job?.phase === 'done') status = `Deleted ${job.redacted.toLocaleString()} messages.`; const resumable = job && !running && job.pending.length > 0; return ( {running && ( )} {resumable && ( <> )} {!running && !resumable && ( )} } /> {confirm && ( start(leave)} requestClose={() => setConfirm(false)} /> )} ); }