feat: Implement core Discord clone functionality including Convex backend, Electron frontend, and UI components for chat, voice, and settings.
This commit is contained in:
@@ -1,16 +1,18 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useConvex } from 'convex/react';
|
||||
import { api } from '../../../../convex/_generated/api';
|
||||
import { useVoice } from '../contexts/VoiceContext';
|
||||
import ChannelSettingsModal from './ChannelSettingsModal';
|
||||
import ServerSettingsModal from './ServerSettingsModal';
|
||||
import ChannelSettingsModal from './ChannelSettingsModal';
|
||||
import ServerSettingsModal from './ServerSettingsModal';
|
||||
import ScreenShareModal from './ScreenShareModal';
|
||||
import DMList from './DMList'; // Import DMList
|
||||
import DMList from './DMList';
|
||||
import { Track } from 'livekit-client';
|
||||
import muteIcon from '../assets/icons/mute.svg';
|
||||
import mutedIcon from '../assets/icons/muted.svg';
|
||||
import defeanIcon from '../assets/icons/defean.svg';
|
||||
import defeanedIcon from '../assets/icons/defeaned.svg';
|
||||
import settingsIcon from '../assets/icons/settings.svg';
|
||||
import voiceIcon from '../assets/icons/voice.svg';
|
||||
import voiceIcon from '../assets/icons/voice.svg';
|
||||
import disconnectIcon from '../assets/icons/disconnect.svg';
|
||||
import cameraIcon from '../assets/icons/camera.svg';
|
||||
import screenIcon from '../assets/icons/screen.svg';
|
||||
@@ -24,28 +26,26 @@ const ColoredIcon = ({ src, color, size = '20px' }) => (
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0 // Prevent shrinking in flex containers
|
||||
flexShrink: 0
|
||||
}}>
|
||||
<img
|
||||
src={src}
|
||||
alt=""
|
||||
<img
|
||||
src={src}
|
||||
alt=""
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
transform: 'translateX(-1000px)',
|
||||
filter: `drop-shadow(1000px 0 0 ${color})`
|
||||
}}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const UserControlPanel = ({ username }) => {
|
||||
const { isMuted, isDeafened, toggleMute, toggleDeafen, connectionState } = useVoice();
|
||||
|
||||
// Check if muted explicitly OR implicitly via deafen
|
||||
// User requested: "turn the mic icon red to show they are muted also"
|
||||
|
||||
const effectiveMute = isMuted || isDeafened;
|
||||
|
||||
|
||||
const getUserColor = (name) => {
|
||||
const colors = ['#5865F2', '#EBA7CD', '#57F287', '#FEE75C', '#EB459E', '#ED4245'];
|
||||
let hash = 0;
|
||||
@@ -70,14 +70,14 @@ const UserControlPanel = ({ username }) => {
|
||||
flexShrink: 0
|
||||
}}>
|
||||
{/* User Info */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginRight: 'auto',
|
||||
padding: '4px',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
':hover': { backgroundColor: 'rgba(255,255,255,0.05)' }
|
||||
':hover': { backgroundColor: 'rgba(255,255,255,0.05)' }
|
||||
}}>
|
||||
<div style={{ position: 'relative', marginRight: '8px' }}>
|
||||
<div style={{
|
||||
@@ -118,7 +118,7 @@ const UserControlPanel = ({ username }) => {
|
||||
|
||||
{/* Controls */}
|
||||
<div style={{ display: 'flex' }}>
|
||||
<button
|
||||
<button
|
||||
onClick={toggleMute}
|
||||
title={effectiveMute ? "Unmute" : "Mute"}
|
||||
style={{
|
||||
@@ -132,12 +132,12 @@ const UserControlPanel = ({ username }) => {
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<ColoredIcon
|
||||
src={effectiveMute ? mutedIcon : muteIcon}
|
||||
color={effectiveMute ? ICON_COLOR_ACTIVE : ICON_COLOR_DEFAULT}
|
||||
<ColoredIcon
|
||||
src={effectiveMute ? mutedIcon : muteIcon}
|
||||
color={effectiveMute ? ICON_COLOR_ACTIVE : ICON_COLOR_DEFAULT}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
onClick={toggleDeafen}
|
||||
title={isDeafened ? "Undeafen" : "Deafen"}
|
||||
style={{
|
||||
@@ -151,12 +151,12 @@ const UserControlPanel = ({ username }) => {
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<ColoredIcon
|
||||
src={isDeafened ? defeanedIcon : defeanIcon}
|
||||
color={isDeafened ? ICON_COLOR_ACTIVE : ICON_COLOR_DEFAULT}
|
||||
<ColoredIcon
|
||||
src={isDeafened ? defeanedIcon : defeanIcon}
|
||||
color={isDeafened ? ICON_COLOR_ACTIVE : ICON_COLOR_DEFAULT}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
title="User Settings"
|
||||
style={{
|
||||
background: 'transparent',
|
||||
@@ -169,9 +169,9 @@ const UserControlPanel = ({ username }) => {
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<ColoredIcon
|
||||
src={settingsIcon}
|
||||
color={ICON_COLOR_DEFAULT}
|
||||
<ColoredIcon
|
||||
src={settingsIcon}
|
||||
color={ICON_COLOR_DEFAULT}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
@@ -181,31 +181,28 @@ const UserControlPanel = ({ username }) => {
|
||||
|
||||
|
||||
|
||||
const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKeys, onChannelCreated, view, onViewChange, onOpenDM, activeDMChannel, setActiveDMChannel, dmChannels }) => {
|
||||
const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKeys, view, onViewChange, onOpenDM, activeDMChannel, setActiveDMChannel, dmChannels }) => {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isServerSettingsOpen, setIsServerSettingsOpen] = useState(false); // New State
|
||||
const [isServerSettingsOpen, setIsServerSettingsOpen] = useState(false);
|
||||
const [newChannelName, setNewChannelName] = useState('');
|
||||
const [newChannelType, setNewChannelType] = useState('text'); // 'text' or 'voice'
|
||||
const [newChannelType, setNewChannelType] = useState('text');
|
||||
const [editingChannel, setEditingChannel] = useState(null);
|
||||
const [isScreenShareModalOpen, setIsScreenShareModalOpen] = useState(false);
|
||||
|
||||
// Callbacks for Modal
|
||||
const convex = useConvex();
|
||||
|
||||
// Callbacks for Modal - Convex is reactive, no need to manually refresh
|
||||
const onRenameChannel = (id, newName) => {
|
||||
if (onChannelCreated) onChannelCreated();
|
||||
// Convex reactive queries auto-update
|
||||
};
|
||||
|
||||
const onDeleteChannel = (id) => {
|
||||
if (activeChannel === id) onSelectChannel(null);
|
||||
if (onChannelCreated) onChannelCreated();
|
||||
// Convex reactive queries auto-update
|
||||
};
|
||||
|
||||
const { connectToVoice, activeChannelId: voiceChannelId, connectionState, disconnectVoice, activeChannelName: voiceChannelName, voiceStates, room, activeSpeakers, setScreenSharing } = useVoice();
|
||||
|
||||
// ... helper for public key ...
|
||||
const getMyPublicKey = async (userId) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleStartCreate = () => {
|
||||
setIsCreating(true);
|
||||
setNewChannelName('');
|
||||
@@ -222,7 +219,7 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
const name = newChannelName.trim();
|
||||
const type = newChannelType;
|
||||
const userId = localStorage.getItem('userId');
|
||||
|
||||
|
||||
if (!userId) {
|
||||
alert("Please login first.");
|
||||
setIsCreating(false);
|
||||
@@ -230,38 +227,27 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Create Channel
|
||||
const createRes = await fetch('http://localhost:3000/api/channels/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, type })
|
||||
});
|
||||
const { id: channelId, error } = await createRes.json();
|
||||
if (error) throw new Error(error);
|
||||
// 1. Create Channel via Convex
|
||||
const { id: channelId } = await convex.mutation(api.channels.create, { name, type });
|
||||
|
||||
// 2. Generate Key (Only needed for encrypted TEXT channels roughly, but we do it for all to simplify logic?
|
||||
// Actually, Voice only needs access token. But keeping key logic doesn't hurt for consistent DB.
|
||||
// Voice channels might use the key for text chat INTside the voice channel later?)
|
||||
// 2. Generate Key
|
||||
const keyBytes = new Uint8Array(32);
|
||||
crypto.getRandomValues(keyBytes);
|
||||
const keyHex = Array.from(keyBytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
// 3. Encrypt Key for ALL Users (Group Logic)
|
||||
// 3. Encrypt Key for ALL Users
|
||||
try {
|
||||
// Fetch all public keys
|
||||
const usersRes = await fetch('http://localhost:3000/api/auth/users/public-keys');
|
||||
const users = await usersRes.json();
|
||||
|
||||
const users = await convex.query(api.auth.getPublicKeys, {});
|
||||
|
||||
const batchKeys = [];
|
||||
|
||||
for (const u of users) {
|
||||
if (!u.public_identity_key) continue;
|
||||
|
||||
|
||||
try {
|
||||
// Correct Format: JSON Stringify { [channelId]: key }
|
||||
const payload = JSON.stringify({ [channelId]: keyHex });
|
||||
const encryptedKeyHex = await window.cryptoAPI.publicEncrypt(u.public_identity_key, payload);
|
||||
|
||||
|
||||
batchKeys.push({
|
||||
channelId,
|
||||
userId: u.id,
|
||||
@@ -273,24 +259,18 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Upload Keys Batch
|
||||
await fetch('http://localhost:3000/api/channels/keys', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(batchKeys)
|
||||
});
|
||||
|
||||
// 5. Notify Everyone (NOW it is safe)
|
||||
await fetch(`http://localhost:3000/api/channels/${channelId}/notify`, { method: 'POST' });
|
||||
// 4. Upload Keys Batch via Convex
|
||||
await convex.mutation(api.channelKeys.uploadKeys, { keys: batchKeys });
|
||||
|
||||
// No need to notify - Convex queries are reactive!
|
||||
|
||||
} catch (keyErr) {
|
||||
console.error("Critical: Failed to distribute keys", keyErr);
|
||||
alert("Channel created but key distribution failed.");
|
||||
}
|
||||
|
||||
// 6. Refresh
|
||||
// 5. Done - Convex reactive queries auto-update the channel list
|
||||
setIsCreating(false);
|
||||
if (onChannelCreated) onChannelCreated();
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -299,11 +279,6 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// ... (rest of logic)
|
||||
|
||||
|
||||
const handleCreateInvite = async () => {
|
||||
const userId = localStorage.getItem('userId');
|
||||
if (!userId) {
|
||||
@@ -320,8 +295,8 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
|
||||
// 2. Prepare Key Bundle
|
||||
const generalChannel = channels.find(c => c.name === 'general');
|
||||
const targetChannelId = generalChannel ? generalChannel.id : activeChannel; // Fallback to active if no general
|
||||
|
||||
const targetChannelId = generalChannel ? generalChannel._id : activeChannel;
|
||||
|
||||
if (!targetChannelId) {
|
||||
alert("No channel selected.");
|
||||
return;
|
||||
@@ -334,33 +309,27 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = JSON.stringify({
|
||||
[targetChannelId]: targetKey
|
||||
const payload = JSON.stringify({
|
||||
[targetChannelId]: targetKey
|
||||
});
|
||||
|
||||
// 3. Encrypt Payload
|
||||
const encrypted = await window.cryptoAPI.encryptData(payload, inviteSecret);
|
||||
|
||||
const blob = JSON.stringify({
|
||||
c: encrypted.content,
|
||||
t: encrypted.tag,
|
||||
iv: encrypted.iv
|
||||
|
||||
const blob = JSON.stringify({
|
||||
c: encrypted.content,
|
||||
t: encrypted.tag,
|
||||
iv: encrypted.iv
|
||||
});
|
||||
|
||||
// 4. Send to Server
|
||||
const res = await fetch('http://localhost:3000/api/invites/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
code: inviteCode,
|
||||
encryptedPayload: blob,
|
||||
createdBy: userId,
|
||||
keyVersion: 1
|
||||
})
|
||||
// 4. Create invite via Convex
|
||||
await convex.mutation(api.invites.create, {
|
||||
code: inviteCode,
|
||||
encryptedPayload: blob,
|
||||
createdBy: userId,
|
||||
keyVersion: 1
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Server rejected invite creation');
|
||||
|
||||
// 5. Show Link
|
||||
const link = `http://localhost:5173/#/register?code=${inviteCode}&key=${inviteSecret}`;
|
||||
navigator.clipboard.writeText(link);
|
||||
@@ -375,7 +344,7 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
// Screen Share Handler
|
||||
const handleScreenShareSelect = async (selection) => {
|
||||
if (!room) return;
|
||||
|
||||
|
||||
try {
|
||||
// Unpublish existing screen share if any
|
||||
if (room.localParticipant.isScreenShareEnabled) {
|
||||
@@ -409,9 +378,9 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
name: 'screen_share',
|
||||
source: Track.Source.ScreenShare
|
||||
});
|
||||
|
||||
|
||||
setScreenSharing(true);
|
||||
|
||||
|
||||
track.onended = () => {
|
||||
setScreenSharing(false);
|
||||
room.localParticipant.setScreenShareEnabled(false).catch(console.error);
|
||||
@@ -423,7 +392,7 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
alert("Failed to share screen: " + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Toggle Modal instead of direct toggle
|
||||
const handleScreenShareClick = () => {
|
||||
if (room?.localParticipant.isScreenShareEnabled) {
|
||||
@@ -440,31 +409,29 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
<div style={{ display: 'flex', flex: 1, minHeight: 0 }}>
|
||||
<div className="server-list">
|
||||
{/* Home Button */}
|
||||
<div
|
||||
<div
|
||||
className={`server-icon ${view === 'me' ? 'active' : ''}`}
|
||||
onClick={() => onViewChange('me')}
|
||||
style={{
|
||||
style={{
|
||||
backgroundColor: view === 'me' ? '#5865F2' : '#36393f',
|
||||
color: view === 'me' ? '#fff' : '#dcddde',
|
||||
marginBottom: '8px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
{/* Discord Logo / Home Icon */}
|
||||
<svg width="28" height="20" viewBox="0 0 28 20">
|
||||
<path fill="currentColor" d="M23.0212 1.67671C21.3107 0.879656 19.5079 0.318797 17.6584 0C17.4062 0.461742 17.1749 0.934541 16.9708 1.4184C15.0172 1.11817 13.0907 1.11817 11.1372 1.4184C10.9331 0.934541 10.7018 0.461742 10.4496 0C8.59007 0.318797 6.78726 0.879656 5.0768 1.67671C1.65217 6.84883 0.706797 11.8997 1.166 16.858C3.39578 18.5135 5.56066 19.5165 7.6473 20.1417C8.16912 19.4246 8.63212 18.6713 9.02347 17.8863C8.25707 17.5929 7.52187 17.2415 6.82914 16.8374C7.0147 16.6975 7.19515 16.5518 7.36941 16.402C11.66 18.396 16.4523 18.396 20.7186 16.402C20.8953 16.5518 21.0758 16.6975 21.2613 16.8374C20.5663 17.2435 19.8288 17.5947 19.0624 17.8863C19.4537 18.6713 19.9167 19.4246 20.4385 20.1417C22.5276 19.5165 24.6925 18.5135 26.9223 16.858C27.4684 11.2365 25.9961 6.22055 23.0212 1.67671ZM9.68041 13.6383C8.39754 13.6383 7.34085 12.4456 7.34085 10.994C7.34085 9.5424 8.37555 8.34973 9.68041 8.34973C10.9893 8.34973 12.0395 9.5424 12.0175 10.994C12.0175 12.4456 10.9893 13.6383 9.68041 13.6383ZM18.3161 13.6383C17.0332 13.6383 15.9765 12.4456 15.9765 10.994C15.9765 9.5424 17.0112 8.34973 18.3161 8.34973C19.625 8.34973 20.6752 9.5424 20.6532 10.994C20.6532 12.4456 19.625 13.6383 18.3161 13.6383Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
{/* Add separator logic if needed, or just list other servers (currently just 1 hardcoded placeholder in UI) */}
|
||||
|
||||
|
||||
{/* The Server Icon (Secure Chat) */}
|
||||
<div
|
||||
<div
|
||||
className={`server-icon ${view === 'server' ? 'active' : ''}`}
|
||||
onClick={() => onViewChange('server')}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>Sc</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Channel List Area */}
|
||||
{view === 'me' ? (
|
||||
<div className="channel-list" style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column' }}>
|
||||
@@ -484,7 +451,7 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
) : (
|
||||
<div className="channel-list" style={{ flex: 1, overflowY: 'auto' }}>
|
||||
<div className="channel-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span
|
||||
<span
|
||||
style={{ cursor: 'pointer', maxWidth: '120px', overflow: 'hidden', textOverflow: 'ellipsis' }}
|
||||
onClick={() => setIsServerSettingsOpen(true)}
|
||||
title="Server Settings"
|
||||
@@ -492,7 +459,7 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
Secure Chat ▾
|
||||
</span>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
<button
|
||||
onClick={handleStartCreate}
|
||||
title="Create New Channel"
|
||||
style={{
|
||||
@@ -507,7 +474,7 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
onClick={handleCreateInvite}
|
||||
title="Create Invite Link"
|
||||
style={{
|
||||
@@ -523,7 +490,7 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Inline Create Channel Input */}
|
||||
{isCreating && (
|
||||
<div style={{ padding: '0 8px', marginBottom: '4px' }}>
|
||||
@@ -561,37 +528,37 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
)}
|
||||
|
||||
{channels.map(channel => (
|
||||
<React.Fragment key={channel.id}>
|
||||
<React.Fragment key={channel._id}>
|
||||
<div
|
||||
className={`channel-item ${activeChannel === channel.id ? 'active' : ''} ${voiceChannelId === channel.id ? 'voice-active' : ''}`}
|
||||
className={`channel-item ${activeChannel === channel._id ? 'active' : ''} ${voiceChannelId === channel._id ? 'voice-active' : ''}`}
|
||||
onClick={() => {
|
||||
if (channel.type === 'voice') {
|
||||
if (voiceChannelId === channel.id) {
|
||||
onSelectChannel(channel.id);
|
||||
if (voiceChannelId === channel._id) {
|
||||
onSelectChannel(channel._id);
|
||||
} else {
|
||||
connectToVoice(channel.id, channel.name, localStorage.getItem('userId'));
|
||||
connectToVoice(channel._id, channel.name, localStorage.getItem('userId'));
|
||||
}
|
||||
} else {
|
||||
onSelectChannel(channel.id);
|
||||
onSelectChannel(channel._id);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingRight: '8px' // Space for icon
|
||||
paddingRight: '8px'
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden', flex: 1 }}>
|
||||
{channel.type === 'voice' ? (
|
||||
<div style={{ marginRight: 6 }}>
|
||||
<ColoredIcon
|
||||
src={voiceIcon}
|
||||
<ColoredIcon
|
||||
src={voiceIcon}
|
||||
size="16px"
|
||||
color={voiceStates[channel.id]?.length > 0
|
||||
? "color-mix(in oklab, hsl(132.809 calc(1*34.902%) 50% /1) 100%, #000 0%)" // Active Green
|
||||
: "#8e9297" // Default Gray
|
||||
color={voiceStates[channel._id]?.length > 0
|
||||
? "color-mix(in oklab, hsl(132.809 calc(1*34.902%) 50% /1) 100%, #000 0%)"
|
||||
: "#8e9297"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@@ -602,7 +569,7 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="channel-settings-icon"
|
||||
className="channel-settings-icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditingChannel(channel);
|
||||
@@ -621,20 +588,18 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
>
|
||||
⚙️
|
||||
</button>
|
||||
|
||||
{/* Participant List (Only for Voice Channels) */}
|
||||
</div>
|
||||
{channel.type === 'voice' && voiceStates[channel.id] && voiceStates[channel.id].length > 0 && (
|
||||
{channel.type === 'voice' && voiceStates[channel._id] && voiceStates[channel._id].length > 0 && (
|
||||
<div style={{ marginLeft: 32, marginBottom: 8 }}>
|
||||
{voiceStates[channel.id].map(user => (
|
||||
{voiceStates[channel._id].map(user => (
|
||||
<div key={user.userId} style={{ display: 'flex', alignItems: 'center', marginBottom: 4 }}>
|
||||
<div style={{
|
||||
width: 24, height: 24, borderRadius: '50%',
|
||||
backgroundColor: '#5865F2',
|
||||
backgroundColor: '#5865F2',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
marginRight: 8, fontSize: 10, color: 'white',
|
||||
boxShadow: activeSpeakers.has(user.userId)
|
||||
? '0 0 0 0px hsl(134.526 calc(1*41.485%) 44.902% /1), inset 0 0 0 2px hsl(134.526 calc(1*41.485%) 44.902% /1), inset 0 0 0 3px color-mix(in oklab, hsl(240 calc(1*7.143%) 10.98% /1) 100%, #000 0%)'
|
||||
boxShadow: activeSpeakers.has(user.userId)
|
||||
? '0 0 0 0px hsl(134.526 calc(1*41.485%) 44.902% /1), inset 0 0 0 2px hsl(134.526 calc(1*41.485%) 44.902% /1), inset 0 0 0 3px color-mix(in oklab, hsl(240 calc(1*7.143%) 10.98% /1) 100%, #000 0%)'
|
||||
: 'none'
|
||||
}}>
|
||||
{user.username.substring(0, 1).toUpperCase()}
|
||||
@@ -643,7 +608,7 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
<div style={{ display: 'flex', marginLeft: 'auto', gap: 4, alignItems: 'center' }}>
|
||||
{user.isScreenSharing && (
|
||||
<div style={{
|
||||
backgroundColor: '#ed4245', // var(--red-400) fallback
|
||||
backgroundColor: '#ed4245',
|
||||
borderRadius: '8px',
|
||||
padding: '0 6px',
|
||||
textOverflow: 'ellipsis',
|
||||
@@ -695,7 +660,7 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
|
||||
<div style={{ color: '#43b581', fontWeight: 'bold', fontSize: 13 }}>Voice Connected</div>
|
||||
<button
|
||||
<button
|
||||
onClick={disconnectVoice}
|
||||
title="Disconnect"
|
||||
style={{
|
||||
@@ -707,7 +672,7 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
</div>
|
||||
<div style={{ color: '#dcddde', fontSize: 12, marginBottom: 8 }}>{voiceChannelName} / Secure Chat</div>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<button
|
||||
<button
|
||||
onClick={() => room?.localParticipant.setCameraEnabled(!room.localParticipant.isCameraEnabled)}
|
||||
title="Turn On Camera"
|
||||
style={{
|
||||
@@ -716,7 +681,7 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
>
|
||||
<ColoredIcon src={cameraIcon} color="#b9bbbe" size="20px" />
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
onClick={handleScreenShareClick}
|
||||
title="Share Screen"
|
||||
style={{
|
||||
@@ -734,8 +699,8 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
|
||||
{/* Modals */}
|
||||
{editingChannel && (
|
||||
<ChannelSettingsModal
|
||||
channel={editingChannel}
|
||||
<ChannelSettingsModal
|
||||
channel={editingChannel}
|
||||
onClose={() => setEditingChannel(null)}
|
||||
onRename={onRenameChannel}
|
||||
onDelete={onDeleteChannel}
|
||||
@@ -745,7 +710,7 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
<ServerSettingsModal onClose={() => setIsServerSettingsOpen(false)} />
|
||||
)}
|
||||
{isScreenShareModalOpen && (
|
||||
<ScreenShareModal
|
||||
<ScreenShareModal
|
||||
onClose={() => setIsScreenShareModalOpen(false)}
|
||||
onSelectSource={handleScreenShareSelect}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user