socket notification

This commit is contained in:
hamid zarghami
2025-12-13 09:54:55 +03:30
parent baa55860d4
commit 90f04f8c82
4 changed files with 193 additions and 36 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
VITE_TOKEN_NAME = 'dmnu_a_t' VITE_TOKEN_NAME = 'dmnu_a_t'
VITE_REFRESH_TOKEN_NAME = 'dmnu-a-rt' 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_BASE_URL = 'http://192.168.1.112:4001'
# VITE_SOCKET_URL = 'https://dmenuplus-api.dev.danakcorp.com/pager' # VITE_SOCKET_URL = 'https://dmenuplus-api.dev.danakcorp.com/pager'
VITE_SOCKET_URL = 'http://192.168.99.242:4000' VITE_SOCKET_URL = 'http://192.168.99.242:4000'
+8 -35
View File
@@ -1,19 +1,13 @@
import { getToken } from '@/config/func' import { getToken } from '@/config/func'
import { useSocket } from '@/pages/pager/hooks/useSocket' import { useSocket } from '@/pages/pager/hooks/useSocket'
import { type FC, useEffect, useCallback } from 'react' import { type FC, useEffect, useCallback } from 'react'
import type { NotificationPayload } from '@/types/notification.types'
const Notification: FC = () => { const Notification: FC = () => {
// توکن بدون Bearer prefix (مثل کد HTML)
const token = getToken() const token = getToken()
// اضافه کردن /notifications namespace به URL
const socketUrl = `${import.meta.env.VITE_SOCKET_URL}/notifications` const socketUrl = `${import.meta.env.VITE_SOCKET_URL}/notifications`
const { connect, socket, isConnected, error } = useSocket(socketUrl, token!) const { connect, socket, error } = useSocket(socketUrl, token!)
// لاگ کردن وضعیت اتصال
useEffect(() => {
console.log('🔄 Notification: وضعیت اتصال:', isConnected ? 'متصل ✅' : 'قطع ❌')
}, [isConnected])
// لاگ کردن خطاها // لاگ کردن خطاها
useEffect(() => { useEffect(() => {
@@ -24,52 +18,31 @@ const Notification: FC = () => {
useEffect(() => { useEffect(() => {
console.log('🔌 Notification: تلاش برای اتصال...', socketUrl)
connect() connect()
}, [connect]) }, [connect])
const handleGetNotification = useCallback((data: unknown) => { const handleGetNotification = useCallback((data: NotificationPayload) => {
console.log('✅ Notification: دریافت نوتیفیکیشن‌ها:', data) console.log('✅ Notification: دریافت نوتیفیکیشن‌ها:', data)
// alert('دریافت نوتیفیکیشن‌ها: ' + JSON.stringify(data)) // alert('دریافت نوتیفیکیشن‌ها: ' + JSON.stringify(data))
}, []) }, [])
useEffect(() => {
if (isConnected) {
console.log('✅ Notification: متصل شد! ارسال درخواست...')
socket?.emit('get:notifications', { limit: 50 })
}
}, [isConnected, socket])
useEffect(() => { useEffect(() => {
if (socket) { if (socket) {
console.log('📡 Notification: تنظیم listener برای notifications:list')
const errorHandler = (error: unknown) => { socket.on('joined', () => null)
console.error('❌ Notification Error:', error) 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 () => { return () => {
socket.off('notifications:list', handleGetNotification) socket.off('notifications', handleGetNotification)
socket.off('error', errorHandler)
socket.off('exception', exceptionHandler)
} }
} }
}, [socket, handleGetNotification]) }, [socket, handleGetNotification])
return ( return null
<div></div>
)
} }
export default Notification export default Notification
+180
View File
@@ -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<Socket | null>(null);
const [state, setState] = useState<NotificationSocketState>({
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,
};
}
+4
View File
@@ -0,0 +1,4 @@
export interface NotificationPayload {
subject: string;
body: string;
}