feat: Add initial Discord clone application with Convex backend services and Electron React frontend components.

This commit is contained in:
Bryan1029384756
2026-02-10 05:27:10 -06:00
parent 47f173c79b
commit 34e9790db9
29 changed files with 3254 additions and 1398 deletions

View File

@@ -12,43 +12,51 @@ import unmuteSound from '../assets/sounds/unmute.mp3';
import deafenSound from '../assets/sounds/deafen.mp3';
import undeafenSound from '../assets/sounds/undeafen.mp3';
const soundMap = {
join: joinSound,
leave: leaveSound,
mute: muteSound,
unmute: unmuteSound,
deafen: deafenSound,
undeafen: undeafenSound
};
const VoiceContext = createContext();
export const useVoice = () => useContext(VoiceContext);
function playSound(type) {
const src = soundMap[type];
if (!src) return;
const audio = new Audio(src);
audio.volume = 0.5;
audio.play().catch(e => console.error("Sound play failed", e));
}
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 [activeSpeakers, setActiveSpeakers] = useState(new Set()); // Set<userId>
const [activeSpeakers, setActiveSpeakers] = useState(new Set());
const [isMuted, setIsMuted] = useState(false);
const [isDeafened, setIsDeafened] = useState(false);
const [isScreenSharing, setIsScreenSharingLocal] = useState(false);
const convex = useConvex();
// Reactive voice states from Convex (replaces socket.io)
const voiceStates = useQuery(api.voiceState.getAll) || {};
// 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));
async function updateVoiceState(fields) {
const userId = localStorage.getItem('userId');
if (!userId || !activeChannelId) return;
try {
await convex.mutation(api.voiceState.updateState, { userId, ...fields });
} catch (e) {
console.error('Failed to update voice state:', e);
}
};
}
const connectToVoice = async (channelId, channelName, userId) => {
if (activeChannelId === channelId) return;
@@ -60,7 +68,6 @@ export const VoiceProvider = ({ children }) => {
setConnectionState('connecting');
try {
// Get LiveKit token via Convex action
const { token: lkToken } = await convex.action(api.voice.getToken, {
channelId,
userId,
@@ -71,30 +78,24 @@ export const VoiceProvider = ({ children }) => {
setToken(lkToken);
// Disable adaptiveStream to ensure all tracks are available/subscribed immediately
const newRoom = new Room({ adaptiveStream: false, dynacast: false, autoSubscribe: true });
const liveKitUrl = import.meta.env.VITE_LIVEKIT_URL;
await newRoom.connect(liveKitUrl, lkToken);
await newRoom.connect(import.meta.env.VITE_LIVEKIT_URL, lkToken);
// Auto-enable microphone & Apply Mute/Deafen State
const shouldEnableMic = !isMuted && !isDeafened;
await newRoom.localParticipant.setMicrophoneEnabled(shouldEnableMic);
await newRoom.localParticipant.setMicrophoneEnabled(!isMuted && !isDeafened);
setRoom(newRoom);
setConnectionState('connected');
window.voiceRoom = newRoom; // For debugging
window.voiceRoom = newRoom;
playSound('join');
// Update voice state in Convex
await convex.mutation(api.voiceState.join, {
channelId,
userId,
username: localStorage.getItem('username') || 'Unknown',
isMuted: isMuted,
isDeafened: isDeafened,
isMuted,
isDeafened,
});
// Events
newRoom.on(RoomEvent.Disconnected, async (reason) => {
console.warn('Voice Room Disconnected. Reason:', reason);
playSound('leave');
@@ -104,7 +105,6 @@ export const VoiceProvider = ({ children }) => {
setToken(null);
setActiveSpeakers(new Set());
// Remove voice state in Convex
try {
await convex.mutation(api.voiceState.leave, { userId });
} catch (e) {
@@ -113,9 +113,7 @@ export const VoiceProvider = ({ children }) => {
});
newRoom.on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
const newActive = new Set();
speakers.forEach(p => newActive.add(p.identity));
setActiveSpeakers(newActive);
setActiveSpeakers(new Set(speakers.map(p => p.identity)));
});
} catch (err) {
@@ -137,63 +135,22 @@ export const VoiceProvider = ({ children }) => {
if (room) {
room.localParticipant.setMicrophoneEnabled(!nextState);
}
const userId = localStorage.getItem('userId');
if (userId && activeChannelId) {
try {
await convex.mutation(api.voiceState.updateState, {
userId,
isMuted: nextState,
});
} catch (e) {
console.error('Failed to update mute state:', e);
}
}
await updateVoiceState({ isMuted: nextState });
};
const toggleDeafen = async () => {
const nextState = !isDeafened;
setIsDeafened(nextState);
playSound(nextState ? 'deafen' : 'undeafen');
if (nextState) {
if (room && !isMuted) {
room.localParticipant.setMicrophoneEnabled(false);
}
} else {
if (room && !isMuted) {
room.localParticipant.setMicrophoneEnabled(true);
}
}
const userId = localStorage.getItem('userId');
if (userId && activeChannelId) {
try {
await convex.mutation(api.voiceState.updateState, {
userId,
isDeafened: nextState,
});
} catch (e) {
console.error('Failed to update deafen state:', e);
}
if (room && !isMuted) {
room.localParticipant.setMicrophoneEnabled(!nextState);
}
await updateVoiceState({ isDeafened: nextState });
};
const [isScreenSharing, setIsScreenSharingLocal] = useState(false);
const setScreenSharing = async (active) => {
setIsScreenSharingLocal(active);
const userId = localStorage.getItem('userId');
if (userId && activeChannelId) {
try {
await convex.mutation(api.voiceState.updateState, {
userId,
isScreenSharing: active,
});
} catch (e) {
console.error('Failed to update screen sharing state:', e);
}
}
await updateVoiceState({ isScreenSharing: active });
};
return (
@@ -220,7 +177,6 @@ export const VoiceProvider = ({ children }) => {
room={room}
style={{ position: 'absolute', width: 0, height: 0, overflow: 'hidden' }}
>
{/* Mute audio renderer if deafened */}
<RoomAudioRenderer muted={isDeafened} />
</LiveKitRoom>
)}