import { useEffect, useMemo, useState } from 'react'; import { useSession } from '../context/SessionContext.jsx'; import { useSocket } from '../context/SocketContext.jsx'; import ChatMessageRow from './ChatMessageRow.jsx'; import ChatTypingRow from './ChatTypingRow.jsx'; import ButtonBoxTile from './ButtonBoxTile.jsx'; const LIFETIME_MS = 3000; const BUTTONBOX_LIFETIME_MS = 3500; const DEFAULT_COLOR = '#2196f3'; function buildKey(alert) { if (alert.id) return alert.id; if (alert.timestamp) return `${alert.timestamp}-${alert.message}`; return `${alert.title || 'alert'}-${alert.message}`; } export default function AlertFeed({ scale = 1 }) { const { alerts, session, pushAlert } = useSession(); const socket = useSocket(); const [now, setNow] = useState(() => Date.now()); useEffect(() => { function onButtonIncrement(payload = {}) { const buttonId = Number(payload.buttonId); if (!Number.isFinite(buttonId) || buttonId < 1 || buttonId > 4) return; const buttons = Array.isArray(session?.buttonBox?.buttons) ? session.buttonBox.buttons : []; const button = buttons.find((entry) => Number(entry?.id) === buttonId) || {}; const count = Number.isFinite(payload.count) ? payload.count : Number(button.count) || 0; const goal = Number.isFinite(button.goal) ? button.goal : 0; const rewardNumber = Number.isFinite(button.rewardNumber) ? button.rewardNumber : '?'; const rewardName = typeof button.rewardName === 'string' && button.rewardName.trim() ? button.rewardName.trim() : 'Unassigned'; pushAlert({ id: `buttonbox-active-${buttonId}`, kind: 'buttonbox-active', lifetimeMs: BUTTONBOX_LIFETIME_MS, payload: { buttonId, count, goal, rewardNumber, rewardName }, }); } socket.on('buttonBox:increment', onButtonIncrement); return () => { socket.off('buttonBox:increment', onButtonIncrement); }; }, [pushAlert, session?.buttonBox?.buttons, socket]); const latest = useMemo(() => alerts.slice(-12).map((alert) => ({ alert, key: buildKey(alert) })), [alerts]); useEffect(() => { if (!latest.length) return undefined; const interval = setInterval(() => setNow(Date.now()), 200); return () => clearInterval(interval); }, [latest.length]); const visibleByKey = new Map(); latest.forEach((item) => { const age = now - (item.alert.receivedAt ?? item.alert.timestamp ?? 0); const lifetimeMs = Number.isFinite(item.alert.lifetimeMs) ? item.alert.lifetimeMs : LIFETIME_MS; if (age <= lifetimeMs) { visibleByKey.set(item.key, { ...item, age }); } }); const visible = Array.from(visibleByKey.values()).slice(-3); if (!visible.length) return null; const containerStyle = scale === 1 ? undefined : { transform: `translateX(-50%) scale(${scale})`, transformOrigin: 'top center', }; const containerClass = scale === 1 ? 'pointer-events-none fixed top-0.5 left-1/2 z-50 flex -translate-x-1/2 flex-col gap-0.5' : 'pointer-events-none fixed top-0.5 left-1/2 z-50 flex flex-col gap-0.5'; return (
{visible.map((toast) => ( ))}
); } function hexToRgb(hex) { const safe = typeof hex === 'string' ? hex.trim() : ''; const match = /^#?([0-9a-fA-F]{6})$/.exec(safe); if (!match) return null; const value = match[1]; return { r: parseInt(value.slice(0, 2), 16), g: parseInt(value.slice(2, 4), 16), b: parseInt(value.slice(4, 6), 16), }; } function AlertToast({ alert }) { if (alert.kind === 'buttonbox-active' && alert.payload) { const payload = alert.payload; return (
); } if (alert.kind === 'chat' && alert.payload) { return ; } if (alert.kind === 'chat-typing' && alert.payload) { return ; } const rgb = hexToRgb(alert.color) || hexToRgb(DEFAULT_COLOR); const backgroundColor = rgb ? `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.18)` : 'rgba(33, 150, 243, 0.18)'; const borderColor = rgb ? `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.45)` : 'rgba(33, 150, 243, 0.45)'; return (

{alert.title || 'Alert'} ยท {alert.message}

); }