fix(audit): correctness wave — ghost sends, Escape coordination, panel exclusion

- ScheduledMessagesTray: cancel prunes local state ONLY on confirmed server
  cancel; failures keep the item + show an inline error (was: a failed cancel
  looked cancelled but still sent at the scheduled time).
- Escape semantics: the composer consumes Escape (preventDefault+stopPropagation)
  iff autocomplete is open or a reply draft is set; the thread panel and Room's
  markAsRead act only on unconsumed Escape, and markAsRead defers entirely while
  a thread panel is open (listener order made it fire before the panel closed).
- Room: thread panel / media gallery are mutually exclusive (most-recently-
  opened wins); on mobile at most one right panel renders (thread > gallery >
  members) instead of stacked fullscreen overlays.
- RemindMeDialog: busy-disabled presets (no more double-click duplicates),
  try/catch with inline error, close only on success.
- ThreadTimeline: "Jump to Latest" floating chip when scrolled up (RoomTimeline
  idiom).

From the 4-auditor deep-audit wave; reviewer-verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 00:18:51 -04:00
parent 7f960b026b
commit 664dcd4cd8
7 changed files with 166 additions and 51 deletions
@@ -1,8 +1,9 @@
import React, { useMemo } from 'react';
import React, { useMemo, useState } from 'react';
import FocusTrap from 'focus-trap-react';
import {
Box,
Button,
color,
config,
Dialog,
Header,
@@ -43,15 +44,25 @@ export function RemindMeDialog({ roomId, eventId, previewText, onClose }: Remind
const modalStyle = useModalStyle(320);
const { addReminder } = useReminders();
const presets = useMemo(() => getPresets(), []);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const handlePick = async (ms: number) => {
await addReminder({
roomId,
eventId,
timestamp: Date.now() + ms,
message: previewText || 'Reminder',
});
onClose();
if (busy) return;
setBusy(true);
setError(null);
try {
await addReminder({
roomId,
eventId,
timestamp: Date.now() + ms,
message: previewText || 'Reminder',
});
onClose();
} catch {
setBusy(false);
setError('Could not set reminder. Try again.');
}
};
return (
@@ -108,6 +119,7 @@ export function RemindMeDialog({ roomId, eventId, previewText, onClose }: Remind
variant="Secondary"
fill="Soft"
radii="300"
disabled={busy}
onClick={() => handlePick(p.ms)}
>
<Text size="B300" truncate>
@@ -115,6 +127,14 @@ export function RemindMeDialog({ roomId, eventId, previewText, onClose }: Remind
</Text>
</Button>
))}
{error && (
<Text
size="T200"
style={{ color: color.Critical.Main, paddingTop: config.space.S100 }}
>
{error}
</Text>
)}
</Box>
</Dialog>
</FocusTrap>