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
+11 -3
View File
@@ -679,15 +679,23 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
submit();
}
if (isKeyHotkey('escape', evt)) {
evt.preventDefault();
// Only consume Escape (and stop it bubbling to the thread panel / room
// window handlers) when the composer actually has something to dismiss.
// If we did nothing, let Escape propagate so those handlers can run.
if (autocompleteQuery) {
evt.preventDefault();
evt.stopPropagation();
setAutocompleteQuery(undefined);
return;
}
setReplyDraft(undefined);
if (replyDraft) {
evt.preventDefault();
evt.stopPropagation();
setReplyDraft(undefined);
}
}
},
[submit, setReplyDraft, enterForNewline, autocompleteQuery, isComposing],
[submit, replyDraft, setReplyDraft, enterForNewline, autocompleteQuery, isComposing],
);
const handleKeyUp: KeyboardEventHandler = useCallback(