test / client /src /Chat.jsx
akborana4's picture
Update client/src/Chat.jsx
7c539f4 verified
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 (
<div className="chat" style={{ position: 'relative' }}>
<div ref={logRef} className="chat-log">
{messages.map((m, i) => (
<div key={i} style={{ marginBottom:6 }}>
<b style={{ color: m.system ? 'var(--accent)' : 'var(--accent-2)' }}>{m.name}:</b> {m.text}
</div>
))}
</div>
<div className="chat-input">
<input
ref={inputRef}
className="input"
value={text}
onChange={onInputChange}
placeholder="Type — /play <song>, /kick @user, /promote @user, /mute @user"
/>
<button className="btn primary" onClick={handleSend}>Send</button>
</div>
{showMentions && (
<div className="mentions" style={{ left: 8, bottom: 52 }}>
{mentionCandidates.length ? mentionCandidates.map(m => (
<div key={m.id} className="item" onClick={() => chooseMention(m)}>
@{m.name} {m.role !== 'member' ? `(${m.role})` : ''}
</div>
)) : <div className="item" style={{ opacity:.7 }}>No matches</div>}
</div>
)}
</div>
);
}