// Render module: handles chat UI rendering of messages and notices import { getThreads, getRootIds, getReplyTo, getFocused } from "./data.js"; /** * Render the chat history and input container into the provided chat element. * @param {HTMLElement} chatEl - The container element for chat entries. * @param {HTMLElement} inputContainer - The message input area to append at bottom. */ export function renderChat(chatEl, inputContainer) { // Clear existing content. chatEl.innerHTML = ""; const threads = getThreads(); const rootIds = getRootIds(); const replyTo = getReplyTo(); let replyDepth = 0; if (replyTo !== null) { let cur = threads.get(replyTo); while (cur && cur.parent != null) { replyDepth++; cur = threads.get(cur.parent); } } /** * Recursively render a thread node and its children. * @param {number} id - The message or notice ID. * @param {number} depth - Nesting level (for indentation). */ function renderNode(id, depth = 0) { const msg = threads.get(id); if (!msg) return; // Create wrapper div. const div = document.createElement("div"); div.dataset.id = id; div.style.marginLeft = `${depth}em`; // Format timestamp. const date = new Date(msg.ts * 1000); let h = date.getHours(); const ampm = h >= 12 ? "PM" : "AM"; h = h % 12 || 12; let m = date.getMinutes(); if (m < 10) m = "0" + m; const fullTimestamp = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()} ` + `${h}:${m} ${ampm}`; const shortTimestamp = `${h}:${m} ${ampm}`; const tsSpan = `${shortTimestamp}`; // Distinguish notice vs normal message. if (msg.username) { div.classList.add("msg"); if (getFocused() === id) div.classList.add("focused"); div.innerHTML = `${tsSpan} ${new Option(msg.username).innerHTML}: ${new Option(msg.content).innerHTML}`; } else { div.classList.add("notice"); div.innerHTML = `${tsSpan} ${msg.content}`; } // Append to chat and render children. chatEl.appendChild(div); msg.children.forEach((childId) => renderNode(childId, depth + 1)); } // Render all root-level nodes in order. rootIds.forEach((id) => renderNode(id)); // Append input container at appropriate thread level. { const indent = replyTo !== null ? replyDepth + 1 : 0; inputContainer.style.paddingLeft = `${indent}em`; // Place input under the last descendant if replying, else at bottom. let refDiv = null; if (replyTo !== null) { function lastDesc(id) { const node = threads.get(id); return !node || node.children.length === 0 ? id : lastDesc(node.children[node.children.length - 1]); } const lastId = lastDesc(replyTo); refDiv = chatEl.querySelector(`div[data-id="${lastId}"]`); } if (refDiv) { refDiv.insertAdjacentElement("afterend", inputContainer); } else { chatEl.appendChild(inputContainer); } } // Scroll to bottom. chatEl.scrollTop = chatEl.scrollHeight; const input = inputContainer.querySelector("input"); if (input) { input.focus(); // Ensure inputContainer margin resets if at root. if (replyTo === null) { inputContainer.style.marginLeft = "0"; } } }