Spaces:
Sleeping
Sleeping
File size: 5,396 Bytes
b9c2e83 9fb1628 b9c2e83 9fb1628 b9c2e83 8567472 9fb1628 b9c2e83 7c539f4 b9c2e83 9fb1628 b9c2e83 9fb1628 8567472 b9c2e83 7c539f4 b9c2e83 7c539f4 b9c2e83 9fb1628 b9c2e83 9fb1628 b9c2e83 7c539f4 b9c2e83 9fb1628 b9c2e83 8567472 9fb1628 8567472 b9c2e83 8567472 9fb1628 8567472 b9c2e83 9fb1628 b9c2e83 7c539f4 b9c2e83 7c539f4 b9c2e83 7c539f4 b9c2e83 9fb1628 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
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>
);
}
|