import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useQuery, useConvex } from 'convex/react'; import { api } from '../../../../convex/_generated/api'; import Sidebar from '../components/Sidebar'; import ChatArea from '../components/ChatArea'; import VoiceStage from '../components/VoiceStage'; import { useVoice } from '../contexts/VoiceContext'; import FriendsView from '../components/FriendsView'; import MembersList from '../components/MembersList'; import ChatHeader from '../components/ChatHeader'; import { useToasts } from '../components/Toast'; import { PresenceProvider } from '../contexts/PresenceContext'; const Chat = () => { const [view, setView] = useState('server'); const [activeChannel, setActiveChannel] = useState(null); const [username, setUsername] = useState(''); const [userId, setUserId] = useState(null); const [channelKeys, setChannelKeys] = useState({}); const [activeDMChannel, setActiveDMChannel] = useState(null); const [showMembers, setShowMembers] = useState(true); const [showPinned, setShowPinned] = useState(false); const convex = useConvex(); const { toasts, addToast, removeToast, ToastContainer } = useToasts(); const prevDmChannelsRef = useRef(null); const { toggleMute } = useVoice(); // Keyboard shortcuts useEffect(() => { const handler = (e) => { if (e.ctrlKey && e.key === 'k') { e.preventDefault(); // Quick switcher placeholder - could open a search modal } if (e.ctrlKey && e.shiftKey && e.key === 'M') { e.preventDefault(); toggleMute(); } }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, [toggleMute]); const channels = useQuery(api.channels.list) || []; const categories = useQuery(api.categories.list) || []; const rawChannelKeys = useQuery( api.channelKeys.getKeysForUser, userId ? { userId } : "skip" ); const dmChannels = useQuery( api.dms.listDMs, userId ? { userId } : "skip" ) || []; useEffect(() => { const storedUsername = localStorage.getItem('username'); const storedUserId = localStorage.getItem('userId'); if (storedUsername) setUsername(storedUsername); if (storedUserId) setUserId(storedUserId); }, []); useEffect(() => { if (!rawChannelKeys || rawChannelKeys.length === 0) return; const privateKey = sessionStorage.getItem('privateKey'); if (!privateKey) return; async function decryptKeys() { const keys = {}; for (const item of rawChannelKeys) { try { const bundleJson = await window.cryptoAPI.privateDecrypt(privateKey, item.encrypted_key_bundle); Object.assign(keys, JSON.parse(bundleJson)); } catch (e) { console.error(`Failed to decrypt keys for channel ${item.channel_id}`, e); } } setChannelKeys(keys); } decryptKeys(); }, [rawChannelKeys]); useEffect(() => { if (activeChannel || channels.length === 0) return; const firstTextChannel = channels.find(c => c.type === 'text'); if (firstTextChannel) { setActiveChannel(firstTextChannel._id); } }, [channels, activeChannel]); const openDM = useCallback(async (targetUserId, targetUsername) => { const uid = localStorage.getItem('userId'); if (!uid) return; try { const { channelId, created } = await convex.mutation(api.dms.openDM, { userId: uid, targetUserId }); if (created) { const keyBytes = new Uint8Array(32); crypto.getRandomValues(keyBytes); const keyHex = Array.from(keyBytes).map(b => b.toString(16).padStart(2, '0')).join(''); const allUsers = await convex.query(api.auth.getPublicKeys, {}); const participants = allUsers.filter(u => u.id === uid || u.id === targetUserId); const batchKeys = []; for (const u of participants) { if (!u.public_identity_key) continue; try { 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 DM key for user', u.id, e); } } if (batchKeys.length > 0) { await convex.mutation(api.channelKeys.uploadKeys, { keys: batchKeys }); } } setActiveDMChannel({ channel_id: channelId, other_username: targetUsername }); setView('me'); } catch (err) { console.error('Error opening DM:', err); } }, [convex]); const handleSelectChannel = useCallback((channelId) => { setActiveChannel(channelId); setShowPinned(false); }, []); const activeChannelObj = channels.find(c => c._id === activeChannel); const { room, voiceStates } = useVoice(); const isDMView = view === 'me' && activeDMChannel; const isServerTextChannel = view === 'server' && activeChannel && activeChannelObj?.type !== 'voice'; const currentChannelId = isDMView ? activeDMChannel.channel_id : activeChannel; function renderMainContent() { if (view === 'me') { if (activeDMChannel) { return (
No channels found.
Click the + in the sidebar to create your first encrypted channel.