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 [messages, setMessages] = useState<Message[]>([]);
|
||||||
const [replyTo, setReplyTo] = useState<string | null>(null);
|
const [replyTo, setReplyTo] = useState<string | null>(null);
|
||||||
const scrollRef = useRef<HTMLDivElement>(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 loadChannels = useCallback(async () => {
|
||||||
const chs = await getChannels();
|
const chs = await getChannels();
|
||||||
setChannels(chs);
|
setChannels(chs);
|
||||||
if (!activeChannelId && chs.length > 0) {
|
// Auto-select first channel only if none selected
|
||||||
setActiveChannelId(chs[0].id);
|
setActiveChannelId((prev) => (prev ? prev : chs[0]?.id ?? null));
|
||||||
}
|
}, []);
|
||||||
}, [activeChannelId]);
|
|
||||||
|
|
||||||
const loadMessages = useCallback(async () => {
|
const loadMessages = useCallback(async () => {
|
||||||
if (!activeChannelId) return;
|
const channelId = activeChannelRef.current;
|
||||||
const msgs = await getMessages(activeChannelId);
|
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);
|
setMessages(msgs);
|
||||||
}, [activeChannelId]);
|
}
|
||||||
|
} catch {
|
||||||
|
// Silently ignore fetch errors during polling
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Initial channel load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadChannels();
|
loadChannels();
|
||||||
}, [loadChannels]);
|
}, [loadChannels]);
|
||||||
|
|
||||||
|
// Load messages on channel switch
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadMessages();
|
setMessages([]); // Clear immediately on switch
|
||||||
setReplyTo(null);
|
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(() => {
|
useEffect(() => {
|
||||||
if (scrollRef.current) {
|
if (messages.length > prevMsgCountRef.current && scrollRef.current) {
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
}
|
}
|
||||||
|
prevMsgCountRef.current = messages.length;
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
// Poll until WebSocket (S5)
|
// Poll until WebSocket (S5)
|
||||||
@@ -51,13 +68,14 @@ export default function App() {
|
|||||||
|
|
||||||
const messagesById = new Map(messages.map((m) => [m.id, m]));
|
const messagesById = new Map(messages.map((m) => [m.id, m]));
|
||||||
const activeChannel = channels.find((c) => c.id === activeChannelId);
|
const activeChannel = channels.find((c) => c.id === activeChannelId);
|
||||||
|
const _currentUser = getCurrentUsername();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full">
|
<div className="flex h-full">
|
||||||
<ChannelSidebar
|
<ChannelSidebar
|
||||||
channels={channels}
|
channels={channels}
|
||||||
activeId={activeChannelId}
|
activeId={activeChannelId}
|
||||||
onSelect={(id) => setActiveChannelId(id)}
|
onSelect={setActiveChannelId}
|
||||||
onChannelCreated={loadChannels}
|
onChannelCreated={loadChannels}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -105,9 +123,10 @@ export default function App() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Compose */}
|
{/* Compose — key forces reset on channel switch */}
|
||||||
{activeChannelId && (
|
{activeChannelId && (
|
||||||
<ComposeBox
|
<ComposeBox
|
||||||
|
key={activeChannelId}
|
||||||
channelId={activeChannelId}
|
channelId={activeChannelId}
|
||||||
replyTo={replyTo}
|
replyTo={replyTo}
|
||||||
onClearReply={() => setReplyTo(null)}
|
onClearReply={() => setReplyTo(null)}
|
||||||
|
|||||||
@@ -21,10 +21,13 @@ export function ChannelSidebar({
|
|||||||
async function handleCreate() {
|
async function handleCreate() {
|
||||||
if (!newName.trim()) return;
|
if (!newName.trim()) return;
|
||||||
setCreating(true);
|
setCreating(true);
|
||||||
|
try {
|
||||||
await createChannel({ name: newName.trim(), description: "" });
|
await createChannel({ name: newName.trim(), description: "" });
|
||||||
setNewName("");
|
setNewName("");
|
||||||
setCreating(false);
|
|
||||||
onChannelCreated();
|
onChannelCreated();
|
||||||
|
} finally {
|
||||||
|
setCreating(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export function ComposeBox({
|
|||||||
async function handleSend() {
|
async function handleSend() {
|
||||||
if (!content.trim() || sending) return;
|
if (!content.trim() || sending) return;
|
||||||
setSending(true);
|
setSending(true);
|
||||||
|
try {
|
||||||
await postMessage(channelId, {
|
await postMessage(channelId, {
|
||||||
content: content.trim(),
|
content: content.trim(),
|
||||||
type: msgType,
|
type: msgType,
|
||||||
@@ -39,8 +40,10 @@ export function ComposeBox({
|
|||||||
setMsgType("text");
|
setMsgType("text");
|
||||||
onClearReply();
|
onClearReply();
|
||||||
onMessageSent();
|
onMessageSent();
|
||||||
|
} finally {
|
||||||
setSending(false);
|
setSending(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-t border-border bg-card px-4 py-3">
|
<div className="border-t border-border bg-card px-4 py-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user