replace chat polling with websocket

This commit is contained in:
2026-04-15 17:04:23 -06:00
parent b2f3bc117d
commit 89e6e05e8e
7 changed files with 280 additions and 116 deletions

View File

@@ -1,6 +1,7 @@
"use client";
import { createContext, useContext, useState, useRef, useCallback, useEffect } from "react";
import { createStompClient } from "@/lib/chatSocket";
const ChatWidgetContext = createContext(null);
const API_BASE = "";
@@ -60,24 +61,30 @@ export function ChatWidgetProvider({ children }) {
const [liveSending, setLiveSending] = useState(false);
const [switchingToHuman, setSwitchingToHuman] = useState(false);
const pollRef = useRef(null);
const stompRef = useRef(null);
const activeConvIdRef = useRef(null);
const tokenRef = useRef(null); // FIX: store token so polling can restart
const tokenRef = useRef(null);
const stopPolling = useCallback(() => {
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
const disconnectStomp = useCallback(() => {
if (stompRef.current) {
stompRef.current.deactivate();
stompRef.current = null;
}
}, []);
const fetchLiveMessages = useCallback(async (convId, token) => {
if (!convId || !token) return;
try {
const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${convId}/messages`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) return;
const data = await res.json();
if (Array.isArray(data)) setLiveMessages(data);
} catch { /* silent */ }
const subscribeToConversation = useCallback((client, convId) => {
client.subscribe(`/topic/chat/conversations/${convId}`, (frame) => {
try {
const msg = JSON.parse(frame.body);
setLiveMessages((prev) => prev.some((m) => m.id === msg.id) ? prev : [...prev, msg]);
} catch { /* silent */ }
});
client.subscribe(`/user/queue/chat/conversations`, (frame) => {
try {
const conv = JSON.parse(frame.body);
if (conv.id === convId) setActiveConv(conv);
} catch { /* silent */ }
});
}, []);
const loadConversations = useCallback(async (token) => {
@@ -97,21 +104,35 @@ export function ChatWidgetProvider({ children }) {
const openLiveConversation = useCallback(async (convId, token) => {
if (!convId || !token) return;
stopPolling();
tokenRef.current = token; // FIX: save token for polling restart
disconnectStomp();
tokenRef.current = token;
setActiveConvId(convId);
activeConvIdRef.current = convId;
setLiveMessages([]);
setView("live");
try {
const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${convId}`, {
headers: { Authorization: `Bearer ${token}` },
});
if (res.ok) setActiveConv(await res.json());
} catch { /* silent */ }
await fetchLiveMessages(convId, token);
pollRef.current = setInterval(() => fetchLiveMessages(convId, token), 2500);
}, [stopPolling, fetchLiveMessages]);
try {
const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${convId}/messages`, {
headers: { Authorization: `Bearer ${token}` },
});
if (res.ok) {
const data = await res.json();
if (Array.isArray(data)) setLiveMessages(data);
}
} catch { /* silent */ }
const client = createStompClient(token);
client.onConnect = () => subscribeToConversation(client, convId);
stompRef.current = client;
client.activate();
}, [disconnectStomp, subscribeToConversation]);
const sendLiveMessage = useCallback(async (text, token, convId) => {
if (!text.trim() || liveSending || !token || !convId) return;
@@ -122,11 +143,14 @@ export function ChatWidgetProvider({ children }) {
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
body: JSON.stringify({ content: text }),
});
if (res.ok) await fetchLiveMessages(convId, token);
if (res.ok) {
const msg = await res.json();
setLiveMessages((prev) => prev.some((m) => m.id === msg.id) ? prev : [...prev, msg]);
}
} catch { /* silent */ } finally {
setLiveSending(false);
}
}, [liveSending, fetchLiveMessages]);
}, [liveSending]);
const startLiveChat = useCallback(async (token) => {
if (!token || switchingToHuman) return;
@@ -149,19 +173,22 @@ export function ChatWidgetProvider({ children }) {
}
}, [switchingToHuman, openLiveConversation]);
// FIX: Single effect that handles both stopping AND restarting polling
useEffect(() => {
if (!isOpen || view !== "live") {
stopPolling();
} else if (isOpen && view === "live" && activeConvIdRef.current && tokenRef.current) {
stopPolling();
fetchLiveMessages(activeConvIdRef.current, tokenRef.current);
pollRef.current = setInterval(
() => fetchLiveMessages(activeConvIdRef.current, tokenRef.current),
2500
);
disconnectStomp();
} else if (isOpen && view === "live" && activeConvIdRef.current && tokenRef.current && !stompRef.current) {
const convId = activeConvIdRef.current;
const token = tokenRef.current;
const client = createStompClient(token);
client.onConnect = () => subscribeToConversation(client, convId);
stompRef.current = client;
client.activate();
}
}, [isOpen, view, stopPolling, fetchLiveMessages]);
}, [isOpen, view, disconnectStomp, subscribeToConversation]);
useEffect(() => {
return () => disconnectStomp();
}, [disconnectStomp]);
const toggleOpen = useCallback(() => setIsOpen((o) => !o), []);
const openView = useCallback((v) => setView(v), []);
@@ -185,4 +212,4 @@ export function useChatWidget() {
const ctx = useContext(ChatWidgetContext);
if (!ctx) throw new Error("useChatWidget must be used within ChatWidgetProvider");
return ctx;
}
}