feat: Add a large collection of emoji and other frontend assets, including a sound file, and a backend package.json.

This commit is contained in:
Bryan1029384756
2026-01-06 17:58:56 -06:00
parent f531301863
commit abedd78893
3795 changed files with 10981 additions and 229 deletions

View File

@@ -1,25 +1,746 @@
import React from 'react';
import React, { useState } from 'react';
import { useVoice } from '../contexts/VoiceContext';
import ChannelSettingsModal from './ChannelSettingsModal';
import ServerSettingsModal from './ServerSettingsModal';
import ScreenShareModal from './ScreenShareModal';
import DMList from './DMList'; // Import 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 disconnectIcon from '../assets/icons/disconnect.svg';
import cameraIcon from '../assets/icons/camera.svg';
import screenIcon from '../assets/icons/screen.svg';
// Helper Component for coloring SVGs
const ColoredIcon = ({ src, color, size = '20px' }) => (
<div style={{
width: size,
height: size,
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0 // Prevent shrinking in flex containers
}}>
<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;
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash);
}
return colors[Math.abs(hash) % colors.length];
};
const ICON_COLOR_DEFAULT = 'color-mix(in oklab, hsl(240 4.294% 68.039% / 1) 100%, #000 0%)';
const ICON_COLOR_ACTIVE = 'hsl(357.692 67.826% 54.902% / 1)';
const Sidebar = ({ channels, activeChannel, onSelectChannel }) => {
return (
<div className="sidebar">
<div className="server-list">
<div className="server-icon active">H</div>
</div>
<div className="channel-list">
<div className="channel-header">Secure Chat</div>
{channels.map(channel => (
<div
key={channel.id}
className={`channel-item ${activeChannel === channel.id ? 'active' : ''}`}
onClick={() => onSelectChannel(channel.id)}
>
# {channel.name}
<div style={{
height: '64px',
margin: '0 8px 8px 8px',
borderRadius: connectionState === 'connected' ? '0px 0px 8px 8px' : '8px',
backgroundColor: '#292b2f',
display: 'flex',
alignItems: 'center',
padding: '0 8px',
flexShrink: 0
}}>
{/* User Info */}
<div style={{
display: 'flex',
alignItems: 'center',
marginRight: 'auto',
padding: '4px',
borderRadius: '4px',
cursor: 'pointer',
':hover': { backgroundColor: 'rgba(255,255,255,0.05)' }
}}>
<div style={{ position: 'relative', marginRight: '8px' }}>
<div style={{
width: '32px',
height: '32px',
borderRadius: '50%',
backgroundColor: getUserColor(username || 'U'),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontWeight: '600',
fontSize: '14px'
}}>
{(username || '?').substring(0, 1).toUpperCase()}
</div>
))}
{/* Status Indicator */}
<div style={{
position: 'absolute',
bottom: '-2px',
right: '-2px',
width: '10px',
height: '10px',
borderRadius: '50%',
backgroundColor: '#3ba55c',
border: '2px solid #292b2f'
}} />
</div>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ color: 'white', fontWeight: '600', fontSize: '14px', lineHeight: '18px' }}>
{username || 'Unknown'}
</div>
<div style={{ color: '#b9bbbe', fontSize: '12px', lineHeight: '13px' }}>
#{Math.floor(Math.random() * 9000) + 1000}
</div>
</div>
</div>
{/* Controls */}
<div style={{ display: 'flex' }}>
<button
onClick={toggleMute}
title={effectiveMute ? "Unmute" : "Mute"}
style={{
background: 'transparent',
border: 'none',
cursor: 'pointer',
padding: '6px',
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<ColoredIcon
src={effectiveMute ? mutedIcon : muteIcon}
color={effectiveMute ? ICON_COLOR_ACTIVE : ICON_COLOR_DEFAULT}
/>
</button>
<button
onClick={toggleDeafen}
title={isDeafened ? "Undeafen" : "Deafen"}
style={{
background: 'transparent',
border: 'none',
cursor: 'pointer',
padding: '6px',
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<ColoredIcon
src={isDeafened ? defeanedIcon : defeanIcon}
color={isDeafened ? ICON_COLOR_ACTIVE : ICON_COLOR_DEFAULT}
/>
</button>
<button
title="User Settings"
style={{
background: 'transparent',
border: 'none',
cursor: 'pointer',
padding: '6px',
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<ColoredIcon
src={settingsIcon}
color={ICON_COLOR_DEFAULT}
/>
</button>
</div>
</div>
);
};
const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKeys, onChannelCreated, view, onViewChange }) => {
const [isCreating, setIsCreating] = useState(false);
const [isServerSettingsOpen, setIsServerSettingsOpen] = useState(false); // New State
const [newChannelName, setNewChannelName] = useState('');
const [newChannelType, setNewChannelType] = useState('text'); // 'text' or 'voice'
const [editingChannel, setEditingChannel] = useState(null);
const [isScreenShareModalOpen, setIsScreenShareModalOpen] = useState(false);
// Callbacks for Modal
const onRenameChannel = (id, newName) => {
if (onChannelCreated) onChannelCreated();
};
const onDeleteChannel = (id) => {
if (activeChannel === id) onSelectChannel(null);
if (onChannelCreated) onChannelCreated();
};
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('');
setNewChannelType('text');
};
const handleSubmitCreate = async (e) => {
if (e) e.preventDefault();
if (!newChannelName.trim()) {
setIsCreating(false);
return;
}
const name = newChannelName.trim();
const type = newChannelType;
const userId = localStorage.getItem('userId');
if (!userId) {
alert("Please login first.");
setIsCreating(false);
return;
}
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);
// 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?)
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)
try {
// Fetch all public keys
const usersRes = await fetch('http://localhost:3000/api/auth/users/public-keys');
const users = await usersRes.json();
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,
encryptedKeyBundle: encryptedKeyHex,
keyVersion: 1
});
} catch (e) {
console.error("Failed to encrypt for user", u.id, e);
}
}
// 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' });
} catch (keyErr) {
console.error("Critical: Failed to distribute keys", keyErr);
alert("Channel created but key distribution failed.");
}
// 6. Refresh
setIsCreating(false);
if (onChannelCreated) onChannelCreated();
} catch (err) {
console.error(err);
alert("Failed to create channel: " + err.message);
setIsCreating(false);
}
};
// ... (rest of logic)
const handleCreateInvite = async () => {
const userId = localStorage.getItem('userId');
if (!userId) {
alert("Error: No User ID found. Please login again.");
return;
}
try {
// 1. Generate Invite Code & Secret
const inviteCode = crypto.randomUUID();
const inviteSecretBytes = new Uint8Array(32);
crypto.getRandomValues(inviteSecretBytes);
const inviteSecret = Array.from(inviteSecretBytes).map(b => b.toString(16).padStart(2, '0')).join('');
// 2. Prepare Key Bundle
const generalChannel = channels.find(c => c.name === 'general');
const targetChannelId = generalChannel ? generalChannel.id : activeChannel; // Fallback to active if no general
if (!targetChannelId) {
alert("No channel selected.");
return;
}
const targetKey = channelKeys ? channelKeys[targetChannelId] : null;
if (!targetKey) {
alert("Error: You don't have the key for this channel yet, so you can't invite others.");
return;
}
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
});
// 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
})
});
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);
alert(`Invite Link Copied to Clipboard!\n\n${link}`);
} catch (e) {
console.error("Invite Error:", e);
alert("Failed to create invite. See console.");
}
};
// Screen Share Handler
const handleScreenShareSelect = async (selection) => {
if (!room) return;
try {
// Unpublish existing screen share if any
if (room.localParticipant.isScreenShareEnabled) {
await room.localParticipant.setScreenShareEnabled(false);
}
// Capture based on selection
let stream;
if (selection.type === 'device') {
stream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: selection.deviceId } },
audio: false
});
} else {
// Electron Screen/Window
stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: selection.sourceId
}
}
});
}
// Publish the video track
const track = stream.getVideoTracks()[0];
if (track) {
await room.localParticipant.publishTrack(track, {
name: 'screen_share',
source: Track.Source.ScreenShare
});
setScreenSharing(true);
track.onended = () => {
setScreenSharing(false);
room.localParticipant.setScreenShareEnabled(false).catch(console.error);
};
}
} catch (err) {
console.error("Error sharing screen:", err);
alert("Failed to share screen: " + err.message);
}
};
// Toggle Modal instead of direct toggle
const handleScreenShareClick = () => {
if (room?.localParticipant.isScreenShareEnabled) {
room.localParticipant.setScreenShareEnabled(false);
setScreenSharing(false);
} else {
setIsScreenShareModalOpen(true);
}
};
return (
<div className="sidebar" style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', flex: 1, minHeight: 0 }}>
<div style={{ display: 'flex', flex: 1, minHeight: 0 }}>
<div className="server-list">
{/* Home Button */}
<div
className={`server-icon ${view === 'me' ? 'active' : ''}`}
onClick={() => onViewChange('me')}
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
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' }}>
<DMList onSelectDM={() => {}} />
</div>
) : (
<div className="channel-list" style={{ flex: 1, overflowY: 'auto' }}>
<div className="channel-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span
style={{ cursor: 'pointer', maxWidth: '120px', overflow: 'hidden', textOverflow: 'ellipsis' }}
onClick={() => setIsServerSettingsOpen(true)}
title="Server Settings"
>
Secure Chat
</span>
<div style={{ display: 'flex', gap: '8px' }}>
<button
onClick={handleStartCreate}
title="Create New Channel"
style={{
background: 'transparent',
border: 'none',
color: '#b9bbbe',
cursor: 'pointer',
fontSize: '18px',
padding: '0 4px',
marginRight: '4px'
}}
>
+
</button>
<button
onClick={handleCreateInvite}
title="Create Invite Link"
style={{
background: 'transparent',
border: 'none',
color: '#b9bbbe',
cursor: 'pointer',
fontSize: '18px',
padding: '0 4px'
}}
>
🔗
</button>
</div>
</div>
{/* Inline Create Channel Input */}
{isCreating && (
<div style={{ padding: '0 8px', marginBottom: '4px' }}>
<form onSubmit={handleSubmitCreate}>
<div style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
<label style={{ color: newChannelType==='text'?'white':'#b9bbbe', fontSize: 10, cursor: 'pointer' }} onClick={()=>setNewChannelType('text')}>
Text
</label>
<label style={{ color: newChannelType==='voice'?'white':'#b9bbbe', fontSize: 10, cursor: 'pointer' }} onClick={()=>setNewChannelType('voice')}>
Voice
</label>
</div>
<input
autoFocus
type="text"
placeholder={`new-${newChannelType}-channel`}
value={newChannelName}
onChange={(e) => setNewChannelName(e.target.value)}
style={{
width: '100%',
background: '#202225',
border: '1px solid #7289da',
borderRadius: '4px',
color: '#dcddde',
padding: '4px 8px',
fontSize: '14px',
outline: 'none'
}}
/>
</form>
<div style={{ fontSize: 10, color: '#b9bbbe', marginTop: 2, textAlign: 'right' }}>
Press Enter to Create {newChannelType === 'voice' && '(Voice)'}
</div>
</div>
)}
{channels.map(channel => (
<React.Fragment key={channel.id}>
<div
className={`channel-item ${activeChannel === channel.id ? 'active' : ''} ${voiceChannelId === channel.id ? 'voice-active' : ''}`}
onClick={() => {
if (channel.type === 'voice') {
if (voiceChannelId === channel.id) {
onSelectChannel(channel.id);
} else {
connectToVoice(channel.id, channel.name, localStorage.getItem('userId'));
}
} else {
onSelectChannel(channel.id);
}
}}
style={{
position: 'relative',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
paddingRight: '8px' // Space for icon
}}
>
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden', flex: 1 }}>
{channel.type === 'voice' ? (
<div style={{ marginRight: 6 }}>
<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
}
/>
</div>
) : (
<span style={{ color: '#8e9297', marginRight: '6px', flexShrink: 0 }}>#</span>
)}
<span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{channel.name}</span>
</div>
<button
className="channel-settings-icon"
onClick={(e) => {
e.stopPropagation();
setEditingChannel(channel);
}}
style={{
background: 'transparent',
border: 'none',
color: '#b9bbbe',
cursor: 'pointer',
fontSize: '12px',
padding: '2px 4px',
display: 'flex', alignItems: 'center',
opacity: '0.7',
transition: 'opacity 0.2s'
}}
>
</button>
{/* Participant List (Only for Voice Channels) */}
</div>
{channel.type === 'voice' && voiceStates[channel.id] && voiceStates[channel.id].length > 0 && (
<div style={{ marginLeft: 32, marginBottom: 8 }}>
{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',
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%)'
: 'none'
}}>
{user.username.substring(0, 1).toUpperCase()}
</div>
<span style={{ color: '#b9bbbe', fontSize: 14 }}>{user.username}</span>
<div style={{ display: 'flex', marginLeft: 'auto', gap: 4, alignItems: 'center' }}>
{user.isScreenSharing && (
<div style={{
backgroundColor: '#ed4245', // var(--red-400) fallback
borderRadius: '8px',
padding: '0 6px',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
textAlign: 'center',
height: '16px',
minHeight: '16px',
minWidth: '16px',
color: 'hsl(0 calc(1*0%) 100% /1)',
fontSize: '12px',
fontWeight: '700',
letterSpacing: '.02em',
lineHeight: '1.3333333333333333',
textTransform: 'uppercase',
display: 'flex',
alignItems: 'center',
marginRight: '4px'
}}>
Live
</div>
)}
{(user.isMuted || user.isDeafened) && (
<ColoredIcon src={mutedIcon} color="#b9bbbe" size="14px" />
)}
{user.isDeafened && (
<ColoredIcon src={defeanedIcon} color="#b9bbbe" size="14px" />
)}
</div>
</div>
))}
</div>
)}
</React.Fragment>
))}
</div>
</div>
{/* Voice Connection Panel */}
{connectionState === 'connected' && (
<div style={{
backgroundColor: '#292b2f',
borderRadius: '8px 8px 0px 0px',
padding: 'calc(16px - 8px + 4px)',
margin: '8px 8px 0px 8px',
display: 'flex',
flexDirection: 'column',
borderBottom: "1px solid color-mix(in oklab,hsl(240 calc(1*4%) 60.784% /0.0392156862745098) 100%,hsl(0 0% 0% /0.0392156862745098) 0%)"
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
<div style={{ color: '#43b581', fontWeight: 'bold', fontSize: 13 }}>Voice Connected</div>
<button
onClick={disconnectVoice}
title="Disconnect"
style={{
background: 'transparent', border: 'none', cursor: 'pointer', padding: '0', display: 'flex', justifyContent: 'center'
}}
>
<ColoredIcon src={disconnectIcon} color="#b9bbbe" size="20px" />
</button>
</div>
<div style={{ color: '#dcddde', fontSize: 12, marginBottom: 8 }}>{voiceChannelName} / Secure Chat</div>
<div style={{ display: 'flex', gap: 4 }}>
<button
onClick={() => room?.localParticipant.setCameraEnabled(!room.localParticipant.isCameraEnabled)}
title="Turn On Camera"
style={{
flex: 1, alignItems: 'center', minHeight: '32px', padding: "calc(var(--space-8) - 1px) calc(var(--space-16) - 1px)", background: 'hsl(240 calc(1*4%) 60.784% /0.0784313725490196)', border: 'hsl(0 calc(1*0%) 100% /0.0784313725490196)', borderColor: 'color-mix(in oklab,hsl(240 calc(1*4%) 60.784% /0.0392156862745098) 100%,hsl(0 0% 0% /0.0392156862745098) 0%)', borderRadius: '8px', cursor: 'pointer', padding: '4px', display: 'flex', justifyContent: 'center'
}}
>
<ColoredIcon src={cameraIcon} color="#b9bbbe" size="20px" />
</button>
<button
onClick={handleScreenShareClick}
title="Share Screen"
style={{
flex: 1, alignItems: 'center', minHeight: '32px', padding: "calc(var(--space-8) - 1px) calc(var(--space-16) - 1px)", background: 'hsl(240 calc(1*4%) 60.784% /0.0784313725490196)', border: 'hsl(0 calc(1*0%) 100% /0.0784313725490196)', borderColor: 'color-mix(in oklab,hsl(240 calc(1*4%) 60.784% /0.0392156862745098) 100%,hsl(0 0% 0% /0.0392156862745098) 0%)', borderRadius: '8px', cursor: 'pointer', padding: '4px', display: 'flex', justifyContent: 'center'
}}
>
<ColoredIcon src={screenIcon} color="#b9bbbe" size="20px" />
</button>
</div>
</div>
)}
{/* User Control Panel at Bottom, Spanning Full Width */}
<UserControlPanel username={username} />
{/* Modals */}
{editingChannel && (
<ChannelSettingsModal
channel={editingChannel}
onClose={() => setEditingChannel(null)}
onRename={onRenameChannel}
onDelete={onDeleteChannel}
/>
)}
{isServerSettingsOpen && (
<ServerSettingsModal onClose={() => setIsServerSettingsOpen(false)} />
)}
{isScreenShareModalOpen && (
<ScreenShareModal
onClose={() => setIsScreenShareModalOpen(false)}
onSelectSource={handleScreenShareSelect}
/>
)}
</div>
);
};
export default Sidebar;