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
Some checks failed
Build and Release / build-and-release (push) Has been cancelled
This commit is contained in:
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user