replace chat polling with websocket
This commit is contained in:
@@ -4,6 +4,8 @@ COPY package*.json ./
|
|||||||
RUN npm ci
|
RUN npm ci
|
||||||
COPY . .
|
COPY . .
|
||||||
ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51TK18lFQ95OLlFb7dNtKXlvhry8IOvHaWJWW7zUNFhicMgyJ2EgAFhiAocxsCyP95IKt7AeQg4cWe5iHF3qoheZyl0034Cd4yij
|
ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51TK18lFQ95OLlFb7dNtKXlvhry8IOvHaWJWW7zUNFhicMgyJ2EgAFhiAocxsCyP95IKt7AeQg4cWe5iHF3qoheZyl0034Cd4yij
|
||||||
|
ARG NEXT_PUBLIC_BACKEND_URL
|
||||||
|
ENV NEXT_PUBLIC_BACKEND_URL=$NEXT_PUBLIC_BACKEND_URL
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM node:22-alpine
|
FROM node:22-alpine
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import dynamic from "next/dynamic";
|
|||||||
import { useState, useEffect, useRef, useCallback } from "react";
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useAuth } from "@/context/AuthContext";
|
import { useAuth } from "@/context/AuthContext";
|
||||||
|
import { createStompClient } from "@/lib/chatSocket";
|
||||||
|
|
||||||
const API_BASE = "";
|
const API_BASE = "";
|
||||||
const POLL_INTERVAL = 2500;
|
|
||||||
|
|
||||||
function AiChatPage() {
|
function AiChatPage() {
|
||||||
const { user, token, loading: authLoading } = useAuth();
|
const { user, token, loading: authLoading } = useAuth();
|
||||||
@@ -27,7 +27,7 @@ function AiChatPage() {
|
|||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
const messagesAreaRef = useRef(null);
|
const messagesAreaRef = useRef(null);
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
const pollRef = useRef(null);
|
const stompRef = useRef(null);
|
||||||
const lastMessageIdRef = useRef(null);
|
const lastMessageIdRef = useRef(null);
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
const lastScrolledIdRef = useRef(null);
|
const lastScrolledIdRef = useRef(null);
|
||||||
@@ -100,37 +100,30 @@ function AiChatPage() {
|
|||||||
}
|
}
|
||||||
}, [token]);
|
}, [token]);
|
||||||
|
|
||||||
const startPolling = useCallback((convId) => {
|
const connectStomp = useCallback((convId) => {
|
||||||
if (pollRef.current) clearInterval(pollRef.current);
|
if (stompRef.current) {
|
||||||
pollRef.current = setInterval(async () => {
|
stompRef.current.deactivate();
|
||||||
if (!token || !convId) return;
|
stompRef.current = null;
|
||||||
try {
|
}
|
||||||
const [msgsRes, convRes] = await Promise.all([
|
const client = createStompClient(token);
|
||||||
fetch(`${API_BASE}/api/v1/chat/conversations/${convId}/messages`, {
|
client.onConnect = () => {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
client.subscribe(`/topic/chat/conversations/${convId}`, (frame) => {
|
||||||
}),
|
try {
|
||||||
fetch(`${API_BASE}/api/v1/chat/conversations/${convId}`, {
|
const msg = JSON.parse(frame.body);
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
setMessages((prev) => prev.some((m) => m.id === msg.id) ? prev : [...prev, msg]);
|
||||||
}),
|
lastMessageIdRef.current = msg.id;
|
||||||
]);
|
} catch { /* silent */ }
|
||||||
if (msgsRes.ok) {
|
});
|
||||||
const data = await msgsRes.json();
|
client.subscribe(`/topic/chat/conversations`, (frame) => {
|
||||||
if (Array.isArray(data)) {
|
try {
|
||||||
const lastId = data.length > 0 ? data[data.length - 1].id : null;
|
const conv = JSON.parse(frame.body);
|
||||||
if (lastId !== lastMessageIdRef.current) {
|
if (conv.id === convId) setConversation(conv);
|
||||||
lastMessageIdRef.current = lastId;
|
setConversations((prev) => prev.map((c) => c.id === conv.id ? conv : c));
|
||||||
setMessages(data);
|
} catch { /* silent */ }
|
||||||
}
|
});
|
||||||
}
|
};
|
||||||
}
|
stompRef.current = client;
|
||||||
if (convRes.ok) {
|
client.activate();
|
||||||
const convData = await convRes.json();
|
|
||||||
setConversation(convData);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// silent
|
|
||||||
}
|
|
||||||
}, POLL_INTERVAL);
|
|
||||||
}, [token]);
|
}, [token]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -191,16 +184,16 @@ function AiChatPage() {
|
|||||||
fetchConversations(),
|
fetchConversations(),
|
||||||
]);
|
]);
|
||||||
setLoadingConv(false);
|
setLoadingConv(false);
|
||||||
startPolling(convId);
|
connectStomp(convId);
|
||||||
router.replace(`/ai-chat?id=${convId}`, { scroll: false });
|
router.replace(`/ai-chat?id=${convId}`, { scroll: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (pollRef.current) clearInterval(pollRef.current);
|
if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; }
|
||||||
};
|
};
|
||||||
}, [token, authLoading, conversationIdParam, fetchConversation, fetchMessages, startPolling, fetchConversations, router]);
|
}, [token, authLoading, conversationIdParam, fetchConversation, fetchMessages, connectStomp, fetchConversations, router]);
|
||||||
|
|
||||||
async function handleSend(e) {
|
async function handleSend(e) {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
@@ -313,7 +306,7 @@ function AiChatPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleNewConversation() {
|
async function handleNewConversation() {
|
||||||
if (pollRef.current) clearInterval(pollRef.current);
|
if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; }
|
||||||
setError(null);
|
setError(null);
|
||||||
setLoadingConv(true);
|
setLoadingConv(true);
|
||||||
try {
|
try {
|
||||||
@@ -335,7 +328,7 @@ function AiChatPage() {
|
|||||||
setConversation(conv);
|
setConversation(conv);
|
||||||
await Promise.all([fetchMessages(conv.id), fetchConversations()]);
|
await Promise.all([fetchMessages(conv.id), fetchConversations()]);
|
||||||
setLoadingConv(false);
|
setLoadingConv(false);
|
||||||
startPolling(conv.id);
|
connectStomp(conv.id);
|
||||||
router.replace(`/ai-chat?id=${conv.id}`, { scroll: false });
|
router.replace(`/ai-chat?id=${conv.id}`, { scroll: false });
|
||||||
} catch {
|
} catch {
|
||||||
setError("Network error. Please try again.");
|
setError("Network error. Please try again.");
|
||||||
@@ -344,14 +337,14 @@ function AiChatPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function switchConversation(convId) {
|
async function switchConversation(convId) {
|
||||||
if (pollRef.current) clearInterval(pollRef.current);
|
if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; }
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
setError(null);
|
setError(null);
|
||||||
setLoadingConv(true);
|
setLoadingConv(true);
|
||||||
await fetchConversation(convId);
|
await fetchConversation(convId);
|
||||||
await fetchMessages(convId);
|
await fetchMessages(convId);
|
||||||
setLoadingConv(false);
|
setLoadingConv(false);
|
||||||
startPolling(convId);
|
connectStomp(convId);
|
||||||
router.replace(`/ai-chat?id=${convId}`, { scroll: false });
|
router.replace(`/ai-chat?id=${convId}`, { scroll: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import dynamic from "next/dynamic";
|
|||||||
import { useState, useEffect, useRef, useCallback } from "react";
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useAuth } from "@/context/AuthContext";
|
import { useAuth } from "@/context/AuthContext";
|
||||||
|
import { createStompClient } from "@/lib/chatSocket";
|
||||||
|
|
||||||
const API_BASE = "";
|
const API_BASE = "";
|
||||||
const POLL_INTERVAL = 2500;
|
|
||||||
|
|
||||||
function ChatPage() {
|
function ChatPage() {
|
||||||
const { user, token, loading: authLoading } = useAuth();
|
const { user, token, loading: authLoading } = useAuth();
|
||||||
@@ -27,7 +27,7 @@ function ChatPage() {
|
|||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
const messagesAreaRef = useRef(null);
|
const messagesAreaRef = useRef(null);
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
const pollRef = useRef(null);
|
const stompRef = useRef(null);
|
||||||
const lastMessageIdRef = useRef(null);
|
const lastMessageIdRef = useRef(null);
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
const lastScrolledIdRef = useRef(null);
|
const lastScrolledIdRef = useRef(null);
|
||||||
@@ -114,39 +114,30 @@ function ChatPage() {
|
|||||||
}
|
}
|
||||||
}, [token]);
|
}, [token]);
|
||||||
|
|
||||||
const startPolling = useCallback((convId) => {
|
const connectStomp = useCallback((convId) => {
|
||||||
if (pollRef.current) clearInterval(pollRef.current);
|
if (stompRef.current) {
|
||||||
pollRef.current = setInterval(async () => {
|
stompRef.current.deactivate();
|
||||||
if (!token || !convId) return;
|
stompRef.current = null;
|
||||||
try {
|
}
|
||||||
const [msgsRes, convRes] = await Promise.all([
|
const client = createStompClient(token);
|
||||||
fetch(`${API_BASE}/api/v1/chat/conversations/${convId}/messages`, {
|
client.onConnect = () => {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
client.subscribe(`/topic/chat/conversations/${convId}`, (frame) => {
|
||||||
}),
|
try {
|
||||||
fetch(`${API_BASE}/api/v1/chat/conversations/${convId}`, {
|
const msg = JSON.parse(frame.body);
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
setMessages((prev) => prev.some((m) => m.id === msg.id) ? prev : [...prev, msg]);
|
||||||
}),
|
lastMessageIdRef.current = msg.id;
|
||||||
]);
|
} catch { /* silent */ }
|
||||||
if (msgsRes.ok) {
|
});
|
||||||
const data = await msgsRes.json();
|
client.subscribe(`/topic/chat/conversations`, (frame) => {
|
||||||
if (Array.isArray(data)) {
|
try {
|
||||||
const lastId = data.length > 0 ? data[data.length - 1].id : null;
|
const conv = JSON.parse(frame.body);
|
||||||
if (lastId !== lastMessageIdRef.current) {
|
if (conv.id === convId) setConversation(conv);
|
||||||
lastMessageIdRef.current = lastId;
|
setConversations((prev) => prev.map((c) => c.id === conv.id ? conv : c));
|
||||||
setMessages(data);
|
} catch { /* silent */ }
|
||||||
}
|
});
|
||||||
}
|
};
|
||||||
}
|
stompRef.current = client;
|
||||||
if (convRes.ok) {
|
client.activate();
|
||||||
const convData = await convRes.json();
|
|
||||||
setConversation(convData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
catch {
|
|
||||||
//Silent
|
|
||||||
}
|
|
||||||
}, POLL_INTERVAL);
|
|
||||||
}, [token]);
|
}, [token]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -188,15 +179,15 @@ function ChatPage() {
|
|||||||
fetchConversations(),
|
fetchConversations(),
|
||||||
]);
|
]);
|
||||||
setLoadingConv(false);
|
setLoadingConv(false);
|
||||||
startPolling(convId);
|
connectStomp(convId);
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (pollRef.current) clearInterval(pollRef.current);
|
if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; }
|
||||||
};
|
};
|
||||||
}, [token, authLoading, conversationIdParam, fetchConversation, fetchMessages, startPolling, fetchConversations]);
|
}, [token, authLoading, conversationIdParam, fetchConversation, fetchMessages, connectStomp, fetchConversations]);
|
||||||
|
|
||||||
async function handleSend(e) {
|
async function handleSend(e) {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
@@ -340,7 +331,7 @@ function ChatPage() {
|
|||||||
setConversation(conv);
|
setConversation(conv);
|
||||||
await Promise.all([fetchMessages(conv.id), fetchConversations()]);
|
await Promise.all([fetchMessages(conv.id), fetchConversations()]);
|
||||||
setLoadingConv(false);
|
setLoadingConv(false);
|
||||||
startPolling(conv.id);
|
connectStomp(conv.id);
|
||||||
router.replace(`/chat?id=${conv.id}`, { scroll: false });
|
router.replace(`/chat?id=${conv.id}`, { scroll: false });
|
||||||
} catch {
|
} catch {
|
||||||
setError("Network error. Please try again.");
|
setError("Network error. Please try again.");
|
||||||
@@ -349,14 +340,14 @@ function ChatPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function switchConversation(convId) {
|
async function switchConversation(convId) {
|
||||||
if (pollRef.current) clearInterval(pollRef.current);
|
if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; }
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
setError(null);
|
setError(null);
|
||||||
setLoadingConv(true);
|
setLoadingConv(true);
|
||||||
await fetchConversation(convId);
|
await fetchConversation(convId);
|
||||||
await fetchMessages(convId);
|
await fetchMessages(convId);
|
||||||
setLoadingConv(false);
|
setLoadingConv(false);
|
||||||
startPolling(convId);
|
connectStomp(convId);
|
||||||
router.replace(`/chat?id=${convId}`, { scroll: false });
|
router.replace(`/chat?id=${convId}`, { scroll: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { createContext, useContext, useState, useRef, useCallback, useEffect } from "react";
|
import { createContext, useContext, useState, useRef, useCallback, useEffect } from "react";
|
||||||
|
import { createStompClient } from "@/lib/chatSocket";
|
||||||
|
|
||||||
const ChatWidgetContext = createContext(null);
|
const ChatWidgetContext = createContext(null);
|
||||||
const API_BASE = "";
|
const API_BASE = "";
|
||||||
@@ -60,24 +61,30 @@ export function ChatWidgetProvider({ children }) {
|
|||||||
const [liveSending, setLiveSending] = useState(false);
|
const [liveSending, setLiveSending] = useState(false);
|
||||||
const [switchingToHuman, setSwitchingToHuman] = useState(false);
|
const [switchingToHuman, setSwitchingToHuman] = useState(false);
|
||||||
|
|
||||||
const pollRef = useRef(null);
|
const stompRef = useRef(null);
|
||||||
const activeConvIdRef = useRef(null);
|
const activeConvIdRef = useRef(null);
|
||||||
const tokenRef = useRef(null); // FIX: store token so polling can restart
|
const tokenRef = useRef(null);
|
||||||
|
|
||||||
const stopPolling = useCallback(() => {
|
const disconnectStomp = useCallback(() => {
|
||||||
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
|
if (stompRef.current) {
|
||||||
|
stompRef.current.deactivate();
|
||||||
|
stompRef.current = null;
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchLiveMessages = useCallback(async (convId, token) => {
|
const subscribeToConversation = useCallback((client, convId) => {
|
||||||
if (!convId || !token) return;
|
client.subscribe(`/topic/chat/conversations/${convId}`, (frame) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${convId}/messages`, {
|
const msg = JSON.parse(frame.body);
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
setLiveMessages((prev) => prev.some((m) => m.id === msg.id) ? prev : [...prev, msg]);
|
||||||
});
|
} catch { /* silent */ }
|
||||||
if (!res.ok) return;
|
});
|
||||||
const data = await res.json();
|
client.subscribe(`/user/queue/chat/conversations`, (frame) => {
|
||||||
if (Array.isArray(data)) setLiveMessages(data);
|
try {
|
||||||
} catch { /* silent */ }
|
const conv = JSON.parse(frame.body);
|
||||||
|
if (conv.id === convId) setActiveConv(conv);
|
||||||
|
} catch { /* silent */ }
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadConversations = useCallback(async (token) => {
|
const loadConversations = useCallback(async (token) => {
|
||||||
@@ -97,21 +104,35 @@ export function ChatWidgetProvider({ children }) {
|
|||||||
|
|
||||||
const openLiveConversation = useCallback(async (convId, token) => {
|
const openLiveConversation = useCallback(async (convId, token) => {
|
||||||
if (!convId || !token) return;
|
if (!convId || !token) return;
|
||||||
stopPolling();
|
disconnectStomp();
|
||||||
tokenRef.current = token; // FIX: save token for polling restart
|
tokenRef.current = token;
|
||||||
setActiveConvId(convId);
|
setActiveConvId(convId);
|
||||||
activeConvIdRef.current = convId;
|
activeConvIdRef.current = convId;
|
||||||
setLiveMessages([]);
|
setLiveMessages([]);
|
||||||
setView("live");
|
setView("live");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${convId}`, {
|
const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${convId}`, {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
if (res.ok) setActiveConv(await res.json());
|
if (res.ok) setActiveConv(await res.json());
|
||||||
} catch { /* silent */ }
|
} catch { /* silent */ }
|
||||||
await fetchLiveMessages(convId, token);
|
|
||||||
pollRef.current = setInterval(() => fetchLiveMessages(convId, token), 2500);
|
try {
|
||||||
}, [stopPolling, fetchLiveMessages]);
|
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) => {
|
const sendLiveMessage = useCallback(async (text, token, convId) => {
|
||||||
if (!text.trim() || liveSending || !token || !convId) return;
|
if (!text.trim() || liveSending || !token || !convId) return;
|
||||||
@@ -122,11 +143,14 @@ export function ChatWidgetProvider({ children }) {
|
|||||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
||||||
body: JSON.stringify({ content: text }),
|
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 {
|
} catch { /* silent */ } finally {
|
||||||
setLiveSending(false);
|
setLiveSending(false);
|
||||||
}
|
}
|
||||||
}, [liveSending, fetchLiveMessages]);
|
}, [liveSending]);
|
||||||
|
|
||||||
const startLiveChat = useCallback(async (token) => {
|
const startLiveChat = useCallback(async (token) => {
|
||||||
if (!token || switchingToHuman) return;
|
if (!token || switchingToHuman) return;
|
||||||
@@ -149,19 +173,22 @@ export function ChatWidgetProvider({ children }) {
|
|||||||
}
|
}
|
||||||
}, [switchingToHuman, openLiveConversation]);
|
}, [switchingToHuman, openLiveConversation]);
|
||||||
|
|
||||||
// FIX: Single effect that handles both stopping AND restarting polling
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen || view !== "live") {
|
if (!isOpen || view !== "live") {
|
||||||
stopPolling();
|
disconnectStomp();
|
||||||
} else if (isOpen && view === "live" && activeConvIdRef.current && tokenRef.current) {
|
} else if (isOpen && view === "live" && activeConvIdRef.current && tokenRef.current && !stompRef.current) {
|
||||||
stopPolling();
|
const convId = activeConvIdRef.current;
|
||||||
fetchLiveMessages(activeConvIdRef.current, tokenRef.current);
|
const token = tokenRef.current;
|
||||||
pollRef.current = setInterval(
|
const client = createStompClient(token);
|
||||||
() => fetchLiveMessages(activeConvIdRef.current, tokenRef.current),
|
client.onConnect = () => subscribeToConversation(client, convId);
|
||||||
2500
|
stompRef.current = client;
|
||||||
);
|
client.activate();
|
||||||
}
|
}
|
||||||
}, [isOpen, view, stopPolling, fetchLiveMessages]);
|
}, [isOpen, view, disconnectStomp, subscribeToConversation]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => disconnectStomp();
|
||||||
|
}, [disconnectStomp]);
|
||||||
|
|
||||||
const toggleOpen = useCallback(() => setIsOpen((o) => !o), []);
|
const toggleOpen = useCallback(() => setIsOpen((o) => !o), []);
|
||||||
const openView = useCallback((v) => setView(v), []);
|
const openView = useCallback((v) => setView(v), []);
|
||||||
@@ -185,4 +212,4 @@ export function useChatWidget() {
|
|||||||
const ctx = useContext(ChatWidgetContext);
|
const ctx = useContext(ChatWidgetContext);
|
||||||
if (!ctx) throw new Error("useChatWidget must be used within ChatWidgetProvider");
|
if (!ctx) throw new Error("useChatWidget must be used within ChatWidgetProvider");
|
||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|||||||
16
web/lib/chatSocket.js
Normal file
16
web/lib/chatSocket.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Client } from "@stomp/stompjs";
|
||||||
|
|
||||||
|
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || "";
|
||||||
|
|
||||||
|
export function createStompClient(token) {
|
||||||
|
return new Client({
|
||||||
|
webSocketFactory: () => {
|
||||||
|
const SockJS = require("sockjs-client");
|
||||||
|
return new SockJS(`${BACKEND_URL}/ws/chat-sockjs`);
|
||||||
|
},
|
||||||
|
connectHeaders: { Authorization: `Bearer ${token}` },
|
||||||
|
reconnectDelay: 5000,
|
||||||
|
heartbeatIncoming: 10000,
|
||||||
|
heartbeatOutgoing: 10000,
|
||||||
|
});
|
||||||
|
}
|
||||||
137
web/package-lock.json
generated
137
web/package-lock.json
generated
@@ -8,11 +8,13 @@
|
|||||||
"name": "threaded-pets",
|
"name": "threaded-pets",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@stomp/stompjs": "^7.3.0",
|
||||||
"@stripe/react-stripe-js": "^3.1.1",
|
"@stripe/react-stripe-js": "^3.1.1",
|
||||||
"@stripe/stripe-js": "^5.5.0",
|
"@stripe/stripe-js": "^5.5.0",
|
||||||
"next": "^16.2.2",
|
"next": "^16.2.2",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3",
|
||||||
|
"sockjs-client": "^1.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
@@ -1232,6 +1234,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@stomp/stompjs": {
|
||||||
|
"version": "7.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-7.3.0.tgz",
|
||||||
|
"integrity": "sha512-nKMLoFfJhrQAqkvvKd1vLq/cVBGCMwPRCD0LqW7UT1fecRx9C3GoKEIR2CYwVuErGeZu8w0kFkl2rlhPlqHVgQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@stripe/react-stripe-js": {
|
"node_modules/@stripe/react-stripe-js": {
|
||||||
"version": "3.10.0",
|
"version": "3.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-3.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-3.10.0.tgz",
|
||||||
@@ -3474,6 +3482,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventsource": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@@ -3535,6 +3552,18 @@
|
|||||||
"reusify": "^1.0.4"
|
"reusify": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/faye-websocket": {
|
||||||
|
"version": "0.11.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
|
||||||
|
"integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"websocket-driver": ">=0.5.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/file-entry-cache": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||||
@@ -3920,6 +3949,12 @@
|
|||||||
"hermes-estree": "0.25.1"
|
"hermes-estree": "0.25.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/http-parser-js": {
|
||||||
|
"version": "0.5.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz",
|
||||||
|
"integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -3957,6 +3992,12 @@
|
|||||||
"node": ">=0.8.19"
|
"node": ">=0.8.19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/internal-slot": {
|
"node_modules/internal-slot": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
||||||
@@ -4932,7 +4973,6 @@
|
|||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
@@ -5402,6 +5442,12 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/querystringify": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/queue-microtask": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
@@ -5494,6 +5540,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/requires-port": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.11",
|
"version": "1.22.11",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||||
@@ -5590,6 +5642,26 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/safe-push-apply": {
|
"node_modules/safe-push-apply": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
|
||||||
@@ -5847,6 +5919,34 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sockjs-client": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.6.1.tgz",
|
||||||
|
"integrity": "sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^3.2.7",
|
||||||
|
"eventsource": "^2.0.2",
|
||||||
|
"faye-websocket": "^0.11.4",
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"url-parse": "^1.5.10"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/sockjs-client"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sockjs-client/node_modules/debug": {
|
||||||
|
"version": "3.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||||
|
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -6414,6 +6514,39 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/url-parse": {
|
||||||
|
"version": "1.5.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||||
|
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"querystringify": "^2.1.1",
|
||||||
|
"requires-port": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/websocket-driver": {
|
||||||
|
"version": "0.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
|
||||||
|
"integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"http-parser-js": ">=0.5.1",
|
||||||
|
"safe-buffer": ">=5.1.0",
|
||||||
|
"websocket-extensions": ">=0.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/websocket-extensions": {
|
||||||
|
"version": "0.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
|
||||||
|
"integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
@@ -9,11 +9,13 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@stomp/stompjs": "^7.3.0",
|
||||||
"@stripe/react-stripe-js": "^3.1.1",
|
"@stripe/react-stripe-js": "^3.1.1",
|
||||||
"@stripe/stripe-js": "^5.5.0",
|
"@stripe/stripe-js": "^5.5.0",
|
||||||
"next": "^16.2.2",
|
"next": "^16.2.2",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3",
|
||||||
|
"sockjs-client": "^1.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
|||||||
Reference in New Issue
Block a user