feat: Implement core Discord features including members list, direct messages, user presence, authentication, and chat UI.
Some checks failed
Build and Release / build-and-release (push) Has been cancelled

This commit is contained in:
Bryan1029384756
2026-02-11 04:36:40 -06:00
parent a29858fd32
commit cb4361da1a
32 changed files with 2051 additions and 144 deletions

View File

@@ -1,5 +1,6 @@
import React, { useState } from 'react';
import { useConvex, useMutation } from 'convex/react';
import { useNavigate } from 'react-router-dom';
import { useConvex, useMutation, useQuery } from 'convex/react';
import { api } from '../../../../convex/_generated/api';
import Tooltip from './Tooltip';
import { useVoice } from '../contexts/VoiceContext';
@@ -8,7 +9,7 @@ import ServerSettingsModal from './ServerSettingsModal';
import ScreenShareModal from './ScreenShareModal';
import DMList from './DMList';
import Avatar from './Avatar';
import ThemeSelector from './ThemeSelector';
import UserSettings from './UserSettings';
import { Track } from 'livekit-client';
import muteIcon from '../assets/icons/mute.svg';
import mutedIcon from '../assets/icons/muted.svg';
@@ -19,6 +20,7 @@ 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';
import inviteUserIcon from '../assets/icons/invite_user.svg';
const USER_COLORS = ['#5865F2', '#EBA7CD', '#57F287', '#FEE75C', '#EB459E', '#ED4245'];
@@ -101,11 +103,46 @@ const STATUS_OPTIONS = [
];
const UserControlPanel = ({ username, userId }) => {
const { isMuted, isDeafened, toggleMute, toggleDeafen, connectionState } = useVoice();
const { isMuted, isDeafened, toggleMute, toggleDeafen, connectionState, disconnectVoice } = useVoice();
const [showStatusMenu, setShowStatusMenu] = useState(false);
const [showThemeSelector, setShowThemeSelector] = useState(false);
const [showUserSettings, setShowUserSettings] = useState(false);
const [currentStatus, setCurrentStatus] = useState('online');
const updateStatusMutation = useMutation(api.auth.updateStatus);
const navigate = useNavigate();
// Fetch stored status preference from server and sync local state
const allUsers = useQuery(api.auth.getPublicKeys) || [];
const myUser = allUsers.find(u => u.id === userId);
React.useEffect(() => {
if (myUser) {
if (myUser.status && myUser.status !== 'offline') {
setCurrentStatus(myUser.status);
} else if (!myUser.status || myUser.status === 'offline') {
// First login or no preference set yet — default to "online"
setCurrentStatus('online');
if (userId) {
updateStatusMutation({ userId, status: 'online' }).catch(() => {});
}
}
}
}, [myUser?.status]);
const handleLogout = async () => {
// Disconnect voice if connected
if (connectionState === 'connected') {
try { disconnectVoice(); } catch {}
}
// Clear persisted session
if (window.sessionPersistence) {
try { await window.sessionPersistence.clear(); } catch {}
}
// Clear storage (preserve theme)
const theme = localStorage.getItem('theme');
localStorage.clear();
if (theme) localStorage.setItem('theme', theme);
sessionStorage.clear();
navigate('/');
};
const effectiveMute = isMuted || isDeafened;
const statusColor = STATUS_OPTIONS.find(s => s.value === currentStatus)?.color || '#3ba55c';
@@ -191,15 +228,31 @@ const UserControlPanel = ({ username, userId }) => {
</button>
</Tooltip>
<Tooltip text="User Settings" position="top">
<button style={controlButtonStyle} onClick={() => setShowThemeSelector(true)}>
<button style={controlButtonStyle} onClick={() => setShowUserSettings(true)}>
<ColoredIcon
src={settingsIcon}
color={ICON_COLOR_DEFAULT}
/>
</button>
</Tooltip>
<Tooltip text="Log Out" position="top">
<button style={controlButtonStyle} onClick={handleLogout}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<path d="M16 17L21 12L16 7" stroke={ICON_COLOR_DEFAULT} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M21 12H9" stroke={ICON_COLOR_DEFAULT} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M9 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H9" stroke={ICON_COLOR_DEFAULT} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
</Tooltip>
</div>
{showThemeSelector && <ThemeSelector onClose={() => setShowThemeSelector(false)} />}
{showUserSettings && (
<UserSettings
onClose={() => setShowUserSettings(false)}
userId={userId}
username={username}
onLogout={handleLogout}
/>
)}
</div>
);
};
@@ -445,7 +498,7 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
};
const renderDMView = () => (
<div className="channel-list" style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column' }}>
<div className="channel-list" style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column', backgroundColor: 'var(--bg-tertiary)', borderLeft: '1px solid var(--app-frame-border)', borderTop: '1px solid var(--app-frame-border)', borderRadius: '8px 0 0 0' }}>
<DMList
dmChannels={dmChannels}
activeDMChannel={activeDMChannel}
@@ -504,13 +557,15 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
}, [channels]);
const renderServerView = () => (
<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 style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column', backgroundColor: 'var(--bg-tertiary)', borderLeft: '1px solid var(--app-frame-border)', borderTop: '1px solid var(--app-frame-border)', borderRadius: '8px 0 0 0' }}>
<div className="server-header" style={{ borderBottom: '1px solid var(--app-frame-border)' }}>
<span className="server-header-name" onClick={() => setIsServerSettingsOpen(true)}>Secure Chat</span>
<button className="server-header-invite" onClick={handleCreateInvite} title="Invite People">
<img src={inviteUserIcon} alt="Invite" />
</button>
</div>
<div className="channel-list" style={{ flex: 1, overflowY: 'auto' }}>
<div className="channel-list" style={{ flex: 1, overflowY: 'auto', backgroundColor: 'var(--bg-tertiary)' }}>
{isCreating && (
<div style={{ padding: '0 8px', marginBottom: '4px' }}>
<form onSubmit={handleSubmitCreate}>