diff --git a/static/index.html b/static/index.html index 5023c8c..623ecec 100644 --- a/static/index.html +++ b/static/index.html @@ -119,27 +119,39 @@

GhostLink

- +
-
-
- +
+
+
+
+ +
+
+
Connected Peer
+
...
+
+
-

Connection Established

-

Direct P2P tunnel is active.

-
-
- Local Endpoint: - ... -
-
- Remote Peer: - ... +
+
+
+ +
+

Connection Established

+

You can now send and receive messages securely via P2P

- - + +
+
+ + +
+
diff --git a/static/script.js b/static/script.js index d8288cf..7a6839b 100644 --- a/static/script.js +++ b/static/script.js @@ -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'), @@ -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); @@ -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); @@ -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) { @@ -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); @@ -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(); diff --git a/static/style.css b/static/style.css index e623b17..fd21b79 100644 --- a/static/style.css +++ b/static/style.css @@ -26,9 +26,7 @@ body { font-family: var(--font-family); -webkit-font-smoothing: antialiased; height: 100vh; - display: flex; - align-items: center; - justify-content: center; + margin: 0; overflow: hidden; } @@ -37,8 +35,13 @@ body { max-width: 480px; /* Default narrow width for Disconnected/Home */ padding: 2rem; position: relative; + margin: 0 auto; /* Smoothly animate width changes */ - transition: max-width 0.6s cubic-bezier(0.25, 1, 0.5, 1); + transition: max-width 0.6s cubic-bezier(0.25, 1, 0.5, 1), padding 0.6s cubic-bezier(0.25, 1, 0.5, 1); + min-height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; } /* Dynamically widen the container ONLY when the Punching view is active */ @@ -46,6 +49,13 @@ body { max-width: 800px; } +/* Make container fullscreen when chat is active */ +.container:has(#view-connected.active) { + max-width: 100%; + padding: 0; + justify-content: flex-start; +} + /* --- View Transitions --- */ .view-section { display: none; @@ -61,7 +71,24 @@ body { } /* --- Header & Status --- */ -.header { text-align: center; margin-bottom: 2.5rem; } +.header { + text-align: center; + margin-bottom: 2.5rem; + transition: all 0.6s cubic-bezier(0.25, 1, 0.5, 1); +} + +/* Compact header when chat is active */ +.container:has(#view-connected.active) .header { + margin-bottom: 0; + padding: 0.75rem 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; + text-align: left; + background: rgba(15, 23, 42, 0.95); + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + .header h1 { font-size: 2.5rem; font-weight: 800; @@ -71,6 +98,13 @@ body { -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-shadow: 0 10px 30px rgba(34, 211, 238, 0.2); + transition: all 0.6s cubic-bezier(0.25, 1, 0.5, 1); +} + +/* Smaller header text when chat is active */ +.container:has(#view-connected.active) .header h1 { + font-size: 1.5rem; + margin-bottom: 0; } .status-badge { display: inline-flex; align-items: center; padding: 0.35rem 0.85rem; @@ -265,18 +299,240 @@ input.valid { border-color: var(--success); } .log-line.in { color: var(--success); } .log-timestamp { color: #52525b; margin-right: 12px; user-select: none; font-size: 0.75rem; } -/* --- CONNECTED --- */ -.success-card { text-align: center; padding: 3rem 2rem; } -.success-icon { - width: 90px; height: 90px; background: rgba(16, 185, 129, 0.1); color: var(--success); - border-radius: 50%; display: flex; align-items: center; justify-content: center; - margin: 0 auto 1.5rem; box-shadow: 0 0 40px rgba(16, 185, 129, 0.25); - border: 1px solid rgba(16, 185, 129, 0.2); -} -.connection-details { margin-top: 2rem; background: rgba(0,0,0,0.2); padding: 1.25rem; border-radius: 12px; text-align: left; border: 1px solid rgba(255,255,255,0.05); } -.detail-row { display: flex; justify-content: space-between; margin-bottom: 0.75rem; font-size: 0.95rem; color: var(--text-secondary); } -.detail-row:last-child { margin-bottom: 0; } -.detail-row .mono { color: var(--text-primary); font-family: var(--font-mono); font-weight: 500; } +/* --- CHAT INTERFACE --- */ +.chat-container { + display: flex; + flex-direction: column; + height: calc(100vh - 60px); /* Full height minus compact header */ + background: var(--card-bg); + border-radius: 0; /* No border radius for fullscreen */ + border: none; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + overflow: hidden; +} + +.chat-header { + background: rgba(15, 23, 42, 0.8); + padding: 0.875rem 1.5rem; /* Reduced padding for compact look */ + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + display: flex; + align-items: center; + justify-content: space-between; + min-height: 60px; /* Fixed height like WhatsApp */ +} + +.chat-peer-info { + display: flex; + align-items: center; + gap: 1rem; +} + +.peer-avatar { + width: 38px; /* Slightly smaller for compact header */ + height: 38px; + border-radius: 50%; + background: rgba(34, 211, 238, 0.15); + display: flex; + align-items: center; + justify-content: center; + color: var(--accent); + border: 2px solid rgba(34, 211, 238, 0.3); +} + +.peer-details { + display: flex; + flex-direction: column; + gap: 0.2rem; /* Reduced gap */ +} + +.peer-name { + font-weight: 600; + color: var(--text-primary); + font-size: 0.9rem; /* Slightly smaller */ +} + +.peer-address { + font-size: 0.7rem; /* Smaller for compact look */ + color: var(--text-secondary); + font-family: var(--font-mono); +} + +.chat-messages { + flex: 1; + overflow-y: auto; + padding: 1.5rem; + display: flex; + flex-direction: column; + gap: 0.75rem; + background: rgba(0, 0, 0, 0.2); +} + +.chat-messages::-webkit-scrollbar { width: 8px; } +.chat-messages::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.2); } +.chat-messages::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.1); border-radius: 4px; } + +.chat-welcome { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 2rem 1rem; + color: var(--text-secondary); +} + +.welcome-icon { + width: 72px; + height: 72px; + border-radius: 50%; + background: rgba(16, 185, 129, 0.1); + display: flex; + align-items: center; + justify-content: center; + color: var(--success); + margin-bottom: 1rem; + border: 2px solid rgba(16, 185, 129, 0.2); +} + +.chat-welcome h3 { + color: var(--text-primary); + margin-bottom: 0.5rem; + font-size: 1.1rem; + font-weight: 600; +} + +.chat-welcome p { + font-size: 0.85rem; +} + +/* Message bubbles */ +.message { + display: flex; + margin-bottom: 0.5rem; + animation: messageSlide 0.2s ease-out; +} + +@keyframes messageSlide { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.message.from-me { + justify-content: flex-end; +} + +.message.from-peer { + justify-content: flex-start; +} + +.message-bubble { + max-width: 70%; + padding: 0.75rem 1rem; + border-radius: 16px; + word-wrap: break-word; + position: relative; + font-size: 0.9rem; + line-height: 1.4; +} + +.message.from-me .message-bubble { + background: linear-gradient(135deg, var(--accent) 0%, #06b6d4 100%); + color: #0f172a; + border-bottom-right-radius: 4px; + box-shadow: 0 2px 8px rgba(34, 211, 238, 0.3); +} + +.message.from-peer .message-bubble { + background: rgba(30, 41, 59, 0.8); + color: var(--text-primary); + border-bottom-left-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.08); +} + +.message-time { + font-size: 0.7rem; + opacity: 0.6; + margin-top: 0.25rem; + display: block; +} + +.message.from-me .message-time { + text-align: right; +} + +.message.from-peer .message-time { + text-align: left; + color: var(--text-secondary); +} + +/* Chat input */ +.chat-input-container { + padding: 1rem 1.5rem; + background: rgba(15, 23, 42, 0.8); + border-top: 1px solid rgba(255, 255, 255, 0.08); +} + +.chat-input-form { + display: flex; + gap: 0.75rem; + align-items: center; +} + +.chat-input-form input { + flex: 1; + padding: 0.875rem 1.25rem; + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 24px; + color: var(--text-primary); + font-size: 0.95rem; + transition: all 0.2s ease; +} + +.chat-input-form input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(34, 211, 238, 0.1); + background: rgba(0, 0, 0, 0.4); +} + +.send-btn { + width: 44px; + height: 44px; + border-radius: 50%; + background: var(--accent); + border: none; + color: #0f172a; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 4px 12px rgba(34, 211, 238, 0.3); +} + +.send-btn:hover:not(:disabled) { + background: var(--accent-hover); + transform: scale(1.05); + box-shadow: 0 6px 16px rgba(34, 211, 238, 0.4); +} + +.send-btn:active:not(:disabled) { + transform: scale(0.95); +} + +.send-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + background: rgba(255, 255, 255, 0.1); +} /* --- Toast --- */ .toast {