feat: Add a large collection of emoji and other frontend assets, including a sound file, and a backend package.json.

This commit is contained in:
Bryan1029384756
2026-01-06 17:58:56 -06:00
parent f531301863
commit abedd78893
3795 changed files with 10981 additions and 229 deletions

View File

@@ -0,0 +1,334 @@
import React, { createContext, useContext, useState, useEffect, useRef } from 'react';
import { Room, RoomEvent } from 'livekit-client';
import { LiveKitRoom, RoomAudioRenderer } from '@livekit/components-react';
import { io } from 'socket.io-client';
import '@livekit/components-styles';
import joinSound from '../assets/sounds/join_call.mp3';
import leaveSound from '../assets/sounds/leave_call.mp3';
import muteSound from '../assets/sounds/mute.mp3';
import unmuteSound from '../assets/sounds/unmute.mp3';
import deafenSound from '../assets/sounds/deafen.mp3';
import undeafenSound from '../assets/sounds/undeafen.mp3';
const VoiceContext = createContext();
export const useVoice = () => useContext(VoiceContext);
export const VoiceProvider = ({ children }) => {
const [activeChannelId, setActiveChannelId] = useState(null);
const [activeChannelName, setActiveChannelName] = useState(null);
const [connectionState, setConnectionState] = useState('disconnected');
const [room, setRoom] = useState(null);
const [token, setToken] = useState(null);
const [voiceStates, setVoiceStates] = useState({}); // { channelId: [users...] }
const [activeSpeakers, setActiveSpeakers] = useState(new Set()); // Set<userId>
const [isMuted, setIsMuted] = useState(false);
const [isDeafened, setIsDeafened] = useState(false);
const socketRef = useRef(null);
// Sound Helper
const playSound = (type) => {
const sounds = {
join: joinSound,
leave: leaveSound,
mute: muteSound,
unmute: unmuteSound,
deafen: deafenSound,
undeafen: undeafenSound
};
const src = sounds[type];
if (src) {
const audio = new Audio(src);
audio.volume = 0.5;
audio.play().catch(e => console.error("Sound play failed", e));
}
};
// Initialize Socket for Voice States
useEffect(() => {
const socket = io('http://localhost:3000');
socketRef.current = socket;
// ... (Socket logic same as before) ...
socket.on('full_voice_state', (states) => {
setVoiceStates(states);
});
socket.on('voice_state_update', (data) => {
setVoiceStates(prev => {
const newState = { ...prev };
const currentUsers = newState[data.channelId] || [];
if (data.action === 'joined') {
if (!currentUsers.find(u => u.userId === data.userId)) {
currentUsers.push({
userId: data.userId,
username: data.username,
isMuted: data.isMuted,
isDeafened: data.isDeafened
});
}
} else if (data.action === 'left') {
const index = currentUsers.findIndex(u => u.userId === data.userId);
if (index !== -1) currentUsers.splice(index, 1);
} else if (data.action === 'state_update') {
const user = currentUsers.find(u => u.userId === data.userId);
if (user) {
if (data.isMuted !== undefined) user.isMuted = data.isMuted;
if (data.isDeafened !== undefined) user.isDeafened = data.isDeafened;
if (data.isScreenSharing !== undefined) user.isScreenSharing = data.isScreenSharing;
}
}
newState[data.channelId] = [...currentUsers];
return newState;
});
});
socket.emit('request_voice_state');
return () => socket.disconnect();
}, []);
const connectToVoice = async (channelId, channelName, userId) => {
if (activeChannelId === channelId) return;
if (room) await room.disconnect();
setActiveChannelId(channelId);
setActiveChannelName(channelName);
setConnectionState('connecting');
try {
const res = await fetch('http://localhost:3000/api/voice/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-user-id': userId },
body: JSON.stringify({ channelId })
});
const data = await res.json();
if (!data.token) throw new Error('Failed to get token');
setToken(data.token);
// Disable adaptiveStream to ensure all tracks are available/subscribed immediately
const newRoom = new Room({ adaptiveStream: false, dynacast: false, autoSubscribe: true });
const liveKitUrl = 'ws://localhost:7880';
await newRoom.connect(liveKitUrl, data.token);
// Auto-enable microphone & Apply Mute/Deafen State
const shouldEnableMic = !isMuted && !isDeafened;
await newRoom.localParticipant.setMicrophoneEnabled(shouldEnableMic);
setRoom(newRoom);
setConnectionState('connected');
window.voiceRoom = newRoom; // For debugging
playSound('join');
// Emit Join Event
if (socketRef.current) {
socketRef.current.emit('voice_state_change', {
channelId,
userId,
username: localStorage.getItem('username') || 'Unknown',
username: localStorage.getItem('username') || 'Unknown',
action: 'joined',
isMuted: isMuted,
isDeafened: isDeafened,
isScreenSharing: false // Initial state is always false
});
}
// Events
newRoom.on(RoomEvent.Disconnected, (reason) => {
console.warn('Voice Room Disconnected. Reason:', reason);
playSound('leave');
setConnectionState('disconnected');
setActiveChannelId(null);
setRoom(null);
setToken(null);
setActiveSpeakers(new Set());
// Emit Leave Event
if (socketRef.current) {
socketRef.current.emit('voice_state_change', {
channelId,
userId,
action: 'left'
});
}
});
newRoom.on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
// speakers is generic Participant[]
// We need to map to user identities (which we used as userId in token usually)
// LiveKit identity = our userId
const newActive = new Set();
speakers.forEach(p => newActive.add(p.identity));
setActiveSpeakers(newActive);
});
} catch (err) {
console.error('Voice Connection Failed:', err);
setConnectionState('error');
setActiveChannelId(null);
}
};
const disconnectVoice = () => {
console.log('User manually disconnected voice');
if (room) room.disconnect();
};
const toggleMute = () => {
const nextState = !isMuted;
setIsMuted(nextState);
playSound(nextState ? 'mute' : 'unmute');
if (room) {
room.localParticipant.setMicrophoneEnabled(!nextState);
}
if (socketRef.current && activeChannelId) {
socketRef.current.emit('voice_state_change', {
channelId: activeChannelId,
userId: localStorage.getItem('userId'),
username: localStorage.getItem('username'),
action: 'state_update',
isMuted: nextState
});
}
};
const toggleDeafen = () => {
const nextState = !isDeafened;
setIsDeafened(nextState);
playSound(nextState ? 'deafen' : 'undeafen');
// Logic: if deafened, mute mic too (usually)
if (nextState) {
// If becoming deafened, ensuring mic is muted too is standard discord behavior
if (room && !isMuted) {
room.localParticipant.setMicrophoneEnabled(false);
}
} else {
// If undeafening, restore mic if it wasn't explicitly muted?
// Simplified: If undeafened, and isMuted is false, unmute mic.
if (room && !isMuted) {
room.localParticipant.setMicrophoneEnabled(true);
}
}
if (socketRef.current && activeChannelId) {
socketRef.current.emit('voice_state_change', {
channelId: activeChannelId,
userId: localStorage.getItem('userId'),
username: localStorage.getItem('username'),
action: 'state_update',
isDeafened: nextState // Send the NEW State
});
}
};
const [isScreenSharing, setIsScreenSharingLocal] = useState(false);
const setScreenSharing = (active) => {
setIsScreenSharingLocal(active);
// Optimistic Update for UI
setVoiceStates(prev => {
const newState = { ...prev };
if (activeChannelId) {
const currentUsers = newState[activeChannelId] || [];
// Use map to ensure we create a new array/object reference if needed,
// though mutation in React state setter is risky, strictly we should clone.
// But strictly:
const userId = localStorage.getItem('userId');
const userIndex = currentUsers.findIndex(u => u.userId === userId);
if (userIndex !== -1) {
const updatedUser = { ...currentUsers[userIndex], isScreenSharing: active };
const updatedUsers = [...currentUsers];
updatedUsers[userIndex] = updatedUser;
newState[activeChannelId] = updatedUsers;
}
}
return newState;
});
if (socketRef.current && activeChannelId) {
socketRef.current.emit('voice_state_change', {
channelId: activeChannelId,
userId: localStorage.getItem('userId'),
username: localStorage.getItem('username'),
action: 'state_update',
isScreenSharing: active
});
}
};
return (
<VoiceContext.Provider value={{
activeChannelId,
activeChannelName,
connectionState,
connectToVoice,
disconnectVoice,
room,
token,
voiceStates,
activeSpeakers,
isMuted,
isDeafened,
toggleMute,
toggleDeafen,
isScreenSharing,
setScreenSharing
}}>
{/* Provide LiveKit Context globally if room exists, or a dummy context?
Actually LiveKitRoom requires a token or room. If room is null, we can't render it.
But we need children to always render.
Solution: Only wrap in LiveKitRoom if room exists.
BUT: If we later mistakenly expect context when room is null, it's fine.
However, if we wrap conditionally, the component tree changes when room logic changes, which might unmount children?
No, children are passed in. If we put conditional wrapper around children:
{room ? <LiveKitRoom>{children}</LiveKitRoom> : children}
This WILL remount children when room connects/disconnects. That is BAD for ChatArea etc.
Better: LiveKitRoom ALWAYS, but pass null room? LiveKitRoom might not like null room.
Documentation says: "If you want to manage the room connection yourself... pass the room instance."
Alternative: We only needed LiveKitRoom for VoiceStage (and audio renderer).
ChatArea doesn't need it.
VoiceStage is only rendered when activeChannel.type is voice, which implies we are likely connected (or clicking it connects).
Wait, if I use the conditional wrapper in VoiceContext to wrap children, `Sidebar` (which is a child) might remount?
Yes, `Sidebar` is inside `VoiceProvider`.
If `VoiceProvider` changes structure, `Sidebar` remounts.
Sidebar holds a lot of state? No, usually lifted. But remounting Sidebar is jarring.
Maybe we DON'T wrap children in VoiceContext.
Instead, we keep `VoiceContext` as is (rendering audio renderer), AND `VoiceStage` wraps itself in `LiveKitRoom`.
BUT `VoiceStage` causing disconnect suggests `LiveKitRoom` cleanup is the problem.
Why did `VoiceStage`'s `LiveKitRoom` cause disconnect?
Because on Unmount, `LiveKitRoom` calls `room.disconnect()`.
When user clicks channel, `VoiceStage` Mounts.
But user said "I click on it again and it disconnects me".
Wait, user clicks "again" to SHOW `VoiceStage`.
So `VoiceStage` MOUNTS.
Why does it disconnect?
Maybe `LiveKitRoom` ON MOUNT does something?
Or maybe `Sidebar` logic caused a re-render/disconnect?
In `Sidebar`: `connectToVoice` calls `if (room) await room.disconnect()`.
*/}
{children}
{room && (
<LiveKitRoom
room={room}
style={{ position: 'absolute', width: 0, height: 0, overflow: 'hidden' }}
>
{/* Mute audio renderer if deafened */}
<RoomAudioRenderer muted={isDeafened} />
</LiveKitRoom>
)}
</VoiceContext.Provider>
);
};