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:
2026-03-29 19:30:00 +02:00
parent 20196c23a8
commit 9e35984813
3 changed files with 53 additions and 28 deletions

View File

@@ -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)}