import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useToasts } from './Toasts.jsx'; export default function Chat({ socket, roomId, name, isHost, members }) { const { push } = useToasts(); const [messages, setMessages] = useState([]); const [text, setText] = useState(''); const inputRef = useRef(null); const logRef = useRef(null); const [showMentions, setShowMentions] = useState(false); const [mentionQuery, setMentionQuery] = useState(''); const [mentionAtIndex, setMentionAtIndex] = useState(-1); const mentionCandidates = useMemo(() => { const q = (mentionQuery || '').toLowerCase(); return (members || []).filter(m => m.name.toLowerCase().includes(q)).slice(0, 8); }, [mentionQuery, members]); useEffect(() => { const onMsg = (m) => setMessages((prev) => [...prev, m]); const onSys = (m) => setMessages((prev) => [...prev, { name: 'System', text: m.text || 'System message', at: Date.now(), system: true }]); socket.on('chat_message', onMsg); socket.on('system', onSys); return () => { socket.off('chat_message', onMsg); socket.off('system', onSys); }; }, [socket]); useEffect(() => { if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight; }, [messages]); const parseCommand = (t) => { const raw = t.trim(); const lower = raw.toLowerCase(); if (lower.startsWith('/play ')) return { cmd: 'play', arg: raw.slice(6).trim() }; if (lower.startsWith('/kick ')) return { cmd: 'kick', arg: raw.slice(6).trim().replace(/^@/, '') }; if (lower.startsWith('/promote ')) return { cmd: 'promote', arg: raw.slice(9).trim().replace(/^@/, '') }; if (lower.startsWith('/mute ')) return { cmd: 'mute', arg: raw.slice(6).trim().replace(/^@/, '') }; return null; }; const handleSend = () => { const t = text.trim(); if (!t) return; const parsed = parseCommand(t); if (parsed) { if (parsed.cmd === 'play') { const query = parsed.arg; if (!query) return; socket.emit('song_request', { roomId, requester: name, query }); setMessages(prev => [...prev, { name: 'You', text: `Requested: ${query}`, at: Date.now(), system: true }]); setText(''); setShowMentions(false); return; } if (parsed.cmd === 'kick' || parsed.cmd === 'promote' || parsed.cmd === 'mute') { if (!isHost) push('Only host/co-host can run admin commands', 'warn'); else if (!parsed.arg) push('Provide a username like /kick @username', 'warn'); else socket.emit('admin_command', { roomId, cmd: parsed.cmd, targetName: parsed.arg }); setText(''); setShowMentions(false); return; } } const m = { name, text: t, at: Date.now() }; setMessages((prev) => [...prev, m]); socket.emit('chat_message', { roomId, name, text: t }); setText(''); setShowMentions(false); }; const onInputChange = (e) => { const val = e.target.value; const caret = e.target.selectionStart ?? val.length; setText(val); let at = -1; for (let i = caret - 1; i >= 0; i--) { if (val[i] === '@') { at = i; break; } if (/\s/.test(val[i])) break; } if (at >= 0) { const q = val.slice(at + 1, caret); setMentionAtIndex(at); setMentionQuery(q); setShowMentions(true); } else { setShowMentions(false); setMentionAtIndex(-1); setMentionQuery(''); } }; const chooseMention = (m) => { if (!inputRef.current) return; const el = inputRef.current; const val = text; const caret = el.selectionStart ?? val.length; const at = mentionAtIndex >= 0 ? mentionAtIndex : val.lastIndexOf('@', caret); if (at < 0) return; const before = val.slice(0, at); const nextSpace = val.slice(at + 1).search(/\s/); const end = nextSpace < 0 ? val.length : (at + 1 + nextSpace); const after = val.slice(end); const inserted = `${before}@${m.name} `; const nextVal = inserted + after; setText(nextVal); const newPos = inserted.length; setTimeout(() => { el.focus(); el.setSelectionRange(newPos, newPos); }, 0); setShowMentions(false); setMentionQuery(''); setMentionAtIndex(-1); }; return (
{messages.map((m, i) => (
{m.name}: {m.text}
))}
{showMentions && (
{mentionCandidates.length ? mentionCandidates.map(m => (
chooseMention(m)}> @{m.name} {m.role !== 'member' ? `(${m.role})` : ''}
)) :
No matches
}
)}
); }