class ChatManager { constructor() { this.chatId = this.getChatId(); this.messages = []; this.websocket = null; this.isDestroyed = false; this.isConnected = false; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.hasJoinedChat = false; this.hasLoadedHistory = false; this.settings = { showImages: localStorage.getItem("chat-show-images") !== "false", use24hTime: localStorage.getItem("chat-24h-time") !== "false", }; this.initializeElements(); this.bindEvents(); this.loadStoredUsername(); this.connectWebSocket(); } getChatId() { const metaTag = document.querySelector('meta[name="chat-id"]'); if (!metaTag) { throw new Error("Chat ID not found in meta tags"); } return metaTag.content; } initializeElements() { this.elements = { loadingState: document.getElementById("loadingState"), errorState: document.getElementById("errorState"), errorMessage: document.getElementById("errorMessage"), retryBtn: document.getElementById("retryBtn"), messagesContainer: document.getElementById("messagesContainer"), emptyState: document.getElementById("emptyState"), chatForm: document.getElementById("chatForm"), usernameInput: document.getElementById("usernameInput"), messageInput: document.getElementById("messageInput"), sendBtn: document.getElementById("sendBtn"), settingsBtn: document.getElementById("settingsBtn"), settingsDropdown: document.getElementById("settingsDropdown"), imageToggle: document.getElementById("imageToggle"), timeToggle: document.getElementById("timeToggle"), }; this.elements.imageToggle.checked = this.settings.showImages; this.elements.timeToggle.checked = this.settings.use24hTime; } bindEvents() { this.elements.chatForm.addEventListener("submit", (e) => { e.preventDefault(); this.sendMessage(); }); this.elements.retryBtn.addEventListener("click", () => { this.connectWebSocket(); }); this.elements.usernameInput.addEventListener("input", () => { const username = this.elements.usernameInput.value.trim(); if (username) { localStorage.setItem("chat-username", username); } this.validateForm(); }); this.elements.messageInput.addEventListener("input", () => { this.validateForm(); }); this.elements.settingsBtn.addEventListener("click", () => { this.toggleSettingsDropdown(); }); this.elements.imageToggle.addEventListener("change", () => { this.settings.showImages = this.elements.imageToggle.checked; localStorage.setItem("chat-show-images", this.settings.showImages); this.renderMessages(); }); this.elements.timeToggle.addEventListener("change", () => { this.settings.use24hTime = this.elements.timeToggle.checked; localStorage.setItem("chat-24h-time", this.settings.use24hTime); this.renderMessages(); }); document.addEventListener("click", (e) => { if (!e.target.closest(".settings-container")) { this.elements.settingsDropdown.style.display = "none"; } }); document.addEventListener("visibilitychange", () => { if (!document.hidden && !this.isConnected) { this.connectWebSocket(); } }); window.addEventListener("beforeunload", () => { this.cleanup(); }); this.elements.messageInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); this.sendMessage(); } }); } loadStoredUsername() { const storedUsername = localStorage.getItem("chat-username"); if (storedUsername) { this.elements.usernameInput.value = storedUsername; } this.validateForm(); } validateForm() { const username = this.elements.usernameInput.value.trim(); const message = this.elements.messageInput.value.trim(); const isValid = username.length > 0 && message.length > 0; this.elements.sendBtn.disabled = !isValid; } connectWebSocket() { if (this.websocket) { this.websocket.close(); } this.showLoading(); const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const wsUrl = `${protocol}//${window.location.host}`; try { this.websocket = new WebSocket(wsUrl); this.websocket.onopen = () => { console.log("WebSocket connected"); this.isConnected = true; this.reconnectAttempts = 0; this.hasLoadedHistory = false; this.loadChatHistory(); }; this.websocket.onmessage = (event) => { try { const message = JSON.parse(event.data); this.handleWebSocketMessage(message); } catch (error) { console.error("Error parsing WebSocket message:", error); } }; this.websocket.onerror = (error) => { console.error("WebSocket error:", error); this.isConnected = false; }; this.websocket.onclose = (event) => { console.log("WebSocket closed:", event.code, event.reason); this.isConnected = false; if ( !this.isDestroyed && this.reconnectAttempts < this.maxReconnectAttempts ) { this.reconnectAttempts++; console.log( `Attempting to reconnect... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`, ); setTimeout( () => { if (!this.isDestroyed) { this.connectWebSocket(); } }, Math.min(1000 * this.reconnectAttempts, 5000), ); } else if (!this.isDestroyed) { this.showError("Connection lost. Please refresh the page."); } }; } catch (error) { console.error("Failed to create WebSocket:", error); this.showError("Failed to connect to chat server"); } } loadChatHistory() { if ( this.websocket && this.websocket.readyState === WebSocket.OPEN && !this.hasLoadedHistory ) { this.websocket.send( JSON.stringify({ type: "load_history", streamId: this.chatId, }), ); this.hasLoadedHistory = true; } } handleWebSocketMessage(message) { switch (message.type) { case "connected": console.log("Connected to chat server"); this.loadChatHistory(); break; case "chat_history": this.messages = message.data.messages || []; this.renderMessages(); this.showMessages(); this.scrollToBottom(); break; case "chat_message": this.messages.push(message.data); this.renderMessages(); this.scrollToBottom(); break; case "user_joined": this.showSystemMessage(`${message.data.username} joined`); break; case "user_left": this.showSystemMessage(`${message.data.username} left`); break; case "error": console.error("WebSocket error:", message.error); this.showError(message.error); break; default: console.log("Unknown message type:", message.type); } } joinChat(username) { if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { this.websocket.send( JSON.stringify({ type: "join_chat", streamId: this.chatId, username: username, }), ); } } sendMessage() { const username = this.elements.usernameInput.value.trim(); const message = this.elements.messageInput.value.trim(); if (!username || !message) { return; } if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) { this.showError("Not connected to chat server"); return; } if (!this.hasJoinedChat) { this.joinChat(username); this.hasJoinedChat = true; } this.elements.sendBtn.disabled = true; this.elements.messageInput.disabled = true; try { this.websocket.send( JSON.stringify({ type: "send_message", message: message, }), ); this.elements.messageInput.value = ""; localStorage.setItem("chat-username", username); } catch (error) { console.error("Failed to send message:", error); } finally { this.elements.messageInput.disabled = false; this.validateForm(); this.elements.messageInput.focus(); } } toggleSettingsDropdown() { const dropdown = this.elements.settingsDropdown; dropdown.style.display = dropdown.style.display === "block" ? "none" : "block"; } processMessageContent(text) { if (!this.settings.showImages) { return this.escapeHtml(text); } const imageUrlRegex = /https:\/\/[^\s]+\.(?:jpg|jpeg|png|gif|webp|svg)(?:\?[^\s]*)?/gi; const parts = text.split(imageUrlRegex); const imageUrls = text.match(imageUrlRegex) || []; let processedContent = ""; for (let i = 0; i < parts.length; i++) { if (parts[i]) { processedContent += this.escapeHtml(parts[i]); } if (imageUrls[i]) { processedContent += this.createImageElement(imageUrls[i]); } } return processedContent; } createImageElement(url) { const imageId = `img_${Math.random().toString(36).substr(2, 9)}`; return `
Shared image
`; } renderMessages() { this.elements.messagesContainer.innerHTML = ""; if (this.messages.length === 0) { this.showEmpty(); return; } for (const message of this.messages) { const messageElement = this.createMessageElement(message); this.elements.messagesContainer.appendChild(messageElement); } } createMessageElement(message) { const messageDiv = document.createElement("div"); messageDiv.className = "message"; const time = new Date(message.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: !this.settings.use24hTime, }); const processedContent = this.processMessageContent(message.message); messageDiv.innerHTML = `
${this.escapeHtml(message.username)} ${time}
${processedContent}
`; setTimeout(() => { const images = messageDiv.querySelectorAll(".message-image"); for (const img of images) { img.addEventListener("error", () => { img.style.display = "none"; const errorDiv = document.createElement("div"); errorDiv.style.cssText = "padding: 8px; color: #ff6b6b; font-style: italic; background: rgba(255,107,107,0.1); border-radius: 4px; margin: 8px 0;"; errorDiv.textContent = "Failed to load image"; img.parentNode.appendChild(errorDiv); }); } }, 0); return messageDiv; } showSystemMessage(text) { const messageDiv = document.createElement("div"); messageDiv.className = "message system-message"; messageDiv.innerHTML = `
${this.escapeHtml(text)}
`; this.elements.messagesContainer.appendChild(messageDiv); this.scrollToBottom(); } scrollToBottom() { setTimeout(() => { const container = this.elements.messagesContainer; container.scrollTop = container.scrollHeight; }, 50); } cleanup() { this.isDestroyed = true; if (this.websocket) { if (this.websocket.readyState === WebSocket.OPEN) { this.websocket.send( JSON.stringify({ type: "leave_chat", }), ); } this.websocket.close(); this.websocket = null; } } escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } showLoading() { this.elements.loadingState.style.display = "flex"; this.elements.errorState.style.display = "none"; this.elements.messagesContainer.style.display = "none"; this.elements.emptyState.style.display = "none"; } showError(message) { this.elements.loadingState.style.display = "none"; this.elements.errorState.style.display = "flex"; this.elements.messagesContainer.style.display = "none"; this.elements.emptyState.style.display = "none"; this.elements.errorMessage.textContent = message; } showMessages() { this.elements.loadingState.style.display = "none"; this.elements.errorState.style.display = "none"; this.elements.messagesContainer.style.display = "flex"; this.elements.emptyState.style.display = "none"; } showEmpty() { this.elements.loadingState.style.display = "none"; this.elements.errorState.style.display = "none"; this.elements.messagesContainer.style.display = "none"; this.elements.emptyState.style.display = "flex"; } } try { new ChatManager(); } catch (error) { console.error("Failed to initialize ChatManager:", error); }