| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Tri-Chat - Premium Chat Experience</title> |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> |
| <style> |
| :root { |
| |
| --primary-bg: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); |
| --secondary-bg: rgba(255, 255, 255, 0.8); |
| --card-bg: rgba(255, 255, 255, 0.9); |
| --text-primary: #1a202c; |
| --text-secondary: #4a5568; |
| --text-muted: #718096; |
| --border-color: rgba(0, 0, 0, 0.08); |
| --accent-primary: #3b82f6; |
| --accent-secondary: #60a5fa; |
| --accent-success: #10b981; |
| --accent-danger: #ef4444; |
| --shadow-light: 0 1px 3px rgba(0, 0, 0, 0.05); |
| --shadow-medium: 0 4px 12px rgba(0, 0, 0, 0.1); |
| --shadow-heavy: 0 8px 24px rgba(0, 0, 0, 0.12); |
| --blur-light: blur(10px); |
| --blur-medium: blur(20px); |
| } |
| |
| [data-theme="dark"] { |
| --primary-bg: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); |
| --secondary-bg: rgba(30, 41, 59, 0.8); |
| --card-bg: rgba(30, 41, 59, 0.9); |
| --text-primary: #f1f5f9; |
| --text-secondary: #cbd5e1; |
| --text-muted: #94a3b8; |
| --border-color: rgba(255, 255, 255, 0.1); |
| --accent-primary: #3b82f6; |
| --accent-secondary: #60a5fa; |
| --accent-success: #10b981; |
| --accent-danger: #ef4444; |
| --shadow-light: 0 1px 3px rgba(0, 0, 0, 0.3); |
| --shadow-medium: 0 4px 12px rgba(0, 0, 0, 0.4); |
| --shadow-heavy: 0 8px 24px rgba(0, 0, 0, 0.5); |
| } |
| |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| body { |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
| background: var(--primary-bg); |
| color: var(--text-primary); |
| height: 100vh; |
| display: flex; |
| flex-direction: column; |
| font-size: 14px; |
| line-height: 1.5; |
| transition: all 0.3s ease; |
| } |
| |
| .header { |
| background: var(--card-bg); |
| backdrop-filter: var(--blur-medium); |
| padding: 1.5rem; |
| border-bottom: 1px solid var(--border-color); |
| box-shadow: var(--shadow-light); |
| position: relative; |
| } |
| |
| .header::before { |
| content: ''; |
| position: absolute; |
| top: 0; |
| left: 0; |
| right: 0; |
| height: 3px; |
| background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); |
| } |
| |
| .header-top { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 1.5rem; |
| } |
| |
| .logo { |
| display: flex; |
| align-items: center; |
| gap: 0.75rem; |
| } |
| |
| .logo i { |
| font-size: 1.5rem; |
| color: var(--accent-primary); |
| } |
| |
| .header h1 { |
| color: var(--text-primary); |
| font-size: 1.5rem; |
| font-weight: 700; |
| letter-spacing: -0.025em; |
| } |
| |
| .theme-toggle { |
| background: var(--secondary-bg); |
| border: 1px solid var(--border-color); |
| border-radius: 12px; |
| padding: 0.5rem; |
| cursor: pointer; |
| transition: all 0.2s ease; |
| color: var(--text-secondary); |
| } |
| |
| .theme-toggle:hover { |
| background: var(--card-bg); |
| transform: translateY(-1px); |
| box-shadow: var(--shadow-light); |
| } |
| |
| .connection-form { |
| display: flex; |
| gap: 1rem; |
| align-items: center; |
| flex-wrap: wrap; |
| } |
| |
| .input-group { |
| display: flex; |
| flex-direction: column; |
| gap: 0.25rem; |
| min-width: 180px; |
| } |
| |
| .input-group label { |
| font-size: 0.75rem; |
| font-weight: 500; |
| color: var(--text-muted); |
| text-transform: uppercase; |
| letter-spacing: 0.05em; |
| } |
| |
| .connection-form input { |
| padding: 0.75rem 1rem; |
| border: 1px solid var(--border-color); |
| border-radius: 12px; |
| background: var(--secondary-bg); |
| backdrop-filter: var(--blur-light); |
| color: var(--text-primary); |
| font-size: 0.875rem; |
| transition: all 0.2s ease; |
| outline: none; |
| } |
| |
| .connection-form input:focus { |
| border-color: var(--accent-primary); |
| box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); |
| } |
| |
| .connection-form input::placeholder { |
| color: var(--text-muted); |
| } |
| |
| .btn { |
| padding: 0.75rem 1.5rem; |
| border: none; |
| border-radius: 12px; |
| background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); |
| color: white; |
| cursor: pointer; |
| font-weight: 600; |
| font-size: 0.875rem; |
| transition: all 0.2s ease; |
| position: relative; |
| overflow: hidden; |
| display: flex; |
| align-items: center; |
| gap: 0.5rem; |
| box-shadow: var(--shadow-light); |
| } |
| |
| .btn::before { |
| content: ''; |
| position: absolute; |
| top: 0; |
| left: -100%; |
| width: 100%; |
| height: 100%; |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); |
| transition: left 0.5s; |
| } |
| |
| .btn:hover::before { |
| left: 100%; |
| } |
| |
| .btn:hover { |
| transform: translateY(-2px); |
| box-shadow: var(--shadow-medium); |
| } |
| |
| .btn:active { |
| transform: translateY(0); |
| } |
| |
| .btn:disabled { |
| background: var(--text-muted); |
| cursor: not-allowed; |
| transform: none; |
| opacity: 0.6; |
| } |
| |
| .btn-secondary { |
| background: transparent; |
| border: 1px solid var(--border-color); |
| color: var(--text-secondary); |
| } |
| |
| .btn-secondary:hover { |
| background: var(--secondary-bg); |
| } |
| |
| .main-content { |
| display: flex; |
| flex: 1; |
| overflow: hidden; |
| gap: 1px; |
| } |
| |
| .sidebar { |
| width: 280px; |
| background: var(--card-bg); |
| backdrop-filter: var(--blur-light); |
| border-right: 1px solid var(--border-color); |
| display: flex; |
| flex-direction: column; |
| transition: all 0.3s ease; |
| } |
| |
| .sidebar-header { |
| padding: 1.5rem; |
| border-bottom: 1px solid var(--border-color); |
| } |
| |
| .sidebar-header h3 { |
| color: var(--text-primary); |
| font-size: 1rem; |
| font-weight: 600; |
| display: flex; |
| align-items: center; |
| gap: 0.5rem; |
| } |
| |
| .sidebar-header i { |
| color: var(--accent-success); |
| } |
| |
| .sidebar-content { |
| flex: 1; |
| padding: 1rem; |
| overflow-y: auto; |
| } |
| |
| .user-list { |
| list-style: none; |
| display: flex; |
| flex-direction: column; |
| gap: 0.5rem; |
| } |
| |
| .user-list li { |
| padding: 0.75rem 1rem; |
| color: var(--text-secondary); |
| background: var(--secondary-bg); |
| border-radius: 8px; |
| transition: all 0.2s ease; |
| display: flex; |
| align-items: center; |
| gap: 0.75rem; |
| } |
| |
| .user-list li:hover { |
| background: var(--card-bg); |
| transform: translateX(2px); |
| } |
| |
| .user-list li::before { |
| content: ''; |
| width: 8px; |
| height: 8px; |
| border-radius: 50%; |
| background: var(--accent-success); |
| flex-shrink: 0; |
| } |
| |
| .chat-container { |
| flex: 1; |
| display: flex; |
| flex-direction: column; |
| background: var(--secondary-bg); |
| backdrop-filter: var(--blur-light); |
| } |
| |
| .messages { |
| flex: 1; |
| overflow-y: auto; |
| padding: 1.5rem; |
| display: flex; |
| flex-direction: column; |
| gap: 1rem; |
| scroll-behavior: smooth; |
| } |
| |
| .messages::-webkit-scrollbar { |
| width: 6px; |
| } |
| |
| .messages::-webkit-scrollbar-track { |
| background: transparent; |
| } |
| |
| .messages::-webkit-scrollbar-thumb { |
| background: var(--border-color); |
| border-radius: 3px; |
| } |
| |
| .messages::-webkit-scrollbar-thumb:hover { |
| background: var(--text-muted); |
| } |
| |
| .message { |
| background: var(--card-bg); |
| backdrop-filter: var(--blur-light); |
| border-radius: 16px; |
| padding: 1rem 1.25rem; |
| max-width: 75%; |
| border: 1px solid var(--border-color); |
| box-shadow: var(--shadow-light); |
| position: relative; |
| animation: messageSlide 0.3s ease-out; |
| } |
| |
| @keyframes messageSlide { |
| from { |
| opacity: 0; |
| transform: translateY(10px); |
| } |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| |
| .message.own { |
| align-self: flex-end; |
| background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); |
| color: white; |
| border: none; |
| } |
| |
| .message.own .username, |
| .message.own .timestamp { |
| color: rgba(255, 255, 255, 0.9); |
| } |
| |
| .message.system { |
| align-self: center; |
| background: var(--secondary-bg); |
| border: 1px solid var(--border-color); |
| font-style: italic; |
| color: var(--text-muted); |
| max-width: 60%; |
| text-align: center; |
| font-size: 0.8rem; |
| } |
| |
| .message-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 0.5rem; |
| font-size: 0.75rem; |
| } |
| |
| .username { |
| font-weight: 600; |
| color: var(--accent-primary); |
| } |
| |
| .message.own .username { |
| color: rgba(255, 255, 255, 0.9); |
| } |
| |
| .timestamp { |
| color: var(--text-muted); |
| font-size: 0.7rem; |
| } |
| |
| .message-content { |
| word-wrap: break-word; |
| line-height: 1.4; |
| } |
| |
| .file-preview img { |
| max-width: 200px; |
| max-height: 150px; |
| border-radius: 8px; |
| margin-top: 0.75rem; |
| box-shadow: var(--shadow-light); |
| } |
| |
| .file-download { |
| display: inline-flex; |
| align-items: center; |
| gap: 0.5rem; |
| margin-top: 0.75rem; |
| padding: 0.5rem 1rem; |
| background: var(--secondary-bg); |
| border-radius: 8px; |
| color: var(--accent-primary); |
| text-decoration: none; |
| font-size: 0.8rem; |
| transition: all 0.2s ease; |
| border: 1px solid var(--border-color); |
| } |
| |
| .file-download:hover { |
| background: var(--card-bg); |
| transform: translateY(-1px); |
| box-shadow: var(--shadow-light); |
| } |
| |
| .input-area { |
| padding: 1.5rem; |
| background: var(--card-bg); |
| backdrop-filter: var(--blur-medium); |
| border-top: 1px solid var(--border-color); |
| } |
| |
| .input-row { |
| display: flex; |
| gap: 1rem; |
| align-items: flex-end; |
| } |
| |
| #messageInput { |
| flex: 1; |
| padding: 1rem 1.25rem; |
| border: 1px solid var(--border-color); |
| border-radius: 20px; |
| background: var(--secondary-bg); |
| backdrop-filter: var(--blur-light); |
| color: var(--text-primary); |
| resize: none; |
| outline: none; |
| transition: all 0.2s ease; |
| font-family: inherit; |
| max-height: 100px; |
| min-height: 44px; |
| } |
| |
| #messageInput:focus { |
| border-color: var(--accent-primary); |
| box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); |
| } |
| |
| #messageInput::placeholder { |
| color: var(--text-muted); |
| } |
| |
| .file-input-wrapper { |
| position: relative; |
| } |
| |
| .file-input-wrapper input[type=file] { |
| position: absolute; |
| opacity: 0; |
| width: 100%; |
| height: 100%; |
| cursor: pointer; |
| } |
| |
| .file-input-label { |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| width: 44px; |
| height: 44px; |
| background: var(--secondary-bg); |
| border: 1px solid var(--border-color); |
| border-radius: 50%; |
| cursor: pointer; |
| transition: all 0.2s ease; |
| color: var(--text-secondary); |
| } |
| |
| .file-input-label:hover { |
| background: var(--accent-primary); |
| color: white; |
| transform: translateY(-2px); |
| box-shadow: var(--shadow-light); |
| } |
| |
| .status { |
| padding: 0.75rem 1rem; |
| text-align: center; |
| font-size: 0.8rem; |
| border-radius: 8px; |
| margin-top: 1rem; |
| font-weight: 500; |
| } |
| |
| .status.connected { |
| color: var(--accent-success); |
| background: rgba(16, 185, 129, 0.1); |
| border: 1px solid rgba(16, 185, 129, 0.2); |
| } |
| |
| .status.disconnected { |
| color: var(--accent-danger); |
| background: rgba(239, 68, 68, 0.1); |
| border: 1px solid rgba(239, 68, 68, 0.2); |
| } |
| |
| .status.neutral { |
| color: var(--text-muted); |
| background: var(--secondary-bg); |
| border: 1px solid var(--border-color); |
| } |
| |
| |
| @media (max-width: 768px) { |
| .header { |
| padding: 1rem; |
| } |
| |
| .header-top { |
| margin-bottom: 1rem; |
| } |
| |
| .header h1 { |
| font-size: 1.25rem; |
| } |
| |
| .connection-form { |
| flex-direction: column; |
| gap: 0.75rem; |
| width: 100%; |
| } |
| |
| .input-group { |
| width: 100%; |
| min-width: auto; |
| } |
| |
| .connection-form input, |
| .btn { |
| width: 100%; |
| } |
| |
| .main-content { |
| flex-direction: column; |
| height: calc(100vh - 200px); |
| } |
| |
| .sidebar { |
| width: 100%; |
| height: 120px; |
| flex-direction: row; |
| border-right: none; |
| border-bottom: 1px solid var(--border-color); |
| } |
| |
| .sidebar-header { |
| min-width: 120px; |
| border-right: 1px solid var(--border-color); |
| border-bottom: none; |
| } |
| |
| .sidebar-header h3 { |
| font-size: 0.9rem; |
| } |
| |
| .sidebar-content { |
| overflow-x: auto; |
| overflow-y: hidden; |
| } |
| |
| .user-list { |
| flex-direction: row; |
| white-space: nowrap; |
| padding-bottom: 0.5rem; |
| } |
| |
| .user-list li { |
| flex-shrink: 0; |
| font-size: 0.8rem; |
| padding: 0.5rem 0.75rem; |
| } |
| |
| .message { |
| max-width: 90%; |
| padding: 0.75rem 1rem; |
| } |
| |
| .messages { |
| padding: 1rem; |
| } |
| |
| .input-area { |
| padding: 1rem; |
| } |
| |
| .input-row { |
| gap: 0.75rem; |
| } |
| } |
| |
| @media (max-width: 480px) { |
| .header { |
| padding: 0.75rem; |
| } |
| |
| .messages { |
| padding: 0.75rem; |
| gap: 0.75rem; |
| } |
| |
| .message { |
| padding: 0.75rem; |
| border-radius: 12px; |
| } |
| |
| .input-area { |
| padding: 0.75rem; |
| } |
| } |
| |
| |
| * { |
| transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; |
| } |
| </style> |
| </head> |
| <body> |
| <div class="header"> |
| <div class="header-top"> |
| <div class="logo"> |
| <i class="fas fa-comments"></i> |
| <h1>Tri-Chat</h1> |
| </div> |
| <button class="theme-toggle" id="themeToggle"> |
| <i class="fas fa-moon" id="themeIcon"></i> |
| </button> |
| </div> |
| |
| <div class="connection-form"> |
| <div class="input-group"> |
| <label for="usernameInput">Your Name</label> |
| <input type="text" id="usernameInput" placeholder="Enter your name" maxlength="20"> |
| </div> |
| <div class="input-group"> |
| <label for="roomInput">Room</label> |
| <input type="text" id="roomInput" placeholder="global" maxlength="30"> |
| </div> |
| <button class="btn" id="connectBtn"> |
| <i class="fas fa-plug"></i> |
| Connect |
| </button> |
| <button class="btn btn-secondary" id="disconnectBtn" style="display: none;"> |
| <i class="fas fa-sign-out-alt"></i> |
| Disconnect |
| </button> |
| </div> |
| |
| <div class="status neutral" id="status"> |
| <i class="fas fa-info-circle"></i> |
| Enter your name and click Connect to start chatting |
| </div> |
| </div> |
| |
| <div class="main-content"> |
| <div class="sidebar"> |
| <div class="sidebar-header"> |
| <h3> |
| <i class="fas fa-users"></i> |
| Online |
| </h3> |
| </div> |
| <div class="sidebar-content"> |
| <ul class="user-list" id="userList"></ul> |
| </div> |
| </div> |
| |
| <div class="chat-container"> |
| <div class="messages" id="messages"></div> |
| |
| <div class="input-area" style="display: none;" id="inputArea"> |
| <div class="input-row"> |
| <div class="file-input-wrapper"> |
| <input type="file" id="fileInput" accept="*/*"> |
| <label for="fileInput" class="file-input-label"> |
| <i class="fas fa-paperclip"></i> |
| </label> |
| </div> |
| <textarea id="messageInput" placeholder="Type your message..." maxlength="500" rows="1"></textarea> |
| <button class="btn" id="sendBtn"> |
| <i class="fas fa-paper-plane"></i> |
| Send |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| class TriChat { |
| constructor() { |
| this.ws = null; |
| this.username = ''; |
| this.room = 'global'; |
| this.isConnected = false; |
| this.currentTheme = 'light'; |
| |
| this.initElements(); |
| this.bindEvents(); |
| this.initTheme(); |
| this.setupMessageInputResize(); |
| } |
| |
| initElements() { |
| this.elements = { |
| usernameInput: document.getElementById('usernameInput'), |
| roomInput: document.getElementById('roomInput'), |
| connectBtn: document.getElementById('connectBtn'), |
| disconnectBtn: document.getElementById('disconnectBtn'), |
| status: document.getElementById('status'), |
| messages: document.getElementById('messages'), |
| messageInput: document.getElementById('messageInput'), |
| sendBtn: document.getElementById('sendBtn'), |
| fileInput: document.getElementById('fileInput'), |
| inputArea: document.getElementById('inputArea'), |
| userList: document.getElementById('userList'), |
| themeToggle: document.getElementById('themeToggle'), |
| themeIcon: document.getElementById('themeIcon') |
| }; |
| } |
| |
| initTheme() { |
| const savedTheme = localStorage.getItem('tri-chat-theme') || 'light'; |
| this.setTheme(savedTheme); |
| } |
| |
| setTheme(theme) { |
| this.currentTheme = theme; |
| document.documentElement.setAttribute('data-theme', theme); |
| |
| if (theme === 'dark') { |
| this.elements.themeIcon.className = 'fas fa-sun'; |
| } else { |
| this.elements.themeIcon.className = 'fas fa-moon'; |
| } |
| |
| localStorage.setItem('tri-chat-theme', theme); |
| } |
| |
| toggleTheme() { |
| const newTheme = this.currentTheme === 'light' ? 'dark' : 'light'; |
| this.setTheme(newTheme); |
| } |
| |
| setupMessageInputResize() { |
| this.elements.messageInput.addEventListener('input', () => { |
| this.elements.messageInput.style.height = 'auto'; |
| this.elements.messageInput.style.height = Math.min(this.elements.messageInput.scrollHeight, 100) + 'px'; |
| }); |
| } |
| |
| bindEvents() { |
| this.elements.connectBtn.addEventListener('click', () => this.connect()); |
| this.elements.disconnectBtn.addEventListener('click', () => this.disconnect()); |
| this.elements.sendBtn.addEventListener('click', () => this.sendMessage()); |
| this.elements.fileInput.addEventListener('change', (e) => this.handleFileSelect(e)); |
| this.elements.themeToggle.addEventListener('click', () => this.toggleTheme()); |
| |
| this.elements.messageInput.addEventListener('keypress', (e) => { |
| if (e.key === 'Enter' && !e.shiftKey) { |
| e.preventDefault(); |
| this.sendMessage(); |
| } |
| }); |
| |
| this.elements.usernameInput.addEventListener('keypress', (e) => { |
| if (e.key === 'Enter') { |
| this.connect(); |
| } |
| }); |
| } |
| |
| async connect() { |
| const username = this.elements.usernameInput.value.trim(); |
| const room = this.elements.roomInput.value.trim() || 'global'; |
| |
| if (!username) { |
| this.showNotification('Please enter your name', 'error'); |
| this.elements.usernameInput.focus(); |
| return; |
| } |
| |
| this.username = username; |
| this.room = room; |
| |
| try { |
| this.updateStatus('Connecting...', 'neutral'); |
| |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; |
| const wsUrl = `${protocol}//${window.location.host}/ws/${room}?username=${encodeURIComponent(username)}`; |
| |
| this.ws = new WebSocket(wsUrl); |
| |
| this.ws.onopen = () => { |
| this.isConnected = true; |
| this.updateUI(); |
| this.updateStatus(`Connected to ${room}`, 'connected'); |
| this.showNotification('Successfully connected!', 'success'); |
| }; |
| |
| this.ws.onmessage = (event) => { |
| const message = JSON.parse(event.data); |
| this.displayMessage(message); |
| if (message.type === 'user_list') { |
| this.updateUserList(message.users); |
| } |
| }; |
| |
| this.ws.onclose = () => { |
| this.isConnected = false; |
| this.updateUI(); |
| this.updateStatus('Disconnected', 'disconnected'); |
| }; |
| |
| this.ws.onerror = (error) => { |
| console.error('WebSocket error:', error); |
| this.updateStatus('Connection error', 'disconnected'); |
| this.showNotification('Connection failed', 'error'); |
| }; |
| |
| } catch (error) { |
| console.error('Connection failed:', error); |
| this.updateStatus('Failed to connect', 'disconnected'); |
| this.showNotification('Failed to connect', 'error'); |
| } |
| } |
| |
| disconnect() { |
| if (this.ws) { |
| this.ws.close(); |
| } |
| } |
| |
| updateUI() { |
| if (this.isConnected) { |
| this.elements.connectBtn.style.display = 'none'; |
| this.elements.disconnectBtn.style.display = 'flex'; |
| this.elements.inputArea.style.display = 'block'; |
| this.elements.usernameInput.disabled = true; |
| this.elements.roomInput.disabled = true; |
| this.elements.messageInput.focus(); |
| } else { |
| this.elements.connectBtn.style.display = 'flex'; |
| this.elements.disconnectBtn.style.display = 'none'; |
| this.elements.inputArea.style.display = 'none'; |
| this.elements.usernameInput.disabled = false; |
| this.elements.roomInput.disabled = false; |
| this.elements.messages.innerHTML = ''; |
| this.elements.userList.innerHTML = ''; |
| } |
| } |
| |
| updateStatus(text, type) { |
| this.elements.status.innerHTML = ` |
| <i class="fas ${type === 'connected' ? 'fa-check-circle' : type === 'disconnected' ? 'fa-times-circle' : 'fa-info-circle'}"></i> |
| ${text} |
| `; |
| this.elements.status.className = 'status ' + type; |
| } |
| |
| showNotification(message, type) { |
| |
| console.log(`${type.toUpperCase()}: ${message}`); |
| } |
| |
| updateUserList(users) { |
| this.elements.userList.innerHTML = ''; |
| users.forEach(user => { |
| const li = document.createElement('li'); |
| li.textContent = user; |
| this.elements.userList.appendChild(li); |
| }); |
| } |
| |
| sendMessage() { |
| const text = this.elements.messageInput.value.trim(); |
| if (!text || !this.isConnected) return; |
| |
| const message = { |
| type: 'text', |
| username: this.username, |
| text: text, |
| timestamp: new Date().toISOString(), |
| room: this.room |
| }; |
| |
| this.ws.send(JSON.stringify(message)); |
| this.elements.messageInput.value = ''; |
| this.elements.messageInput.style.height = 'auto'; |
| } |
| |
| async handleFileSelect(event) { |
| const file = event.target.files[0]; |
| if (!file) return; |
| |
| if (file.size > 5 * 1024 * 1024) { |
| this.showNotification('File size must be less than 5MB', 'error'); |
| return; |
| } |
| |
| try { |
| const base64Data = await this.fileToBase64(file); |
| |
| const message = { |
| type: 'file', |
| username: this.username, |
| fileName: file.name, |
| fileType: file.type, |
| fileSize: file.size, |
| fileData: base64Data, |
| timestamp: new Date().toISOString(), |
| room: this.room |
| }; |
| |
| this.ws.send(JSON.stringify(message)); |
| event.target.value = ''; |
| this.showNotification('File uploaded successfully', 'success'); |
| |
| } catch (error) { |
| console.error('File upload error:', error); |
| this.showNotification('Failed to upload file', 'error'); |
| } |
| } |
| |
| fileToBase64(file) { |
| return new Promise((resolve, reject) => { |
| const reader = new FileReader(); |
| reader.onload = () => resolve(reader.result.split(',')[1]); |
| reader.onerror = reject; |
| reader.readAsDataURL(file); |
| }); |
| } |
| |
| displayMessage(message) { |
| const messageElement = document.createElement('div'); |
| messageElement.className = 'message'; |
| |
| if (message.type === 'system') { |
| messageElement.className += ' system'; |
| messageElement.innerHTML = ` |
| <div class="message-content"> |
| <i class="fas fa-info-circle"></i> |
| ${this.escapeHtml(message.message)} |
| </div> |
| `; |
| } else { |
| if (message.username === this.username) { |
| messageElement.className += ' own'; |
| } |
| |
| const timestamp = new Date(message.timestamp).toLocaleTimeString([], { |
| hour: '2-digit', |
| minute: '2-digit' |
| }); |
| |
| if (message.type === 'text') { |
| messageElement.innerHTML = ` |
| <div class="message-header"> |
| <span class="username">${this.escapeHtml(message.username)}</span> |
| <span class="timestamp">${timestamp}</span> |
| </div> |
| <div class="message-content">${this.escapeHtml(message.text)}</div> |
| `; |
| } else if (message.type === 'file') { |
| const downloadUrl = 'data:' + message.fileType + ';base64,' + message.fileData; |
| let preview = ''; |
| let fileIcon = 'fas fa-file'; |
| |
| if (message.fileType.startsWith('image/')) { |
| preview = `<div class="file-preview"><img src="${downloadUrl}" alt="${message.fileName}"></div>`; |
| fileIcon = 'fas fa-image'; |
| } else if (message.fileType.startsWith('video/')) { |
| fileIcon = 'fas fa-video'; |
| } else if (message.fileType.startsWith('audio/')) { |
| fileIcon = 'fas fa-music'; |
| } else if (message.fileType.includes('pdf')) { |
| fileIcon = 'fas fa-file-pdf'; |
| } else if (message.fileType.includes('document') || message.fileType.includes('text')) { |
| fileIcon = 'fas fa-file-alt'; |
| } |
| |
| messageElement.innerHTML = ` |
| <div class="message-header"> |
| <span class="username">${this.escapeHtml(message.username)}</span> |
| <span class="timestamp">${timestamp}</span> |
| </div> |
| <div class="message-content"> |
| <div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;"> |
| <i class="${fileIcon}"></i> |
| <strong>${this.escapeHtml(message.fileName)}</strong> |
| </div> |
| <div style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 0.5rem;"> |
| ${this.formatFileSize(message.fileSize)} |
| </div> |
| ${preview} |
| <a href="${downloadUrl}" download="${message.fileName}" class="file-download"> |
| <i class="fas fa-download"></i> |
| Download |
| </a> |
| </div> |
| `; |
| } |
| } |
| |
| this.elements.messages.appendChild(messageElement); |
| this.elements.messages.scrollTop = this.elements.messages.scrollHeight; |
| |
| |
| messageElement.style.opacity = '0'; |
| messageElement.style.transform = 'translateY(10px)'; |
| |
| requestAnimationFrame(() => { |
| messageElement.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; |
| messageElement.style.opacity = '1'; |
| messageElement.style.transform = 'translateY(0)'; |
| }); |
| } |
| |
| escapeHtml(text) { |
| const div = document.createElement('div'); |
| div.textContent = text; |
| return div.innerHTML; |
| } |
| |
| formatFileSize(bytes) { |
| if (bytes === 0) return '0 Bytes'; |
| const k = 1024; |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
| } |
| } |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| const triChat = new TriChat(); |
| |
| |
| if (!triChat.isConnected) { |
| setTimeout(() => { |
| const welcomeMessage = document.createElement('div'); |
| welcomeMessage.className = 'message system'; |
| welcomeMessage.innerHTML = ` |
| <div class="message-content"> |
| <i class="fas fa-rocket"></i> |
| Welcome to Tri-Chat! Connect with your name to start chatting. |
| </div> |
| `; |
| triChat.elements.messages.appendChild(welcomeMessage); |
| }, 500); |
| } |
| }); |
| </script> |
| </body> |
| </html> |