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