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:
+13
-3
@@ -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-name"></span>
|
<span class="viewer-channel-avatar"></span>
|
||||||
<span class="viewer-channel-meta"></span>
|
<span class="viewer-channel-copy">
|
||||||
|
<span class="viewer-channel-row">
|
||||||
|
<span class="viewer-channel-name"></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
@@ -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 ||
|
||||||
|
|||||||
Reference in New Issue
Block a user