feat: add multi-account message viewer support

- viewer.html shows account name in header
- viewer.js loads account list from /api/accounts, auto-selects first account
- Account-aware API endpoints (/api/accounts/{id}/... or legacy /api/...)
- Chat list redesign with avatar initials, preview text, time, message count
- loadViewerAccount() helper fetches account info and user ID
This commit is contained in:
2026-06-27 15:20:56 +02:00
parent 62a6a492e0
commit eb69a89d91
2 changed files with 87 additions and 15 deletions
+12 -2
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Telegram Scraper Viewer</title> <title>Telegram Scraper Viewer</title>
<link rel="stylesheet" href="/static/style.css?v=2" /> <link rel="stylesheet" href="/static/style.css?v=4" />
</head> </head>
<body> <body>
<div class="viewer-shell"> <div class="viewer-shell">
@@ -13,6 +13,7 @@
<div> <div>
<div class="eyebrow">Export Viewer</div> <div class="eyebrow">Export Viewer</div>
<h1>Messages</h1> <h1>Messages</h1>
<p class="muted small">Account: <span id="viewer-account-name">Loading...</span></p>
</div> </div>
<button id="sidebar-toggle" class="button button-small sidebar-toggle"></button> <button id="sidebar-toggle" class="button button-small sidebar-toggle"></button>
<a class="button button-small" href="/">Dashboard</a> <a class="button button-small" href="/">Dashboard</a>
@@ -36,8 +37,17 @@
<template id="viewer-channel-template"> <template id="viewer-channel-template">
<button class="viewer-channel-item"> <button class="viewer-channel-item">
<span class="viewer-channel-avatar"></span>
<span class="viewer-channel-copy">
<span class="viewer-channel-row">
<span class="viewer-channel-name"></span> <span class="viewer-channel-name"></span>
<span class="viewer-channel-meta"></span> <span class="viewer-channel-time"></span>
</span>
<span class="viewer-channel-row">
<span class="viewer-channel-preview"></span>
<span class="viewer-channel-count"></span>
</span>
</span>
</button> </button>
</template> </template>
+74 -12
View File
@@ -1,6 +1,8 @@
if ("scrollRestoration" in history) history.scrollRestoration = "manual"; if ("scrollRestoration" in history) history.scrollRestoration = "manual";
const viewerState = { const viewerState = {
accountId: null,
accounts: [],
channels: [], channels: [],
channelId: null, channelId: null,
oldestMessageId: null, oldestMessageId: null,
@@ -42,6 +44,24 @@ function formatTime(dateStr) {
return d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }); return d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" });
} }
function chatListTime(dateStr) {
if (!dateStr) return "";
const d = new Date(dateStr.replace(" ", "T"));
if (isNaN(d.getTime())) return "";
const now = new Date();
if (d.toDateString() === now.toDateString()) {
return d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" });
}
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}
function initials(value) {
const source = String(value || "?").replace(/^@/, "").trim();
const words = source.split(/\s+/).filter(Boolean);
if (words.length > 1) return (words[0][0] + words[1][0]).toUpperCase();
return source.slice(0, 2).toUpperCase();
}
function dateKey(dateStr) { function dateKey(dateStr) {
if (!dateStr) return ""; if (!dateStr) return "";
return dateStr.slice(0, 10); return dateStr.slice(0, 10);
@@ -75,8 +95,14 @@ function renderChannelList() {
if (!template) return; if (!template) return;
viewerState.channels.forEach((channel) => { viewerState.channels.forEach((channel) => {
const node = template.content.firstElementChild.cloneNode(true); const node = template.content.firstElementChild.cloneNode(true);
const preview =
channel.last_message_preview ||
(channel.has_database ? "No text in the last saved message" : "No local database yet");
node.querySelector(".viewer-channel-avatar").textContent = initials(channel.name || channel.channel_id);
node.querySelector(".viewer-channel-name").textContent = channel.name; node.querySelector(".viewer-channel-name").textContent = channel.name;
node.querySelector(".viewer-channel-meta").textContent = `${channel.message_count} messages`; node.querySelector(".viewer-channel-time").textContent = chatListTime(channel.last_date);
node.querySelector(".viewer-channel-preview").textContent = preview;
node.querySelector(".viewer-channel-count").textContent = String(channel.message_count || 0);
if (channel.channel_id === viewerState.channelId) { if (channel.channel_id === viewerState.channelId) {
node.classList.add("active"); node.classList.add("active");
} }
@@ -90,6 +116,50 @@ function renderChannelList() {
}); });
} }
function messageEndpoint(channelId, before = "") {
if (viewerState.accountId) {
return `/api/accounts/${encodeURIComponent(viewerState.accountId)}/channels/${encodeURIComponent(channelId)}/messages?limit=80${before}`;
}
return `/api/channels/${encodeURIComponent(channelId)}/messages?limit=80${before}`;
}
function channelsEndpoint() {
if (viewerState.accountId) {
return `/api/accounts/${encodeURIComponent(viewerState.accountId)}/channels`;
}
return "/api/channels";
}
async function loadViewerAccount(params) {
const requested = params.get("account");
try {
const payload = await api("/api/accounts");
viewerState.accounts = payload.accounts || [];
} catch {
viewerState.accounts = [];
}
viewerState.accountId =
requested ||
viewerState.accounts[0]?.id ||
null;
const accountName =
viewerState.accounts.find((item) => item.id === viewerState.accountId)?.label ||
viewerState.accountId ||
"Legacy";
const accountEl = document.getElementById("viewer-account-name");
if (accountEl) accountEl.textContent = accountName;
try {
const authData = viewerState.accountId
? await api(`/api/accounts/${encodeURIComponent(viewerState.accountId)}/auth`)
: await api("/api/auth");
viewerState.userId = authData.user_id || null;
} catch (e) {
console.warn("Could not fetch user ID:", e);
}
}
function buildMessageNode(message, root) { function buildMessageNode(message, root) {
const template = document.getElementById("message-template"); const template = document.getElementById("message-template");
const wrap = template.content.firstElementChild.cloneNode(true); const wrap = template.content.firstElementChild.cloneNode(true);
@@ -240,9 +310,7 @@ async function loadChannel(channelId, append = false) {
? `&before=${viewerState.oldestMessageId}` ? `&before=${viewerState.oldestMessageId}`
: ""; : "";
try { try {
const payload = await api( const payload = await api(messageEndpoint(channelId, before));
`/api/channels/${encodeURIComponent(channelId)}/messages?limit=80${before}`
);
if (append) { if (append) {
const list = document.getElementById("messages-list"); const list = document.getElementById("messages-list");
const prevScrollHeight = list.scrollHeight; const prevScrollHeight = list.scrollHeight;
@@ -285,15 +353,9 @@ function setupInfiniteScroll() {
} }
async function main() { async function main() {
try {
const authData = await api("/api/auth");
viewerState.userId = authData.user_id || null;
} catch (e) {
console.warn("Could not fetch user ID:", e);
}
viewerState.channels = await api("/api/channels");
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
await loadViewerAccount(params);
viewerState.channels = await api(channelsEndpoint());
viewerState.channelId = viewerState.channelId =
params.get("channel") || params.get("channel") ||
viewerState.channels[0]?.channel_id || viewerState.channels[0]?.channel_id ||