import type { FC } from 'react'; import { useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { MessageCircle } from 'lucide-react'; import { toast } from './toast'; import { odooApi } from '../services/odoo'; import { useAuth } from './auth'; // Notificaciones del chat interno sin recargar: // polling ligero sobre getChatUsers (mismo mecanismo que useChatUnread); // al detectar mensajes nuevos de un remitente que NO está en pantalla, // muestra un toast discreto (clic → abre /chat con esa conversación) y un // beep sutil generado con WebAudio (sin archivos de audio). // ---- Conversación activa (la registra Chat.tsx) ---- let activeChatUserId: number | null = null; export const setActiveChatUser = (id: number | null) => { activeChatUserId = id; }; // ---- Beep sutil con WebAudio ---- // El AudioContext solo se crea/reanuda tras la primera interacción del // usuario (política de autoplay de los navegadores). let audioCtx: AudioContext | null = null; let audioArmed = false; const armAudio = () => { if (audioArmed) return; audioArmed = true; const init = () => { try { if (!audioCtx) { const Ctor = window.AudioContext || (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext; if (!Ctor) return; audioCtx = new Ctor(); } if (audioCtx.state === 'suspended') audioCtx.resume().catch(() => {}); } catch { // audio no disponible: se omite el sonido } }; window.addEventListener('pointerdown', init); window.addEventListener('keydown', init); }; const playChatBeep = () => { try { if (!audioCtx || audioCtx.state !== 'running') return; const t0 = audioCtx.currentTime; const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(880, t0); // Volumen bajo con caída rápida: beep corto y discreto gain.gain.setValueAtTime(0.0001, t0); gain.gain.exponentialRampToValueAtTime(0.06, t0 + 0.015); gain.gain.exponentialRampToValueAtTime(0.0001, t0 + 0.18); osc.connect(gain).connect(audioCtx.destination); osc.start(t0); osc.stop(t0 + 0.2); } catch { // silencioso } }; const POLL_MS = 15000; /** Polling global de mensajes nuevos: toast + beep por remitente. Montar una sola vez. */ export const useChatNotifications = (intervalMs = POLL_MS) => { const { user } = useAuth(); const navigate = useNavigate(); const prevUnread = useRef>({}); const baselineDone = useRef(false); useEffect(() => { if (!user) return; armAudio(); let alive = true; const fetchUsers = () => { odooApi.getChatUsers() .then((res) => { if (!alive || res.status !== 'success') return; const prev = prevUnread.current; const next: Record = {}; for (const u of res.users) next[u.id] = u.unread; // Primera lectura: solo establece la línea base (no notificar lo viejo) if (!baselineDone.current) { baselineDone.current = true; prevUnread.current = next; return; } for (const u of res.users) { const before = prev[u.id] ?? 0; if (u.unread <= before) continue; // No notificar si esa conversación está abierta en pantalla if (activeChatUserId === u.id) continue; // Un solo toast por remitente por ráfaga (el polling ya agrupa; // se notifica solo cuando el conteo sube) const preview = (u.last_message || '').slice(0, 90); playChatBeep(); toast.custom((t) => ( ), { duration: 6000 }); } prevUnread.current = next; }) .catch(() => {}); }; fetchUsers(); const id = setInterval(fetchUsers, intervalMs); return () => { alive = false; clearInterval(id); }; }, [user, intervalMs, navigate]); }; /** Componente mudo para montar el polling global dentro del Router. */ export const ChatNotifications: FC = () => { useChatNotifications(); return null; }; export default ChatNotifications;