// 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";
}
}
}