feat: Add initial Discord clone application with Convex backend services and Electron React frontend components.
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user