feat: initialize Discord clone application with core backend services and Electron frontend.
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user