fix: codex S2 review — race conditions, try/finally, scroll, compose reset
- channel switch clears messages immediately, prevents stale fetch overwrite - auto-scroll only on NEW messages (not every poll cycle) - ComposeBox keyed by channelId — resets draft on switch - try/finally on all mutations — failed sends don't disable compose - loadChannels no longer re-fetches on every channel select Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,35 +12,52 @@ export default function App() {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [replyTo, setReplyTo] = useState<string | null>(null);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const prevMsgCountRef = useRef(0);
|
||||
const activeChannelRef = useRef(activeChannelId);
|
||||
|
||||
// Keep ref in sync
|
||||
activeChannelRef.current = activeChannelId;
|
||||
|
||||
const loadChannels = useCallback(async () => {
|
||||
const chs = await getChannels();
|
||||
setChannels(chs);
|
||||
if (!activeChannelId && chs.length > 0) {
|
||||
setActiveChannelId(chs[0].id);
|
||||
}
|
||||
}, [activeChannelId]);
|
||||
// Auto-select first channel only if none selected
|
||||
setActiveChannelId((prev) => (prev ? prev : chs[0]?.id ?? null));
|
||||
}, []);
|
||||
|
||||
const loadMessages = useCallback(async () => {
|
||||
if (!activeChannelId) return;
|
||||
const msgs = await getMessages(activeChannelId);
|
||||
setMessages(msgs);
|
||||
}, [activeChannelId]);
|
||||
const channelId = activeChannelRef.current;
|
||||
if (!channelId) return;
|
||||
try {
|
||||
const msgs = await getMessages(channelId);
|
||||
// Only update if we're still on the same channel (prevent race)
|
||||
if (activeChannelRef.current === channelId) {
|
||||
setMessages(msgs);
|
||||
}
|
||||
} catch {
|
||||
// Silently ignore fetch errors during polling
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Initial channel load
|
||||
useEffect(() => {
|
||||
loadChannels();
|
||||
}, [loadChannels]);
|
||||
|
||||
// Load messages on channel switch
|
||||
useEffect(() => {
|
||||
loadMessages();
|
||||
setMessages([]); // Clear immediately on switch
|
||||
setReplyTo(null);
|
||||
}, [loadMessages]);
|
||||
prevMsgCountRef.current = 0;
|
||||
loadMessages();
|
||||
}, [activeChannelId, loadMessages]);
|
||||
|
||||
// Auto-scroll on new messages
|
||||
// Auto-scroll only when NEW messages arrive (not on every poll)
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
if (messages.length > prevMsgCountRef.current && scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
prevMsgCountRef.current = messages.length;
|
||||
}, [messages]);
|
||||
|
||||
// Poll until WebSocket (S5)
|
||||
@@ -51,13 +68,14 @@ export default function App() {
|
||||
|
||||
const messagesById = new Map(messages.map((m) => [m.id, m]));
|
||||
const activeChannel = channels.find((c) => c.id === activeChannelId);
|
||||
const _currentUser = getCurrentUsername();
|
||||
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
<ChannelSidebar
|
||||
channels={channels}
|
||||
activeId={activeChannelId}
|
||||
onSelect={(id) => setActiveChannelId(id)}
|
||||
onSelect={setActiveChannelId}
|
||||
onChannelCreated={loadChannels}
|
||||
/>
|
||||
|
||||
@@ -105,9 +123,10 @@ export default function App() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Compose */}
|
||||
{/* Compose — key forces reset on channel switch */}
|
||||
{activeChannelId && (
|
||||
<ComposeBox
|
||||
key={activeChannelId}
|
||||
channelId={activeChannelId}
|
||||
replyTo={replyTo}
|
||||
onClearReply={() => setReplyTo(null)}
|
||||
|
||||
Reference in New Issue
Block a user