Redesign message viewer as Telegram-style chat with bubbles, replies, infinite scroll, and orientation

- Chat bubbles with 1px border-radius, angular tail arrows
- Own messages right (blue), others left (dark) via sender_id vs user_id
- Reply preview with left border accent, clickable scroll to original
- Infinite scroll up via IntersectionObserver on scroll-sentinel
- Date separators (Today/Yesterday/date) between messages
- Full-height messages panel with sidebar (channels) + header
- Backend: reply_to_message data in load_messages(), user_id exposed in auth snapshot
- Auto-scroll to bottom on channel load (triple scrollTop fallback)
- history.scrollRestoration = manual to prevent mid-page jumps
This commit is contained in:
2026-05-27 01:01:18 +02:00
parent 1dfe41950e
commit c5361f2271
4 changed files with 532 additions and 106 deletions
+245 -37
View File
@@ -46,35 +46,25 @@ button {
font: inherit; font: inherit;
} }
.app-shell, .app-shell {
.viewer-shell {
min-height: 100vh; min-height: 100vh;
display: grid; display: grid;
}
.app-shell,
.viewer-shell {
grid-template-columns: 300px minmax(0, 1fr); grid-template-columns: 300px minmax(0, 1fr);
} }
.sidebar, .sidebar {
.viewer-sidebar {
background: var(--panel-alt); background: var(--panel-alt);
border-right: 1px dashed var(--line); border-right: 1px dashed var(--line);
padding: 28px 24px; padding: 28px 24px;
} }
.content, .content {
.viewer-main {
padding: 28px; padding: 28px;
max-width: 1320px; max-width: 1320px;
width: 100%; width: 100%;
} }
.brand-block h1, .brand-block h1,
.viewer-sidebar-head h1,
.viewer-header h2,
.panel-header h2,
.dialog-header h2 { .dialog-header h2 {
margin: 0; margin: 0;
color: var(--accent); color: var(--accent);
@@ -105,8 +95,7 @@ button {
.nav-links, .nav-links,
.jobs-list, .jobs-list,
.continuous-form, .continuous-form {
.viewer-channel-list {
display: grid; display: grid;
gap: 10px; gap: 10px;
} }
@@ -227,10 +216,7 @@ input,
} }
.panel-header, .panel-header,
.viewer-sidebar-head,
.viewer-header,
.dialog-header, .dialog-header,
.message-meta,
.job-head { .job-head {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
@@ -489,6 +475,45 @@ tbody tr:hover {
display: none !important; display: none !important;
} }
/* ---------- Viewer layout: sidebar + chat ---------- */
.viewer-shell {
display: grid;
grid-template-columns: 300px minmax(0, 1fr);
height: 100vh;
overflow: hidden;
}
.viewer-sidebar {
background: var(--panel-alt);
border-right: 1px dashed var(--line);
padding: 28px 24px;
display: flex;
flex-direction: column;
gap: 20px;
overflow-y: auto;
}
.viewer-sidebar-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.viewer-sidebar-head h1 {
margin: 0;
color: var(--accent);
font-size: 1.45rem;
text-transform: uppercase;
letter-spacing: 0;
}
.viewer-channel-list {
display: grid;
gap: 10px;
}
.viewer-channel-item { .viewer-channel-item {
display: grid; display: grid;
gap: 4px; gap: 4px;
@@ -496,6 +521,9 @@ tbody tr:hover {
padding: 12px; padding: 12px;
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
border: 1px solid var(--line);
background: var(--panel);
color: var(--text-color);
} }
.viewer-channel-item.active { .viewer-channel-item.active {
@@ -503,52 +531,232 @@ tbody tr:hover {
box-shadow: inset 3px 0 0 var(--accent); box-shadow: inset 3px 0 0 var(--accent);
} }
.viewer-channel-name, .viewer-channel-name {
.message-author {
color: var(--accent); color: var(--accent);
font-weight: 700; font-weight: 700;
} }
.viewer-channel-meta, .viewer-channel-meta {
.message-date {
color: var(--dim); color: var(--dim);
font-size: 0.82rem; font-size: 0.82rem;
} }
/* ---------- Main panel ---------- */
.viewer-main {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
.viewer-header {
flex-shrink: 0;
padding: 20px 28px;
border-bottom: 1px dashed var(--line);
background: var(--panel-alt);
}
.viewer-header h2 {
margin: 0;
color: var(--accent);
font-size: 1.45rem;
text-transform: uppercase;
letter-spacing: 0;
}
.messages-list { .messages-list {
display: grid; flex: 1;
overflow-y: auto;
padding: 12px 28px 20px;
display: flex;
flex-direction: column;
}
.scroll-sentinel {
flex-shrink: 0;
height: 1px;
width: 100%;
}
/* ---------- Date separator ---------- */
.chat-date-sep {
display: flex;
align-items: center;
justify-content: center;
gap: 12px; gap: 12px;
align-content: start; margin: 12px 0;
font-size: 0.78rem;
color: var(--dim);
text-transform: uppercase;
letter-spacing: 0.03em;
flex-shrink: 0;
} }
.message-card { .chat-date-sep::before,
max-width: 820px; .chat-date-sep::after {
content: "";
flex: 1;
height: 1px;
background: var(--line);
} }
.message-reply:empty, .chat-date-sep:empty {
.message-text:empty,
.message-footer:empty,
.message-media:empty {
display: none; display: none;
} }
.message-text { /* ---------- Bubble wrap ---------- */
.chat-bubble-wrap {
display: flex;
justify-content: flex-start;
flex-shrink: 0;
}
.chat-bubble-wrap--own {
justify-content: flex-end;
}
/* ---------- Chat bubble ---------- */
.chat-bubble {
max-width: 75%;
padding: 7px 11px;
background: #1c1c1c;
border: 1px solid var(--line);
border-radius: 1px;
margin-bottom: 4px;
overflow-wrap: anywhere;
word-break: break-word;
position: relative;
}
.chat-bubble::before {
content: "";
position: absolute;
top: 8px;
border: 6px solid transparent;
}
.chat-bubble-wrap:not(.chat-bubble-wrap--own) .chat-bubble::before {
left: -12px;
border-right-color: #1c1c1c;
border-left: 0;
}
.chat-bubble-wrap--own .chat-bubble::before {
right: -12px;
border-left-color: #1e3a5f;
border-right: 0;
}
.chat-bubble-wrap--own .chat-bubble {
background: #1e3a5f;
border-color: #2b5278;
}
.chat-bubble--highlight {
animation: chat-highlight 0.5s ease-in-out 3;
}
@keyframes chat-highlight {
0%, 100% { box-shadow: 0 0 0 0 rgba(139, 180, 248, 0); }
50% { box-shadow: 0 0 0 3px rgba(139, 180, 248, 0.4); }
}
/* ---------- Sender ---------- */
.chat-sender {
font-size: 0.82rem;
color: var(--info);
font-weight: 700;
margin-bottom: 2px;
}
.chat-sender:empty {
display: none;
}
/* ---------- Reply preview ---------- */
.chat-reply {
display: flex;
gap: 6px;
margin-bottom: 4px;
padding: 4px 0;
cursor: pointer;
}
.chat-reply.hidden {
display: none;
}
.chat-reply-border {
flex-shrink: 0;
width: 2px;
background: var(--accent);
border-radius: 1px;
opacity: 0.5;
}
.chat-reply-body {
min-width: 0;
}
.chat-reply-author {
font-size: 0.78rem;
color: var(--info);
font-weight: 700;
}
.chat-reply-text {
font-size: 0.82rem;
color: var(--dim);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ---------- Message text ---------- */
.chat-text {
white-space: pre-wrap; white-space: pre-wrap;
line-height: 1.45;
} }
.message-media { .chat-text:empty {
margin-top: 12px; display: none;
} }
.message-media img, /* ---------- Media ---------- */
.message-media video {
.chat-media {
margin-top: 6px;
}
.chat-media:empty {
display: none;
}
.chat-media img,
.chat-media video {
display: block; display: block;
max-width: 100%; max-width: 100%;
border: 1px solid var(--line); border: 1px solid var(--line);
} }
.message-footer { /* ---------- Footer ---------- */
margin-top: 10px;
.chat-footer {
margin-top: 4px;
font-size: 0.72rem;
color: var(--dim);
text-align: right;
}
.chat-footer:empty {
display: none;
} }
.api-docs { .api-docs {
+18 -14
View File
@@ -6,7 +6,7 @@
<title>Telegram Scraper Viewer</title> <title>Telegram Scraper Viewer</title>
<link rel="stylesheet" href="/static/style.css" /> <link rel="stylesheet" href="/static/style.css" />
</head> </head>
<body class="viewer-body"> <body>
<div class="viewer-shell"> <div class="viewer-shell">
<aside class="viewer-sidebar"> <aside class="viewer-sidebar">
<div class="viewer-sidebar-head"> <div class="viewer-sidebar-head">
@@ -25,12 +25,11 @@
<h2 id="viewer-title">Select a channel</h2> <h2 id="viewer-title">Select a channel</h2>
<p id="viewer-subtitle" class="muted">Reading messages from the local SQLite database.</p> <p id="viewer-subtitle" class="muted">Reading messages from the local SQLite database.</p>
</div> </div>
<div class="viewer-actions">
<button class="button" id="load-older-btn" disabled>Load older</button>
</div>
</header> </header>
<section id="messages-list" class="messages-list"></section> <section id="messages-list" class="messages-list">
<div id="scroll-sentinel" class="scroll-sentinel"></div>
</section>
</main> </main>
</div> </div>
@@ -42,16 +41,21 @@
</template> </template>
<template id="message-template"> <template id="message-template">
<article class="message-card"> <div class="chat-bubble-wrap">
<div class="message-meta"> <div class="chat-bubble">
<span class="message-author"></span> <div class="chat-sender"></div>
<span class="message-date"></span> <div class="chat-reply">
<div class="chat-reply-border"></div>
<div class="chat-reply-body">
<div class="chat-reply-author"></div>
<div class="chat-reply-text"></div>
</div>
</div>
<div class="chat-text"></div>
<div class="chat-media"></div>
<div class="chat-footer"></div>
</div> </div>
<div class="message-reply muted small"></div> </div>
<div class="message-text"></div>
<div class="message-media"></div>
<div class="message-footer muted small"></div>
</article>
</template> </template>
<script src="/static/viewer.js"></script> <script src="/static/viewer.js"></script>
+230 -55
View File
@@ -1,7 +1,13 @@
if ("scrollRestoration" in history) history.scrollRestoration = "manual";
const viewerState = { const viewerState = {
channels: [], channels: [],
channelId: null, channelId: null,
oldestMessageId: null, oldestMessageId: null,
newestMessageId: null,
userId: null,
loading: false,
sentinelObserver: null,
}; };
async function api(path) { async function api(path) {
@@ -13,9 +19,38 @@ async function api(path) {
return data; return data;
} }
function formatDateHeader(dateStr) {
if (!dateStr) return "";
const d = new Date(dateStr.replace(" ", "T"));
if (isNaN(d.getTime())) return "";
const now = new Date();
const diff = now - d;
const oneDay = 86400000;
if (diff < oneDay && d.getDate() === now.getDate()) return "Today";
if (diff < 2 * oneDay && d.getDate() === now.getDate() - 1) return "Yesterday";
return d.toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: d.getFullYear() !== now.getFullYear() ? "numeric" : undefined,
});
}
function formatTime(dateStr) {
if (!dateStr) return "";
const d = new Date(dateStr.replace(" ", "T"));
if (isNaN(d.getTime())) return "";
return d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" });
}
function dateKey(dateStr) {
if (!dateStr) return "";
return dateStr.slice(0, 10);
}
function textNodeWithBreaks(text) { function textNodeWithBreaks(text) {
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
const parts = (text || "").split("\n"); if (!text) return fragment;
const parts = text.split("\n");
parts.forEach((part, index) => { parts.forEach((part, index) => {
if (index > 0) fragment.appendChild(document.createElement("br")); if (index > 0) fragment.appendChild(document.createElement("br"));
fragment.appendChild(document.createTextNode(part)); fragment.appendChild(document.createTextNode(part));
@@ -23,10 +58,21 @@ function textNodeWithBreaks(text) {
return fragment; return fragment;
} }
function scrollToBottom() {
const list = document.getElementById("messages-list");
if (!list) return;
const go = () => { list.scrollTop = list.scrollHeight; };
go();
requestAnimationFrame(go);
setTimeout(go, 150);
}
function renderChannelList() { function renderChannelList() {
const root = document.getElementById("viewer-channel-list"); const root = document.getElementById("viewer-channel-list");
if (!root) return;
root.innerHTML = ""; root.innerHTML = "";
const template = document.getElementById("viewer-channel-template"); const template = document.getElementById("viewer-channel-template");
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);
node.querySelector(".viewer-channel-name").textContent = channel.name; node.querySelector(".viewer-channel-name").textContent = channel.name;
@@ -36,99 +82,228 @@ function renderChannelList() {
} }
node.addEventListener("click", () => { node.addEventListener("click", () => {
viewerState.oldestMessageId = null; viewerState.oldestMessageId = null;
viewerState.newestMessageId = null;
loadChannel(channel.channel_id); loadChannel(channel.channel_id);
}); });
root.appendChild(node); root.appendChild(node);
}); });
} }
function buildMessageNode(message, root) {
const template = document.getElementById("message-template");
const wrap = template.content.firstElementChild.cloneNode(true);
const bubble = wrap.querySelector(".chat-bubble");
bubble.dataset.messageId = String(message.message_id);
const isOwn = viewerState.userId && message.sender_id === viewerState.userId;
if (isOwn) {
wrap.classList.add("chat-bubble-wrap--own");
}
const sender = bubble.querySelector(".chat-sender");
sender.textContent = message.sender_name || "";
const replyEl = bubble.querySelector(".chat-reply");
const replyMsg = message.reply_to_message;
if (message.reply_to) {
replyEl.classList.remove("hidden");
bubble.querySelector(".chat-reply-author").textContent =
replyMsg?.sender_name || `#${message.reply_to}`;
bubble.querySelector(".chat-reply-text").textContent =
replyMsg?.text || "(message not available)";
replyEl.addEventListener("click", () => {
const target = root.querySelector(`[data-message-id="${message.reply_to}"]`);
if (target) {
target.scrollIntoView({ behavior: "smooth", block: "center" });
target.classList.add("chat-bubble--highlight");
setTimeout(() => target.classList.remove("chat-bubble--highlight"), 2000);
}
});
} else {
replyEl.classList.add("hidden");
}
bubble.querySelector(".chat-text").appendChild(textNodeWithBreaks(message.text || ""));
const media = bubble.querySelector(".chat-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.date) footerParts.push(formatTime(message.date));
if (message.views) footerParts.push(`views: ${message.views}`);
if (message.forwards) footerParts.push(`forwards: ${message.forwards}`);
if (message.reactions) footerParts.push(`reactions: ${message.reactions}`);
bubble.querySelector(".chat-footer").textContent = footerParts.join(" | ");
return { wrap, dateKey: dateKey(message.date) };
}
function makeDateSep(text) {
const el = document.createElement("div");
el.className = "chat-date-sep";
el.textContent = text;
return el;
}
function renderMessages(payload, append = false) { function renderMessages(payload, append = false) {
const root = document.getElementById("messages-list"); const root = document.getElementById("messages-list");
const template = document.getElementById("message-template"); const sentinel = document.getElementById("scroll-sentinel");
if (!append) root.innerHTML = ""; if (!root) return;
const channel = payload.channel; const channel = payload.channel;
document.getElementById("viewer-title").textContent = channel?.name || payload.channel_id; document.getElementById("viewer-title").textContent =
channel?.name || payload.channel_id;
document.getElementById("viewer-subtitle").textContent = channel document.getElementById("viewer-subtitle").textContent = channel
? `${channel.message_count} messages, latest: ${channel.last_date || "-"}` ? `${channel.message_count} messages, latest: ${channel.last_date || "-"}`
: "No local database found for this channel yet."; : "No local database found for this channel yet.";
payload.messages.forEach((message) => { if (!append) {
const node = template.content.firstElementChild.cloneNode(true); root.innerHTML = "";
node.dataset.messageId = String(message.message_id); root.appendChild(sentinel);
node.querySelector(".message-author").textContent = message.sender_name || "Unknown"; }
node.querySelector(".message-date").textContent = message.date || "";
const reply = node.querySelector(".message-reply"); let lastKey = null;
if (message.reply_to) { if (append) {
reply.textContent = `Reply to message #${message.reply_to}`; const firstExisting = root.querySelector("[data-message-id]");
if (firstExisting) {
const candidate = firstExisting.closest(".chat-bubble-wrap");
if (candidate && candidate._dateKey) lastKey = candidate._dateKey;
} }
}
node.querySelector(".message-text").appendChild(textNodeWithBreaks(message.text || "")); const items = payload.messages.map((m) => {
const { wrap, dateKey: dk } = buildMessageNode(m, root);
const media = node.querySelector(".message-media"); wrap._dateKey = dk;
if (message.media_url && message.media_kind === "image") { return { wrap, dk };
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 (append) {
for (let i = items.length - 1; i >= 0; i--) {
const { wrap, dk } = items[i];
const target = sentinel.nextSibling;
if (dk && dk !== lastKey) {
root.insertBefore(makeDateSep(formatDateHeader(payload.messages[i].date)), target);
lastKey = dk;
}
root.insertBefore(wrap, target);
}
} else {
for (let i = 0; i < items.length; i++) {
const { wrap, dk } = items[i];
if (dk && dk !== lastKey) {
root.appendChild(makeDateSep(formatDateHeader(payload.messages[i].date)));
lastKey = dk;
}
root.appendChild(wrap);
}
}
if (payload.messages.length) { if (payload.messages.length) {
viewerState.oldestMessageId = payload.messages[0].message_id; viewerState.oldestMessageId = payload.messages[0].message_id;
viewerState.newestMessageId =
payload.messages[payload.messages.length - 1].message_id;
} }
document.getElementById("load-older-btn").disabled = !payload.messages.length; if (!append) {
requestAnimationFrame(() => scrollToBottom());
}
} }
async function loadChannel(channelId, append = false) { async function loadChannel(channelId, append = false) {
if (viewerState.loading) return;
viewerState.loading = true;
viewerState.channelId = channelId; viewerState.channelId = channelId;
renderChannelList(); renderChannelList();
const before = append && viewerState.oldestMessageId ? `&before=${viewerState.oldestMessageId}` : "";
const payload = await api(`/api/channels/${encodeURIComponent(channelId)}/messages?limit=80${before}`); const before =
renderMessages(payload, append); append && viewerState.oldestMessageId
? `&before=${viewerState.oldestMessageId}`
: "";
try {
const payload = await api(
`/api/channels/${encodeURIComponent(channelId)}/messages?limit=80${before}`
);
if (append) {
const list = document.getElementById("messages-list");
const prevScrollHeight = list.scrollHeight;
renderMessages(payload, true);
requestAnimationFrame(() => {
list.scrollTop = list.scrollHeight - prevScrollHeight;
});
} else {
renderMessages(payload, false);
}
} catch (err) {
console.error("Failed to load messages:", err);
} finally {
viewerState.loading = false;
}
}
function setupInfiniteScroll() {
const sentinel = document.getElementById("scroll-sentinel");
if (!sentinel) return;
if (viewerState.sentinelObserver) {
viewerState.sentinelObserver.disconnect();
}
viewerState.sentinelObserver = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (
entry.isIntersecting &&
viewerState.channelId &&
viewerState.oldestMessageId &&
!viewerState.loading
) {
loadChannel(viewerState.channelId, true);
}
}
},
{ root: document.getElementById("messages-list"), threshold: 0.1 }
);
viewerState.sentinelObserver.observe(sentinel);
} }
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"); viewerState.channels = await api("/api/channels");
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
viewerState.channelId = params.get("channel") || viewerState.channels[0]?.channel_id || null; viewerState.channelId =
params.get("channel") ||
viewerState.channels[0]?.channel_id ||
null;
renderChannelList(); renderChannelList();
if (viewerState.channelId) { if (viewerState.channelId) {
await loadChannel(viewerState.channelId); await loadChannel(viewerState.channelId);
} }
document.getElementById("load-older-btn").addEventListener("click", async () => { setupInfiniteScroll();
if (!viewerState.channelId || !viewerState.oldestMessageId) return;
await loadChannel(viewerState.channelId, true);
});
} }
main().catch((error) => { main().catch((error) => {
+39
View File
@@ -205,6 +205,37 @@ def load_messages(
"reactions": row["reactions"], "reactions": row["reactions"],
} }
) )
reply_ids = list({m["reply_to"] for m in messages if m["reply_to"]})
if reply_ids:
placeholders = ",".join("?" for _ in reply_ids)
try:
conn2 = sqlite3.connect(str(db_path))
conn2.row_factory = sqlite3.Row
try:
rows2 = conn2.execute(
f"SELECT message_id, message, first_name, last_name, username "
f"FROM messages WHERE message_id IN ({placeholders})",
reply_ids,
).fetchall()
reply_map: Dict[int, Dict[str, str]] = {}
for r in rows2:
r_sender = " ".join(
part for part in [r["first_name"], r["last_name"]] if part
).strip() or (r["username"] or "")
reply_map[r["message_id"]] = {
"text": (r["message"] or "")[:300],
"sender_name": r_sender,
}
for m in messages:
rt = m.get("reply_to")
if rt and rt in reply_map:
m["reply_to_message"] = reply_map[rt]
finally:
conn2.close()
except sqlite3.Error:
pass
return messages return messages
@@ -328,6 +359,7 @@ class TelegramAuthManager:
self.thread = threading.Thread(target=self._run_loop, daemon=True) self.thread = threading.Thread(target=self._run_loop, daemon=True)
self.thread.start() self.thread.start()
self.client: Optional[TelegramClient] = None self.client: Optional[TelegramClient] = None
self.user_id: Optional[int] = None
self.qr_login = None self.qr_login = None
self.qr_wait_task = None self.qr_wait_task = None
self.phone = None self.phone = None
@@ -359,6 +391,7 @@ class TelegramAuthManager:
auth = auth_status() auth = auth_status()
with self.lock: with self.lock:
snapshot = dict(self.state) snapshot = dict(self.state)
snapshot["user_id"] = self.user_id
snapshot["saved_credentials"] = { snapshot["saved_credentials"] = {
"api_id": load_state().get("api_id"), "api_id": load_state().get("api_id"),
"api_hash_present": bool(load_state().get("api_hash")), "api_hash_present": bool(load_state().get("api_hash")),
@@ -376,6 +409,12 @@ class TelegramAuthManager:
self.client = TelegramClient(str(SESSION_DIR / "session"), api_id, api_hash) self.client = TelegramClient(str(SESSION_DIR / "session"), api_id, api_hash)
if not self.client.is_connected(): if not self.client.is_connected():
await self.client.connect() await self.client.connect()
if self.user_id is None and await self.client.is_user_authorized():
try:
me = await self.client.get_me()
self.user_id = me.id
except Exception:
pass
return self.client return self.client
async def _is_authorized(self) -> bool: async def _is_authorized(self) -> bool: