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
All checks were successful
Build and Release / build-and-release (push) Successful in 9m14s
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -1255,7 +1255,7 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
|
||||
<div className="loading-spinner" />
|
||||
</div>
|
||||
)}
|
||||
{status === 'Exhausted' && decryptedMessages.length > 0 && (
|
||||
{status === 'Exhausted' && (decryptedMessages.length > 0 || rawMessages.length === 0) && (
|
||||
<div className="channel-beginning">
|
||||
<div className="channel-beginning-icon">{isDM ? '@' : '#'}</div>
|
||||
<h1 className="channel-beginning-title">
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 (
|
||||
<VoiceContext.Provider value={{
|
||||
activeChannelId,
|
||||
@@ -616,6 +642,9 @@ export const VoiceProvider = ({ children }) => {
|
||||
serverSettings,
|
||||
watchingStreamOf,
|
||||
setWatchingStreamOf,
|
||||
switchDevice,
|
||||
globalOutputVolume,
|
||||
setGlobalOutputVolume,
|
||||
}}>
|
||||
{children}
|
||||
{room && (
|
||||
|
||||
12
TODO.md
12
TODO.md
@@ -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 -->
|
||||
|
||||
- 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. -->
|
||||
|
||||
@@ -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.
|
||||
<!-- - 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 -->
|
||||
Reference in New Issue
Block a user