const sendButton = document.querySelector("button#send"); const username = new URLSearchParams(window.location.search).get("u") || "Anonymous"; const isStreamMode = new URLSearchParams(window.location.search).get("stream") === "1"; if (isStreamMode) { document.body.classList.add("stream-mode"); } sendButton.addEventListener("click", send); let cooldownTime = 0; let cooldownInterval = null; function send() { if (cooldownTime > 0) return; const messageText = window.getCurrentText ? window.getCurrentText() : ""; let data = canvas.toDataURL("image/png"); // Crop white space top and bottom if there's no text preventing it if (!messageText || messageText.trim() === "") { const ctx = canvas.getContext("2d"); const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data32 = new Uint32Array(imgData.data.buffer); let top = canvas.height; let bottom = -1; for (let y = 0; y < canvas.height; y++) { let rowHasPixel = false; for (let x = 0; x < canvas.width; x++) { const p = data32[y * canvas.width + x]; // ABGR: 0xFFFFFFFF is white. 0x00000000 is transparent. if (p !== 0xFFFFFFFF && p !== 0) { rowHasPixel = true; break; } } if (rowHasPixel) { if (y < top) top = y; if (y > bottom) bottom = y; } } if (top <= bottom) { // Add a little padding const padding = 10; top = Math.max(0, top - padding); bottom = Math.min(canvas.height - 1, bottom + padding); const cropHeight = bottom - top + 1; const tempCanvas = document.createElement("canvas"); tempCanvas.width = canvas.width; tempCanvas.height = cropHeight; const tempCtx = tempCanvas.getContext("2d"); tempCtx.putImageData(ctx.getImageData(0, top, canvas.width, cropHeight), 0, 0); data = tempCanvas.toDataURL("image/png"); } else { // Canvas is entirely white/blank const tempCanvas = document.createElement("canvas"); tempCanvas.width = canvas.width; tempCanvas.height = 20; // extremely minimal height const tempCtx = tempCanvas.getContext("2d"); tempCtx.fillStyle = "white"; tempCtx.fillRect(0, 0, canvas.width, 20); data = tempCanvas.toDataURL("image/png"); } } const payload = { from: username, text: messageText, doodle: data, color: userColor, timestamp: new Date().toISOString(), }; fetch("/api/messages", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(payload), }).then(response => { if (response.status === 401) { window.location.href = "/"; } }); // clear canvas after sending if (window.resetCanvas) { window.resetCanvas(); } else { canvas.width = canvas.width; window.clearCurrentText(); } // start cooldown cooldownTime = 60; sendButton.disabled = true; sendButton.innerHTML = `wait ${cooldownTime}s`; cooldownInterval = setInterval(() => { cooldownTime--; if (cooldownTime <= 0) { clearInterval(cooldownInterval); sendButton.disabled = false; sendButton.innerHTML = "send"; } else { sendButton.innerHTML = `wait ${cooldownTime}s`; } }, 1000); } function addToChatLog(msg) { const chatLog = document.getElementById("chat-log"); const messageBox = document.createElement("div"); messageBox.className = "message-box"; if (msg.color && msg.color.startsWith('#')) { messageBox.style.borderColor = msg.color; messageBox.style.boxShadow = `0 4px 12px ${msg.color}33`; } else { messageBox.classList.add(`accent-${msg.color}`); } const messageHeader = document.createElement("div"); messageHeader.className = "message-header"; if (msg.color && msg.color.startsWith('#')) { messageHeader.style.color = msg.color; messageHeader.style.borderBottomColor = msg.color; } const usernameSpan = document.createElement("span"); usernameSpan.className = "username"; usernameSpan.textContent = msg.from; messageHeader.appendChild(usernameSpan); messageBox.appendChild(messageHeader); const canvasDiv = document.createElement("div"); canvasDiv.className = "chat-canvas"; const messageTextSpan = document.createElement("span"); messageTextSpan.className = "message-text"; messageTextSpan.innerHTML = msg.text.replace(/\n/g, "
"); canvasDiv.appendChild(messageTextSpan); if (msg.doodle) { const img = document.createElement("img"); img.src = msg.doodle; canvasDiv.appendChild(img); } messageBox.appendChild(canvasDiv); animatePreviousMessages(chatLog, false); chatLog.insertBefore(messageBox, chatLog.firstChild); } function addSystemMessageToChatLog(content) { const chatLog = document.getElementById("chat-log"); const messageBox = document.createElement("div"); messageBox.className = "system-message-box"; messageBox.innerHTML = content; animatePreviousMessages(chatLog, true); chatLog.insertBefore(messageBox, chatLog.firstChild); } function animatePreviousMessages(chatLog, newIsSystem) { const previousMessages = chatLog.querySelectorAll( ".message-box, .system-message-box" ); previousMessages.forEach((message) => { const prevIsSystem = message.classList.contains("system-message-box"); const startTranslate = prevIsSystem ? newIsSystem ? "translateY(calc(100% + 5px))" : "translateY(calc(500% + 5px))" : newIsSystem ? "translateY(calc(20% + 5px))" : "translateY(calc(100% + 5px))"; message.animate( [{ transform: startTranslate }, { transform: "translateY(0%)" }], { duration: 100, easing: "linear", fill: "forwards", } ); }); } // run when a new message is received from the server function handleMessage(msg) { const data = JSON.parse(msg); if (data.type === "system") { addSystemMessageToChatLog(data.content); return; } addToChatLog(data); } // listens for new messages from the server async function listen() { try { const url = isStreamMode ? "/api/messages?stream=1" : "/api/messages"; const response = await fetch(url); if (response.status === 401 && !isStreamMode) { window.location.href = "/"; return; } if (response.status === 200) { const msg = await response.text(); handleMessage(msg); } } catch (err) { console.error("error listening messages:", err); } finally { // goes back to listening just after receiving a message or on error setTimeout(listen, 50); // small delay to avoid stack overflow } } // load backlog first, then start listening async function loadBacklog() { try { const url = isStreamMode ? "/api/backlog?stream=1" : "/api/backlog"; const response = await fetch(url); if (response.status === 401 && !isStreamMode) { window.location.href = "/"; return; } if (response.status === 200) { const messages = await response.json(); // append chronologically for (const msg of messages) { if (msg.type === "system") { addSystemMessageToChatLog(msg.content); } else { addToChatLog(msg); } } } } catch (err) { console.error("error loading backlog:", err); } } // start listening for messages right after loading the app backlog loadBacklog().then(() => listen()); // sets the username label in the user message box const usernameLabel = document.querySelector( "#user-message-box > .message-header > .username" ); usernameLabel.innerHTML = username;