feat: Add initial Discord clone application with Convex backend services and Electron React frontend components.

This commit is contained in:
Bryan1029384756
2026-02-10 05:27:10 -06:00
parent 47f173c79b
commit 34e9790db9
29 changed files with 3254 additions and 1398 deletions

View File

@@ -8,33 +8,27 @@ import { useVoice } from '../contexts/VoiceContext';
import FriendsView from '../components/FriendsView';
const Chat = () => {
const [view, setView] = useState('server'); // 'server' | 'me'
const [view, setView] = useState('server');
const [activeChannel, setActiveChannel] = useState(null);
const [username, setUsername] = useState('');
const [userId, setUserId] = useState(null);
const [channelKeys, setChannelKeys] = useState({}); // { channelId: key_hex }
// DM state
const [activeDMChannel, setActiveDMChannel] = useState(null); // { channel_id, other_username }
const [channelKeys, setChannelKeys] = useState({});
const [activeDMChannel, setActiveDMChannel] = useState(null);
const convex = useConvex();
// Reactive channel list from Convex (auto-updates!)
const channels = useQuery(api.channels.list) || [];
// Reactive channel keys from Convex
const rawChannelKeys = useQuery(
api.channelKeys.getKeysForUser,
userId ? { userId } : "skip"
);
// Reactive DM channels from Convex
const dmChannels = useQuery(
api.dms.listDMs,
userId ? { userId } : "skip"
) || [];
// Initialize user from localStorage
useEffect(() => {
const storedUsername = localStorage.getItem('username');
const storedUserId = localStorage.getItem('userId');
@@ -42,58 +36,50 @@ const Chat = () => {
if (storedUserId) setUserId(storedUserId);
}, []);
// Decrypt channel keys when raw keys change
useEffect(() => {
if (!rawChannelKeys || rawChannelKeys.length === 0) return;
const privateKey = sessionStorage.getItem('privateKey');
if (!privateKey) return;
const decryptKeys = async () => {
async function decryptKeys() {
const keys = {};
for (const item of rawChannelKeys) {
try {
const bundleJson = await window.cryptoAPI.privateDecrypt(privateKey, item.encrypted_key_bundle);
const bundle = JSON.parse(bundleJson);
Object.assign(keys, 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]);
// Auto-select first text channel when channels load
useEffect(() => {
if (!activeChannel && channels.length > 0) {
const firstTextChannel = channels.find(c => c.type === 'text');
if (firstTextChannel) {
setActiveChannel(firstTextChannel._id);
}
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');
const privateKey = sessionStorage.getItem('privateKey');
if (!uid) return;
try {
// 1. Find or create the DM channel
const { channelId, created } = await convex.mutation(api.dms.openDM, {
userId: uid,
targetUserId
});
// 2. If newly created, generate + distribute an AES key for both users
if (created) {
const keyBytes = new Uint8Array(32);
crypto.getRandomValues(keyBytes);
const keyHex = Array.from(keyBytes).map(b => b.toString(16).padStart(2, '0')).join('');
// Fetch both users' public keys
const allUsers = await convex.query(api.auth.getPublicKeys, {});
const participants = allUsers.filter(u => u.id === uid || u.id === targetUserId);
@@ -117,10 +103,8 @@ const Chat = () => {
if (batchKeys.length > 0) {
await convex.mutation(api.channelKeys.uploadKeys, { keys: batchKeys });
}
// Channel keys will auto-update via reactive query
}
// 3. Set active DM and switch to me view
setActiveDMChannel({ channel_id: channelId, other_username: targetUsername });
setView('me');
@@ -129,13 +113,10 @@ const Chat = () => {
}
}, [convex]);
// Helper to get active channel object
const activeChannelObj = channels.find(c => c._id === activeChannel);
const { room, voiceStates } = useVoice();
// Determine what to render in the main area
const renderMainContent = () => {
function renderMainContent() {
if (view === 'me') {
if (activeDMChannel) {
return (
@@ -173,7 +154,7 @@ const Chat = () => {
<p>Click the <b>+</b> in the sidebar to create your first encrypted channel.</p>
</div>
);
};
}
return (
<div className="app-container">
@@ -184,9 +165,7 @@ const Chat = () => {
username={username}
channelKeys={channelKeys}
view={view}
onViewChange={(v) => {
setView(v);
}}
onViewChange={setView}
onOpenDM={openDM}
activeDMChannel={activeDMChannel}
setActiveDMChannel={setActiveDMChannel}