feat: Add initial Discord clone application with Convex backend services and Electron React frontend components.
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user