739 lines
18 KiB
JavaScript
739 lines
18 KiB
JavaScript
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);
|
|
}
|
|
|
|
async function editMessage(messageId, content) {
|
|
const newContent = prompt("Edit message:", content);
|
|
if (newContent === null) {
|
|
return;
|
|
}
|
|
if (newContent === "") {
|
|
deleteMessage(messageId);
|
|
return;
|
|
}
|
|
if (newContent === content) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch("/messages", {
|
|
method: "PATCH",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
messageId: messageId,
|
|
messageContent: newContent,
|
|
}),
|
|
});
|
|
|
|
if (response.ok) {
|
|
updateMessagesInPlace();
|
|
} else {
|
|
console.error("Failed to edit message");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error editing message:", error);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
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(
|
|
"<br>",
|
|
);
|
|
|
|
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;
|
|
console.log(contentString);
|
|
|
|
if (
|
|
compareUsername ===
|
|
document.getElementById(
|
|
"current-user",
|
|
).textContent
|
|
) {
|
|
deleteHtml =
|
|
`<button class="delete-button" title="Delete message" onclick="deleteMessage('${messageId}')" style="display: inline;">🗑️</button>`;
|
|
editHtml =
|
|
`<button class="edit-button" title="Edit message" onclick="editMessage('${messageId}', '${contentString}')" style="display: inline;">📝</button>`;
|
|
}
|
|
messageDiv.innerHTML = `
|
|
<div class="message-header">
|
|
<div class="username">${username} ${timestamp} ${deleteHtml} ${editHtml}</div>
|
|
</div>
|
|
<div class="content">${embeddedContent}</div>`;
|
|
|
|
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 `<div class="video-embed"><iframe
|
|
width="100%"
|
|
height="315"
|
|
src="https://www.youtube.com/embed/${videoId}"
|
|
frameborder="0"
|
|
onerror="console.log('Video failed to load:', this.src); this.style.display='none'"
|
|
onload="console.log('Video loaded successfully:', this.src)"
|
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
allowfullscreen></iframe></div>`;
|
|
} else if (isImageUrl(url)) {
|
|
return `<div class="image-embed"><img
|
|
src="${url}"
|
|
alt="Embedded image"
|
|
loading="lazy"
|
|
onerror="console.log('Image failed to load:', this.src); this.style.display='none'"
|
|
onload="console.log('Image loaded successfully:', this.src)"></div>`;
|
|
}
|
|
return `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`;
|
|
},
|
|
);
|
|
}
|
|
|
|
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) {
|
|
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 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 response = await fetch("/messages", {
|
|
method: "PUT",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ 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() {
|
|
usersPanel = document.getElementById("users-panel");
|
|
if (usersPanel) {
|
|
usersPanel.style.display = "none";
|
|
}
|
|
settingsPanel = document.getElementById("settings-panel");
|
|
if (settingsPanel) {
|
|
settingsPanel.style.display = "none";
|
|
}
|
|
initializeTheme();
|
|
await checkUsername();
|
|
await updateCurrentUser();
|
|
await timeZoneCheck();
|
|
setInterval(loadMessages, 1000);
|
|
setInterval(loadUsers, 1000);
|
|
setInterval(pingCheck, 3000);
|
|
await loadMessages(true);
|
|
initializeSearchBox();
|
|
}
|
|
|
|
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) =>
|
|
`<span class="search-highlight">${match}</span>`,
|
|
);
|
|
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();
|