Files
SKEEN-Proyecto/frontend/src/lib/chat-notifications.tsx

142 lines
5.0 KiB
TypeScript

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<Record<number, number>>({});
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<number, number> = {};
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) => (
<button
type="button"
onClick={() => {
toast.dismiss(t);
navigate(`/chat?with=${u.id}`);
}}
className="flex items-center gap-3 w-full max-w-sm rounded-xl border border-theme-border bg-theme-surface px-4 py-3 text-left shadow-soft"
>
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-brand-lilac text-theme-heading">
<MessageCircle size={16} />
</span>
<span className="min-w-0">
<span className="block truncate text-sm font-semibold text-theme-heading">{u.name}</span>
<span className="block truncate text-xs text-theme-muted">
{preview || 'Nuevo mensaje'}
</span>
</span>
</button>
), { 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;