"use client" import { useCallback, useEffect, useRef, useState } from "react" import { useQueryClient } from "@tanstack/react-query" export interface WsNotification { id: string type: string title: string message: string link: string read: boolean createdAt: string } interface UseNotificationsOptions { userId?: string enabled?: boolean } /** * WebSocket hook for real-time notifications via STOMP over SockJS. * Connects to /ws, subscribes to /user/queue/notifications. * Falls back gracefully if WebSocket is unavailable. */ export function useNotifications({ userId, enabled = true, }: UseNotificationsOptions) { const [connected, setConnected] = useState(false) const [lastNotification, setLastNotification] = useState(null) const stompClientRef = useRef(null) const queryClient = useQueryClient() const connect = useCallback(async () => { if (!userId || !enabled) return try { // Dynamic import to avoid SSR issues const { Client } = await import("@stomp/stompjs") const SockJS = (await import("sockjs-client")).default const backendUrl = process.env.NEXT_PUBLIC_WS_URL || "http://localhost:8080" const client = new Client({ webSocketFactory: () => new SockJS(`${backendUrl}/ws`), reconnectDelay: 5000, heartbeatIncoming: 10000, heartbeatOutgoing: 10000, onConnect: () => { setConnected(true) client.subscribe(`/user/${userId}/queue/notifications`, (message) => { try { const notification: WsNotification = JSON.parse(message.body) setLastNotification(notification) // Invalidate notifications query to refresh badge count queryClient.invalidateQueries({ queryKey: ["notifications"] }) } catch { console.error("Failed to parse notification message") } }) }, onDisconnect: () => { setConnected(false) }, onStompError: (frame) => { console.error("STOMP error:", frame.headers["message"]) setConnected(false) }, }) client.activate() stompClientRef.current = client } catch { // WebSocket libraries not available (SSR or missing deps) — fail silently console.warn("WebSocket connection unavailable, falling back to polling") } }, [userId, enabled, queryClient]) const disconnect = useCallback(() => { const client = stompClientRef.current as { deactivate?: () => void } | null if (client?.deactivate) { client.deactivate() } stompClientRef.current = null setConnected(false) }, []) useEffect(() => { connect() return () => disconnect() }, [connect, disconnect]) return { connected, lastNotification, disconnect, } }