feat: initialize Discord clone application with core backend services and Electron frontend.

This commit is contained in:
Bryan1029384756
2026-02-09 23:54:49 -06:00
parent e64cf20116
commit 516cfdbbd8
26 changed files with 622 additions and 161 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { io } from 'socket.io-client';
import Sidebar from '../components/Sidebar';
import ChatArea from '../components/ChatArea';
@@ -14,6 +14,10 @@ const Chat = () => {
const [userId, setUserId] = useState(null);
const [channelKeys, setChannelKeys] = useState({}); // { channelId: key_hex }
// DM state
const [activeDMChannel, setActiveDMChannel] = useState(null); // { channel_id, other_username }
const [dmChannels, setDMChannels] = useState([]);
const refreshData = () => {
const storedUsername = localStorage.getItem('username');
const userId = localStorage.getItem('userId');
@@ -56,9 +60,97 @@ const Chat = () => {
.catch(err => console.error('Error fetching channels:', err));
};
const fetchDMChannels = useCallback(() => {
const uid = localStorage.getItem('userId');
if (!uid) return;
fetch(`http://localhost:3000/api/dms/user/${uid}`)
.then(res => res.json())
.then(data => setDMChannels(data))
.catch(err => console.error('Error fetching DM channels:', err));
}, []);
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 res = await fetch('http://localhost:3000/api/dms/open', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: uid, targetUserId })
});
const { channelId, created } = await res.json();
// 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 usersRes = await fetch('http://localhost:3000/api/auth/users/public-keys');
const allUsers = await usersRes.json();
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 fetch('http://localhost:3000/api/channels/keys', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(batchKeys)
});
}
// Refresh channel keys so the new DM key is available
if (privateKey) {
const keysRes = await fetch(`http://localhost:3000/api/channels/keys/${uid}`);
const keysData = await keysRes.json();
const keys = {};
for (const item of keysData) {
try {
const bundleJson = await window.cryptoAPI.privateDecrypt(privateKey, item.encrypted_key_bundle);
const bundle = JSON.parse(bundleJson);
Object.assign(keys, bundle);
} catch (e) {
console.error(`Failed to decrypt keys for channel ${item.channel_id}`, e);
}
}
setChannelKeys(keys);
}
}
// 3. Set active DM and switch to me view
setActiveDMChannel({ channel_id: channelId, other_username: targetUsername });
setView('me');
fetchDMChannels();
} catch (err) {
console.error('Error opening DM:', err);
}
}, [fetchDMChannels]);
useEffect(() => {
refreshData();
fetchDMChannels();
// Listen for updates (requires socket connection)
const socket = io('http://localhost:3000');
@@ -82,6 +174,47 @@ const Chat = () => {
const { room, voiceStates } = useVoice();
// Determine what to render in the main area
const renderMainContent = () => {
if (view === 'me') {
if (activeDMChannel) {
return (
<ChatArea
channelId={activeDMChannel.channel_id}
channelName={activeDMChannel.other_username}
channelKey={channelKeys[activeDMChannel.channel_id]}
username={username}
userId={userId}
/>
);
}
return <FriendsView onOpenDM={openDM} />;
}
if (activeChannel) {
if (activeChannelObj?.type === 'voice') {
return <VoiceStage room={room} channelId={activeChannel} voiceStates={voiceStates} channelName={activeChannelObj?.name} />;
}
return (
<ChatArea
channelId={activeChannel}
channelName={activeChannelObj?.name || activeChannel}
channelKey={channelKeys[activeChannel]}
username={username}
userId={userId}
/>
);
}
return (
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#b9bbbe', flexDirection: 'column' }}>
<h2>Welcome to Secure Chat</h2>
<p>No channels found.</p>
<p>Click the <b>+</b> in the sidebar to create your first encrypted channel.</p>
</div>
);
};
return (
<div className="app-container">
<Sidebar
@@ -89,32 +222,21 @@ const Chat = () => {
activeChannel={activeChannel}
onSelectChannel={setActiveChannel}
username={username}
channelKeys={channelKeys}
onChannelCreated={refreshData} // Use refresh instead of reload
channelKeys={channelKeys}
onChannelCreated={refreshData}
view={view}
onViewChange={setView}
onViewChange={(v) => {
setView(v);
if (v === 'me') {
fetchDMChannels();
}
}}
onOpenDM={openDM}
activeDMChannel={activeDMChannel}
setActiveDMChannel={setActiveDMChannel}
dmChannels={dmChannels}
/>
{view === 'me' ? (
<FriendsView />
) : activeChannel ? (
activeChannelObj?.type === 'voice' ? (
<VoiceStage room={room} channelId={activeChannel} voiceStates={voiceStates} channelName={activeChannelObj?.name} />
) : (
<ChatArea
channelId={activeChannel}
channelName={activeChannelObj?.name || activeChannel}
channelKey={channelKeys[activeChannel]}
username={username}
userId={userId}
/>
)
) : (
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#b9bbbe', flexDirection: 'column' }}>
<h2>Welcome to Secure Chat</h2>
<p>No channels found.</p>
<p>Click the <b>+</b> in the sidebar to create your first encrypted channel.</p>
</div>
)}
{renderMainContent()}
</div>
);
};