feat: initialize Discord clone application with core backend services and Electron frontend.
This commit is contained in:
@@ -1,20 +1,13 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ColoredIcon } from './Sidebar'; // Reusing helper if valid, or will define local
|
||||
import friendsIcon from '../assets/icons/friends.svg'; // Need to import or mock
|
||||
// Attempting to reuse common styles or define new ones.
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
const DMList = ({ onSelectDM }) => {
|
||||
const [users, setUsers] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch all users to simulate DMs
|
||||
fetch('http://localhost:3000/api/auth/users/public-keys')
|
||||
.then(res => res.json())
|
||||
.then(data => setUsers(data))
|
||||
.catch(err => console.error(err));
|
||||
}, []);
|
||||
const DMList = ({ dmChannels, activeDMChannel, onSelectDM, onOpenDM }) => {
|
||||
const [showUserPicker, setShowUserPicker] = useState(false);
|
||||
const [allUsers, setAllUsers] = useState([]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const searchRef = useRef(null);
|
||||
|
||||
const getUserColor = (username) => {
|
||||
if (!username) return '#5865F2';
|
||||
const colors = ['#5865F2', '#EBA7CD', '#57F287', '#FEE75C', '#EB459E', '#ED4245'];
|
||||
let hash = 0;
|
||||
for (let i = 0; i < username.length; i++) {
|
||||
@@ -23,9 +16,34 @@ const DMList = ({ onSelectDM }) => {
|
||||
return colors[Math.abs(hash) % colors.length];
|
||||
};
|
||||
|
||||
const handleOpenUserPicker = () => {
|
||||
setShowUserPicker(true);
|
||||
setSearchQuery('');
|
||||
// Fetch all users for the picker
|
||||
fetch('http://localhost:3000/api/auth/users/public-keys')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const myId = localStorage.getItem('userId');
|
||||
setAllUsers(data.filter(u => u.id !== myId));
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (showUserPicker && searchRef.current) {
|
||||
searchRef.current.focus();
|
||||
}
|
||||
}, [showUserPicker]);
|
||||
|
||||
const filteredUsers = allUsers.filter(u =>
|
||||
u.username?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '8px', flex: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
<button
|
||||
{/* Search / New DM Button */}
|
||||
<button
|
||||
onClick={handleOpenUserPicker}
|
||||
style={{
|
||||
width: '100%',
|
||||
textAlign: 'left',
|
||||
@@ -42,75 +60,183 @@ const DMList = ({ onSelectDM }) => {
|
||||
Find or start a conversation
|
||||
</button>
|
||||
|
||||
<div
|
||||
{/* User Picker Modal/Dropdown */}
|
||||
{showUserPicker && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||
zIndex: 100,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
onClick={() => setShowUserPicker(false)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: '#36393f',
|
||||
borderRadius: '8px',
|
||||
padding: '16px',
|
||||
width: '400px',
|
||||
maxHeight: '500px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<h3 style={{ color: '#fff', margin: '0 0 4px 0', fontSize: '16px' }}>Select a User</h3>
|
||||
<p style={{ color: '#b9bbbe', fontSize: '12px', margin: '0 0 12px 0' }}>Start a new direct message conversation.</p>
|
||||
<input
|
||||
ref={searchRef}
|
||||
type="text"
|
||||
placeholder="Type a username..."
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
backgroundColor: '#202225',
|
||||
border: '1px solid #040405',
|
||||
borderRadius: '4px',
|
||||
color: '#dcddde',
|
||||
padding: '8px 12px',
|
||||
fontSize: '14px',
|
||||
outline: 'none',
|
||||
marginBottom: '8px',
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
/>
|
||||
<div style={{ flex: 1, overflowY: 'auto', maxHeight: '300px' }}>
|
||||
{filteredUsers.map(user => (
|
||||
<div
|
||||
key={user.id}
|
||||
onClick={() => {
|
||||
setShowUserPicker(false);
|
||||
onOpenDM(user.id, user.username);
|
||||
}}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
color: '#dcddde'
|
||||
}}
|
||||
onMouseEnter={e => e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.06)'}
|
||||
onMouseLeave={e => e.currentTarget.style.backgroundColor = 'transparent'}
|
||||
>
|
||||
<div style={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: getUserColor(user.username),
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: 'white', fontWeight: '600', marginRight: '12px', flexShrink: 0
|
||||
}}>
|
||||
{(user.username ?? '?').substring(0, 1).toUpperCase()}
|
||||
</div>
|
||||
<span style={{ fontWeight: '500' }}>{user.username}</span>
|
||||
</div>
|
||||
))}
|
||||
{filteredUsers.length === 0 && (
|
||||
<div style={{ color: '#72767d', textAlign: 'center', padding: '16px', fontSize: '13px' }}>
|
||||
No users found.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Friends Button */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '10px 8px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'rgba(255,255,255,0.04)', // Selected state for "Friends"
|
||||
color: '#fff',
|
||||
backgroundColor: !activeDMChannel ? 'rgba(255,255,255,0.04)' : 'transparent',
|
||||
color: !activeDMChannel ? '#fff' : '#96989d',
|
||||
cursor: 'pointer',
|
||||
marginBottom: '16px'
|
||||
}}
|
||||
onClick={() => onSelectDM('friends')}
|
||||
onMouseEnter={e => { if (activeDMChannel) e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.02)'; }}
|
||||
onMouseLeave={e => { if (activeDMChannel) e.currentTarget.style.backgroundColor = 'transparent'; }}
|
||||
>
|
||||
<div style={{ marginRight: '12px' }}>
|
||||
{/* Friends Icon Mock */}
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M13.5 2C13.5 2.82843 12.8284 3.5 12 3.5C11.1716 3.5 10.5 2.82843 10.5 2C10.5 1.17157 11.1716 0.5 12 0.5C12.8284 0.5 13.5 1.17157 13.5 2Z" fill="currentColor"/>
|
||||
{/* Use a generic user-group icon path later, keeping simple for now */}
|
||||
<path d="M7 13C7 11.8954 7.89543 11 9 11H15C16.1046 11 17 11.8954 17 13V15H7V13Z" fill="#fff"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span style={{ fontWeight: 500 }}>Friends</span>
|
||||
</div>
|
||||
|
||||
{/* DM List Header */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '0 8px 8px', color: '#96989d', fontSize: '11px', fontWeight: 'bold', textTransform: 'uppercase' }}>
|
||||
<span>Direct Messages</span>
|
||||
<span style={{ cursor: 'pointer', fontSize: '16px' }}>+</span>
|
||||
<span
|
||||
style={{ cursor: 'pointer', fontSize: '16px' }}
|
||||
onClick={handleOpenUserPicker}
|
||||
title="New DM"
|
||||
>+</span>
|
||||
</div>
|
||||
|
||||
{/* DM Channel List */}
|
||||
<div style={{ flex: 1, overflowY: 'auto' }}>
|
||||
{users.map(user => (
|
||||
<div
|
||||
key={user.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
color: '#96989d',
|
||||
':hover': { backgroundColor: 'rgba(255,255,255,0.02)', color: '#dcddde' }
|
||||
}}
|
||||
>
|
||||
<div style={{ position: 'relative', marginRight: '12px' }}>
|
||||
<div style={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: getUserColor(user.username),
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: 'white', fontWeight: '600'
|
||||
}}>
|
||||
{user.username.substring(0,1).toUpperCase()}
|
||||
</div>
|
||||
<div style={{
|
||||
position: 'absolute', bottom: -2, right: -2,
|
||||
width: 10, height: 10, borderRadius: '50%',
|
||||
backgroundColor: '#3ba55c', // Assume online
|
||||
border: '2px solid #2f3136'
|
||||
}} />
|
||||
</div>
|
||||
<div style={{ overflow: 'hidden' }}>
|
||||
<div style={{ color: '#dcddde', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden', fontWeight: '500' }}>{user.username}</div>
|
||||
<div style={{ fontSize: '11px', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }}>
|
||||
Online
|
||||
{(dmChannels || []).map(dm => {
|
||||
const isActive = activeDMChannel?.channel_id === dm.channel_id;
|
||||
return (
|
||||
<div
|
||||
key={dm.channel_id}
|
||||
onClick={() => onSelectDM({ channel_id: dm.channel_id, other_username: dm.other_username })}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
color: isActive ? '#fff' : '#96989d',
|
||||
backgroundColor: isActive ? 'rgba(255,255,255,0.06)' : 'transparent'
|
||||
}}
|
||||
onMouseEnter={e => { if (!isActive) e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.02)'; }}
|
||||
onMouseLeave={e => { if (!isActive) e.currentTarget.style.backgroundColor = 'transparent'; }}
|
||||
>
|
||||
<div style={{ position: 'relative', marginRight: '12px' }}>
|
||||
<div style={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: getUserColor(dm.other_username),
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: 'white', fontWeight: '600'
|
||||
}}>
|
||||
{(dm.other_username ?? '?').substring(0, 1).toUpperCase()}
|
||||
</div>
|
||||
<div style={{
|
||||
position: 'absolute', bottom: -2, right: -2,
|
||||
width: 10, height: 10, borderRadius: '50%',
|
||||
backgroundColor: '#3ba55c',
|
||||
border: '2px solid #2f3136'
|
||||
}} />
|
||||
</div>
|
||||
<div style={{ overflow: 'hidden' }}>
|
||||
<div style={{ color: isActive ? '#fff' : '#dcddde', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden', fontWeight: '500' }}>
|
||||
{dm.other_username}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{(!dmChannels || dmChannels.length === 0) && (
|
||||
<div style={{ color: '#72767d', fontSize: '13px', textAlign: 'center', padding: '16px 8px' }}>
|
||||
No DMs yet. Click + to start a conversation.
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const FriendsView = () => {
|
||||
const FriendsView = ({ onOpenDM }) => {
|
||||
const [users, setUsers] = useState([]);
|
||||
const [activeTab, setActiveTab] = useState('Online'); // Online, All, Pending, Blocked, AddFriend
|
||||
const [activeTab, setActiveTab] = useState('Online');
|
||||
|
||||
useEffect(() => {
|
||||
const myId = localStorage.getItem('userId');
|
||||
fetch('http://localhost:3000/api/auth/users/public-keys')
|
||||
.then(res => res.json())
|
||||
.then(data => setUsers(data))
|
||||
.then(data => setUsers(data.filter(u => u.id !== myId)))
|
||||
.catch(err => console.error(err));
|
||||
}, []);
|
||||
|
||||
const getUserColor = (username) => {
|
||||
if (!username) return '#747f8d';
|
||||
const colors = ['#5865F2', '#EBA7CD', '#57F287', '#FEE75C', '#EB459E', '#ED4245'];
|
||||
let hash = 0;
|
||||
for (let i = 0; i < username.length; i++) {
|
||||
@@ -25,7 +27,7 @@ const FriendsView = () => {
|
||||
// In real app, "Online" would filter by status.
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, backgroundColor: '#36393f', height: '100vh' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, backgroundColor: 'hsl(240 calc(1*7.143%) 10.98% /1)', height: '100vh' }}>
|
||||
{/* Top Bar */}
|
||||
<div style={{
|
||||
height: '48px',
|
||||
@@ -46,8 +48,8 @@ const FriendsView = () => {
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '16px' }}>
|
||||
{['Online', 'All', 'Pending', 'Blocked'].map(tab => (
|
||||
<div
|
||||
{['Online', 'All'].map(tab => (
|
||||
<div
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
style={{
|
||||
@@ -61,17 +63,6 @@ const FriendsView = () => {
|
||||
{tab}
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
color: '#fff',
|
||||
backgroundColor: '#3ba55c',
|
||||
padding: '2px 8px',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
>
|
||||
Add Friend
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -112,7 +103,7 @@ const FriendsView = () => {
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: 'white', fontWeight: '600'
|
||||
}}>
|
||||
{user.username.substring(0,1).toUpperCase()}
|
||||
{(user.username ?? '?').substring(0,1).toUpperCase()}
|
||||
</div>
|
||||
<div style={{
|
||||
position: 'absolute', bottom: -2, right: -2,
|
||||
@@ -123,7 +114,7 @@ const FriendsView = () => {
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: '#fff', fontWeight: '600' }}>
|
||||
{user.username}
|
||||
{user.username ?? 'Unknown'}
|
||||
</div>
|
||||
<div style={{ color: '#b9bbbe', fontSize: '12px' }}>
|
||||
Online
|
||||
@@ -133,7 +124,11 @@ const FriendsView = () => {
|
||||
|
||||
{/* Actions */}
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<div style={{ padding: 8, backgroundColor: '#2f3136', borderRadius: '50%', cursor: 'pointer' }}>
|
||||
<div
|
||||
style={{ padding: 8, backgroundColor: '#2f3136', borderRadius: '50%', cursor: 'pointer' }}
|
||||
onClick={() => onOpenDM && onOpenDM(user.id, user.username)}
|
||||
title="Message"
|
||||
>
|
||||
💬
|
||||
</div>
|
||||
<div style={{ padding: 8, backgroundColor: '#2f3136', borderRadius: '50%', cursor: 'pointer' }}>
|
||||
|
||||
@@ -181,7 +181,7 @@ const UserControlPanel = ({ username }) => {
|
||||
|
||||
|
||||
|
||||
const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKeys, onChannelCreated, view, onViewChange }) => {
|
||||
const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKeys, onChannelCreated, view, onViewChange, onOpenDM, activeDMChannel, setActiveDMChannel, dmChannels }) => {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isServerSettingsOpen, setIsServerSettingsOpen] = useState(false); // New State
|
||||
const [newChannelName, setNewChannelName] = useState('');
|
||||
@@ -437,7 +437,6 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
|
||||
return (
|
||||
<div className="sidebar" style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', flex: 1, minHeight: 0 }}>
|
||||
<div style={{ display: 'flex', flex: 1, minHeight: 0 }}>
|
||||
<div className="server-list">
|
||||
{/* Home Button */}
|
||||
@@ -469,7 +468,18 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
{/* Channel List Area */}
|
||||
{view === 'me' ? (
|
||||
<div className="channel-list" style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column' }}>
|
||||
<DMList onSelectDM={() => {}} />
|
||||
<DMList
|
||||
dmChannels={dmChannels}
|
||||
activeDMChannel={activeDMChannel}
|
||||
onSelectDM={(dm) => {
|
||||
if (dm === 'friends') {
|
||||
setActiveDMChannel(null);
|
||||
} else {
|
||||
setActiveDMChannel(dm);
|
||||
}
|
||||
}}
|
||||
onOpenDM={onOpenDM}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="channel-list" style={{ flex: 1, overflowY: 'auto' }}>
|
||||
@@ -670,6 +680,7 @@ const Sidebar = ({ channels, activeChannel, onSelectChannel, username, channelKe
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Voice Connection Panel */}
|
||||
{connectionState === 'connected' && (
|
||||
|
||||
@@ -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