diff --git a/ui/colony/src/App.tsx b/ui/colony/src/App.tsx index 882ee61..3042d73 100644 --- a/ui/colony/src/App.tsx +++ b/ui/colony/src/App.tsx @@ -12,35 +12,52 @@ export default function App() { const [messages, setMessages] = useState([]); const [replyTo, setReplyTo] = useState(null); const scrollRef = useRef(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 (
setActiveChannelId(id)} + onSelect={setActiveChannelId} onChannelCreated={loadChannels} /> @@ -105,9 +123,10 @@ export default function App() { ))}
- {/* Compose */} + {/* Compose — key forces reset on channel switch */} {activeChannelId && ( setReplyTo(null)} diff --git a/ui/colony/src/components/ChannelSidebar.tsx b/ui/colony/src/components/ChannelSidebar.tsx index 2d60768..6dfc28d 100644 --- a/ui/colony/src/components/ChannelSidebar.tsx +++ b/ui/colony/src/components/ChannelSidebar.tsx @@ -21,10 +21,13 @@ export function ChannelSidebar({ async function handleCreate() { if (!newName.trim()) return; setCreating(true); - await createChannel({ name: newName.trim(), description: "" }); - setNewName(""); - setCreating(false); - onChannelCreated(); + try { + await createChannel({ name: newName.trim(), description: "" }); + setNewName(""); + onChannelCreated(); + } finally { + setCreating(false); + } } return ( diff --git a/ui/colony/src/components/ComposeBox.tsx b/ui/colony/src/components/ComposeBox.tsx index 199ec9e..bd19bd8 100644 --- a/ui/colony/src/components/ComposeBox.tsx +++ b/ui/colony/src/components/ComposeBox.tsx @@ -30,16 +30,19 @@ export function ComposeBox({ async function handleSend() { if (!content.trim() || sending) return; setSending(true); - await postMessage(channelId, { - content: content.trim(), - type: msgType, - reply_to: replyTo ?? undefined, - }); - setContent(""); - setMsgType("text"); - onClearReply(); - onMessageSent(); - setSending(false); + try { + await postMessage(channelId, { + content: content.trim(), + type: msgType, + reply_to: replyTo ?? undefined, + }); + setContent(""); + setMsgType("text"); + onClearReply(); + onMessageSent(); + } finally { + setSending(false); + } } return (