const viewerState = { channels: [], channelId: null, oldestMessageId: null, }; async function api(path) { const response = await fetch(path); const data = await response.json(); if (!response.ok) { throw new Error(data.error || "Request failed"); } return data; } function textNodeWithBreaks(text) { const fragment = document.createDocumentFragment(); const parts = (text || "").split("\n"); parts.forEach((part, index) => { if (index > 0) fragment.appendChild(document.createElement("br")); fragment.appendChild(document.createTextNode(part)); }); return fragment; } function renderChannelList() { const root = document.getElementById("viewer-channel-list"); root.innerHTML = ""; const template = document.getElementById("viewer-channel-template"); viewerState.channels.forEach((channel) => { const node = template.content.firstElementChild.cloneNode(true); node.querySelector(".viewer-channel-name").textContent = channel.name; node.querySelector(".viewer-channel-meta").textContent = `${channel.message_count} messages`; if (channel.channel_id === viewerState.channelId) { node.classList.add("active"); } node.addEventListener("click", () => { viewerState.oldestMessageId = null; loadChannel(channel.channel_id); }); root.appendChild(node); }); } function renderMessages(payload, append = false) { const root = document.getElementById("messages-list"); const template = document.getElementById("message-template"); if (!append) root.innerHTML = ""; const channel = payload.channel; document.getElementById("viewer-title").textContent = channel?.name || payload.channel_id; document.getElementById("viewer-subtitle").textContent = channel ? `${channel.message_count} messages, latest: ${channel.last_date || "-"}` : "No local database found for this channel yet."; payload.messages.forEach((message) => { const node = template.content.firstElementChild.cloneNode(true); node.dataset.messageId = String(message.message_id); node.querySelector(".message-author").textContent = message.sender_name || "Unknown"; node.querySelector(".message-date").textContent = message.date || ""; const reply = node.querySelector(".message-reply"); if (message.reply_to) { reply.textContent = `Reply to message #${message.reply_to}`; } node.querySelector(".message-text").appendChild(textNodeWithBreaks(message.text || "")); const media = node.querySelector(".message-media"); if (message.media_url && message.media_kind === "image") { const img = document.createElement("img"); img.src = message.media_url; img.loading = "lazy"; media.appendChild(img); } else if (message.media_url && message.media_kind === "video") { const video = document.createElement("video"); video.src = message.media_url; video.controls = true; video.preload = "metadata"; media.appendChild(video); } else if (message.media_url) { const link = document.createElement("a"); link.href = message.media_url; link.target = "_blank"; link.rel = "noreferrer"; link.textContent = "Open file"; media.appendChild(link); } const footerParts = []; if (message.views) footerParts.push(`views: ${message.views}`); if (message.forwards) footerParts.push(`forwards: ${message.forwards}`); if (message.reactions) footerParts.push(`reactions: ${message.reactions}`); node.querySelector(".message-footer").textContent = footerParts.join(" | "); if (append) { root.prepend(node); } else { root.appendChild(node); } }); if (payload.messages.length) { viewerState.oldestMessageId = payload.messages[0].message_id; } document.getElementById("load-older-btn").disabled = !payload.messages.length; } async function loadChannel(channelId, append = false) { viewerState.channelId = channelId; renderChannelList(); const before = append && viewerState.oldestMessageId ? `&before=${viewerState.oldestMessageId}` : ""; const payload = await api(`/api/channels/${encodeURIComponent(channelId)}/messages?limit=80${before}`); renderMessages(payload, append); } async function main() { viewerState.channels = await api("/api/channels"); const params = new URLSearchParams(window.location.search); viewerState.channelId = params.get("channel") || viewerState.channels[0]?.channel_id || null; renderChannelList(); if (viewerState.channelId) { await loadChannel(viewerState.channelId); } document.getElementById("load-older-btn").addEventListener("click", async () => { if (!viewerState.channelId || !viewerState.oldestMessageId) return; await loadChannel(viewerState.channelId, true); }); } main().catch((error) => { console.error(error); alert(error.message); });