Files
coms/client/public/render.js
2026-03-11 01:20:31 -04:00

99 lines
3.2 KiB
JavaScript

// this one sucked.
import { getThreads, getRootIds, getReplyTo, getFocused } from "./data.js";
// Render the chat history and input container into the provided chat element.
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 = `<span class="ts" title="${fullTimestamp}">${shortTimestamp}</span>`;
// Distinguish notices and normal messages.
if (msg.username) {
div.classList.add("msg");
if (getFocused() === id) div.classList.add("focused");
div.innerHTML = `${tsSpan} <span class="name">${new Option(msg.username).innerHTML}:</span> <span class="msg_content">${new Option(msg.content).innerHTML}</span>`;
} else {
div.classList.add("notice");
div.innerHTML = `${tsSpan} ${msg.content}`;
}
// Append to chat and render the 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. Or at least try. Let the browser know that is our
// intention. May or may not actually work.
chatEl.scrollTop = chatEl.scrollHeight;
const input = inputContainer.querySelector("input");
if (input) {
input.focus();
if (replyTo === null) inputContainer.style.marginLeft = "0";
}
}