diff --git a/src/contexts/room-context.tsx b/src/contexts/room-context.tsx index 9322d36..0c36d56 100644 --- a/src/contexts/room-context.tsx +++ b/src/contexts/room-context.tsx @@ -595,11 +595,17 @@ export function RoomProvider({ }, [wsToken]); useEffect(() => { - if (!wsClientRef.current) return; - wsClientRef.current.connect().catch((e) => { + // NOTE: intentionally omitted [wsClient] from deps. + // In React StrictMode the component mounts twice — if wsClient were a dep, + // the first mount's effect would connect client-1, then StrictMode cleanup + // would disconnect it, then the second mount's effect would connect client-2, + // then immediately the first mount's *second* cleanup would fire and + // disconnect client-2 — leaving WS unconnected. Using a ref for the initial + // connect avoids this. The client is always ready by the time this runs. + wsClientRef.current?.connect().catch((e) => { console.error('[RoomContext] WS connect error:', e); }); - }, [wsClient]); + }, []); const connectWs = useCallback(async () => { const client = wsClientRef.current; @@ -899,10 +905,25 @@ export function RoomProvider({ async (messageId: string) => { const client = wsClientRef.current; if (!client) return; - await client.messageRevoke(messageId); - setMessages((prev) => prev.filter((m) => m.id !== messageId)); - // Persist to IndexedDB - deleteMessageFromIdb(messageId).catch(() => {}); + + // Optimistic removal: hide message immediately + let rollbackMsg: MessageWithMeta | null = null; + setMessages((prev) => { + rollbackMsg = prev.find((m) => m.id === messageId) ?? null; + return prev.filter((m) => m.id !== messageId); + }); + + try { + await client.messageRevoke(messageId); + deleteMessageFromIdb(messageId).catch(() => {}); + } catch (err) { + // Rollback: restore message on server rejection + if (rollbackMsg) { + setMessages((prev) => [...prev, rollbackMsg!]); + saveMessage(rollbackMsg!).catch(() => {}); + } + handleRoomError('Delete message', err); + } }, [], );