| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Messenger</title> |
| <style> |
| * { margin: 0; padding: 0; box-sizing: border-box; } |
| body { |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; |
| background: #0f0f0f; color: #e0e0e0; height: 100dvh; |
| display: flex; flex-direction: column; |
| } |
| header { |
| padding: 12px 16px; background: #1a1a1a; border-bottom: 1px solid #2a2a2a; |
| font-weight: 600; font-size: 16px; flex-shrink: 0; |
| } |
| header span { color: #888; font-weight: 400; font-size: 13px; margin-left: 8px; } |
| |
| |
| #landing { |
| display: flex; align-items: center; justify-content: center; |
| flex: 1; padding: 20px; |
| } |
| #landing .card { |
| background: #1a1a1a; border-radius: 12px; padding: 32px; |
| max-width: 400px; width: 100%; text-align: center; |
| } |
| #landing h1 { font-size: 24px; margin-bottom: 8px; } |
| #landing p { color: #888; margin-bottom: 24px; font-size: 14px; } |
| #landing input { |
| width: 100%; padding: 12px; border-radius: 8px; border: 1px solid #333; |
| background: #0f0f0f; color: #e0e0e0; font-size: 15px; margin-bottom: 12px; |
| outline: none; |
| } |
| #landing input:focus { border-color: #5b7bd5; } |
| #landing button { |
| width: 100%; padding: 12px; border-radius: 8px; border: none; |
| background: #5b7bd5; color: white; font-size: 15px; font-weight: 600; |
| cursor: pointer; |
| } |
| #landing button:hover { background: #4a6bc4; } |
| |
| |
| #join-dialog { |
| display: flex; align-items: center; justify-content: center; |
| flex: 1; padding: 20px; |
| } |
| #join-dialog .card { |
| background: #1a1a1a; border-radius: 12px; padding: 32px; |
| max-width: 400px; width: 100%; text-align: center; |
| } |
| #join-dialog h2 { font-size: 20px; margin-bottom: 16px; } |
| #join-dialog input { |
| width: 100%; padding: 12px; border-radius: 8px; border: 1px solid #333; |
| background: #0f0f0f; color: #e0e0e0; font-size: 15px; margin-bottom: 12px; |
| outline: none; |
| } |
| #join-dialog input:focus { border-color: #5b7bd5; } |
| #join-dialog button { |
| width: 100%; padding: 12px; border-radius: 8px; border: none; |
| background: #5b7bd5; color: white; font-size: 15px; font-weight: 600; |
| cursor: pointer; |
| } |
| #join-dialog button:hover { background: #4a6bc4; } |
| |
| |
| #chat { display: none; flex-direction: column; flex: 1; min-height: 0; } |
| #messages { |
| flex: 1; overflow-y: auto; padding: 16px; display: flex; |
| flex-direction: column; gap: 4px; |
| } |
| .msg { |
| padding: 6px 0; line-height: 1.4; |
| } |
| .msg .name { font-weight: 600; margin-right: 8px; } |
| .msg .time { color: #555; font-size: 11px; margin-left: 6px; } |
| .msg.self .name { color: #5b7bd5; } |
| .msg.system { color: #666; font-style: italic; font-size: 13px; } |
| #input-bar { |
| padding: 12px 16px; background: #1a1a1a; border-top: 1px solid #2a2a2a; |
| display: flex; gap: 8px; flex-shrink: 0; |
| } |
| #msg-input { |
| flex: 1; padding: 10px 14px; border-radius: 8px; border: 1px solid #333; |
| background: #0f0f0f; color: #e0e0e0; font-size: 15px; outline: none; |
| } |
| #msg-input:focus { border-color: #5b7bd5; } |
| #send-btn { |
| padding: 10px 20px; border-radius: 8px; border: none; |
| background: #5b7bd5; color: white; font-size: 15px; font-weight: 600; |
| cursor: pointer; |
| } |
| #send-btn:hover { background: #4a6bc4; } |
| .hidden { display: none !important; } |
| </style> |
| </head> |
| <body> |
|
|
| <header id="header"> |
| Messenger <span id="room-label"></span> |
| </header> |
|
|
| |
| <div id="landing" class="hidden"> |
| <div class="card"> |
| <h1>Messenger</h1> |
| <p>Create a chat room and share the link</p> |
| <input type="text" id="room-name-input" placeholder="Room name (e.g. my-project)"> |
| <button onclick="goToRoom()">Create Room</button> |
| </div> |
| </div> |
|
|
| |
| <div id="join-dialog" class="hidden"> |
| <div class="card"> |
| <h2 id="join-title">Join room</h2> |
| <input type="text" id="display-name" placeholder="Your name" autofocus> |
| <button onclick="joinRoom()">Join</button> |
| </div> |
| </div> |
|
|
| |
| <div id="chat"> |
| <div id="messages"></div> |
| <div id="input-bar"> |
| <input type="text" id="msg-input" placeholder="Type a message..." autofocus> |
| <button id="send-btn" onclick="sendMessage()">Send</button> |
| </div> |
| </div> |
|
|
| <script> |
| const path = window.location.pathname; |
| const roomMatch = path.match(/^\/ch\/([^/]+)/); |
| const roomName = roomMatch ? roomMatch[1] : null; |
| |
| let token = null; |
| let userId = null; |
| let lastMessageId = null; |
| let pollTimer = null; |
| |
| |
| if (!roomName) { |
| document.getElementById('landing').classList.remove('hidden'); |
| document.getElementById('chat').style.display = 'none'; |
| } else { |
| document.getElementById('room-label').textContent = '#' + roomName; |
| |
| |
| const saved = localStorage.getItem('messenger_' + roomName); |
| if (saved) { |
| const s = JSON.parse(saved); |
| token = s.token; |
| userId = s.userId; |
| startChat(); |
| } else { |
| document.getElementById('join-dialog').classList.remove('hidden'); |
| document.getElementById('join-title').textContent = 'Join #' + roomName; |
| document.getElementById('chat').style.display = 'none'; |
| } |
| } |
| |
| function goToRoom() { |
| const name = document.getElementById('room-name-input').value.trim() |
| .toLowerCase().replace(/[^a-z0-9_-]/g, '-'); |
| if (name) window.location.href = '/ch/' + name; |
| } |
| |
| async function joinRoom() { |
| const name = document.getElementById('display-name').value.trim(); |
| if (!name) return; |
| |
| try { |
| const resp = await fetch('/api/ch/' + roomName + '/join', { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({name}), |
| }); |
| const data = await resp.json(); |
| if (!resp.ok) throw new Error(data.detail || 'Join failed'); |
| |
| token = data.token; |
| userId = name; |
| localStorage.setItem('messenger_' + roomName, JSON.stringify({token, userId})); |
| startChat(); |
| } catch (e) { |
| alert('Failed to join: ' + e.message); |
| } |
| } |
| |
| function startChat() { |
| document.getElementById('landing').classList.add('hidden'); |
| document.getElementById('join-dialog').classList.add('hidden'); |
| document.getElementById('chat').style.display = 'flex'; |
| document.getElementById('msg-input').focus(); |
| loadMessages(); |
| pollTimer = setInterval(loadMessages, 2000); |
| } |
| |
| async function loadMessages() { |
| try { |
| const resp = await fetch(`/api/ch/${roomName}/messages?token=${encodeURIComponent(token)}&limit=100`); |
| const data = await resp.json(); |
| renderMessages(data.messages || []); |
| } catch (e) { |
| console.error('Failed to load messages:', e); |
| } |
| } |
| |
| function renderMessages(msgs) { |
| const container = document.getElementById('messages'); |
| const wasAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 50; |
| |
| |
| const lastId = msgs.length ? msgs[msgs.length - 1].id : null; |
| if (lastId === lastMessageId) return; |
| lastMessageId = lastId; |
| |
| container.innerHTML = ''; |
| for (const msg of msgs) { |
| const div = document.createElement('div'); |
| const senderName = msg.sender_name || msg.sender.split(':')[0].replace('@', ''); |
| const isSelf = senderName.toLowerCase() === userId?.toLowerCase() || |
| msg.sender_name === userId; |
| div.className = 'msg' + (isSelf ? ' self' : ''); |
| |
| const time = new Date(msg.timestamp).toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'}); |
| div.innerHTML = `<span class="name">${esc(senderName)}</span>${esc(msg.text)}<span class="time">${time}</span>`; |
| container.appendChild(div); |
| } |
| |
| if (wasAtBottom) container.scrollTop = container.scrollHeight; |
| } |
| |
| async function sendMessage() { |
| const input = document.getElementById('msg-input'); |
| const text = input.value.trim(); |
| if (!text || !token) return; |
| |
| input.value = ''; |
| try { |
| await fetch(`/api/ch/${roomName}/send`, { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({token, text}), |
| }); |
| loadMessages(); |
| } catch (e) { |
| console.error('Send failed:', e); |
| } |
| } |
| |
| function esc(s) { |
| const d = document.createElement('div'); |
| d.textContent = s; |
| return d.innerHTML; |
| } |
| |
| |
| document.addEventListener('keydown', (e) => { |
| if (e.key === 'Enter' && !e.shiftKey) { |
| if (document.activeElement === document.getElementById('msg-input')) { |
| e.preventDefault(); |
| sendMessage(); |
| } else if (document.activeElement === document.getElementById('display-name')) { |
| e.preventDefault(); |
| joinRoom(); |
| } else if (document.activeElement === document.getElementById('room-name-input')) { |
| e.preventDefault(); |
| goToRoom(); |
| } |
| } |
| }); |
| </script> |
| </body> |
| </html> |
|
|