feat: Initialize the Electron frontend with core UI components and integrate Convex backend services.

This commit is contained in:
Bryan1029384756
2026-02-10 18:29:42 -06:00
parent 34e9790db9
commit 17790afa9b
64 changed files with 149216 additions and 628 deletions

View File

@@ -1,11 +1,14 @@
import React, { useState } from 'react';
import { useConvex } from 'convex/react';
import { useConvex, useMutation } from 'convex/react';
import { api } from '../../../../convex/_generated/api';
import Tooltip from './Tooltip';
import { useVoice } from '../contexts/VoiceContext';
import ChannelSettingsModal from './ChannelSettingsModal';
import ServerSettingsModal from './ServerSettingsModal';
import ScreenShareModal from './ScreenShareModal';
import DMList from './DMList';
import Avatar from './Avatar';
import ThemeSelector from './ThemeSelector';
import { Track } from 'livekit-client';
import muteIcon from '../assets/icons/mute.svg';
import mutedIcon from '../assets/icons/muted.svg';
@@ -74,47 +77,80 @@ const ColoredIcon = ({ src, color, size = '20px' }) => (
</div>
);
const UserControlPanel = ({ username }) => {
const VoiceTimer = () => {
const [elapsed, setElapsed] = React.useState(0);
React.useEffect(() => {
const start = Date.now();
const interval = setInterval(() => setElapsed(Math.floor((Date.now() - start) / 1000)), 1000);
return () => clearInterval(interval);
}, []);
const hours = Math.floor(elapsed / 3600);
const mins = Math.floor((elapsed % 3600) / 60);
const secs = elapsed % 60;
const time = hours > 0
? `${hours}:${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
: `${mins}:${String(secs).padStart(2, '0')}`;
return <span className="voice-timer">{time}</span>;
};
const STATUS_OPTIONS = [
{ value: 'online', label: 'Online', color: '#3ba55c' },
{ value: 'idle', label: 'Idle', color: '#faa61a' },
{ value: 'dnd', label: 'Do Not Disturb', color: '#ed4245' },
{ value: 'invisible', label: 'Invisible', color: '#747f8d' },
];
const UserControlPanel = ({ username, userId }) => {
const { isMuted, isDeafened, toggleMute, toggleDeafen, connectionState } = useVoice();
const [showStatusMenu, setShowStatusMenu] = useState(false);
const [showThemeSelector, setShowThemeSelector] = useState(false);
const [currentStatus, setCurrentStatus] = useState('online');
const updateStatusMutation = useMutation(api.auth.updateStatus);
const effectiveMute = isMuted || isDeafened;
const statusColor = STATUS_OPTIONS.find(s => s.value === currentStatus)?.color || '#3ba55c';
const handleStatusChange = async (status) => {
setCurrentStatus(status);
setShowStatusMenu(false);
if (userId) {
try {
await updateStatusMutation({ userId, status });
} catch (e) {
console.error('Failed to update status:', e);
}
}
};
return (
<div style={{
height: '64px',
margin: '0 8px 8px 8px',
borderRadius: connectionState === 'connected' ? '0px 0px 8px 8px' : '8px',
backgroundColor: '#292b2f',
backgroundColor: 'var(--panel-bg)',
display: 'flex',
alignItems: 'center',
padding: '0 8px',
flexShrink: 0
flexShrink: 0,
position: 'relative'
}}>
{/* User Info */}
<div style={{
display: 'flex',
alignItems: 'center',
marginRight: 'auto',
padding: '4px',
borderRadius: '4px',
cursor: 'pointer',
':hover': { backgroundColor: 'rgba(255,255,255,0.05)' }
}}>
{showStatusMenu && (
<div className="status-menu">
{STATUS_OPTIONS.map(opt => (
<div
key={opt.value}
className="status-menu-item"
onClick={() => handleStatusChange(opt.value)}
>
<div className="status-menu-dot" style={{ backgroundColor: opt.color }} />
<span>{opt.label}</span>
</div>
))}
</div>
)}
<div className="user-control-info" onClick={() => setShowStatusMenu(!showStatusMenu)}>
<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>
<Avatar username={username} size={32} />
<div style={{
position: 'absolute',
bottom: '-2px',
@@ -122,41 +158,48 @@ const UserControlPanel = ({ username }) => {
width: '10px',
height: '10px',
borderRadius: '50%',
backgroundColor: '#3ba55c',
border: '2px solid #292b2f'
backgroundColor: statusColor,
border: '2px solid var(--panel-bg)',
cursor: 'pointer'
}} />
</div>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ color: 'white', fontWeight: '600', fontSize: '14px', lineHeight: '18px' }}>
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<div style={{ color: 'var(--header-primary)', fontWeight: '600', fontSize: '14px', lineHeight: '18px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{username || 'Unknown'}
</div>
<div style={{ color: '#b9bbbe', fontSize: '12px', lineHeight: '13px' }}>
#{Math.floor(Math.random() * 9000) + 1000}
<div style={{ color: 'var(--header-secondary)', fontSize: '12px', lineHeight: '13px' }}>
{STATUS_OPTIONS.find(s => s.value === currentStatus)?.label || 'Online'}
</div>
</div>
</div>
{/* Controls */}
<div style={{ display: 'flex' }}>
<button onClick={toggleMute} title={effectiveMute ? "Unmute" : "Mute"} style={controlButtonStyle}>
<ColoredIcon
src={effectiveMute ? mutedIcon : muteIcon}
color={effectiveMute ? ICON_COLOR_ACTIVE : ICON_COLOR_DEFAULT}
/>
</button>
<button onClick={toggleDeafen} title={isDeafened ? "Undeafen" : "Deafen"} style={controlButtonStyle}>
<ColoredIcon
src={isDeafened ? defeanedIcon : defeanIcon}
color={isDeafened ? ICON_COLOR_ACTIVE : ICON_COLOR_DEFAULT}
/>
</button>
<button title="User Settings" style={controlButtonStyle}>
<ColoredIcon
src={settingsIcon}
color={ICON_COLOR_DEFAULT}
/>
</button>
<Tooltip text={effectiveMute ? "Unmute" : "Mute"} position="top">
<button onClick={toggleMute} style={controlButtonStyle}>
<ColoredIcon
src={effectiveMute ? mutedIcon : muteIcon}
color={effectiveMute ? ICON_COLOR_ACTIVE : ICON_COLOR_DEFAULT}
/>
</button>
</Tooltip>
<Tooltip text={isDeafened ? "Undeafen" : "Deafen"} position="top">
<button onClick={toggleDeafen} style={controlButtonStyle}>
<ColoredIcon
src={isDeafened ? defeanedIcon : defeanIcon}
color={isDeafened ? ICON_COLOR_ACTIVE : ICON_COLOR_DEFAULT}
/>
</button>
</Tooltip>
<Tooltip text="User Settings" position="top">
<button style={controlButtonStyle} onClick={() => setShowThemeSelector(true)}>
<ColoredIcon
src={settingsIcon}
color={ICON_COLOR_DEFAULT}
/>
</button>
</Tooltip>
</div>
{showThemeSelector && <ThemeSelector onClose={() => setShowThemeSelector(false)} />}
</div>
);
};
@@ -166,7 +209,7 @@ const UserControlPanel = ({ username }) => {
const headerButtonStyle = {
background: 'transparent',
border: 'none',
color: '#b9bbbe',
color: 'var(--header-secondary)',
cursor: 'pointer',
fontSize: '18px',
padding: '0 4px'
@@ -249,13 +292,14 @@ function getScreenCaptureConstraints(selection) {
};
}
const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKeys, view, onViewChange, onOpenDM, activeDMChannel, setActiveDMChannel, dmChannels }) => {
const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKeys, view, onViewChange, onOpenDM, activeDMChannel, setActiveDMChannel, dmChannels, userId }) => {
const [isCreating, setIsCreating] = useState(false);
const [isServerSettingsOpen, setIsServerSettingsOpen] = useState(false);
const [newChannelName, setNewChannelName] = useState('');
const [newChannelType, setNewChannelType] = useState('text');
const [editingChannel, setEditingChannel] = useState(null);
const [isScreenShareModalOpen, setIsScreenShareModalOpen] = useState(false);
const [collapsedCategories, setCollapsedCategories] = useState({});
const convex = useConvex();
@@ -419,23 +463,22 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
<div style={{ marginLeft: 32, marginBottom: 8 }}>
{users.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) ? ACTIVE_SPEAKER_SHADOW : 'none'
}}>
{user.username.substring(0, 1).toUpperCase()}
</div>
<span style={{ color: '#b9bbbe', fontSize: 14 }}>{user.username}</span>
<Avatar
username={user.username}
size={24}
style={{
marginRight: 8,
boxShadow: activeSpeakers.has(user.userId) ? ACTIVE_SPEAKER_SHADOW : 'none'
}}
/>
<span style={{ color: 'var(--header-secondary)', fontSize: 14 }}>{user.username}</span>
<div style={{ display: 'flex', marginLeft: 'auto', gap: 4, alignItems: 'center' }}>
{user.isScreenSharing && <div style={liveBadgeStyle}>Live</div>}
{(user.isMuted || user.isDeafened) && (
<ColoredIcon src={mutedIcon} color="#b9bbbe" size="14px" />
<ColoredIcon src={mutedIcon} color="var(--header-secondary)" size="14px" />
)}
{user.isDeafened && (
<ColoredIcon src={defeanedIcon} color="#b9bbbe" size="14px" />
<ColoredIcon src={defeanedIcon} color="var(--header-secondary)" size="14px" />
)}
</div>
</div>
@@ -444,113 +487,129 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
);
};
const toggleCategory = (cat) => {
setCollapsedCategories(prev => ({ ...prev, [cat]: !prev[cat] }));
};
const groupedChannels = React.useMemo(() => {
const groups = {};
channels.forEach(ch => {
const cat = ch.type === 'voice'
? (ch.category || 'Voice Channels')
: (ch.category || 'Text Channels');
if (!groups[cat]) groups[cat] = [];
groups[cat].push(ch);
});
return groups;
}, [channels]);
const renderServerView = () => (
<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={{ ...headerButtonStyle, marginRight: '4px' }}>
+
</button>
<button onClick={handleCreateInvite} title="Create Invite Link" style={headerButtonStyle}>
🔗
</button>
</div>
<div style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column' }}>
<div className="server-header" onClick={() => setIsServerSettingsOpen(true)}>
<span>Secure Chat</span>
<span className="server-header-chevron"></span>
</div>
{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={() => handleChannelClick(channel)}
style={{
position: 'relative',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
paddingRight: '8px'
}}
>
<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 ? VOICE_ACTIVE_COLOR : "#8e9297"}
/>
</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>
<div className="channel-list" style={{ flex: 1, overflowY: 'auto' }}>
{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':'var(--interactive-normal)', fontSize: 10, cursor: 'pointer' }} onClick={()=>setNewChannelType('text')}>
Text
</label>
<label style={{ color: newChannelType==='voice'?'white':'var(--interactive-normal)', 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: 'var(--bg-tertiary)',
border: '1px solid var(--brand-experiment)',
borderRadius: '4px',
color: 'var(--text-normal)',
padding: '4px 8px',
fontSize: '14px',
outline: 'none'
}}
/>
</form>
<div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 2, textAlign: 'right' }}>
Press Enter to Create {newChannelType === 'voice' && '(Voice)'}
</div>
</div>
{renderVoiceUsers(channel)}
</React.Fragment>
))}
)}
{Object.entries(groupedChannels).map(([category, catChannels]) => (
<div key={category}>
<div className="channel-category-header" onClick={() => toggleCategory(category)}>
<span className={`category-chevron ${collapsedCategories[category] ? 'collapsed' : ''}`}></span>
<span className="category-label">{category}</span>
<button className="category-add-btn" onClick={(e) => { e.stopPropagation(); handleStartCreate(); }} title="Create Channel">
+
</button>
</div>
{!collapsedCategories[category] && catChannels.map(channel => (
<React.Fragment key={channel._id}>
<div
className={`channel-item ${activeChannel === channel._id ? 'active' : ''} ${voiceChannelId === channel._id ? 'voice-active' : ''}`}
onClick={() => handleChannelClick(channel)}
style={{
position: 'relative',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
paddingRight: '8px'
}}
>
<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 ? VOICE_ACTIVE_COLOR : "var(--interactive-normal)"}
/>
</div>
) : (
<span style={{ color: 'var(--interactive-normal)', 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: 'var(--interactive-normal)',
cursor: 'pointer',
fontSize: '12px',
padding: '2px 4px',
display: 'flex', alignItems: 'center',
opacity: '0',
transition: 'opacity 0.2s'
}}
>
</button>
</div>
{renderVoiceUsers(channel)}
</React.Fragment>
))}
</div>
))}
</div>
</div>
);
@@ -558,26 +617,37 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
<div className="sidebar" style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', flex: 1, minHeight: 0 }}>
<div className="server-list">
<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'
}}
>
<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 className="server-item-wrapper">
<div className={`server-pill ${view === 'me' ? 'active' : ''}`} />
<Tooltip text="Direct Messages" position="right">
<div
className={`server-icon ${view === 'me' ? 'active' : ''}`}
onClick={() => onViewChange('me')}
style={{
backgroundColor: view === 'me' ? 'var(--brand-experiment)' : 'var(--bg-primary)',
color: view === 'me' ? '#fff' : 'var(--text-normal)',
cursor: 'pointer'
}}
>
<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>
</Tooltip>
</div>
<div
className={`server-icon ${view === 'server' ? 'active' : ''}`}
onClick={() => onViewChange('server')}
style={{ cursor: 'pointer' }}
>Sc</div>
<div className="server-separator" />
<div className="server-item-wrapper">
<div className={`server-pill ${view === 'server' ? 'active' : ''}`} />
<Tooltip text="Secure Chat" position="right">
<div
className={`server-icon ${view === 'server' ? 'active' : ''}`}
onClick={() => onViewChange('server')}
style={{ cursor: 'pointer' }}
>Sc</div>
</Tooltip>
</div>
</div>
{view === 'me' ? renderDMView() : renderServerView()}
@@ -585,7 +655,7 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
{connectionState === 'connected' && (
<div style={{
backgroundColor: '#292b2f',
backgroundColor: 'var(--panel-bg)',
borderRadius: '8px 8px 0px 0px',
padding: 'calc(16px - 8px + 4px)',
margin: '8px 8px 0px 8px',
@@ -602,22 +672,23 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
background: 'transparent', border: 'none', cursor: 'pointer', padding: '0', display: 'flex', justifyContent: 'center'
}}
>
<ColoredIcon src={disconnectIcon} color="#b9bbbe" size="20px" />
<ColoredIcon src={disconnectIcon} color="var(--header-secondary)" size="20px" />
</button>
</div>
<div style={{ color: '#dcddde', fontSize: 12, marginBottom: 8 }}>{voiceChannelName} / Secure Chat</div>
<div style={{ color: 'var(--text-normal)', fontSize: 12, marginBottom: 4 }}>{voiceChannelName} / Secure Chat</div>
<div style={{ marginBottom: 8 }}><VoiceTimer /></div>
<div style={{ display: 'flex', gap: 4 }}>
<button onClick={() => room?.localParticipant.setCameraEnabled(!room.localParticipant.isCameraEnabled)} title="Turn On Camera" style={voicePanelButtonStyle}>
<ColoredIcon src={cameraIcon} color="#b9bbbe" size="20px" />
<ColoredIcon src={cameraIcon} color="var(--header-secondary)" size="20px" />
</button>
<button onClick={handleScreenShareClick} title="Share Screen" style={voicePanelButtonStyle}>
<ColoredIcon src={screenIcon} color="#b9bbbe" size="20px" />
<ColoredIcon src={screenIcon} color="var(--header-secondary)" size="20px" />
</button>
</div>
</div>
)}
<UserControlPanel username={username} />
<UserControlPanel username={username} userId={userId} />
{editingChannel && (
<ChannelSettingsModal