class MoveableChatFrame { constructor() { try { this.initializeElements(); if (!this.frame) { console.warn( "MoveableChatFrame: Required elements not found, skipping initialization", ); return; } this.isDragging = false; this.isResizing = false; this.isMinimized = false; this.dragOffset = { x: 0, y: 0 }; this.resizeHandle = null; this.chatLoaded = false; this.initializeEventListeners(); this.loadSavedState(); } catch (error) { console.error("MoveableChatFrame initialization failed:", error); } } initializeElements() { this.frame = document.getElementById("moveableChatFrame"); this.header = document.getElementById("chatFrameHeader"); this.toggleBtn = document.getElementById("toggleChatBtn"); this.minimizeBtn = document.getElementById("minimizeChatBtn"); this.closeBtn = document.getElementById("closeChatBtn"); if (!this.frame || !this.header || !this.toggleBtn) { console.warn("MoveableChatFrame: Missing required DOM elements"); return false; } return true; } initializeEventListeners() { if ( !this.toggleBtn || !this.minimizeBtn || !this.closeBtn || !this.header ) { console.warn("MoveableChatFrame: Cannot bind events - missing elements"); return; } try { this.toggleBtn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); this.toggleChat(); }); this.minimizeBtn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); this.toggleMinimize(); }); this.closeBtn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); this.closeChat(); }); this.header.addEventListener("mousedown", (e) => { if (e.target.closest(".chat-control-btn")) return; e.preventDefault(); this.startDragging(e); }); const resizeHandles = this.frame.querySelectorAll(".resize-handle"); for (const handle of resizeHandles) { handle.addEventListener("mousedown", (e) => { e.preventDefault(); this.startResizing(e, handle); }); } document.addEventListener( "mousemove", (e) => { if (this.isDragging) { this.drag(e); } else if (this.isResizing) { this.resize(e); } }, { passive: true }, ); document.addEventListener( "mouseup", () => { this.stopDragging(); this.stopResizing(); }, { passive: true }, ); this.header.addEventListener( "touchstart", (e) => { if (e.target.closest(".chat-control-btn")) return; e.preventDefault(); this.startDragging(e.touches[0]); }, { passive: false }, ); document.addEventListener( "touchmove", (e) => { if (this.isDragging) { e.preventDefault(); this.drag(e.touches[0]); } }, { passive: false }, ); document.addEventListener( "touchend", () => { this.stopDragging(); }, { passive: true }, ); window.addEventListener( "resize", () => { this.constrainToViewport(); }, { passive: true }, ); } catch (error) { console.error("Error binding MoveableChatFrame events:", error); } } toggleChat() { if (!this.frame) return; if (this.frame.style.display === "none") { this.openChat(); } else { this.closeChat(); } } openChat() { if (!this.frame) return; this.frame.style.display = "flex"; document.body.classList.add("chat-open"); if (!this.chatLoaded) { this.loadChatContent(); } this.saveState(); } loadChatContent() { const streamId = document.querySelector('meta[name="stream-id"]')?.content; if (!streamId) { this.showChatError("Stream ID not found"); return; } console.log("Loading chat for stream ID:", streamId); this.showLoading(); const iframe = document.createElement("iframe"); iframe.className = "chat-frame-iframe"; iframe.src = `/chat/${encodeURIComponent(streamId)}`; iframe.style.cssText = ` width: 100%; height: 100%; border: none; border-radius: 0 0 12px 12px; background: var(--bg-card); `; iframe.onload = () => { this.chatLoaded = true; this.hideLoading(); console.log("Chat iframe loaded successfully"); }; iframe.onerror = () => { console.error("Failed to load chat iframe"); this.showChatError("Failed to load chat"); }; const loadTimeout = setTimeout(() => { if (!this.chatLoaded) { console.warn("Chat loading timeout"); this.showChatError("Chat loading timeout"); } }, 10000); iframe.addEventListener("load", () => { clearTimeout(loadTimeout); }); const content = this.frame.querySelector(".chat-frame-content"); if (content) { content.innerHTML = ""; content.appendChild(iframe); } } closeChat() { if (!this.frame) return; this.frame.style.display = "none"; document.body.classList.remove("chat-open"); const content = this.frame.querySelector(".chat-frame-content"); if (content) { content.innerHTML = ""; } this.chatLoaded = false; this.saveState(); } toggleMinimize() { if (!this.frame || !this.minimizeBtn) return; this.isMinimized = !this.isMinimized; this.frame.classList.toggle("minimized", this.isMinimized); const icon = this.minimizeBtn.querySelector("svg path"); if (icon) { if (this.isMinimized) { icon.setAttribute("d", "M18 15l-6-6-6 6"); this.minimizeBtn.title = "Maximize"; } else { icon.setAttribute("d", "M6 9l6 6 6-6"); this.minimizeBtn.title = "Minimize"; } } this.saveState(); } showLoading() { if (!this.frame) return; const loadingDiv = document.createElement("div"); loadingDiv.className = "chat-frame-loading"; loadingDiv.innerHTML = `
Loading chat... `; const content = this.frame.querySelector(".chat-frame-content"); if (content) { content.innerHTML = ""; content.appendChild(loadingDiv); } } hideLoading() {} showChatError(message) { if (!this.frame) return; const errorDiv = document.createElement("div"); errorDiv.className = "chat-frame-error"; errorDiv.innerHTML = `
${message}
`; const content = this.frame.querySelector(".chat-frame-content"); if (content) { content.innerHTML = ""; content.appendChild(errorDiv); } } startDragging(event) { if (!this.frame) return; this.isDragging = true; const rect = this.frame.getBoundingClientRect(); this.dragOffset.x = event.clientX - rect.left; this.dragOffset.y = event.clientY - rect.top; this.frame.style.transition = "none"; document.body.style.userSelect = "none"; } drag(event) { if (!this.isDragging || !this.frame) return; const x = event.clientX - this.dragOffset.x; const y = event.clientY - this.dragOffset.y; const maxX = window.innerWidth - this.frame.offsetWidth; const maxY = window.innerHeight - this.frame.offsetHeight; const constrainedX = Math.max(0, Math.min(x, maxX)); const constrainedY = Math.max(0, Math.min(y, maxY)); this.frame.style.left = `${constrainedX}px`; this.frame.style.top = `${constrainedY}px`; this.frame.style.right = "auto"; this.frame.style.bottom = "auto"; } stopDragging() { if (this.isDragging && this.frame) { this.isDragging = false; this.frame.style.transition = "all 0.3s ease"; document.body.style.userSelect = ""; this.savePosition(); } } startResizing(event, handle) { if (this.isMinimized || !this.frame) return; this.isResizing = true; this.resizeHandle = handle; this.frame.style.transition = "none"; document.body.style.userSelect = "none"; event.preventDefault(); } resize(event) { if (!this.isResizing || !this.resizeHandle || !this.frame) return; const rect = this.frame.getBoundingClientRect(); const handleClass = this.resizeHandle.className; let newWidth = rect.width; let newHeight = rect.height; let newLeft = rect.left; let newTop = rect.top; if (handleClass.includes("resize-handle-e")) { newWidth = event.clientX - rect.left; } if (handleClass.includes("resize-handle-w")) { newWidth = rect.right - event.clientX; newLeft = event.clientX; } if (handleClass.includes("resize-handle-s")) { newHeight = event.clientY - rect.top; } if (handleClass.includes("resize-handle-n")) { newHeight = rect.bottom - event.clientY; newTop = event.clientY; } newWidth = Math.max(280, Math.min(newWidth, window.innerWidth)); newHeight = Math.max(200, Math.min(newHeight, window.innerHeight)); this.frame.style.width = `${newWidth}px`; this.frame.style.height = `${newHeight}px`; this.frame.style.left = `${newLeft}px`; this.frame.style.top = `${newTop}px`; this.frame.style.right = "auto"; this.frame.style.bottom = "auto"; } stopResizing() { if (this.isResizing && this.frame) { this.isResizing = false; this.resizeHandle = null; this.frame.style.transition = "all 0.3s ease"; document.body.style.userSelect = ""; this.savePosition(); } } constrainToViewport() { if (!this.frame || this.frame.style.display === "none") return; const rect = this.frame.getBoundingClientRect(); const maxX = window.innerWidth - rect.width; const maxY = window.innerHeight - rect.height; const currentX = Number.parseInt(this.frame.style.left) || rect.left; const currentY = Number.parseInt(this.frame.style.top) || rect.top; const constrainedX = Math.max(0, Math.min(currentX, maxX)); const constrainedY = Math.max(0, Math.min(currentY, maxY)); this.frame.style.left = `${constrainedX}px`; this.frame.style.top = `${constrainedY}px`; this.frame.style.right = "auto"; this.frame.style.bottom = "auto"; } savePosition() { if (!this.frame) return; try { const rect = this.frame.getBoundingClientRect(); const position = { left: rect.left, top: rect.top, width: rect.width, height: rect.height, }; localStorage.setItem("moveable-chat-position", JSON.stringify(position)); } catch (error) { console.warn("Failed to save chat position:", error); } } saveState() { if (!this.frame) return; try { const state = { visible: this.frame.style.display !== "none", minimized: this.isMinimized, }; localStorage.setItem("moveable-chat-state", JSON.stringify(state)); } catch (error) { console.warn("Failed to save chat state:", error); } } loadSavedState() { if (!this.frame) return; const savedPosition = localStorage.getItem("moveable-chat-position"); if (savedPosition) { try { const position = JSON.parse(savedPosition); this.frame.style.left = `${position.left}px`; this.frame.style.top = `${position.top}px`; this.frame.style.width = `${position.width}px`; this.frame.style.height = `${position.height}px`; this.frame.style.right = "auto"; this.frame.style.bottom = "auto"; } catch (e) { console.warn("Failed to load saved chat position:", e); } } const savedState = localStorage.getItem("moveable-chat-state"); if (savedState) { try { const state = JSON.parse(savedState); if (state.visible) { this.openChat(); } if (state.minimized) { this.isMinimized = true; this.toggleMinimize(); } } catch (e) { console.warn("Failed to load saved chat state:", e); } } setTimeout(() => { this.constrainToViewport(); }, 100); } } function initializeMoveableChat() { try { const requiredElements = [ "moveableChatFrame", "chatFrameHeader", "toggleChatBtn", ]; const missingElements = requiredElements.filter( (id) => !document.getElementById(id), ); if (missingElements.length > 0) { console.warn( "MoveableChatFrame: Missing required elements:", missingElements, ); return; } window.moveableChatFrame = new MoveableChatFrame(); } catch (error) { console.error("Failed to initialize MoveableChatFrame:", error); } } setTimeout(initializeMoveableChat, 100);