Skip to content
This repository was archived by the owner on Feb 28, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 28 additions & 16 deletions static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -119,27 +119,39 @@ <h1>GhostLink</h1>
</div>
</div>

<!-- CONNECTED VIEW -->
<!-- CONNECTED VIEW / CHAT -->
<div id="view-connected" class="view-section">
<div class="card success-card">
<div class="success-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>
<div class="chat-container">
<div class="chat-header">
<div class="chat-peer-info">
<div class="peer-avatar">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
</div>
<div class="peer-details">
<div class="peer-name">Connected Peer</div>
<div class="peer-address" id="chatPeerIp">...</div>
</div>
</div>
</div>
<h2>Connection Established</h2>
<p>Direct P2P tunnel is active.</p>

<div class="connection-details">
<div class="detail-row">
<span>Local Endpoint:</span>
<span class="mono" id="connLocalIp">...</span>
</div>
<div class="detail-row">
<span>Remote Peer:</span>
<span class="mono" id="connRemoteIp">...</span>
<div class="chat-messages" id="chatMessages">
<div class="chat-welcome">
<div class="welcome-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>
</div>
<h3>Connection Established</h3>
<p>You can now send and receive messages securely via P2P</p>
</div>
</div>

<button id="disconnectBtn" class="btn-primary" style="background-color: var(--card-bg); border: 1px solid rgba(255,255,255,0.1); margin-top: 1.5rem;">Disconnect</button>

<div class="chat-input-container">
<form id="chatForm" class="chat-input-form">
<input type="text" id="chatInput" placeholder="Type a message..." autocomplete="off">
<button type="submit" id="sendBtn" class="send-btn">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
</button>
</form>
</div>
</div>
</div>

Expand Down
107 changes: 90 additions & 17 deletions static/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ const els = {
punchLogs: document.getElementById('punchLogs'),
punchTimeout: document.getElementById('punchTimeout'),

// Connected
connLocalIp: document.getElementById('connLocalIp'),
connRemoteIp: document.getElementById('connRemoteIp'),
disconnectBtn: document.getElementById('disconnectBtn'),
// Connected / Chat
chatMessages: document.getElementById('chatMessages'),
chatPeerIp: document.getElementById('chatPeerIp'),
chatForm: document.getElementById('chatForm'),
chatInput: document.getElementById('chatInput'),
sendBtn: document.getElementById('sendBtn'),

// Toast
toast: document.getElementById('toast'),
Expand Down Expand Up @@ -205,8 +207,8 @@ async function enterPunchingState(data) {
async function enterConnectedState(data) {
els.viewConnected.classList.add('active');

els.connLocalIp.innerText = state.fullAddress;
els.connRemoteIp.innerText = state.peerAddress || "Connected Peer";
// Update chat header with peer info
els.chatPeerIp.innerText = state.peerAddress || "Connected Peer";

if (data.message) {
console.log("Connected:", data.message);
Expand All @@ -230,9 +232,15 @@ function connectSSE() {
// { status: "DISCONNECTED", state: { ... } }
// { status: "PUNCHING", timeout: 10, message: "..." }
// { status: "CONNECTED", message: "..." }
// { status: "MESSAGE", content: "...", from_me: true/false }

if (data.status) {
handleStatusChange(data.status, data);
if (data.status === 'MESSAGE') {
// Handle chat message
addChatMessage(data.content, data.from_me);
} else {
handleStatusChange(data.status, data);
}
}
} catch (e) {
console.error("SSE Parse Error", e);
Expand Down Expand Up @@ -318,6 +326,80 @@ function addLog(message) {
els.punchLogs.scrollTop = els.punchLogs.scrollHeight;
}

// --- Chat Functions ---

/**
* Adds a chat message to the chat UI
* @param {string} content - Message content
* @param {boolean} fromMe - True if message was sent by the user, false if received from peer
*/
function addChatMessage(content, fromMe) {
// Remove welcome message if it exists
const welcome = els.chatMessages.querySelector('.chat-welcome');
if (welcome) {
welcome.remove();
}

const messageDiv = document.createElement('div');
messageDiv.className = `message ${fromMe ? 'from-me' : 'from-peer'}`;

const bubbleDiv = document.createElement('div');
bubbleDiv.className = 'message-bubble';

const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.textContent = content;

const timeDiv = document.createElement('span');
timeDiv.className = 'message-time';
const now = new Date();
timeDiv.textContent = now.toLocaleTimeString(undefined, {hour: '2-digit', minute: '2-digit', hour12: true});

bubbleDiv.appendChild(contentDiv);
bubbleDiv.appendChild(timeDiv);
messageDiv.appendChild(bubbleDiv);

els.chatMessages.appendChild(messageDiv);

// Auto-scroll to bottom
els.chatMessages.scrollTop = els.chatMessages.scrollHeight;
}

/**
* Handles chat form submission
*/
async function handleChatSubmit(e) {
e.preventDefault();

const message = els.chatInput.value.trim();
if (!message) return;

// Disable send button temporarily
els.sendBtn.disabled = true;

try {
const res = await fetch('/api/message', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
});

if (!res.ok) {
throw new Error('Failed to send message');
}

// Clear input and refocus
els.chatInput.value = '';
els.chatInput.focus();

} catch (err) {
console.error('Failed to send message:', err);
showToast('Failed to send message');
} finally {
els.sendBtn.disabled = false;
}
}

// --- Interactions ---

async function handleConnect(e) {
Expand Down Expand Up @@ -351,15 +433,6 @@ async function handleConnect(e) {
}
}

async function handleDisconnect() {
try {
await fetch('/api/disconnect', { method: 'POST' });
// UI updates via SSE Disconnected event
} catch(e) {
console.error(e);
}
}

// --- Validation & Utilities ---
function toggleSubmitButton() {
els.submitBtn.disabled = !(state.isIpValid && state.isPortValid);
Expand Down Expand Up @@ -482,10 +555,10 @@ function setupEventListeners() {
if(els.copyBtn) els.copyBtn.addEventListener('click', copyToClipboard);
if(els.copyLocalBtn) els.copyLocalBtn.addEventListener('click', copyLocalToClipboard);
els.connectForm.addEventListener('submit', handleConnect);
els.disconnectBtn.addEventListener('click', handleDisconnect);
els.peerIpInput.addEventListener('input', () => handleIpValidation('input'));
els.peerIpInput.addEventListener('blur', () => handleIpValidation('blur'));
els.peerPortInput.addEventListener('input', handlePortValidation);
if(els.chatForm) els.chatForm.addEventListener('submit', handleChatSubmit);
}

init();
Loading
Loading