import * as chatClient from "./chat-client.js"; let panelOpen = false; let initialized = false; let bubble = null; let panel = null; let messageList = null; let inputArea = null; let sendBtn = null; let muteDropdown = null; let muteDropdownMenu = null; let muteDropdownLabel = null; let oldestTimestamp = null; let loadingMore = false; let hasMore = true; const ROLE_COLORS = { admin: "bg-rose-500/80 text-white", operator: "bg-sky-500/80 text-white", viewer: "bg-zinc-500/80 text-white", }; function escapeHtml(str) { const div = document.createElement("div"); div.textContent = str; return div.innerHTML.replace(/"/g, """).replace(/'/g, "'"); } function formatTime(ts) { const d = new Date(ts); const now = new Date(); const sameDay = d.toDateString() === now.toDateString(); if (sameDay) { return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } return d.toLocaleDateString([], { month: "short", day: "numeric" }) + " " + d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } function createMessageEl(msg) { const wrapper = document.createElement("div"); wrapper.className = "flex flex-col gap-0.5 px-3 py-1.5 hover:bg-zinc-800/40 transition-colors"; wrapper.dataset.msgId = msg.id; wrapper.dataset.ts = msg.createdAt; const header = document.createElement("div"); header.className = "flex items-center gap-2 text-xs"; const badge = document.createElement("span"); badge.className = `px-1.5 py-0.5 rounded text-[10px] font-medium ${ROLE_COLORS[msg.userRole] || ROLE_COLORS.viewer}`; badge.textContent = msg.userRole; const name = document.createElement("span"); name.className = "font-semibold text-slate-200"; name.textContent = msg.username; const onlineCount = typeof msg.onlineClients === "number" ? msg.onlineClients : 0; const clientsBadge = document.createElement("span"); clientsBadge.className = "px-1.5 py-0.5 rounded text-[10px] font-medium " + (onlineCount > 0 ? "bg-emerald-500/20 text-emerald-400 border border-emerald-500/30" : "bg-zinc-700/50 text-zinc-500"); clientsBadge.textContent = onlineCount > 0 ? `${onlineCount} online` : "0 online"; const time = document.createElement("span"); time.className = "text-slate-500 ml-auto whitespace-nowrap"; time.textContent = formatTime(msg.createdAt); header.appendChild(badge); header.appendChild(name); header.appendChild(clientsBadge); header.appendChild(time); const body = document.createElement("div"); body.className = "text-sm text-slate-300 break-words whitespace-pre-wrap pl-0.5"; body.textContent = msg.message; wrapper.appendChild(header); wrapper.appendChild(body); return wrapper; } function scrollToBottom() { if (messageList) { messageList.scrollTop = messageList.scrollHeight; } } function updateBubbleBadge() { if (!bubble) return; const count = chatClient.getUnreadCount(); const badge = bubble.querySelector("[data-chat-badge]"); if (!badge) return; if (count > 0) { badge.textContent = count > 99 ? "99+" : String(count); badge.classList.remove("hidden"); } else { badge.classList.add("hidden"); } } function updateMuteVisibility() { const mute = chatClient.getMuteMode(); if (!bubble || !panel) return; if (mute === "hide") { bubble.classList.add("hidden"); panel.classList.add("hidden"); panelOpen = false; } else { bubble.classList.remove("hidden"); } if (mute === "mute_collapse" && panelOpen) { // auto collapse handled on new messages } } function togglePanel() { if (!panel) return; panelOpen = !panelOpen; if (panelOpen) { panel.classList.remove("hidden"); panel.style.opacity = "0"; panel.style.transform = "translateY(10px) scale(0.98)"; requestAnimationFrame(() => { panel.style.transition = "opacity 0.15s ease, transform 0.15s ease"; panel.style.opacity = "1"; panel.style.transform = "translateY(0) scale(1)"; }); chatClient.resetUnread(); updateBubbleBadge(); scrollToBottom(); if (inputArea) inputArea.focus(); } else { panel.style.transition = "opacity 0.1s ease, transform 0.1s ease"; panel.style.opacity = "0"; panel.style.transform = "translateY(10px) scale(0.98)"; setTimeout(() => { if (!panelOpen) panel.classList.add("hidden"); }, 100); } } async function loadMoreHistory() { if (loadingMore || !hasMore || !messageList) return; loadingMore = true; const result = await chatClient.fetchHistory(oldestTimestamp, 50); const msgs = result.messages || []; if (msgs.length === 0) { hasMore = false; loadingMore = false; return; } const prevHeight = messageList.scrollHeight; const prevTop = messageList.scrollTop; const frag = document.createDocumentFragment(); for (const msg of msgs) { frag.appendChild(createMessageEl(msg)); if (!oldestTimestamp || msg.createdAt < oldestTimestamp) { oldestTimestamp = msg.createdAt; } } messageList.insertBefore(frag, messageList.firstChild); const newHeight = messageList.scrollHeight; messageList.scrollTop = prevTop + (newHeight - prevHeight); if (msgs.length < 50) hasMore = false; loadingMore = false; } function appendMessage(msg) { if (!messageList) return; const wasAtBottom = messageList.scrollHeight - messageList.scrollTop - messageList.clientHeight < 40; messageList.appendChild(createMessageEl(msg)); if (!oldestTimestamp || msg.createdAt < oldestTimestamp) { oldestTimestamp = msg.createdAt; } if (wasAtBottom) scrollToBottom(); } function handleSend() { if (!inputArea) return; const text = inputArea.value; if (chatClient.sendMessage(text)) { inputArea.value = ""; inputArea.style.height = "auto"; } } function createWidget() { bubble = document.createElement("div"); bubble.id = "chat-bubble"; bubble.className = "fixed bottom-5 right-5 z-[9998] cursor-pointer select-none"; bubble.innerHTML = `
`; bubble.addEventListener("click", togglePanel); panel = document.createElement("div"); panel.id = "chat-panel"; panel.className = "hidden fixed bottom-[4.5rem] right-5 z-[9998] w-[360px] max-h-[500px] flex flex-col rounded-xl border border-zinc-700/70 bg-zinc-900/95 backdrop-blur-md shadow-2xl shadow-black/40 overflow-hidden"; panel.innerHTML = `
Team Chat
`; document.body.appendChild(bubble); document.body.appendChild(panel); messageList = panel.querySelector("#chat-messages"); inputArea = panel.querySelector("#chat-input"); sendBtn = panel.querySelector("#chat-send-btn"); muteDropdown = panel.querySelector("#chat-mute-dropdown"); muteDropdownMenu = panel.querySelector("#chat-mute-menu"); muteDropdownLabel = panel.querySelector("#chat-mute-label"); const closeBtn = panel.querySelector("#chat-close-btn"); const inputContainer = panel.querySelector("#chat-input-area"); const readonlyBar = panel.querySelector("#chat-readonly-bar"); const MUTE_LABELS = { none: "All notifications", mute: "Mute", mute_collapse: "Mute + Collapse", hide: "Hide", }; muteDropdownLabel.textContent = MUTE_LABELS[chatClient.getMuteMode()] || MUTE_LABELS.none; panel.querySelector("#chat-mute-btn").addEventListener("click", (e) => { e.stopPropagation(); muteDropdownMenu.classList.toggle("hidden"); }); muteDropdownMenu.querySelectorAll(".chat-mute-option").forEach((opt) => { opt.addEventListener("click", () => { const value = opt.dataset.value; chatClient.setMuteMode(value); muteDropdownLabel.textContent = MUTE_LABELS[value] || MUTE_LABELS.none; muteDropdownMenu.classList.add("hidden"); updateMuteVisibility(); }); }); document.addEventListener("click", () => { if (muteDropdownMenu && !muteDropdownMenu.classList.contains("hidden")) { muteDropdownMenu.classList.add("hidden"); } }); closeBtn.addEventListener("click", () => { if (panelOpen) togglePanel(); }); sendBtn.addEventListener("click", handleSend); inputArea.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } }); inputArea.addEventListener("input", () => { inputArea.style.height = "auto"; inputArea.style.height = Math.min(inputArea.scrollHeight, 100) + "px"; }); messageList.addEventListener("scroll", () => { if (messageList.scrollTop < 40 && hasMore && !loadingMore) { loadMoreHistory(); } }); document.addEventListener("keydown", (e) => { if (e.key === "Escape" && panelOpen) { togglePanel(); } }); document.addEventListener("mousedown", (e) => { if (panelOpen && !panel.contains(e.target) && !bubble.contains(e.target)) { togglePanel(); } }); chatClient.onReady((history, writable) => { messageList.innerHTML = ""; oldestTimestamp = null; hasMore = true; for (const msg of history) { messageList.appendChild(createMessageEl(msg)); if (!oldestTimestamp || msg.createdAt < oldestTimestamp) { oldestTimestamp = msg.createdAt; } } scrollToBottom(); if (writable) { inputContainer.classList.remove("hidden"); readonlyBar.classList.add("hidden"); } else { inputContainer.classList.add("hidden"); readonlyBar.classList.remove("hidden"); } }); chatClient.onMessage((msg) => { appendMessage(msg); const mute = chatClient.getMuteMode(); if (panelOpen) { chatClient.resetUnread(); updateBubbleBadge(); } if (mute === "mute_collapse" && panelOpen) { togglePanel(); } }); chatClient.onUnreadChanged(() => { updateBubbleBadge(); }); updateBubbleBadge(); updateMuteVisibility(); chatClient.start(); } export function init() { if (initialized) return; initialized = true; createWidget(); } export function show() { if (chatClient.getMuteMode() === "hide") { chatClient.setMuteMode("none"); } if (bubble) bubble.classList.remove("hidden"); updateMuteVisibility(); } export function isHidden() { return chatClient.getMuteMode() === "hide"; }