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 (
setIsCreateServerOpen(true)} onLeaveServer={(topicHex) => { network.leaveServer(topicHex); if (activeView === topicHex) setActiveView('dms'); }} /> {activeView === 'dms' ? ( { 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} /> ) : ( { 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 */}
{/* Chat Area (Hidden if CallView is active) */}
{activeView === 'dms' && activeDm === 'friends' ? ( ) : ( 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)} /> )}
{/* 1-on-1 Call View */} {activeCall && ( setShowChatInCall(true)} onConnected={() => setActiveCall(prev => prev ? { ...prev, status: 'connected' } : null)} /> )} {/* Group Call View (Used for both DMs and Server VCs) */} {(activeGroupCall || activeVc) && ( { 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 */}
setShowMembersDrawer(false)} />
{isSettingsOpen && ( setIsSettingsOpen(false)} onSave={handleSaveProfile} onLogout={onLogout} dms={dms} servers={servers} knownUsers={knownUsers} updateState={updateState} simulatedProgress={simulatedProgress} triggerRestart={triggerRestart} /> )} {isCreateServerOpen && ( setIsCreateServerOpen(false)} onSave={handleCreateServer} /> )} {isCreateGroupOpen && ( setIsCreateGroupOpen(false)} onSave={handleCreateGroup} dms={dms} /> )} {inviteModalServer && ( setInviteModalServer(null)} serverTopicHex={inviteModalServer} dms={dms} serverMembers={serverMembers} isGroupChat={inviteServerObj?.isGroupChat} /> )} {settingsModalServer && ( setSettingsModalServer(null)} activeServerObj={servers.find(s => s.topicHex === settingsModalServer)} onDeleteServer={() => { network.deleteServer(settingsModalServer); setSettingsModalServer(null); }} /> )} {incomingCall && ( )}
); }