Add lightweight web control panel
This commit is contained in:
+137
@@ -0,0 +1,137 @@
|
||||
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} сообщений`;
|
||||
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} сообщений, последнее: ${channel.last_date || "—"}`
|
||||
: "База данных для канала пока не найдена.";
|
||||
|
||||
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 = `Ответ на сообщение #${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 = "Открыть файл";
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user