From 63d4208933898ecc232f8c15a034ac2d4da6ff9c Mon Sep 17 00:00:00 2001 From: Bryan1029384756 <23323626+Bryan1029384756@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:39:01 -0600 Subject: [PATCH] feat: Implement initial Electron frontend with chat area, user settings, and voice context, and update the TODO list. --- Frontend/Electron/package.json | 2 +- Frontend/Electron/src/components/ChatArea.jsx | 2 +- .../Electron/src/components/UserSettings.jsx | 5 +++ .../Electron/src/contexts/VoiceContext.jsx | 41 ++++++++++++++++--- TODO.md | 12 +----- 5 files changed, 44 insertions(+), 18 deletions(-) diff --git a/Frontend/Electron/package.json b/Frontend/Electron/package.json index a93fc3d..7c114de 100644 --- a/Frontend/Electron/package.json +++ b/Frontend/Electron/package.json @@ -1,7 +1,7 @@ { "name": "discord", "private": true, - "version": "1.0.12", + "version": "1.0.13", "description": "A Discord clone built with Convex, React, and Electron", "author": "Moyettes", "type": "module", diff --git a/Frontend/Electron/src/components/ChatArea.jsx b/Frontend/Electron/src/components/ChatArea.jsx index 7e7f142..75a3a77 100644 --- a/Frontend/Electron/src/components/ChatArea.jsx +++ b/Frontend/Electron/src/components/ChatArea.jsx @@ -1255,7 +1255,7 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
)} - {status === 'Exhausted' && decryptedMessages.length > 0 && ( + {status === 'Exhausted' && (decryptedMessages.length > 0 || rawMessages.length === 0) && (
{isDM ? '@' : '#'}

diff --git a/Frontend/Electron/src/components/UserSettings.jsx b/Frontend/Electron/src/components/UserSettings.jsx index dc9ada3..d902671 100644 --- a/Frontend/Electron/src/components/UserSettings.jsx +++ b/Frontend/Electron/src/components/UserSettings.jsx @@ -4,6 +4,7 @@ import { api } from '../../../../convex/_generated/api'; import Avatar from './Avatar'; import AvatarCropModal from './AvatarCropModal'; import { useTheme, THEMES, THEME_LABELS } from '../contexts/ThemeContext'; +import { useVoice } from '../contexts/VoiceContext'; const THEME_PREVIEWS = { [THEMES.LIGHT]: { bg: '#ffffff', sidebar: '#f2f3f5', tertiary: '#e3e5e8', text: '#313338' }, @@ -600,6 +601,7 @@ const AppearanceTab = () => { VOICE & VIDEO TAB ========================================= */ const VoiceVideoTab = () => { + const { switchDevice, setGlobalOutputVolume } = useVoice(); const [inputDevices, setInputDevices] = useState([]); const [outputDevices, setOutputDevices] = useState([]); const [selectedInput, setSelectedInput] = useState(() => localStorage.getItem('voiceInputDevice') || 'default'); @@ -631,10 +633,12 @@ const VoiceVideoTab = () => { useEffect(() => { localStorage.setItem('voiceInputDevice', selectedInput); + switchDevice('audioinput', selectedInput); }, [selectedInput]); useEffect(() => { localStorage.setItem('voiceOutputDevice', selectedOutput); + switchDevice('audiooutput', selectedOutput); }, [selectedOutput]); useEffect(() => { @@ -643,6 +647,7 @@ const VoiceVideoTab = () => { useEffect(() => { localStorage.setItem('voiceOutputVolume', String(outputVolume)); + setGlobalOutputVolume(outputVolume); }, [outputVolume]); const startMicTest = async () => { diff --git a/Frontend/Electron/src/contexts/VoiceContext.jsx b/Frontend/Electron/src/contexts/VoiceContext.jsx index 7ee8057..2989202 100644 --- a/Frontend/Electron/src/contexts/VoiceContext.jsx +++ b/Frontend/Electron/src/contexts/VoiceContext.jsx @@ -54,6 +54,9 @@ export const VoiceProvider = ({ children }) => { const [isMuted, setIsMuted] = useState(false); const [isDeafened, setIsDeafened] = useState(false); const [isScreenSharing, setIsScreenSharingLocal] = useState(false); + const [globalOutputVolume, setGlobalOutputVolume] = useState(() => + parseInt(localStorage.getItem('voiceOutputVolume') || '100') + ); const isMovingRef = useRef(false); const convex = useConvex(); @@ -105,9 +108,10 @@ export const VoiceProvider = ({ children }) => { localStorage.setItem('userVolumes', JSON.stringify(next)); return next; }); - // Apply volume to LiveKit participant + // Apply volume to LiveKit participant (factoring in global output volume) const participant = room?.remoteParticipants?.get(userId); - if (participant) participant.setVolume(volume / 100); + const globalVol = globalOutputVolume / 100; + if (participant) participant.setVolume((volume / 100) * globalVol); // Sync personal mute state if (volume === 0) { setPersonallyMutedUsers(prev => { @@ -125,13 +129,14 @@ export const VoiceProvider = ({ children }) => { return next; }); } - }, [room]); + }, [room, globalOutputVolume]); const getUserVolume = useCallback((userId) => { return userVolumes[userId] ?? 100; }, [userVolumes]); const togglePersonalMute = (userId) => { + const globalVol = globalOutputVolume / 100; setPersonallyMutedUsers(prev => { const next = new Set(prev); if (next.has(userId)) { @@ -140,7 +145,7 @@ export const VoiceProvider = ({ children }) => { const vol = userVolumes[userId] ?? 100; const restoreVol = vol === 0 ? 100 : vol; const participant = room?.remoteParticipants?.get(userId); - if (participant) participant.setVolume(restoreVol / 100); + if (participant) participant.setVolume((restoreVol / 100) * globalVol); // Update stored volume if it was 0 if (vol === 0) { setUserVolumes(p => { @@ -224,6 +229,9 @@ export const VoiceProvider = ({ children }) => { setToken(lkToken); + const storedInputDevice = localStorage.getItem('voiceInputDevice'); + const storedOutputDevice = localStorage.getItem('voiceOutputDevice'); + const newRoom = new Room({ adaptiveStream: true, dynacast: true, @@ -234,6 +242,7 @@ export const VoiceProvider = ({ children }) => { noiseSuppression: false, channelCount: 1, sampleRate: 48000, + ...(storedInputDevice && storedInputDevice !== 'default' ? { deviceId: { exact: storedInputDevice } } : {}), }, publishDefaults: { audioPreset: { maxBitrate: 96_000 }, @@ -248,6 +257,12 @@ export const VoiceProvider = ({ children }) => { }); await newRoom.connect(import.meta.env.VITE_LIVEKIT_URL, lkToken); + if (storedOutputDevice && storedOutputDevice !== 'default') { + await newRoom.switchActiveDevice('audiooutput', storedOutputDevice).catch(e => + console.warn('Failed to switch output device on connect:', e) + ); + } + await newRoom.localParticipant.setMicrophoneEnabled(!isMuted && !isDeafened); setRoom(newRoom); @@ -354,19 +369,21 @@ export const VoiceProvider = ({ children }) => { // Re-apply personal mutes/volumes when room or participants change useEffect(() => { if (!room) return; + const globalVol = globalOutputVolume / 100; const applyVolumes = () => { for (const [identity, participant] of room.remoteParticipants) { if (personallyMutedUsers.has(identity)) { participant.setVolume(0); } else { - participant.setVolume((userVolumes[identity] ?? 100) / 100); + const userVol = (userVolumes[identity] ?? 100) / 100; + participant.setVolume(userVol * globalVol); } } }; applyVolumes(); room.on(RoomEvent.ParticipantConnected, applyVolumes); return () => room.off(RoomEvent.ParticipantConnected, applyVolumes); - }, [room, personallyMutedUsers, userVolumes]); + }, [room, personallyMutedUsers, userVolumes, globalOutputVolume]); // AFK idle polling: move user to AFK channel when idle exceeds timeout useEffect(() => { @@ -587,6 +604,15 @@ export const VoiceProvider = ({ children }) => { await updateVoiceState({ isScreenSharing: active }); }; + const switchDevice = useCallback(async (kind, deviceId) => { + if (!room) return; + try { + await room.switchActiveDevice(kind, deviceId); + } catch (e) { + console.warn(`Failed to switch ${kind} device:`, e); + } + }, [room]); + return ( { serverSettings, watchingStreamOf, setWatchingStreamOf, + switchDevice, + globalOutputVolume, + setGlobalOutputVolume, }}> {children} {room && ( diff --git a/TODO.md b/TODO.md index 8935b5d..9012b40 100644 --- a/TODO.md +++ b/TODO.md @@ -2,7 +2,7 @@ -- We should play a sound (the ping sound) when a user mentions you or you recieve a private message. + @@ -38,12 +38,4 @@ Can we make sure Voice and Video work. We have the users input and output devices but if i select any it dosent show it changed. I want to make sure that the users can select their input and output devices and that it works for livekit. -- Lets make it so we can upload a custom image for the server that will show on the sidebar. Make the image editing like how we do it for avatars but instead of a circle that we have to show users cut off its a square with a border radius, match it to the boarder radius of the server-item-wrapper - - -Why dont all my chats have a - -Welcome to #chatName -This is the start of the #chatName channel. - -Its only the first one that has this. \ No newline at end of file + \ No newline at end of file