import React, { createContext, useContext, useEffect, useState } from 'react'; import { io } from 'socket.io-client'; import { useAuth } from './AuthContext'; import toast from 'react-hot-toast'; import APP_CONFIG from '../config/app'; const SocketContext = createContext(); export const SocketProvider = ({ children }) => { const [socket, setSocket] = useState(null); const [connected, setConnected] = useState(false); const [recentDetections, setRecentDetections] = useState([]); const [deviceStatus, setDeviceStatus] = useState({}); const [movementAlerts, setMovementAlerts] = useState([]); const [droneTracking, setDroneTracking] = useState(new Map()); const [notificationsEnabled, setNotificationsEnabled] = useState( localStorage.getItem('notificationsEnabled') !== 'false' // Default to enabled ); const { isAuthenticated } = useAuth(); // Mobile notification management const [notificationCooldown, setNotificationCooldown] = useState(new Map()); const [pendingNotifications, setPendingNotifications] = useState([]); // Check if device is mobile const isMobile = window.innerWidth <= 768; // Mobile-friendly notification handler const handleMobileNotification = (alertData) => { // Skip if notifications are disabled if (!notificationsEnabled) return; const now = Date.now(); const cooldownKey = `${alertData.droneId}-${alertData.analysis.alertLevel}`; const lastNotification = notificationCooldown.get(cooldownKey) || 0; // Rate limiting based on device type and alert level const cooldownPeriod = isMobile ? (alertData.analysis.alertLevel >= 3 ? 5000 : 15000) // 5s for critical, 15s for others on mobile : (alertData.analysis.alertLevel >= 3 ? 2000 : 8000); // 2s for critical, 8s for others on desktop if (now - lastNotification < cooldownPeriod) { // Store pending notification for potential grouping setPendingNotifications(prev => [...prev, alertData]); return; } // Update cooldown setNotificationCooldown(prev => new Map(prev).set(cooldownKey, now)); // Show notification with mobile-optimized settings const alertIcon = alertData.analysis.alertLevel >= 3 ? '🚨' : alertData.analysis.alertLevel >= 2 ? '⚠️' : '📍'; const toastOptions = { duration: isMobile ? (alertData.analysis.alertLevel >= 3 ? 4000 : 2500) // Shorter durations on mobile : (alertData.analysis.alertLevel >= 2 ? 6000 : 4000), icon: alertIcon, style: { background: alertData.analysis.alertLevel >= 3 ? '#fee2e2' : alertData.analysis.alertLevel >= 2 ? '#fef3c7' : '#e0f2fe', fontSize: isMobile ? '13px' : '14px', padding: isMobile ? '0.75rem' : '1rem', maxWidth: isMobile ? '85vw' : '400px', } }; // Create condensed message for mobile const message = isMobile ? `${alertIcon} Drone ${alertData.droneId}: ${getShortDescription(alertData.analysis)}` : alertData.analysis.description; if (alertData.analysis.alertLevel >= 3) { toast.error(message, toastOptions); } else if (alertData.analysis.alertLevel >= 2) { toast.error(message, toastOptions); } else { toast(message, toastOptions); } }; // Helper function to create short descriptions for mobile const getShortDescription = (analysis) => { if (analysis.alertLevel >= 3) return 'Critical proximity'; if (analysis.alertLevel >= 2) return 'Approaching'; return analysis.movement === 'approaching' ? 'Detected' : 'Movement'; }; // Handle grouped notifications for mobile useEffect(() => { if (pendingNotifications.length > 0 && isMobile) { const timer = setTimeout(() => { const criticalCount = pendingNotifications.filter(n => n.analysis.alertLevel >= 3).length; const warningCount = pendingNotifications.filter(n => n.analysis.alertLevel === 2).length; const infoCount = pendingNotifications.filter(n => n.analysis.alertLevel < 2).length; if (criticalCount > 0) { toast.error(`🚨 ${criticalCount} critical alert${criticalCount > 1 ? 's' : ''}`, { duration: 5000, style: { background: '#fee2e2', fontSize: '13px' } }); } else if (warningCount > 0) { toast.error(`⚠️ ${warningCount} warning${warningCount > 1 ? 's' : ''}`, { duration: 3000, style: { background: '#fef3c7', fontSize: '13px' } }); } else if (infoCount > 0) { toast(`📍 ${infoCount} detection${infoCount > 1 ? 's' : ''}`, { duration: 2000, style: { background: '#e0f2fe', fontSize: '13px' } }); } setPendingNotifications([]); }, 3000); // Group notifications for 3 seconds return () => clearTimeout(timer); } }, [pendingNotifications, isMobile]); useEffect(() => { if (isAuthenticated) { // Initialize socket connection const newSocket = io(APP_CONFIG.isProduction ? window.location.origin : 'http://localhost:3001', { path: '/ws' }); newSocket.on('connect', () => { console.log('Connected to server'); setConnected(true); // Join dashboard room for general updates newSocket.emit('join_dashboard'); toast.success('Connected to real-time updates'); }); newSocket.on('disconnect', () => { console.log('Disconnected from server'); setConnected(false); toast.error('Disconnected from server'); }); newSocket.on('connect_error', (error) => { console.error('Connection error:', error); setConnected(false); toast.error('Failed to connect to server'); }); // Listen for drone detections newSocket.on('drone_detection', (detection) => { console.log('New drone detection:', detection); setRecentDetections(prev => [detection, ...prev.slice(0, 49)]); // Keep last 50 // Update drone tracking if (detection.movement_analysis) { const trackingKey = `${detection.drone_id}_${detection.device_id}`; setDroneTracking(prev => { const newTracking = new Map(prev); newTracking.set(trackingKey, { droneId: detection.drone_id, deviceId: detection.device_id, lastDetection: detection, analysis: detection.movement_analysis, timestamp: Date.now() }); return newTracking; }); } // Show toast notification toast.error( `Drone ${detection.drone_id} detected by ${detection.device?.name || `Device ${detection.device_id}`}`, { duration: 5000, icon: '🚨', } ); }); // Listen for drone movement alerts newSocket.on('drone_movement_alert', (alertData) => { console.log('Drone movement alert:', alertData); setMovementAlerts(prev => [alertData, ...prev.slice(0, 19)]); // Keep last 20 alerts // Mobile-friendly notification management handleMobileNotification(alertData); }); // Listen for device heartbeats newSocket.on('device_heartbeat', (heartbeat) => { console.log('Device heartbeat:', heartbeat); setDeviceStatus(prev => ({ ...prev, [heartbeat.device_id]: { ...heartbeat, last_seen: new Date() } })); }); // Listen for device updates newSocket.on('device_updated', (device) => { console.log('Device updated:', device); toast.success(`Device ${device.name || device.id} updated`); }); setSocket(newSocket); return () => { newSocket.disconnect(); }; } else { // Disconnect if not authenticated if (socket) { socket.disconnect(); setSocket(null); setConnected(false); } } }, [isAuthenticated]); const joinDeviceRoom = (deviceId) => { if (socket) { socket.emit('join_device_room', deviceId); } }; const leaveDeviceRoom = (deviceId) => { if (socket) { socket.emit('leave_device_room', deviceId); } }; const clearRecentDetections = () => { setRecentDetections([]); }; const clearMovementAlerts = () => { setMovementAlerts([]); }; const getDroneTracking = (droneId, deviceId) => { const trackingKey = `${droneId}_${deviceId}`; return droneTracking.get(trackingKey); }; // Toggle notifications function const toggleNotifications = () => { const newValue = !notificationsEnabled; setNotificationsEnabled(newValue); localStorage.setItem('notificationsEnabled', newValue.toString()); toast.success( newValue ? 'Notifications enabled' : 'Notifications disabled', { duration: 2000 } ); }; const value = { socket, connected, recentDetections, deviceStatus, movementAlerts, droneTracking, notificationsEnabled, toggleNotifications, joinDeviceRoom, leaveDeviceRoom, clearRecentDetections, clearMovementAlerts, getDroneTracking }; return ( {children} ); }; export const useSocket = () => { const context = useContext(SocketContext); if (!context) { throw new Error('useSocket must be used within a SocketProvider'); } return context; };