Spaces:
Sleeping
Sleeping
Download templates/index.html from abdullahalioo/whatsapp_working: direct link, hf CLI and curl.
- Browser
- Download file 31 kB
-
https://huggingface.co/spaces/abdullahalioo/whatsapp_working/resolve/main/templates/index.html
- Command line
-
hf download hf://spaces/abdullahalioo/whatsapp_working/templates/index.html
-
curl -L -o index.html https://huggingface.co/spaces/abdullahalioo/whatsapp_working/resolve/main/templates/index.html
31 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>WhatsApp-like Chat</title> | |
| <script src="https://cdn.socket.io/4.6.0/socket.io.min.js"></script> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: #111b21; | |
| height: 100vh; | |
| overflow: hidden; | |
| } | |
| .auth-container { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| height: 100vh; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| } | |
| .auth-box { | |
| background: white; | |
| padding: 40px; | |
| border-radius: 10px; | |
| box-shadow: 0 10px 40px rgba(0,0,0,0.2); | |
| width: 400px; | |
| max-width: 90%; | |
| } | |
| .auth-box h2 { | |
| margin-bottom: 20px; | |
| color: #333; | |
| text-align: center; | |
| } | |
| .form-group { | |
| margin-bottom: 20px; | |
| } | |
| .form-group label { | |
| display: block; | |
| margin-bottom: 5px; | |
| color: #555; | |
| font-weight: 500; | |
| } | |
| .form-group input { | |
| width: 100%; | |
| padding: 12px; | |
| border: 2px solid #ddd; | |
| border-radius: 5px; | |
| font-size: 14px; | |
| transition: border-color 0.3s; | |
| } | |
| .form-group input:focus { | |
| outline: none; | |
| border-color: #667eea; | |
| } | |
| .btn { | |
| width: 100%; | |
| padding: 12px; | |
| background: #667eea; | |
| color: white; | |
| border: none; | |
| border-radius: 5px; | |
| font-size: 16px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: background 0.3s; | |
| } | |
| .btn:hover { | |
| background: #5568d3; | |
| } | |
| .toggle-auth { | |
| text-align: center; | |
| margin-top: 15px; | |
| color: #666; | |
| } | |
| .toggle-auth a { | |
| color: #667eea; | |
| text-decoration: none; | |
| font-weight: 600; | |
| } | |
| .error-msg { | |
| background: #fee; | |
| color: #c33; | |
| padding: 10px; | |
| border-radius: 5px; | |
| margin-bottom: 15px; | |
| font-size: 14px; | |
| } | |
| .success-msg { | |
| background: #efe; | |
| color: #3c3; | |
| padding: 10px; | |
| border-radius: 5px; | |
| margin-bottom: 15px; | |
| font-size: 14px; | |
| } | |
| .chat-container { | |
| display: none; | |
| height: 100vh; | |
| background: #222e35; | |
| } | |
| .chat-layout { | |
| display: flex; | |
| height: 100%; | |
| } | |
| .sidebar { | |
| width: 400px; | |
| background: #111b21; | |
| border-right: 1px solid #2a3942; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .sidebar-header { | |
| background: #202c33; | |
| padding: 15px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .user-info { | |
| color: #e9edef; | |
| font-weight: 500; | |
| } | |
| .user-id-display { | |
| font-size: 12px; | |
| color: #8696a0; | |
| } | |
| .logout-btn { | |
| background: #dc3545; | |
| color: white; | |
| border: none; | |
| padding: 8px 16px; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| } | |
| .add-contact-section { | |
| background: #202c33; | |
| padding: 15px; | |
| border-bottom: 1px solid #2a3942; | |
| } | |
| .add-contact-form { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .add-contact-input { | |
| flex: 1; | |
| padding: 10px; | |
| border: 1px solid #2a3942; | |
| border-radius: 5px; | |
| background: #2a3942; | |
| color: #e9edef; | |
| font-size: 14px; | |
| } | |
| .add-contact-btn { | |
| background: #00a884; | |
| color: white; | |
| border: none; | |
| padding: 10px 20px; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| } | |
| .contacts-list { | |
| flex: 1; | |
| overflow-y: auto; | |
| } | |
| .contact-item { | |
| padding: 15px; | |
| border-bottom: 1px solid #2a3942; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| transition: background 0.2s; | |
| } | |
| .contact-item:hover { | |
| background: #202c33; | |
| } | |
| .contact-item.active { | |
| background: #2a3942; | |
| } | |
| .contact-avatar { | |
| width: 50px; | |
| height: 50px; | |
| border-radius: 50%; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-weight: bold; | |
| font-size: 20px; | |
| margin-right: 15px; | |
| } | |
| .contact-info { | |
| flex: 1; | |
| } | |
| .contact-name { | |
| color: #e9edef; | |
| font-weight: 500; | |
| margin-bottom: 3px; | |
| } | |
| .contact-status { | |
| font-size: 12px; | |
| color: #8696a0; | |
| } | |
| .contact-status.online { | |
| color: #00a884; | |
| } | |
| .unread-badge { | |
| background: #00a884; | |
| color: white; | |
| border-radius: 50%; | |
| width: 24px; | |
| height: 24px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 12px; | |
| font-weight: bold; | |
| } | |
| .chat-area { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| background: #0b141a; | |
| } | |
| .chat-header { | |
| background: #202c33; | |
| padding: 15px 20px; | |
| display: flex; | |
| align-items: center; | |
| border-bottom: 1px solid #2a3942; | |
| } | |
| .chat-header-info { | |
| flex: 1; | |
| } | |
| .chat-partner-name { | |
| color: #e9edef; | |
| font-weight: 500; | |
| margin-bottom: 3px; | |
| } | |
| .chat-partner-status { | |
| font-size: 13px; | |
| color: #8696a0; | |
| } | |
| .typing-indicator { | |
| font-size: 13px; | |
| color: #00a884; | |
| display: none; | |
| } | |
| .messages-container { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 20px; | |
| background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZGVmcz48cGF0dGVybiBpZD0iYSIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiPjxyZWN0IHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiBmaWxsPSIjMGIxNDFhIi8+PHBhdGggZD0iTTUwIDUwbTAtMzVhMzUgMzUgMCAxIDAgNzAgMGEzNSAzNSAwIDEgMC03MCAwIiBzdHJva2U9IiMxMTFiMjEiIHN0cm9rZS13aWR0aD0iMC41IiBmaWxsPSJub25lIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+'); | |
| } | |
| .message { | |
| margin-bottom: 12px; | |
| display: flex; | |
| animation: messageSlideIn 0.3s ease; | |
| } | |
| @keyframes messageSlideIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(10px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .message.sent { | |
| justify-content: flex-end; | |
| } | |
| .message-bubble { | |
| max-width: 60%; | |
| padding: 8px 12px; | |
| border-radius: 8px; | |
| position: relative; | |
| } | |
| .message.received .message-bubble { | |
| background: #202c33; | |
| color: #e9edef; | |
| } | |
| .message.sent .message-bubble { | |
| background: #005c4b; | |
| color: #e9edef; | |
| } | |
| .message-text { | |
| margin-bottom: 5px; | |
| word-wrap: break-word; | |
| } | |
| .message-meta { | |
| display: flex; | |
| justify-content: flex-end; | |
| align-items: center; | |
| gap: 5px; | |
| font-size: 11px; | |
| color: #8696a0; | |
| } | |
| .message-status { | |
| font-size: 16px; | |
| } | |
| .status-sent { | |
| color: #8696a0; | |
| } | |
| .status-delivered { | |
| color: #8696a0; | |
| } | |
| .status-read { | |
| color: #53bdeb; | |
| } | |
| .message-input-area { | |
| background: #202c33; | |
| padding: 15px; | |
| display: flex; | |
| gap: 10px; | |
| align-items: center; | |
| } | |
| .message-input { | |
| flex: 1; | |
| padding: 12px 15px; | |
| border: none; | |
| border-radius: 8px; | |
| background: #2a3942; | |
| color: #e9edef; | |
| font-size: 15px; | |
| resize: none; | |
| max-height: 100px; | |
| } | |
| .message-input:focus { | |
| outline: none; | |
| } | |
| .send-btn { | |
| background: #00a884; | |
| color: white; | |
| border: none; | |
| width: 50px; | |
| height: 50px; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| font-size: 20px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: background 0.2s; | |
| } | |
| .send-btn:hover { | |
| background: #06cf9c; | |
| } | |
| .empty-chat { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| height: 100%; | |
| color: #8696a0; | |
| text-align: center; | |
| padding: 40px; | |
| } | |
| .empty-chat-icon { | |
| font-size: 80px; | |
| margin-bottom: 20px; | |
| opacity: 0.3; | |
| } | |
| .empty-chat-text { | |
| font-size: 18px; | |
| } | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: #111b21; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #2a3942; | |
| border-radius: 3px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #374955; | |
| } | |
| @media (max-width: 768px) { | |
| .sidebar { | |
| width: 100%; | |
| } | |
| .chat-area { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| display: none; | |
| } | |
| .chat-area.mobile-visible { | |
| display: flex; | |
| } | |
| .sidebar.mobile-hidden { | |
| display: none; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="authContainer" class="auth-container"> | |
| <div class="auth-box"> | |
| <div id="registerForm"> | |
| <h2>Register</h2> | |
| <div id="authMessage"></div> | |
| <div class="form-group"> | |
| <label>Name</label> | |
| <input type="text" id="registerName" placeholder="Enter your name"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Email</label> | |
| <input type="email" id="registerEmail" placeholder="Enter your email"> | |
| </div> | |
| <button class="btn" onclick="register()">Register</button> | |
| <div class="toggle-auth"> | |
| Already have an account? <a href="#" onclick="toggleAuth('login'); return false;">Login</a> | |
| </div> | |
| </div> | |
| <div id="loginForm" style="display: none;"> | |
| <h2>Login</h2> | |
| <div id="loginMessage"></div> | |
| <div class="form-group"> | |
| <label>User ID (8 digits)</label> | |
| <input type="text" id="loginUserId" placeholder="Enter your 8-digit ID" maxlength="8"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Token</label> | |
| <input type="text" id="loginToken" placeholder="Enter your token"> | |
| </div> | |
| <button class="btn" onclick="login()">Login</button> | |
| <div class="toggle-auth"> | |
| Don't have an account? <a href="#" onclick="toggleAuth('register'); return false;">Register</a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="chatContainer" class="chat-container"> | |
| <div class="chat-layout"> | |
| <div class="sidebar" id="sidebar"> | |
| <div class="sidebar-header"> | |
| <div> | |
| <div class="user-info" id="currentUserName">User</div> | |
| <div class="user-id-display">ID: <span id="currentUserId"></span></div> | |
| </div> | |
| <button class="logout-btn" onclick="logout()">Logout</button> | |
| </div> | |
| <div class="add-contact-section"> | |
| <div class="add-contact-form"> | |
| <input type="text" class="add-contact-input" id="addContactId" | |
| placeholder="Enter 8-digit User ID" maxlength="8"> | |
| <button class="add-contact-btn" onclick="addContact()">Add</button> | |
| </div> | |
| </div> | |
| <div class="contacts-list" id="contactsList"></div> | |
| </div> | |
| <div class="chat-area" id="chatArea"> | |
| <div class="empty-chat"> | |
| <div class="empty-chat-icon">💬</div> | |
| <div class="empty-chat-text">Select a contact to start chatting</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let socket; | |
| let currentUser = null; | |
| let currentChat = null; | |
| let contacts = []; | |
| let unreadCounts = {}; | |
| let typingTimeout = null; | |
| function toggleAuth(mode) { | |
| if (mode === 'login') { | |
| document.getElementById('registerForm').style.display = 'none'; | |
| document.getElementById('loginForm').style.display = 'block'; | |
| document.getElementById('authMessage').innerHTML = ''; | |
| document.getElementById('loginMessage').innerHTML = ''; | |
| } else { | |
| document.getElementById('registerForm').style.display = 'block'; | |
| document.getElementById('loginForm').style.display = 'none'; | |
| document.getElementById('authMessage').innerHTML = ''; | |
| document.getElementById('loginMessage').innerHTML = ''; | |
| } | |
| } | |
| async function register() { | |
| const name = document.getElementById('registerName').value.trim(); | |
| const email = document.getElementById('registerEmail').value.trim(); | |
| const msgDiv = document.getElementById('authMessage'); | |
| if (!name || !email) { | |
| msgDiv.innerHTML = '<div class="error-msg">Please fill all fields</div>'; | |
| return; | |
| } | |
| try { | |
| const response = await fetch('/api/register', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ name, email }) | |
| }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| msgDiv.innerHTML = `<div class="success-msg"> | |
| Registration successful!<br> | |
| <strong>Your ID: ${data.user_id}</strong><br> | |
| <strong>Your Token: ${data.token}</strong><br> | |
| <small>Save these credentials to login</small> | |
| </div>`; | |
| localStorage.setItem('user_id', data.user_id); | |
| localStorage.setItem('token', data.token); | |
| setTimeout(() => { | |
| currentUser = data; | |
| initChat(); | |
| }, 2000); | |
| } else { | |
| msgDiv.innerHTML = `<div class="error-msg">${data.error}</div>`; | |
| } | |
| } catch (error) { | |
| msgDiv.innerHTML = `<div class="error-msg">Registration failed: ${error.message}</div>`; | |
| } | |
| } | |
| async function login() { | |
| const user_id = document.getElementById('loginUserId').value.trim(); | |
| const token = document.getElementById('loginToken').value.trim(); | |
| const msgDiv = document.getElementById('loginMessage'); | |
| if (!user_id || !token) { | |
| msgDiv.innerHTML = '<div class="error-msg">Please fill all fields</div>'; | |
| return; | |
| } | |
| try { | |
| const response = await fetch('/api/login', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ user_id, token }) | |
| }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| localStorage.setItem('user_id', user_id); | |
| localStorage.setItem('token', token); | |
| currentUser = { user_id, token, ...data }; | |
| initChat(); | |
| } else { | |
| msgDiv.innerHTML = `<div class="error-msg">${data.error}</div>`; | |
| } | |
| } catch (error) { | |
| msgDiv.innerHTML = `<div class="error-msg">Login failed: ${error.message}</div>`; | |
| } | |
| } | |
| function initChat() { | |
| document.getElementById('authContainer').style.display = 'none'; | |
| document.getElementById('chatContainer').style.display = 'block'; | |
| document.getElementById('currentUserName').textContent = currentUser.name; | |
| document.getElementById('currentUserId').textContent = currentUser.user_id; | |
| initWebSocket(); | |
| loadContacts(); | |
| } | |
| function initWebSocket() { | |
| socket = io(); | |
| socket.on('connect', () => { | |
| console.log('Connected to server'); | |
| socket.emit('authenticate', { token: currentUser.token }); | |
| }); | |
| socket.on('authenticated', (data) => { | |
| if (data.success) { | |
| console.log('Authenticated successfully'); | |
| } else { | |
| alert('Authentication failed: ' + data.error); | |
| logout(); | |
| } | |
| }); | |
| socket.on('user_status', (data) => { | |
| updateContactStatus(data.user_id, data.online); | |
| }); | |
| socket.on('new_message', (message) => { | |
| if (currentChat && message.from === currentChat.user_id) { | |
| appendMessage(message, false); | |
| socket.emit('mark_conversation_read', { | |
| token: currentUser.token, | |
| contact_id: currentChat.user_id | |
| }); | |
| } else { | |
| unreadCounts[message.from] = (unreadCounts[message.from] || 0) + 1; | |
| updateContactsList(); | |
| } | |
| }); | |
| socket.on('message_sent', (message) => { | |
| appendMessage(message, true); | |
| }); | |
| socket.on('message_status_update', (data) => { | |
| updateMessageStatus(data.message_id, data.status); | |
| }); | |
| socket.on('user_typing', (data) => { | |
| if (currentChat && data.user_id === currentChat.user_id) { | |
| const typingIndicator = document.querySelector('.typing-indicator'); | |
| if (typingIndicator) { | |
| typingIndicator.style.display = data.typing ? 'block' : 'none'; | |
| } | |
| } | |
| }); | |
| } | |
| async function loadContacts() { | |
| try { | |
| const response = await fetch('/api/contacts', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ token: currentUser.token }) | |
| }); | |
| const data = await response.json(); | |
| contacts = data.contacts || []; | |
| updateContactsList(); | |
| } catch (error) { | |
| console.error('Failed to load contacts:', error); | |
| } | |
| } | |
| function updateContactsList() { | |
| const list = document.getElementById('contactsList'); | |
| list.innerHTML = ''; | |
| contacts.forEach(contact => { | |
| const item = document.createElement('div'); | |
| item.className = 'contact-item'; | |
| if (currentChat && currentChat.user_id === contact.user_id) { | |
| item.className += ' active'; | |
| } | |
| const initial = contact.name.charAt(0).toUpperCase(); | |
| const unreadCount = unreadCounts[contact.user_id] || 0; | |
| const onlineStatus = contact.online ? 'online' : 'offline'; | |
| item.innerHTML = ` | |
| <div class="contact-avatar">${initial}</div> | |
| <div class="contact-info"> | |
| <div class="contact-name">${contact.name}</div> | |
| <div class="contact-status ${onlineStatus}"> | |
| ${contact.online ? 'Online' : 'Offline'} | |
| </div> | |
| </div> | |
| ${unreadCount > 0 ? `<div class="unread-badge">${unreadCount}</div>` : ''} | |
| `; | |
| item.onclick = () => openChat(contact); | |
| list.appendChild(item); | |
| }); | |
| } | |
| function updateContactStatus(userId, online) { | |
| const contact = contacts.find(c => c.user_id === userId); | |
| if (contact) { | |
| contact.online = online; | |
| updateContactsList(); | |
| if (currentChat && currentChat.user_id === userId) { | |
| const statusEl = document.querySelector('.chat-partner-status'); | |
| if (statusEl && !statusEl.classList.contains('typing-indicator')) { | |
| statusEl.textContent = online ? 'Online' : 'Offline'; | |
| } | |
| } | |
| } | |
| } | |
| async function addContact() { | |
| const contactId = document.getElementById('addContactId').value.trim(); | |
| if (!contactId || contactId.length !== 8) { | |
| alert('Please enter a valid 8-digit User ID'); | |
| return; | |
| } | |
| try { | |
| const response = await fetch('/api/contacts/add', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| token: currentUser.token, | |
| contact_id: contactId | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| document.getElementById('addContactId').value = ''; | |
| loadContacts(); | |
| alert(`Contact added: ${data.name}`); | |
| } else { | |
| alert(data.error); | |
| } | |
| } catch (error) { | |
| alert('Failed to add contact: ' + error.message); | |
| } | |
| } | |
| async function openChat(contact) { | |
| currentChat = contact; | |
| unreadCounts[contact.user_id] = 0; | |
| updateContactsList(); | |
| const chatArea = document.getElementById('chatArea'); | |
| chatArea.innerHTML = ` | |
| <div class="chat-header"> | |
| <div class="chat-header-info"> | |
| <div class="chat-partner-name">${contact.name}</div> | |
| <div class="chat-partner-status">${contact.online ? 'Online' : 'Offline'}</div> | |
| <div class="typing-indicator">typing...</div> | |
| </div> | |
| </div> | |
| <div class="messages-container" id="messagesContainer"></div> | |
| <div class="message-input-area"> | |
| <textarea class="message-input" id="messageInput" | |
| placeholder="Type a message" rows="1"></textarea> | |
| <button class="send-btn" onclick="sendMessage()">â–¶</button> | |
| </div> | |
| `; | |
| chatArea.classList.add('mobile-visible'); | |
| document.getElementById('sidebar').classList.add('mobile-hidden'); | |
| const messageInput = document.getElementById('messageInput'); | |
| messageInput.addEventListener('input', handleTyping); | |
| messageInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }); | |
| await loadMessages(contact.user_id); | |
| socket.emit('mark_conversation_read', { | |
| token: currentUser.token, | |
| contact_id: contact.user_id | |
| }); | |
| } | |
| async function loadMessages(contactId) { | |
| try { | |
| const response = await fetch('/api/messages', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| token: currentUser.token, | |
| contact_id: contactId | |
| }) | |
| }); | |
| const data = await response.json(); | |
| const messages = data.messages || []; | |
| messages.forEach(msg => { | |
| const isSent = msg.from === currentUser.user_id; | |
| appendMessage(msg, isSent); | |
| }); | |
| scrollToBottom(); | |
| } catch (error) { | |
| console.error('Failed to load messages:', error); | |
| } | |
| } | |
| function appendMessage(message, isSent) { | |
| const container = document.getElementById('messagesContainer'); | |
| if (!container) return; | |
| const msgDiv = document.createElement('div'); | |
| msgDiv.className = `message ${isSent ? 'sent' : 'received'}`; | |
| msgDiv.dataset.messageId = message.message_id; | |
| const time = new Date(message.timestamp).toLocaleTimeString('en-US', { | |
| hour: '2-digit', | |
| minute: '2-digit' | |
| }); | |
| const statusIcon = getStatusIcon(message.status); | |
| msgDiv.innerHTML = ` | |
| <div class="message-bubble"> | |
| <div class="message-text">${escapeHtml(message.message)}</div> | |
| <div class="message-meta"> | |
| <span>${time}</span> | |
| ${isSent ? `<span class="message-status">${statusIcon}</span>` : ''} | |
| </div> | |
| </div> | |
| `; | |
| container.appendChild(msgDiv); | |
| scrollToBottom(); | |
| } | |
| function getStatusIcon(status) { | |
| if (status === 'sent') return '<span class="status-sent">✓</span>'; | |
| if (status === 'delivered') return '<span class="status-delivered">✓✓</span>'; | |
| if (status === 'read') return '<span class="status-read">✓✓</span>'; | |
| return ''; | |
| } | |
| function updateMessageStatus(messageId, status) { | |
| const msgDiv = document.querySelector(`[data-message-id="${messageId}"]`); | |
| if (msgDiv) { | |
| const statusSpan = msgDiv.querySelector('.message-status'); | |
| if (statusSpan) { | |
| statusSpan.innerHTML = getStatusIcon(status); | |
| } | |
| } | |
| } | |
| function sendMessage() { | |
| const input = document.getElementById('messageInput'); | |
| const message = input.value.trim(); | |
| if (!message || !currentChat) return; | |
| socket.emit('send_message', { | |
| token: currentUser.token, | |
| to: currentChat.user_id, | |
| message: message | |
| }); | |
| input.value = ''; | |
| input.style.height = 'auto'; | |
| socket.emit('typing', { | |
| token: currentUser.token, | |
| to: currentChat.user_id, | |
| typing: false | |
| }); | |
| } | |
| function handleTyping() { | |
| if (!currentChat) return; | |
| socket.emit('typing', { | |
| token: currentUser.token, | |
| to: currentChat.user_id, | |
| typing: true | |
| }); | |
| clearTimeout(typingTimeout); | |
| typingTimeout = setTimeout(() => { | |
| socket.emit('typing', { | |
| token: currentUser.token, | |
| to: currentChat.user_id, | |
| typing: false | |
| }); | |
| }, 1000); | |
| } | |
| function scrollToBottom() { | |
| const container = document.getElementById('messagesContainer'); | |
| if (container) { | |
| container.scrollTop = container.scrollHeight; | |
| } | |
| } | |
| function escapeHtml(text) { | |
| const map = { | |
| '&': '&', | |
| '<': '<', | |
| '>': '>', | |
| '"': '"', | |
| "'": ''' | |
| }; | |
| return text.replace(/[&<>"']/g, m => map[m]); | |
| } | |
| function logout() { | |
| localStorage.removeItem('user_id'); | |
| localStorage.removeItem('token'); | |
| if (socket) socket.disconnect(); | |
| location.reload(); | |
| } | |
| window.onload = () => { | |
| const savedUserId = localStorage.getItem('user_id'); | |
| const savedToken = localStorage.getItem('token'); | |
| if (savedUserId && savedToken) { | |
| document.getElementById('loginUserId').value = savedUserId; | |
| document.getElementById('loginToken').value = savedToken; | |
| } | |
| }; | |
| </script> | |
| </body> | |
| </html> | |