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
+74 -12
View File
@@ -1,6 +1,8 @@
if ("scrollRestoration" in history) history.scrollRestoration = "manual";
const viewerState = {
accountId: null,
accounts: [],
channels: [],
channelId: null,
oldestMessageId: null,
@@ -42,6 +44,24 @@ function formatTime(dateStr) {
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) {
if (!dateStr) return "";
return dateStr.slice(0, 10);
@@ -75,8 +95,14 @@ function renderChannelList() {
if (!template) return;
viewerState.channels.forEach((channel) => {
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-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) {
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) {
const template = document.getElementById("message-template");
const wrap = template.content.firstElementChild.cloneNode(true);
@@ -240,9 +310,7 @@ async function loadChannel(channelId, append = false) {
? `&before=${viewerState.oldestMessageId}`
: "";
try {
const payload = await api(
`/api/channels/${encodeURIComponent(channelId)}/messages?limit=80${before}`
);
const payload = await api(messageEndpoint(channelId, before));
if (append) {
const list = document.getElementById("messages-list");
const prevScrollHeight = list.scrollHeight;
@@ -285,15 +353,9 @@ function setupInfiniteScroll() {
}
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);
await loadViewerAccount(params);
viewerState.channels = await api(channelsEndpoint());
viewerState.channelId =
params.get("channel") ||
viewerState.channels[0]?.channel_id ||