706 lines
28 KiB
JavaScript
706 lines
28 KiB
JavaScript
import React, { useEffect, useState, useRef } from 'react';
|
|
import { network, ADMIN_PUBLIC_KEY } from '../p2p/index.js';
|
|
import Sidebar from './Sidebar.jsx';
|
|
import ChannelList from './ChannelList.jsx';
|
|
import DMList from './DMList.jsx';
|
|
import ChatArea from './ChatArea.jsx';
|
|
import FriendsView from './FriendsView.jsx';
|
|
import OnlineUsers from './OnlineUsers.jsx';
|
|
import ProfileSettingsModal from './ProfileSettingsModal.jsx';
|
|
import CreateServerModal from './CreateServerModal.jsx';
|
|
import CreateGroupModal from './CreateGroupModal.jsx';
|
|
import InviteModal from './InviteModal.jsx';
|
|
import ServerSettingsModal from './ServerSettingsModal.jsx';
|
|
import CallView from './CallView.jsx';
|
|
import GroupCallView from './GroupCallView.jsx';
|
|
import IncomingCallModal from './IncomingCallModal.jsx';
|
|
|
|
export default function MainApp({ profile, setProfile, onLogout, updateState, simulatedProgress, triggerRestart, onSystemUpdate }) {
|
|
const[myKey, setMyKey] = useState('');
|
|
const[onlinePeers, setOnlinePeers] = useState([]);
|
|
const[knownUsers, setKnownUsers] = useState([]);
|
|
const[messages, setMessages] = useState([]);
|
|
const[servers, setServers] = useState([]);
|
|
const[serverMembers, setServerMembers] = useState({});
|
|
const[isSyncing, setIsSyncing] = useState(false);
|
|
const[transfers, setTransfers] = useState({});
|
|
|
|
const[activeView, setActiveView] = useState('dms');
|
|
const[activeChannel, setActiveChannel] = useState('general-chat');
|
|
const[activeDm, setActiveDm] = useState('friends');
|
|
|
|
const[dms, setDms] = useState({});
|
|
const[typingUsers, setTypingUsers] = useState({});
|
|
|
|
const[readReceipts, setReadReceipts] = useState(() => JSON.parse(localStorage.getItem('pear_read_receipts') || '{}'));
|
|
const[deliveredReceipts, setDeliveredReceipts] = useState(() => JSON.parse(localStorage.getItem('pear_delivered_receipts') || '{}'));
|
|
const[lastRead, setLastRead] = useState(() => JSON.parse(localStorage.getItem('pear_last_read') || '{}'));
|
|
|
|
const[isSettingsOpen, setIsSettingsOpen] = useState(false);
|
|
const[isCreateServerOpen, setIsCreateServerOpen] = useState(false);
|
|
const[isCreateGroupOpen, setIsCreateGroupOpen] = useState(false);
|
|
const[inviteModalServer, setInviteModalServer] = useState(null);
|
|
const[settingsModalServer, setSettingsModalServer] = useState(null);
|
|
|
|
// Call States
|
|
const[activeCall, setActiveCall] = useState(null);
|
|
const[activeGroupCall, setActiveGroupCall] = useState(null);
|
|
const[activeVc, setActiveVc] = useState(null);
|
|
const[incomingCall, setIncomingCall] = useState(null);
|
|
const[showChatInCall, setShowChatInCall] = useState(false);
|
|
const callTimeoutRef = useRef(null);
|
|
|
|
const[vcStates, setVcStates] = useState({});
|
|
const[showMembersDrawer, setShowMembersDrawer] = useState(false);
|
|
|
|
const [isNetworkOnline, setIsNetworkOnline] = useState(typeof navigator !== 'undefined' ? navigator.onLine : true);
|
|
|
|
const initialized = useRef(false);
|
|
|
|
useEffect(() => {
|
|
const handleOnline = () => {
|
|
setIsNetworkOnline(true);
|
|
setOnlinePeers(network.getPeerList());
|
|
network.reconnect();
|
|
};
|
|
const handleOffline = () => {
|
|
setIsNetworkOnline(false);
|
|
setOnlinePeers([]);
|
|
};
|
|
|
|
window.addEventListener('online', handleOnline);
|
|
window.addEventListener('offline', handleOffline);
|
|
|
|
return () => {
|
|
window.removeEventListener('online', handleOnline);
|
|
window.removeEventListener('offline', handleOffline);
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
localStorage.setItem('pear_last_read', JSON.stringify(lastRead));
|
|
},[lastRead]);
|
|
|
|
useEffect(() => {
|
|
localStorage.setItem('pear_read_receipts', JSON.stringify(readReceipts));
|
|
},[readReceipts]);
|
|
|
|
useEffect(() => {
|
|
localStorage.setItem('pear_delivered_receipts', JSON.stringify(deliveredReceipts));
|
|
},[deliveredReceipts]);
|
|
|
|
useEffect(() => {
|
|
if (!initialized.current && typeof window !== 'undefined') {
|
|
initialized.current = true;
|
|
|
|
network.onInit = (key) => setMyKey(key);
|
|
network.onPeerUpdate = (peers) => {
|
|
if (typeof navigator !== 'undefined' && navigator.onLine) {
|
|
setOnlinePeers([...peers]);
|
|
}
|
|
};
|
|
network.onKnownProfilesUpdate = (users) => setKnownUsers(users);
|
|
network.onMessage = (msgs) => setMessages([...msgs]);
|
|
network.onDMsUpdate = (updatedDms) => setDms(updatedDms);
|
|
network.onTransfersUpdate = (t) => setTransfers({...t});
|
|
network.onServersUpdate = (srvs) => {
|
|
setServers([...srvs]);
|
|
setActiveView(prev => {
|
|
if (prev !== 'dms' && !srvs.some(s => s.topicHex === prev)) return 'dms';
|
|
return prev;
|
|
});
|
|
};
|
|
network.onServerMembersUpdate = (members) => setServerMembers(members);
|
|
network.onSync = (status) => setIsSyncing(status);
|
|
|
|
network.onEphemeral = (peerKey, payload) => {
|
|
if (payload.type === 'system_update') {
|
|
try {
|
|
const b4a = window.require('b4a');
|
|
const sodium = window.require('sodium-native');
|
|
|
|
const sigBuf = b4a.from(payload.signature, 'hex');
|
|
const msgBuf = b4a.from(payload.version + payload.timestamp);
|
|
const pubBuf = b4a.from(ADMIN_PUBLIC_KEY, 'hex');
|
|
|
|
const isValid = sodium.crypto_sign_verify_detached(sigBuf, msgBuf, pubBuf);
|
|
|
|
if (isValid && payload.version !== window.APP_VERSION) {
|
|
if (onSystemUpdate) onSystemUpdate(payload.version, payload);
|
|
} else if (!isValid) {
|
|
console.warn('[P2P] Received invalid update broadcast signature.');
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to verify update broadcast", e);
|
|
}
|
|
} else if (payload.type === 'typing') {
|
|
setTypingUsers(prev => ({
|
|
...prev,[peerKey]: { channel: payload.channel, displayName: payload.displayName, timestamp: Date.now() }
|
|
}));
|
|
} else if (payload.type === 'read') {
|
|
const markChannel = payload.channel === network.myKey ? peerKey : payload.channel;
|
|
setReadReceipts(prev => ({ ...prev, [markChannel]: payload.messageId }));
|
|
} else if (payload.type === 'delivered') {
|
|
const markChannel = payload.channel === network.myKey ? peerKey : payload.channel;
|
|
setDeliveredReceipts(prev => ({ ...prev,[markChannel]: payload.messageId }));
|
|
} else if (payload.type === 'vc-state') {
|
|
setVcStates(prev => {
|
|
const serverVCS = prev[payload.serverTopicHex] || {};
|
|
const channelVCS = serverVCS[payload.channel] || {};
|
|
return {
|
|
...prev,
|
|
[payload.serverTopicHex]: {
|
|
...serverVCS,
|
|
[payload.channel]: {
|
|
...channelVCS,
|
|
[peerKey]: {
|
|
muted: payload.muted,
|
|
screenshare: payload.screenshare,
|
|
timestamp: Date.now()
|
|
}
|
|
}
|
|
}
|
|
};
|
|
});
|
|
} else if (payload.type === 'vc-leave') {
|
|
setVcStates(prev => {
|
|
const serverVCS = prev[payload.serverTopicHex];
|
|
if (!serverVCS) return prev;
|
|
const channelVCS = serverVCS[payload.channel];
|
|
if (!channelVCS) return prev;
|
|
|
|
const newChannelVCS = { ...channelVCS };
|
|
delete newChannelVCS[peerKey];
|
|
|
|
return {
|
|
...prev,
|
|
[payload.serverTopicHex]: {
|
|
...serverVCS,
|
|
[payload.channel]: newChannelVCS
|
|
}
|
|
};
|
|
});
|
|
}
|
|
};
|
|
|
|
network.initialize(profile.seedHex, profile.displayName, profile.username, profile.avatar)
|
|
.catch(err => {
|
|
alert("P2P Initialization Error:\n" + err.message + "\n\nPress F12 to open DevTools for more info.");
|
|
console.error(err);
|
|
});
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
},[]);
|
|
|
|
useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
const now = Date.now();
|
|
setVcStates(prev => {
|
|
let changed = false;
|
|
const next = { ...prev };
|
|
for (const serverId in next) {
|
|
for (const channelId in next[serverId]) {
|
|
for (const peerKey in next[serverId][channelId]) {
|
|
if (now - next[serverId][channelId][peerKey].timestamp > 10000) {
|
|
delete next[serverId][channelId][peerKey];
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return changed ? next : prev;
|
|
});
|
|
}, 5000);
|
|
return () => clearInterval(interval);
|
|
},[]);
|
|
|
|
useEffect(() => {
|
|
if (!myKey) return;
|
|
onlinePeers.forEach(peer => {
|
|
const msgsFromPeer = messages.filter(m => m.sender === peer.key && m.recipient === myKey);
|
|
if (msgsFromPeer.length > 0) {
|
|
const lastMsg = msgsFromPeer[msgsFromPeer.length - 1];
|
|
network.sendDeliveredReceipt(peer.key, lastMsg.id);
|
|
}
|
|
});
|
|
},[messages.length, onlinePeers.length, myKey]);
|
|
|
|
useEffect(() => {
|
|
network.activeCalls = (activeCall ? 1 : 0) + (activeGroupCall ? 1 : 0) + (activeVc ? 1 : 0);
|
|
},[activeCall, activeGroupCall, activeVc]);
|
|
|
|
useEffect(() => {
|
|
if (activeView === 'dms' && activeDm !== 'friends') {
|
|
const isUserDm = !!dms[activeDm];
|
|
const isGc = servers.some(s => s.topicHex === activeDm && s.isGroupChat);
|
|
if (!isUserDm && !isGc) {
|
|
setActiveDm('friends');
|
|
}
|
|
}
|
|
},[servers, dms, activeView, activeDm]);
|
|
|
|
useEffect(() => {
|
|
const handleWebRTC = (peerKey, payload) => {
|
|
if (payload.type === 'webrtc-init') {
|
|
if (!activeCall && !activeGroupCall && !activeVc) {
|
|
const callerProfile = knownUsers.find(u => u.key === peerKey) || dms[peerKey]?.profile || { displayName: 'Unknown' };
|
|
setIncomingCall({ isGroup: false, targetKey: peerKey, profile: callerProfile, callType: payload.callType || 'voice' });
|
|
} else {
|
|
network.sendWebRTCSignal(peerKey, { type: 'webrtc-busy' });
|
|
}
|
|
} else if (payload.type === 'webrtc-cancel') {
|
|
setIncomingCall(current => (current && !current.isGroup && current.targetKey === peerKey) ? null : current);
|
|
} else if (payload.type === 'webrtc-accept') {
|
|
setActiveCall(current => {
|
|
if (current?.targetKey === peerKey) {
|
|
if (callTimeoutRef.current) clearTimeout(callTimeoutRef.current);
|
|
return { ...current, status: 'connecting' };
|
|
}
|
|
return current;
|
|
});
|
|
} else if (payload.type === 'webrtc-decline' || payload.type === 'webrtc-busy') {
|
|
setActiveCall(current => {
|
|
if (current?.targetKey === peerKey) {
|
|
if (callTimeoutRef.current) clearTimeout(callTimeoutRef.current);
|
|
alert(payload.type === 'webrtc-busy' ? 'User is busy' : 'Call declined');
|
|
return null;
|
|
}
|
|
return current;
|
|
});
|
|
} else if (payload.type === 'webrtc-end') {
|
|
setActiveCall(current => current?.targetKey === peerKey ? null : current);
|
|
}
|
|
else if (payload.type === 'webrtc-group-ring') {
|
|
const gc = servers.find(s => s.topicHex === payload.channel && s.isGroupChat);
|
|
if (gc && activeGroupCall?.channel !== payload.channel && !activeVc) {
|
|
setIncomingCall({ isGroup: true, channel: payload.channel, callerName: payload.callerName, gcName: gc.name, callType: payload.callType || 'voice' });
|
|
}
|
|
}
|
|
};
|
|
network.addWebRTCListener(handleWebRTC);
|
|
return () => network.removeWebRTCListener(handleWebRTC);
|
|
},[activeCall, activeGroupCall, activeVc, knownUsers, dms, servers]);
|
|
|
|
const handleSaveProfile = (newName, newAvatar, newUsername) => {
|
|
const updatedProfile = { ...profile, displayName: newName, avatar: newAvatar, username: newUsername || profile.username };
|
|
|
|
const accounts = JSON.parse(localStorage.getItem('pear_saved_accounts') || '[]');
|
|
const existingIndex = accounts.findIndex(a => a.seedHex === profile.seedHex);
|
|
if (existingIndex >= 0) {
|
|
accounts[existingIndex] = updatedProfile;
|
|
localStorage.setItem('pear_saved_accounts', JSON.stringify(accounts));
|
|
}
|
|
|
|
localStorage.setItem('pear_discord_identity', JSON.stringify(updatedProfile));
|
|
setProfile(updatedProfile);
|
|
network.updateProfile(newName, newAvatar, newUsername);
|
|
setIsSettingsOpen(false);
|
|
};
|
|
|
|
const handleCreateServer = async (name, icon, allowAnyone) => {
|
|
const newServer = await network.createServer(name, icon, allowAnyone, false);
|
|
setIsCreateServerOpen(false);
|
|
setActiveView(newServer.topicHex);
|
|
};
|
|
|
|
const handleCreateGroup = async (name, members) => {
|
|
const gc = await network.createServer(name, null, true, true);
|
|
for (const key of members) {
|
|
await network.sendGroupChatAdd(key, gc.topicHex);
|
|
}
|
|
setIsCreateGroupOpen(false);
|
|
setActiveView('dms');
|
|
setActiveDm(gc.topicHex);
|
|
};
|
|
|
|
const endCall = () => {
|
|
if (activeCall) {
|
|
if (activeCall.status === 'ringing' && activeCall.isCaller) {
|
|
network.sendWebRTCSignal(activeCall.targetKey, { type: 'webrtc-cancel' });
|
|
} else {
|
|
network.sendWebRTCSignal(activeCall.targetKey, { type: 'webrtc-end' });
|
|
}
|
|
if (callTimeoutRef.current) clearTimeout(callTimeoutRef.current);
|
|
setActiveCall(null);
|
|
}
|
|
};
|
|
|
|
const startCall = (targetKey, callType = 'voice') => {
|
|
if (activeVc) {
|
|
network.sendEphemeral({ type: 'vc-leave', serverTopicHex: activeVc.serverId, channel: activeVc.channelId });
|
|
setVcStates(prev => {
|
|
const serverVCS = prev[activeVc.serverId];
|
|
if (!serverVCS) return prev;
|
|
const channelVCS = serverVCS[activeVc.channelId];
|
|
if (!channelVCS) return prev;
|
|
const newChannelVCS = { ...channelVCS };
|
|
delete newChannelVCS[myKey];
|
|
return { ...prev, [activeVc.serverId]: { ...serverVCS,[activeVc.channelId]: newChannelVCS } };
|
|
});
|
|
setActiveVc(null);
|
|
}
|
|
const targetProfile = dms[targetKey]?.profile || knownUsers.find(u => u.key === targetKey) || { displayName: 'Unknown' };
|
|
setActiveCall({ targetKey, profile: targetProfile, status: 'ringing', isCaller: true, callType });
|
|
setShowChatInCall(false);
|
|
network.sendWebRTCSignal(targetKey, { type: 'webrtc-init', callType });
|
|
|
|
if (callTimeoutRef.current) clearTimeout(callTimeoutRef.current);
|
|
callTimeoutRef.current = setTimeout(() => {
|
|
setActiveCall(current => {
|
|
if (current && current.targetKey === targetKey && current.status === 'ringing') {
|
|
network.sendWebRTCSignal(targetKey, { type: 'webrtc-cancel' });
|
|
return null;
|
|
}
|
|
return current;
|
|
});
|
|
}, 30000);
|
|
};
|
|
|
|
const startGroupCall = (channel, callType = 'voice') => {
|
|
if (activeVc) {
|
|
network.sendEphemeral({ type: 'vc-leave', serverTopicHex: activeVc.serverId, channel: activeVc.channelId });
|
|
setVcStates(prev => {
|
|
const serverVCS = prev[activeVc.serverId];
|
|
if (!serverVCS) return prev;
|
|
const channelVCS = serverVCS[activeVc.channelId];
|
|
if (!channelVCS) return prev;
|
|
const newChannelVCS = { ...channelVCS };
|
|
delete newChannelVCS[myKey];
|
|
return { ...prev, [activeVc.serverId]: { ...serverVCS,[activeVc.channelId]: newChannelVCS } };
|
|
});
|
|
setActiveVc(null);
|
|
}
|
|
network.sendEphemeral({ type: 'webrtc-group-ring', channel, callerName: profile.displayName, callType });
|
|
setActiveGroupCall({ channel, callType });
|
|
setShowChatInCall(false);
|
|
};
|
|
|
|
const handleJoinVC = (channelId) => {
|
|
if (activeCall) endCall();
|
|
if (activeGroupCall) setActiveGroupCall(null);
|
|
if (activeVc) {
|
|
network.sendEphemeral({ type: 'vc-leave', serverTopicHex: activeVc.serverId, channel: activeVc.channelId });
|
|
setVcStates(prev => {
|
|
const serverVCS = prev[activeVc.serverId];
|
|
if (!serverVCS) return prev;
|
|
const channelVCS = serverVCS[activeVc.channelId];
|
|
if (!channelVCS) return prev;
|
|
const newChannelVCS = { ...channelVCS };
|
|
delete newChannelVCS[myKey];
|
|
return { ...prev, [activeVc.serverId]: { ...serverVCS,[activeVc.channelId]: newChannelVCS } };
|
|
});
|
|
}
|
|
setActiveVc({ serverId: activeView, channelId });
|
|
setShowChatInCall(false);
|
|
};
|
|
|
|
const acceptCall = () => {
|
|
if (activeVc) {
|
|
network.sendEphemeral({ type: 'vc-leave', serverTopicHex: activeVc.serverId, channel: activeVc.channelId });
|
|
setVcStates(prev => {
|
|
const serverVCS = prev[activeVc.serverId];
|
|
if (!serverVCS) return prev;
|
|
const channelVCS = serverVCS[activeVc.channelId];
|
|
if (!channelVCS) return prev;
|
|
const newChannelVCS = { ...channelVCS };
|
|
delete newChannelVCS[myKey];
|
|
return { ...prev, [activeVc.serverId]: { ...serverVCS, [activeVc.channelId]: newChannelVCS } };
|
|
});
|
|
setActiveVc(null);
|
|
}
|
|
if (incomingCall.isGroup) {
|
|
setActiveGroupCall({ channel: incomingCall.channel, callType: incomingCall.callType });
|
|
setActiveView('dms');
|
|
setActiveDm(incomingCall.channel);
|
|
} else {
|
|
setActiveCall({ targetKey: incomingCall.targetKey, profile: incomingCall.profile, status: 'connecting', isCaller: false, callType: incomingCall.callType });
|
|
setActiveView('dms');
|
|
setActiveDm(incomingCall.targetKey);
|
|
network.sendWebRTCSignal(incomingCall.targetKey, { type: 'webrtc-accept' });
|
|
}
|
|
setShowChatInCall(false);
|
|
setIncomingCall(null);
|
|
};
|
|
|
|
const declineCall = () => {
|
|
if (!incomingCall.isGroup) {
|
|
network.sendWebRTCSignal(incomingCall.targetKey, { type: 'webrtc-decline' });
|
|
}
|
|
setIncomingCall(null);
|
|
};
|
|
|
|
const handleReturnToCall = () => {
|
|
if (activeCall) {
|
|
setActiveView('dms');
|
|
setActiveDm(activeCall.targetKey);
|
|
} else if (activeGroupCall) {
|
|
setActiveView('dms');
|
|
setActiveDm(activeGroupCall.channel);
|
|
} else if (activeVc) {
|
|
setActiveView(activeVc.serverId);
|
|
}
|
|
setShowChatInCall(false);
|
|
};
|
|
|
|
const unreadCounts = {};
|
|
messages.forEach(m => {
|
|
const channelId = m.recipient ? (m.sender === myKey ? m.recipient : m.sender) : m.channel;
|
|
if (m.sender !== myKey && m.timestamp > (lastRead[channelId] || 0)) {
|
|
unreadCounts[channelId] = (unreadCounts[channelId] || 0) + 1;
|
|
}
|
|
});
|
|
|
|
const isViewingCallDM = activeCall && activeView === 'dms' && activeDm === activeCall.targetKey;
|
|
const isViewingGroupCall = activeGroupCall && activeView === 'dms' && activeDm === activeGroupCall.channel;
|
|
const isViewingVC = activeVc && activeView === activeVc.serverId;
|
|
const showCallView = (isViewingCallDM || isViewingGroupCall || isViewingVC) && !showChatInCall;
|
|
|
|
const isGroupChat = activeView === 'dms' && servers.some(s => s.topicHex === activeDm && s.isGroupChat);
|
|
const inviteServerObj = servers.find(s => s.topicHex === inviteModalServer);
|
|
|
|
return (
|
|
<div className="flex h-full w-full bg-base font-sans overflow-hidden relative">
|
|
<Sidebar
|
|
activeView={activeView}
|
|
setActiveView={setActiveView}
|
|
servers={servers}
|
|
myKey={myKey}
|
|
onOpenCreateServer={() => setIsCreateServerOpen(true)}
|
|
onLeaveServer={(topicHex) => {
|
|
network.leaveServer(topicHex);
|
|
if (activeView === topicHex) setActiveView('dms');
|
|
}}
|
|
/>
|
|
|
|
{activeView === 'dms' ? (
|
|
<DMList
|
|
activeChannel={activeDm}
|
|
setActiveChannel={(ch) => {
|
|
setActiveDm(ch);
|
|
setLastRead(prev => ({ ...prev,[ch]: Date.now() }));
|
|
}}
|
|
myKey={myKey}
|
|
profile={profile}
|
|
unreadCounts={unreadCounts}
|
|
onOpenSettings={() => setIsSettingsOpen(true)}
|
|
dms={dms}
|
|
servers={servers}
|
|
onlinePeers={onlinePeers}
|
|
typingUsers={typingUsers}
|
|
activeCall={activeCall || activeGroupCall || activeVc}
|
|
onReturnToCall={handleReturnToCall}
|
|
onOpenCreateGroup={() => setIsCreateGroupOpen(true)}
|
|
onLeaveGroup={(topicHex) => {
|
|
network.leaveServer(topicHex);
|
|
if (activeDm === topicHex) setActiveDm('friends');
|
|
}}
|
|
onDeleteGroup={(topicHex) => {
|
|
network.deleteServer(topicHex);
|
|
if (activeDm === topicHex) setActiveDm('friends');
|
|
}}
|
|
isNetworkOnline={isNetworkOnline}
|
|
/>
|
|
) : (
|
|
<ChannelList
|
|
activeChannel={activeChannel}
|
|
setActiveChannel={(ch) => {
|
|
setActiveChannel(ch);
|
|
const netId = `${activeView}-${ch}`;
|
|
setLastRead(prev => ({ ...prev,[netId]: Date.now() }));
|
|
}}
|
|
myKey={myKey}
|
|
profile={profile}
|
|
unreadCounts={unreadCounts}
|
|
onOpenSettings={() => setIsSettingsOpen(true)}
|
|
activeView={activeView}
|
|
servers={servers}
|
|
serverMembers={serverMembers}
|
|
onlinePeers={onlinePeers}
|
|
knownUsers={knownUsers}
|
|
isSyncing={isSyncing}
|
|
onOpenInvite={() => setInviteModalServer(activeView)}
|
|
onOpenServerSettings={() => setSettingsModalServer(activeView)}
|
|
activeCall={activeCall || activeGroupCall || activeVc}
|
|
onReturnToCall={handleReturnToCall}
|
|
vcStates={vcStates}
|
|
activeVc={activeVc}
|
|
onJoinVC={handleJoinVC}
|
|
isNetworkOnline={isNetworkOnline}
|
|
/>
|
|
)}
|
|
|
|
{/* Main Content Area */}
|
|
<div className="flex-1 relative overflow-hidden flex">
|
|
|
|
{/* Chat Area (Hidden if CallView is active) */}
|
|
<div className={`flex-1 flex flex-col ${showCallView ? 'hidden' : ''}`}>
|
|
{activeView === 'dms' && activeDm === 'friends' ? (
|
|
<FriendsView dms={dms} />
|
|
) : (
|
|
<ChatArea
|
|
activeView={activeView}
|
|
activeChannel={activeView === 'dms' ? activeDm : activeChannel}
|
|
messages={messages}
|
|
myKey={myKey}
|
|
profile={profile}
|
|
typingUsers={typingUsers}
|
|
readReceipts={readReceipts}
|
|
deliveredReceipts={deliveredReceipts}
|
|
onlinePeers={onlinePeers}
|
|
markChannelRead={(networkId) => setLastRead(prev => ({ ...prev,[networkId]: Date.now() }))}
|
|
dms={dms}
|
|
servers={servers}
|
|
onStartCall={(ch, type) => {
|
|
const isGC = servers.some(s => s.topicHex === ch && s.isGroupChat);
|
|
if (isGC) startGroupCall(ch, type);
|
|
else startCall(ch, type);
|
|
}}
|
|
activeCall={activeCall || (activeGroupCall ? { targetKey: activeGroupCall.channel } : null)}
|
|
onReturnToCall={() => setShowChatInCall(false)}
|
|
transfers={transfers}
|
|
onOpenInvite={(topicHex) => setInviteModalServer(topicHex)}
|
|
onToggleMembers={() => setShowMembersDrawer(!showMembersDrawer)}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* 1-on-1 Call View */}
|
|
{activeCall && (
|
|
<CallView
|
|
className={showCallView && isViewingCallDM ? 'flex-1 flex flex-col' : 'hidden'}
|
|
targetKey={activeCall.targetKey}
|
|
targetProfile={activeCall.profile}
|
|
myProfile={profile}
|
|
isCaller={activeCall.isCaller}
|
|
status={activeCall.status}
|
|
initialVideoOn={activeCall.callType === 'video'}
|
|
onClose={endCall}
|
|
onToggleChat={() => setShowChatInCall(true)}
|
|
onConnected={() => setActiveCall(prev => prev ? { ...prev, status: 'connected' } : null)}
|
|
/>
|
|
)}
|
|
|
|
{/* Group Call View (Used for both DMs and Server VCs) */}
|
|
{(activeGroupCall || activeVc) && (
|
|
<GroupCallView
|
|
className={showCallView && (isViewingGroupCall || isViewingVC) ? 'flex-1 flex flex-col' : 'hidden'}
|
|
channel={activeGroupCall?.channel || `${activeVc.serverId}-${activeVc.channelId}`}
|
|
serverTopicHex={activeVc?.serverId}
|
|
vcChannelId={activeVc?.channelId}
|
|
initialVideoOn={activeGroupCall?.callType === 'video'}
|
|
myKey={myKey}
|
|
myProfile={profile}
|
|
knownUsers={knownUsers}
|
|
onLocalStateChange={(muted, screenshare) => {
|
|
if (!activeVc) return;
|
|
setVcStates(prev => {
|
|
const serverVCS = prev[activeVc.serverId] || {};
|
|
const channelVCS = serverVCS[activeVc.channelId] || {};
|
|
return {
|
|
...prev,
|
|
[activeVc.serverId]: {
|
|
...serverVCS,
|
|
[activeVc.channelId]: {
|
|
...channelVCS,
|
|
[myKey]: { muted, screenshare, timestamp: Date.now() }
|
|
}
|
|
}
|
|
};
|
|
});
|
|
}}
|
|
onClose={() => {
|
|
if (activeGroupCall) setActiveGroupCall(null);
|
|
if (activeVc) {
|
|
network.sendEphemeral({ type: 'vc-leave', serverTopicHex: activeVc.serverId, channel: activeVc.channelId });
|
|
setVcStates(prev => {
|
|
const serverVCS = prev[activeVc.serverId];
|
|
if (!serverVCS) return prev;
|
|
const channelVCS = serverVCS[activeVc.channelId];
|
|
if (!channelVCS) return prev;
|
|
const newChannelVCS = { ...channelVCS };
|
|
delete newChannelVCS[myKey];
|
|
return { ...prev, [activeVc.serverId]: { ...serverVCS,[activeVc.channelId]: newChannelVCS } };
|
|
});
|
|
setActiveVc(null);
|
|
}
|
|
}}
|
|
onToggleChat={() => setShowChatInCall(true)}
|
|
/>
|
|
)}
|
|
|
|
{/* Members Drawer */}
|
|
<div className={`absolute top-0 right-0 bottom-0 w-64 bg-surface border-l border-base transform transition-transform duration-300 z-40 ${showMembersDrawer && (activeView !== 'dms' || isGroupChat) ? 'translate-x-0' : 'translate-x-full'}`}>
|
|
<OnlineUsers
|
|
onlinePeers={onlinePeers}
|
|
knownUsers={knownUsers}
|
|
dms={dms}
|
|
myKey={myKey}
|
|
profile={profile}
|
|
activeView={activeView === 'dms' ? activeDm : activeView}
|
|
servers={servers}
|
|
serverMembers={serverMembers}
|
|
onClose={() => setShowMembersDrawer(false)}
|
|
/>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{isSettingsOpen && (
|
|
<ProfileSettingsModal
|
|
profile={profile}
|
|
myKey={myKey}
|
|
onClose={() => setIsSettingsOpen(false)}
|
|
onSave={handleSaveProfile}
|
|
onLogout={onLogout}
|
|
dms={dms}
|
|
servers={servers}
|
|
knownUsers={knownUsers}
|
|
updateState={updateState}
|
|
simulatedProgress={simulatedProgress}
|
|
triggerRestart={triggerRestart}
|
|
/>
|
|
)}
|
|
|
|
{isCreateServerOpen && (
|
|
<CreateServerModal onClose={() => setIsCreateServerOpen(false)} onSave={handleCreateServer} />
|
|
)}
|
|
|
|
{isCreateGroupOpen && (
|
|
<CreateGroupModal
|
|
onClose={() => setIsCreateGroupOpen(false)}
|
|
onSave={handleCreateGroup}
|
|
dms={dms}
|
|
/>
|
|
)}
|
|
|
|
{inviteModalServer && (
|
|
<InviteModal
|
|
onClose={() => setInviteModalServer(null)}
|
|
serverTopicHex={inviteModalServer}
|
|
dms={dms}
|
|
serverMembers={serverMembers}
|
|
isGroupChat={inviteServerObj?.isGroupChat}
|
|
/>
|
|
)}
|
|
|
|
{settingsModalServer && (
|
|
<ServerSettingsModal
|
|
onClose={() => setSettingsModalServer(null)}
|
|
activeServerObj={servers.find(s => s.topicHex === settingsModalServer)}
|
|
onDeleteServer={() => {
|
|
network.deleteServer(settingsModalServer);
|
|
setSettingsModalServer(null);
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{incomingCall && (
|
|
<IncomingCallModal
|
|
incomingCall={incomingCall}
|
|
onAccept={acceptCall}
|
|
onDecline={declineCall}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
} |