function toggleSettings() { const panel = document.getElementById("settings-panel"); panel.style.display = panel.style.display === "block" ? "none" : "block"; if (panel.style.display === "block") { const username = document.getElementById("username"); username.focus(); username.selectionStart = username.selectionEnd = username.value .length; } } function toggleUsers() { const panel = document.getElementById("users-panel"); panel.style.display = panel.style.display === "block" ? "none" : "block"; } function toggleTheme() { const currentTheme = document.documentElement.getAttribute( "data-theme", ); const newTheme = currentTheme === "dark" ? "light" : "dark"; document.documentElement.setAttribute("data-theme", newTheme); localStorage.setItem("theme", newTheme); } function replaceDiv(oldDiv, newDiv) { oldDiv.parentNode.replaceChild(newDiv, oldDiv); } function cancelEditMessage(messageId) { const editMessageDiv = document.getElementById( ".edit-message@" + messageId, ); replaceDiv(editMessageDiv, editMessageOriginalDiv); editMessageOriginalDiv = null; editing = false; editMessageNumber = null; } async function submitEditMessage(messageId) { const editMessageDiv = document.getElementById( ".edit-message@" + messageId, ); const content = editMessageOriginalDiv.querySelector(".content").textContent; const newContent = editMessageDiv.querySelector(".edit-message-textarea").value; if (content === null) { cancelEditMessage(messageId); return; } if (content === "") { cancelEditMessage(messageId); deleteMessage(messageId); return; } if (newContent === content) { cancelEditMessage(messageId); return; } try { const response = await fetch("/messages", { method: "PATCH", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ messageId: messageId, messageContent: newContent, }), }); if (response.ok) { editing = false; editMessageNumber = null; editMessageOriginalDiv = null; updateMessagesInPlace(); } else { console.error("Failed to edit message"); } } catch (error) { console.error("Error editing message:", error); } } editMessageOriginalDiv = null; editMessageNumber = null; editing = false; async function editMessage(messageId) { if (editing) { cancelEditMessage(editMessageNumber); } editing = true; editMessageNumber = messageId; const originalMessageDiv = document.getElementById( ".message@" + messageId, ); const height = originalMessageDiv.scrollHeight; const queryContent = originalMessageDiv.querySelector(".content"); // we need to replace all the html elements with their text content, this includes links like img, a, iframe queryContent.querySelectorAll("a").forEach((link) => { link.textContent = link.href; }); queryContent.querySelectorAll("img").forEach((img) => { img.textContent = img.src; }); queryContent.querySelectorAll("iframe").forEach((iframe) => { iframe.textContent = iframe.src; }); const content = originalMessageDiv.querySelector(".content").textContent; editMessageOriginalDiv = originalMessageDiv.cloneNode(true); const editMessageDiv = document.createElement("div"); const editTextArea = document.createElement("textarea"); editTextArea.className = "edit-message-textarea"; editTextArea.id = "edit-message-textarea"; editTextArea.value = content; editTextArea.style.height = height + "px"; editMessageDiv.className = "edit-message"; editMessageDiv.id = ".edit-message@" + messageId; editMessageDiv.innerHTML += `
` + `` + `` + `
`; editMessageDiv.appendChild(editTextArea); replaceDiv(originalMessageDiv, editMessageDiv); } async function deleteMessage(messageId) { if (confirm("Delete this message?")) { try { const response = await fetch("/messages", { method: "DELETE", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ messageId: messageId }), }); if (response.ok) { updateMessagesInPlace(); } else { console.error("Failed to delete message"); } } catch (error) { console.error("Error deleting message:", error); } } } async function updateMessagesInPlace() { lastMessageCount = await getMessageCount(); const currentScrollLocation = getScrollLocation(); loadMessages(true, currentScrollLocation); } function getScrollLocation() { const messagesDiv = document.getElementById("messages"); return messagesDiv.scrollTop; } function setScrollLocation(height) { const messagesDiv = document.getElementById("messages"); messagesDiv.scrollTop = height; } document.addEventListener("click", function(event) { const settingsPanel = document.getElementById("settings-panel"); const settingsButton = document.querySelector(".settings-button"); const usersPanel = document.getElementById("users-panel"); const usersButton = document.querySelector(".users-button"); if ( !settingsPanel.contains(event.target) && !settingsButton.contains(event.target) ) { settingsPanel.style.display = "none"; } if ( !usersPanel.contains(event.target) && !usersButton.contains(event.target) ) { usersPanel.style.display = "none"; } }); document.addEventListener("keypress", function(event) { if (event.key === "Enter" && !event.shiftKey) { const settingsPanel = document.getElementById("settings-panel"); const inputPanel = document.getElementById("message"); if (settingsPanel.contains(event.target)) { setUsername(); } if (inputPanel.contains(event.target)) { event.preventDefault(); sendMessage(); } } }); document.addEventListener("input", function(_) { const msg = document.getElementById("message"); msg.style.height = "auto"; msg.style.height = (msg.scrollHeight) + "px"; }); document.addEventListener("blur", function(_) { const msg = document.getElementById("message"); msg.style.height = "auto"; }, true); async function loadUsers() { try { const response = await fetch("/users"); const data = await response.json(); const usersList = document.getElementById("users-list"); usersList.innerHTML = ""; data.users.sort().forEach((user) => { const userDiv = document.createElement("div"); userDiv.className = "user-item"; userDiv.textContent = user; usersList.appendChild(userDiv); }); } catch (error) { console.error("Error loading users:", error); } } async function getMessageCount() { try { const response = await fetch("/messages/length"); const text = await response.json(); return text.length; } catch (error) { console.error("Error getting message count:", error); } } let lastMessageCount = 0; async function loadMessages(forceUpdate = false, scrollLocation) { if (editing) { return; } try { const newMessageCount = await getMessageCount(); const update = newMessageCount != lastMessageCount || lastMessageCount === 0 || forceUpdate; let messagesDiv = document.getElementById("messages"); while (messagesDiv === null) { await new Promise((resolve) => setTimeout(resolve, 100) ); messagesDiv = document.getElementById( "messages", ); } if (messagesDiv.scrollTop != bottom) { // show a button to scroll to the bottom const scrollToBottomButton = document .getElementById( "scroll", ); scrollToBottomButton.style.display = "block"; scrollToBottomButton.onclick = scrollToBottom; } else { // hide the button const scrollToBottomButton = document .getElementById( "scroll", ); scrollToBottomButton.style.display = "none"; } if (update) { const response = await fetch("/messages"); const text = await response.text(); const tempDiv = document.createElement("div"); tempDiv.innerHTML = text; const messages = tempDiv.getElementsByTagName("p"); messagesDiv.innerHTML = ""; Array.from(messages).forEach((msg) => { const messageDiv = document.createElement( "div", ); messageDiv.className = "message"; const [ messageId, username, timestamp, content, ] = msg .innerHTML.split( "
", ); messageDiv.id = ".message@" + messageId; const usernameDiv = document.createElement( "div", ); usernameDiv.innerHTML = username; compareUsername = usernameDiv.textContent; const embeddedContent = contentEmbedding( content, ); let deleteHtml = ""; let editHtml = ""; const parser = new DOMParser(); const contentHtmlString = content; const doc = parser.parseFromString( contentHtmlString, "text/html", ); const contentString = doc.querySelector("span").textContent; const isMultiline = contentString.match( "\\n", ) && true || false; if ( compareUsername === document.getElementById( "current-user", ).textContent ) { deleteHtml = ``; editHtml = ``; } messageDiv.innerHTML = `
${username} ${timestamp} ${deleteHtml} ${editHtml}
${embeddedContent}
`; messagesDiv.appendChild(messageDiv); }); if (scrollLocation !== undefined) { setScrollLocation(scrollLocation); } else { scrollToBottom(); } lastMessageCount = newMessageCount; } } catch (error) { console.error("Error loading messages:", error); } } function contentEmbedding(content) { return content.replace( /(?![^<]*>)(https?:\/\/[^\s<]+)/g, function(url) { const videoId = getYouTubeID( url, ); if (videoId) { return `
`; } else if (isImageUrl(url)) { return `
${url}
`; } return `${url}`; }, ); } function isImageUrl(url) { return url.match(/\.(jpeg|jpg|gif|png|webp|bmp)($|\?)/i) != null; } function getYouTubeID(url) { // First check if it's a Shorts URL if (url.includes("/shorts/")) { const shortsMatch = url.match(/\/shorts\/([^/?]+)/); return shortsMatch ? shortsMatch[1] : false; } // Otherwise check regular YouTube URLs const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/; const match = url.match(regExp); return (match && match[7].length == 11) ? match[7] : false; } function scrollToBottom() { const messagesDiv = document.getElementById("messages"); messagesDiv.scrollTop = messagesDiv.scrollHeight; bottom = messagesDiv.scrollTop; } async function checkUsername() { try { const response = await fetch("/username/status"); const data = await response.json(); if (!data.hasUsername) { // redirect to login page window.location.href = "/login"; // // // document.getElementById("settings-panel").style // .display = "block"; // const username = document.getElementById("username"); // username.focus(); // username.selectionStart = // username.selectionEnd = // username.value.length; } } catch (error) { console.error("Error checking username status:", error); } } async function getCurrentUsername() { try { const response = await fetch("/username/status"); const data = await response.json(); return data.username; } catch (error) { console.error("Error getting username:", error); } } async function updateCurrentUser() { try { const response = await fetch("/username/status"); const data = await response.json(); const userDiv = document.getElementById("current-user"); if (data.hasUsername) { userDiv.textContent = data.username; } else { userDiv.textContent = ""; } } catch (error) { console.error("Error getting username:", error); } } async function setUsername() { const username = document.getElementById("username").value; if (!username) { showUsernameStatus("Please enter a username", "red"); return; } try { const response = await fetch("/username", { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ username: username }), }); const data = await response.json(); if (response.ok) { showUsernameStatus( "Username set successfully!", "green", ); updateCurrentUser(); setTimeout(() => { document.getElementById("settings-panel").style .display = "none"; }, 750); updateMessagesInPlace(); } else { showUsernameStatus( data.error || "Failed to set username", "red", ); } } catch (error) { showUsernameStatus("Error connecting to server", "red"); } } let lastMessage = ""; async function sendMessage() { const messageInput = document.getElementById("message"); const message = messageInput.value; if (!message) { return; } try { lastMessage = message; const username = await getCurrentUsername(); const response = await fetch("/messages", { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ username: username, message: message }), }); const data = await response.json(); if (response.ok) { messageInput.value = ""; messageInput.style.height = "auto"; loadMessages(true); } else { showStatus( data.error || "Failed to send message", "red", ); } } catch (error) { showStatus("Error connecting to server", "red"); } } function showStatus(message, color) { const status = document.getElementById("status"); status.textContent = message; status.style.color = color; setTimeout(() => { status.textContent = ""; }, 3000); } function showUsernameStatus(message, color) { const status = document.getElementById("username-status"); status.textContent = message; status.style.color = color; setTimeout(() => { status.textContent = ""; }, 3000); } async function pingCheck() { try { await fetch("/ping", { method: "POST" }); } catch (error) { console.error("Ping failed:", error); } } async function timeZoneCheck() { try { const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; const response = await fetch("/timezone", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ timezone: timeZone }), }); const data = await response.json(); if (!response.ok) { console.error("Failed to set timezone:", data.error); } } catch (error) { console.error("Error checking timezone:", error); } } function initializeTheme() { const savedTheme = localStorage.getItem("theme"); if (savedTheme) { document.documentElement.setAttribute("data-theme", savedTheme); } else { // Check system preference if ( window.matchMedia && window.matchMedia("(prefers-color-scheme: light)") .matches ) { document.documentElement.setAttribute( "data-theme", "light", ); } else { document.documentElement.setAttribute( "data-theme", "dark", ); } } } document.addEventListener("keyup", function(event) { const inputPanel = document.getElementById("message"); if (inputPanel.contains(event.target) && event.key === "ArrowUp") { if (inputPanel.value === "") { inputPanel.value = lastMessage; } } }); let bottom = 0; async function initialize() { await checkUsername(); await updateCurrentUser(); await timeZoneCheck(); await loadMessages(true); initializePanels(); initializeTheme(); initializeSearchBox(); setInterval(loadMessages, 1000); setInterval(loadUsers, 1000); setInterval(pingCheck, 3000); setTimeout(scrollToBottom, 100); } function initializePanels() { usersPanel = document.getElementById("users-panel"); if (usersPanel) { usersPanel.style.display = "none"; } settingsPanel = document.getElementById("settings-panel"); if (settingsPanel) { settingsPanel.style.display = "none"; } } function initializeSearchBox() { const searchContainer = document.getElementById("searchContainer"); const searchButton = document.getElementById("searchButton"); const searchInput = document.getElementById("searchInput"); const searchCount = document.getElementById("searchCount"); let currentMatchIndex = -1; let matches = []; function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function clearHighlights() { // Remove all existing highlights const highlights = document.querySelectorAll( ".search-highlight", ); highlights.forEach((highlight) => { const parent = highlight.parentNode; parent.replaceChild( document.createTextNode(highlight.textContent), highlight, ); parent.normalize(); }); matches = []; currentMatchIndex = -1; searchCount.textContent = ""; } function findTextNodes(element, textNodes = []) { // Skip certain elements if (element.nodeType === Node.ELEMENT_NODE) { // Check if it's an element node first if ( element.tagName === "SCRIPT" || element.tagName === "STYLE" || element.tagName === "NOSCRIPT" || (element.classList && element.classList.contains( "search-container", )) || (element.classList && element.classList.contains( "search-highlight", )) ) { return textNodes; } } // Check if this node is a text node with non-whitespace content if ( element.nodeType === Node.TEXT_NODE && element.textContent.trim() ) { textNodes.push(element); } // Recursively check all child nodes const children = element.childNodes; for (let i = 0; i < children.length; i++) { findTextNodes(children[i], textNodes); } return textNodes; } function findAndHighlight(searchText) { if (!searchText) { clearHighlights(); return; } clearHighlights(); const searchRegex = new RegExp(escapeRegExp(searchText), "gi"); const textNodes = findTextNodes(document.body); textNodes.forEach((node) => { const matches = [ ...node.textContent.matchAll(searchRegex), ]; if (matches.length > 0) { const span = document.createElement("span"); span.innerHTML = node.textContent.replace( searchRegex, (match) => `${match}`, ); node.parentNode.replaceChild(span, node); } }); // Collect all highlights matches = Array.from( document.querySelectorAll(".search-highlight"), ); if (matches.length > 0) { currentMatchIndex = 0; matches[0].classList.add("current"); matches[0].scrollIntoView({ behavior: "smooth", block: "center", }); } // Update count searchCount.textContent = matches.length > 0 ? `${currentMatchIndex + 1}/${matches.length}` : "No matches"; } function nextMatch() { if (matches.length === 0) return; matches[currentMatchIndex].classList.remove("current"); currentMatchIndex = (currentMatchIndex + 1) % matches.length; matches[currentMatchIndex].classList.add("current"); matches[currentMatchIndex].scrollIntoView({ behavior: "smooth", block: "center", }); searchCount.textContent = `${currentMatchIndex + 1 }/${matches.length}`; } searchButton.addEventListener("click", () => { if (!searchContainer.classList.contains("expanded")) { searchContainer.classList.add("expanded"); searchInput.focus(); } else { nextMatch(); } }); searchInput.addEventListener("input", (e) => { findAndHighlight(e.target.value.trim()); }); searchInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { nextMatch(); } else if (e.key === "Escape") { searchContainer.classList.remove("expanded"); searchInput.value = ""; clearHighlights(); } }); // Debug function to check search coverage (call from console: checkSearchCoverage()) window.checkSearchCoverage = function() { const textNodes = findTextNodes(document.body); console.log( "Total searchable text nodes found:", textNodes.length, ); textNodes.forEach((node, i) => { console.log(`Node ${i + 1}:`, { text: node.textContent, parent: node.parentElement.tagName, path: getNodePath(node), }); }); }; function getNodePath(node) { const path = []; while (node && node.parentElement) { let index = Array.from(node.parentElement.childNodes) .indexOf(node); path.unshift(`${node.parentElement.tagName}[${index}]`); node = node.parentElement; } return path.join(" > "); } // Close search when clicking outside document.addEventListener("click", (e) => { if (!searchContainer.contains(e.target)) { searchContainer.classList.remove("expanded"); searchInput.value = ""; clearHighlights(); } }); } initialize();