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:
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { Box, Line } from 'folds';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { isKeyHotkey } from 'is-hotkey';
|
||||
@@ -49,15 +49,46 @@ export function Room() {
|
||||
useCallback(
|
||||
(evt) => {
|
||||
if (isKeyHotkey('escape', evt)) {
|
||||
// Skip when a composer already consumed Escape (it preventDefaults).
|
||||
if (evt.defaultPrevented) return;
|
||||
// Skip while a thread panel is open: listener registration order
|
||||
// means this can run BEFORE the panel's own Escape handler, and the
|
||||
// user's intent there is "close the panel", not "mark room read".
|
||||
if (activeThreadId) return;
|
||||
markAsRead(mx, room.roomId, hideActivity);
|
||||
}
|
||||
},
|
||||
[mx, room.roomId, hideActivity],
|
||||
[mx, room.roomId, hideActivity, activeThreadId],
|
||||
),
|
||||
);
|
||||
|
||||
const callView = callEmbed?.roomId === room.roomId || room.isCallRoom() || callMembers.length > 0;
|
||||
|
||||
// Thread panel and media gallery are mutually exclusive on every screen size:
|
||||
// opening one closes the other. Detect the just-opened transition so whichever
|
||||
// was opened most recently wins.
|
||||
const prevThreadRef = useRef(activeThreadId);
|
||||
const prevGalleryRef = useRef(galleryOpen);
|
||||
useEffect(() => {
|
||||
const threadJustOpened = Boolean(activeThreadId) && !prevThreadRef.current;
|
||||
const galleryJustOpened = galleryOpen && !prevGalleryRef.current;
|
||||
if (threadJustOpened && galleryOpen) {
|
||||
setGalleryOpen(false);
|
||||
} else if (galleryJustOpened && activeThreadId) {
|
||||
setActiveThreadId(null);
|
||||
}
|
||||
prevThreadRef.current = activeThreadId;
|
||||
prevGalleryRef.current = galleryOpen;
|
||||
}, [activeThreadId, galleryOpen, setGalleryOpen, setActiveThreadId]);
|
||||
|
||||
// On non-desktop screens at most one right-side panel may show, priority
|
||||
// thread > gallery > members. On desktop thread + members may coexist while
|
||||
// thread + gallery stay mutually exclusive (via the effect above).
|
||||
const isDesktop = screenSize === ScreenSize.Desktop;
|
||||
const showThreadPanel = !callView && Boolean(activeThreadId);
|
||||
const showGallery = !callView && galleryOpen && (isDesktop || !activeThreadId);
|
||||
const showMembers = !callView && isDrawer && (isDesktop || (!activeThreadId && !galleryOpen));
|
||||
|
||||
return (
|
||||
<PowerLevelsContextProvider value={powerLevels}>
|
||||
<Box grow="Yes">
|
||||
@@ -86,7 +117,7 @@ export function Room() {
|
||||
<CallChatView />
|
||||
</>
|
||||
)}
|
||||
{!callView && galleryOpen && (
|
||||
{showGallery && (
|
||||
<>
|
||||
{screenSize === ScreenSize.Desktop && (
|
||||
<Line variant="Background" direction="Vertical" size="300" />
|
||||
@@ -94,7 +125,7 @@ export function Room() {
|
||||
<MediaGallery key={room.roomId} room={room} onClose={() => setGalleryOpen(false)} />
|
||||
</>
|
||||
)}
|
||||
{!callView && activeThreadId && (
|
||||
{showThreadPanel && activeThreadId && (
|
||||
<>
|
||||
{screenSize === ScreenSize.Desktop && (
|
||||
<Line variant="Background" direction="Vertical" size="300" />
|
||||
@@ -107,7 +138,7 @@ export function Room() {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!callView && isDrawer && (
|
||||
{showMembers && (
|
||||
<>
|
||||
{screenSize === ScreenSize.Desktop && (
|
||||
<Line variant="Background" direction="Vertical" size="300" />
|
||||
|
||||
Reference in New Issue
Block a user