From 90f04f8c82f4b295eb1905df2bdc97c9c09518e1 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sat, 13 Dec 2025 09:54:55 +0330 Subject: [PATCH] socket notification --- .env | 2 +- src/components/Notification.tsx | 43 ++----- src/hooks/useNotificationSocket.ts | 180 +++++++++++++++++++++++++++++ src/types/notification.types.ts | 4 + 4 files changed, 193 insertions(+), 36 deletions(-) create mode 100644 src/hooks/useNotificationSocket.ts create mode 100644 src/types/notification.types.ts diff --git a/.env b/.env index 3548a85..c59934f 100644 --- a/.env +++ b/.env @@ -1,6 +1,6 @@ VITE_TOKEN_NAME = 'dmnu_a_t' VITE_REFRESH_TOKEN_NAME = 'dmnu-a-rt' -VITE_BASE_URL = 'https://dmenuplus-api.dev.danakcorp.com' +VITE_BASE_URL = 'http://192.168.99.242:4000' # VITE_BASE_URL = 'http://192.168.1.112:4001' # VITE_SOCKET_URL = 'https://dmenuplus-api.dev.danakcorp.com/pager' VITE_SOCKET_URL = 'http://192.168.99.242:4000' \ No newline at end of file diff --git a/src/components/Notification.tsx b/src/components/Notification.tsx index b4189db..011a0ca 100644 --- a/src/components/Notification.tsx +++ b/src/components/Notification.tsx @@ -1,19 +1,13 @@ import { getToken } from '@/config/func' import { useSocket } from '@/pages/pager/hooks/useSocket' import { type FC, useEffect, useCallback } from 'react' +import type { NotificationPayload } from '@/types/notification.types' const Notification: FC = () => { - // توکن بدون Bearer prefix (مثل کد HTML) const token = getToken() - // اضافه کردن /notifications namespace به URL const socketUrl = `${import.meta.env.VITE_SOCKET_URL}/notifications` - const { connect, socket, isConnected, error } = useSocket(socketUrl, token!) - - // لاگ کردن وضعیت اتصال - useEffect(() => { - console.log('🔄 Notification: وضعیت اتصال:', isConnected ? 'متصل ✅' : 'قطع ❌') - }, [isConnected]) + const { connect, socket, error } = useSocket(socketUrl, token!) // لاگ کردن خطاها useEffect(() => { @@ -24,52 +18,31 @@ const Notification: FC = () => { useEffect(() => { - console.log('🔌 Notification: تلاش برای اتصال...', socketUrl) connect() }, [connect]) - const handleGetNotification = useCallback((data: unknown) => { + const handleGetNotification = useCallback((data: NotificationPayload) => { + console.log('✅ Notification: دریافت نوتیفیکیشن‌ها:', data) // alert('دریافت نوتیفیکیشن‌ها: ' + JSON.stringify(data)) }, []) - useEffect(() => { - if (isConnected) { - console.log('✅ Notification: متصل شد! ارسال درخواست...') - socket?.emit('get:notifications', { limit: 50 }) - } - }, [isConnected, socket]) useEffect(() => { if (socket) { - console.log('📡 Notification: تنظیم listener برای notifications:list') - const errorHandler = (error: unknown) => { - console.error('❌ Notification Error:', error) - } + socket.on('joined', () => null) + socket.on('notifications', handleGetNotification) - const exceptionHandler = (error: unknown) => { - console.error('❌ Notification Exception:', error) - } - - socket.on('notifications:list', handleGetNotification) - socket.on('error', errorHandler) - socket.on('exception', exceptionHandler) - - // cleanup function return () => { - socket.off('notifications:list', handleGetNotification) - socket.off('error', errorHandler) - socket.off('exception', exceptionHandler) + socket.off('notifications', handleGetNotification) } } }, [socket, handleGetNotification]) - return ( -
- ) + return null } export default Notification \ No newline at end of file diff --git a/src/hooks/useNotificationSocket.ts b/src/hooks/useNotificationSocket.ts new file mode 100644 index 0000000..f466998 --- /dev/null +++ b/src/hooks/useNotificationSocket.ts @@ -0,0 +1,180 @@ +import { getToken } from "@/config/func"; +import { useEffect, useRef, useState, useCallback } from "react"; +import type { Socket } from "socket.io-client"; +import { io } from "socket.io-client"; +import type { NotificationPayload } from "@/types/notification.types"; + +export type { NotificationPayload }; + +export interface NotificationSocketOptions { + serverUrl?: string; + token: string; + autoConnect?: boolean; + onNotification?: (notification: NotificationPayload) => void; + onJoined?: (data: { room: string; message: string }) => void; + onError?: (error: { message: string }) => void; + onConnect?: () => void; + onDisconnect?: (reason: string) => void; +} + +export interface NotificationSocketState { + isConnected: boolean; + isConnecting: boolean; + room: string | null; + error: string | null; +} + +export function useNotificationSocket(options: NotificationSocketOptions) { + const { + serverUrl = import.meta.env.VITE_SOCKET_URL, + token = getToken(), + autoConnect = true, + onNotification, + onJoined, + onError, + onConnect, + onDisconnect, + } = options; + + const socketRef = useRef(null); + const [state, setState] = useState({ + isConnected: false, + isConnecting: false, + room: null, + error: null, + }); + + // Store callbacks in refs to avoid recreating socket on callback changes + const callbacksRef = useRef({ + onNotification, + onJoined, + onError, + onConnect, + onDisconnect, + }); + + useEffect(() => { + callbacksRef.current = { + onNotification, + onJoined, + onError, + onConnect, + onDisconnect, + }; + }, [onNotification, onJoined, onError, onConnect, onDisconnect]); + + const connect = useCallback(() => { + if (socketRef.current?.connected) { + return; + } + + if (!token) { + setState((prev) => ({ + ...prev, + error: "Token is required for connection", + isConnecting: false, + })); + return; + } + + setState((prev) => ({ ...prev, isConnecting: true, error: null })); + + // Clean token - remove "Bearer " prefix if present + const cleanToken = token.startsWith("Bearer ") ? token.substring(7) : token; + + const socket = io(`${serverUrl}/notifications`, { + transports: ["websocket", "polling"], + reconnection: true, + reconnectionDelay: 1000, + reconnectionAttempts: 5, + auth: { + token: cleanToken, + }, + }); + + socket.on("connect", () => { + setState((prev) => ({ + ...prev, + isConnected: true, + isConnecting: false, + error: null, + })); + callbacksRef.current.onConnect?.(); + }); + + socket.on("disconnect", (reason: string) => { + setState((prev) => ({ + ...prev, + isConnected: false, + isConnecting: false, + room: null, + })); + callbacksRef.current.onDisconnect?.(reason); + }); + + socket.on("connect_error", (error: Error) => { + setState((prev) => ({ + ...prev, + isConnected: false, + isConnecting: false, + error: error.message || "Connection failed", + })); + callbacksRef.current.onError?.({ + message: error.message || "Connection failed", + }); + }); + + socket.on("joined", (data: { room: string; message: string }) => { + setState((prev) => ({ + ...prev, + room: data.room, + })); + callbacksRef.current.onJoined?.(data); + }); + + socket.on("notifications", (data: NotificationPayload) => { + callbacksRef.current.onNotification?.(data); + }); + + socket.on("error", (error: { message: string }) => { + setState((prev) => ({ + ...prev, + error: error.message, + })); + callbacksRef.current.onError?.(error); + }); + + socketRef.current = socket; + }, [serverUrl, token]); + + const disconnect = useCallback(() => { + if (socketRef.current) { + socketRef.current.disconnect(); + socketRef.current = null; + setState({ + isConnected: false, + isConnecting: false, + room: null, + error: null, + }); + } + }, []); + + useEffect(() => { + if (autoConnect && token) { + connect(); + } + + return () => { + disconnect(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [autoConnect, token]); + + return { + ...state, + connect, + disconnect, + socket: socketRef.current, + }; +} diff --git a/src/types/notification.types.ts b/src/types/notification.types.ts new file mode 100644 index 0000000..377b385 --- /dev/null +++ b/src/types/notification.types.ts @@ -0,0 +1,4 @@ +export interface NotificationPayload { + subject: string; + body: string; +}