feat: Implement initial Electron frontend with chat area, user settings, and voice context, and update the TODO list.
All checks were successful
Build and Release / build-and-release (push) Successful in 9m14s

This commit is contained in:
Bryan1029384756
2026-02-13 10:39:01 -06:00
parent 556a561449
commit 63d4208933
5 changed files with 44 additions and 18 deletions

View File

@@ -1,7 +1,7 @@
{ {
"name": "discord", "name": "discord",
"private": true, "private": true,
"version": "1.0.12", "version": "1.0.13",
"description": "A Discord clone built with Convex, React, and Electron", "description": "A Discord clone built with Convex, React, and Electron",
"author": "Moyettes", "author": "Moyettes",
"type": "module", "type": "module",

View File

@@ -1255,7 +1255,7 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
<div className="loading-spinner" /> <div className="loading-spinner" />
</div> </div>
)} )}
{status === 'Exhausted' && decryptedMessages.length > 0 && ( {status === 'Exhausted' && (decryptedMessages.length > 0 || rawMessages.length === 0) && (
<div className="channel-beginning"> <div className="channel-beginning">
<div className="channel-beginning-icon">{isDM ? '@' : '#'}</div> <div className="channel-beginning-icon">{isDM ? '@' : '#'}</div>
<h1 className="channel-beginning-title"> <h1 className="channel-beginning-title">

View File

@@ -4,6 +4,7 @@ import { api } from '../../../../convex/_generated/api';
import Avatar from './Avatar'; import Avatar from './Avatar';
import AvatarCropModal from './AvatarCropModal'; import AvatarCropModal from './AvatarCropModal';
import { useTheme, THEMES, THEME_LABELS } from '../contexts/ThemeContext'; import { useTheme, THEMES, THEME_LABELS } from '../contexts/ThemeContext';
import { useVoice } from '../contexts/VoiceContext';
const THEME_PREVIEWS = { const THEME_PREVIEWS = {
[THEMES.LIGHT]: { bg: '#ffffff', sidebar: '#f2f3f5', tertiary: '#e3e5e8', text: '#313338' }, [THEMES.LIGHT]: { bg: '#ffffff', sidebar: '#f2f3f5', tertiary: '#e3e5e8', text: '#313338' },
@@ -600,6 +601,7 @@ const AppearanceTab = () => {
VOICE & VIDEO TAB VOICE & VIDEO TAB
========================================= */ ========================================= */
const VoiceVideoTab = () => { const VoiceVideoTab = () => {
const { switchDevice, setGlobalOutputVolume } = useVoice();
const [inputDevices, setInputDevices] = useState([]); const [inputDevices, setInputDevices] = useState([]);
const [outputDevices, setOutputDevices] = useState([]); const [outputDevices, setOutputDevices] = useState([]);
const [selectedInput, setSelectedInput] = useState(() => localStorage.getItem('voiceInputDevice') || 'default'); const [selectedInput, setSelectedInput] = useState(() => localStorage.getItem('voiceInputDevice') || 'default');
@@ -631,10 +633,12 @@ const VoiceVideoTab = () => {
useEffect(() => { useEffect(() => {
localStorage.setItem('voiceInputDevice', selectedInput); localStorage.setItem('voiceInputDevice', selectedInput);
switchDevice('audioinput', selectedInput);
}, [selectedInput]); }, [selectedInput]);
useEffect(() => { useEffect(() => {
localStorage.setItem('voiceOutputDevice', selectedOutput); localStorage.setItem('voiceOutputDevice', selectedOutput);
switchDevice('audiooutput', selectedOutput);
}, [selectedOutput]); }, [selectedOutput]);
useEffect(() => { useEffect(() => {
@@ -643,6 +647,7 @@ const VoiceVideoTab = () => {
useEffect(() => { useEffect(() => {
localStorage.setItem('voiceOutputVolume', String(outputVolume)); localStorage.setItem('voiceOutputVolume', String(outputVolume));
setGlobalOutputVolume(outputVolume);
}, [outputVolume]); }, [outputVolume]);
const startMicTest = async () => { const startMicTest = async () => {

View File

@@ -54,6 +54,9 @@ export const VoiceProvider = ({ children }) => {
const [isMuted, setIsMuted] = useState(false); const [isMuted, setIsMuted] = useState(false);
const [isDeafened, setIsDeafened] = useState(false); const [isDeafened, setIsDeafened] = useState(false);
const [isScreenSharing, setIsScreenSharingLocal] = useState(false); const [isScreenSharing, setIsScreenSharingLocal] = useState(false);
const [globalOutputVolume, setGlobalOutputVolume] = useState(() =>
parseInt(localStorage.getItem('voiceOutputVolume') || '100')
);
const isMovingRef = useRef(false); const isMovingRef = useRef(false);
const convex = useConvex(); const convex = useConvex();
@@ -105,9 +108,10 @@ export const VoiceProvider = ({ children }) => {
localStorage.setItem('userVolumes', JSON.stringify(next)); localStorage.setItem('userVolumes', JSON.stringify(next));
return next; return next;
}); });
// Apply volume to LiveKit participant // Apply volume to LiveKit participant (factoring in global output volume)
const participant = room?.remoteParticipants?.get(userId); 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 // Sync personal mute state
if (volume === 0) { if (volume === 0) {
setPersonallyMutedUsers(prev => { setPersonallyMutedUsers(prev => {
@@ -125,13 +129,14 @@ export const VoiceProvider = ({ children }) => {
return next; return next;
}); });
} }
}, [room]); }, [room, globalOutputVolume]);
const getUserVolume = useCallback((userId) => { const getUserVolume = useCallback((userId) => {
return userVolumes[userId] ?? 100; return userVolumes[userId] ?? 100;
}, [userVolumes]); }, [userVolumes]);
const togglePersonalMute = (userId) => { const togglePersonalMute = (userId) => {
const globalVol = globalOutputVolume / 100;
setPersonallyMutedUsers(prev => { setPersonallyMutedUsers(prev => {
const next = new Set(prev); const next = new Set(prev);
if (next.has(userId)) { if (next.has(userId)) {
@@ -140,7 +145,7 @@ export const VoiceProvider = ({ children }) => {
const vol = userVolumes[userId] ?? 100; const vol = userVolumes[userId] ?? 100;
const restoreVol = vol === 0 ? 100 : vol; const restoreVol = vol === 0 ? 100 : vol;
const participant = room?.remoteParticipants?.get(userId); 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 // Update stored volume if it was 0
if (vol === 0) { if (vol === 0) {
setUserVolumes(p => { setUserVolumes(p => {
@@ -224,6 +229,9 @@ export const VoiceProvider = ({ children }) => {
setToken(lkToken); setToken(lkToken);
const storedInputDevice = localStorage.getItem('voiceInputDevice');
const storedOutputDevice = localStorage.getItem('voiceOutputDevice');
const newRoom = new Room({ const newRoom = new Room({
adaptiveStream: true, adaptiveStream: true,
dynacast: true, dynacast: true,
@@ -234,6 +242,7 @@ export const VoiceProvider = ({ children }) => {
noiseSuppression: false, noiseSuppression: false,
channelCount: 1, channelCount: 1,
sampleRate: 48000, sampleRate: 48000,
...(storedInputDevice && storedInputDevice !== 'default' ? { deviceId: { exact: storedInputDevice } } : {}),
}, },
publishDefaults: { publishDefaults: {
audioPreset: { maxBitrate: 96_000 }, audioPreset: { maxBitrate: 96_000 },
@@ -248,6 +257,12 @@ export const VoiceProvider = ({ children }) => {
}); });
await newRoom.connect(import.meta.env.VITE_LIVEKIT_URL, lkToken); 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); await newRoom.localParticipant.setMicrophoneEnabled(!isMuted && !isDeafened);
setRoom(newRoom); setRoom(newRoom);
@@ -354,19 +369,21 @@ export const VoiceProvider = ({ children }) => {
// Re-apply personal mutes/volumes when room or participants change // Re-apply personal mutes/volumes when room or participants change
useEffect(() => { useEffect(() => {
if (!room) return; if (!room) return;
const globalVol = globalOutputVolume / 100;
const applyVolumes = () => { const applyVolumes = () => {
for (const [identity, participant] of room.remoteParticipants) { for (const [identity, participant] of room.remoteParticipants) {
if (personallyMutedUsers.has(identity)) { if (personallyMutedUsers.has(identity)) {
participant.setVolume(0); participant.setVolume(0);
} else { } else {
participant.setVolume((userVolumes[identity] ?? 100) / 100); const userVol = (userVolumes[identity] ?? 100) / 100;
participant.setVolume(userVol * globalVol);
} }
} }
}; };
applyVolumes(); applyVolumes();
room.on(RoomEvent.ParticipantConnected, applyVolumes); room.on(RoomEvent.ParticipantConnected, applyVolumes);
return () => room.off(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 // AFK idle polling: move user to AFK channel when idle exceeds timeout
useEffect(() => { useEffect(() => {
@@ -587,6 +604,15 @@ export const VoiceProvider = ({ children }) => {
await updateVoiceState({ isScreenSharing: active }); 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 ( return (
<VoiceContext.Provider value={{ <VoiceContext.Provider value={{
activeChannelId, activeChannelId,
@@ -616,6 +642,9 @@ export const VoiceProvider = ({ children }) => {
serverSettings, serverSettings,
watchingStreamOf, watchingStreamOf,
setWatchingStreamOf, setWatchingStreamOf,
switchDevice,
globalOutputVolume,
setGlobalOutputVolume,
}}> }}>
{children} {children}
{room && ( {room && (

12
TODO.md
View File

@@ -2,7 +2,7 @@
<!-- - When a user messages you, you should get a notification. On the server list that user profile picture should be their above all servers. right under the discord and above the server-separator. With a red dot next to it. If you get a private dm you should hear the ping sound also --> <!-- - When a user messages you, you should get a notification. On the server list that user profile picture should be their above all servers. right under the discord and above the server-separator. With a red dot next to it. If you get a private dm you should hear the ping sound also -->
- We should play a sound (the ping sound) when a user mentions you or you recieve a private message. <!-- - We should play a sound (the ping sound) when a user mentions you or you recieve a private message. -->
<!-- - In the mention list we should also have roles, so if people do @everyone they should mention everyone in the server, or they can @ a certain role they want to mention from the server. This does not apply to private messages. --> <!-- - In the mention list we should also have roles, so if people do @everyone they should mention everyone in the server, or they can @ a certain role they want to mention from the server. This does not apply to private messages. -->
@@ -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. 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 <!-- - 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.