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",
|
"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",
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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
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 -->
|
<!-- - 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.
|
|
||||||
Reference in New Issue
Block a user