Aleksmorshen commited on
Commit
60a08df
·
verified ·
1 Parent(s): 8cdee08

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +590 -596
app.py CHANGED
@@ -22,15 +22,9 @@ def read_db():
22
  try:
23
  with open(DB_FILE, 'r') as f:
24
  return json.load(f)
25
- except FileNotFoundError:
26
  init_db()
27
  return read_db()
28
- except json.JSONDecodeError:
29
- return {
30
- "users": {},
31
- "chatrooms": {},
32
- "messages": {}
33
- }
34
 
35
  def write_db(data):
36
  with open(DB_FILE, 'w') as f:
@@ -65,7 +59,7 @@ def index():
65
  --success-color: #28a745;
66
  --error-color: #dc3545;
67
  --font-family: 'Inter', sans-serif;
68
- --navbar-height: 60px;
69
  }
70
 
71
  * {
@@ -89,6 +83,7 @@ def index():
89
  display: flex;
90
  align-items: center;
91
  justify-content: center;
 
92
  }
93
 
94
  .main-container {
@@ -133,37 +128,25 @@ def index():
133
  display: none;
134
  width: 100%;
135
  height: 100%;
136
- flex-direction: column;
137
  }
138
-
139
- .main-content-area {
140
  flex-grow: 1;
141
- overflow: hidden;
142
  display: flex;
143
- flex-direction: row; /* Default desktop layout */
144
- height: calc(100% - var(--navbar-height));
145
  }
146
 
147
- #chatroom-list-view,
148
- #users-list-view {
149
  display: flex;
150
  flex-direction: column;
151
  height: 100%;
152
- width: 100%; /* Mobile default */
153
- flex-shrink: 0;
154
  background-color: var(--bg-secondary);
 
155
  }
156
 
157
- #chat-window-view,
158
- #browser-view {
159
- display: flex;
160
- flex-direction: column;
161
- height: 100%;
162
- width: 100%; /* Mobile default */
163
- flex-grow: 1;
164
- background-color: var(--bg-primary);
165
- }
166
-
167
  .list-header {
168
  padding: 16px;
169
  border-bottom: 1px solid var(--border-color);
@@ -175,25 +158,45 @@ def index():
175
  align-items: center;
176
  margin-bottom: 16px;
177
  }
178
- .list-header-top h2 {
179
  font-size: 1.5rem;
180
  font-weight: 600;
181
  }
182
-
183
- .user-profile-summary {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  padding: 12px;
185
  background-color: var(--bg-tertiary);
186
  border-radius: 8px;
187
  margin-bottom: 16px;
188
- word-break: break-all;
189
  }
190
- #user-wallet, #user-nickname {
191
  font-size: 0.9rem;
192
  color: var(--text-secondary);
193
  margin-bottom: 8px;
 
194
  }
 
195
 
196
- .username-form { display: flex; gap: 8px; }
197
  .username-input {
198
  flex-grow: 1;
199
  background-color: var(--bg-primary);
@@ -228,13 +231,15 @@ def index():
228
  font-size: 0.9rem;
229
  }
230
 
231
- #chatroom-list,
232
- #users-list {
233
  flex-grow: 1;
234
  overflow-y: auto;
 
235
  }
236
- .chatroom-item,
237
- .user-item {
 
 
238
  display: flex;
239
  align-items: center;
240
  gap: 12px;
@@ -243,8 +248,7 @@ def index():
243
  border-bottom: 1px solid var(--border-color);
244
  transition: background-color 0.2s ease;
245
  }
246
- .chatroom-item:hover,
247
- .user-item:hover { background-color: var(--bg-hover); }
248
 
249
  .avatar {
250
  width: 40px;
@@ -256,22 +260,20 @@ def index():
256
  font-weight: 600;
257
  color: white;
258
  flex-shrink: 0;
259
- font-size: 1.1rem;
260
- }
261
- .chatroom-info,
262
- .user-info {
263
- flex-grow: 1;
264
- overflow: hidden;
265
  }
266
- .chatroom-name,
267
- .user-nickname {
 
 
 
268
  font-weight: 500;
269
  white-space: nowrap;
270
  overflow: hidden;
271
  text-overflow: ellipsis;
272
  }
273
- .user-address {
274
- font-size: 0.8rem;
275
  color: var(--text-secondary);
276
  white-space: nowrap;
277
  overflow: hidden;
@@ -284,6 +286,14 @@ def index():
284
  flex-shrink: 0;
285
  }
286
 
 
 
 
 
 
 
 
 
287
  .chat-header {
288
  display: flex;
289
  align-items: center;
@@ -297,7 +307,7 @@ def index():
297
  background: none;
298
  border: none;
299
  cursor: pointer;
300
- display: block; /* Show on mobile by default */
301
  }
302
  .back-btn svg {
303
  width: 24px;
@@ -316,6 +326,7 @@ def index():
316
  display: flex;
317
  flex-direction: column;
318
  gap: 12px;
 
319
  }
320
 
321
  .message {
@@ -327,7 +338,6 @@ def index():
327
  width: 36px;
328
  height: 36px;
329
  align-self: flex-end;
330
- font-size: 1rem;
331
  }
332
  .message-content {
333
  display: flex;
@@ -404,33 +414,6 @@ def index():
404
  }
405
  .send-btn svg { width: 20px; height: 20px; fill: white; }
406
 
407
- #browser-view .browser-url-bar {
408
- display: flex;
409
- padding: 12px 16px;
410
- gap: 8px;
411
- background-color: var(--bg-secondary);
412
- border-bottom: 1px solid var(--border-color);
413
- flex-shrink: 0;
414
- }
415
- #browser-view .browser-url-input {
416
- flex-grow: 1;
417
- padding: 8px 12px;
418
- border: 1px solid var(--border-color);
419
- background-color: var(--bg-tertiary);
420
- color: var(--text-primary);
421
- border-radius: 6px;
422
- outline: none;
423
- font-size: 0.9rem;
424
- }
425
- #browser-view .browser-url-input:focus { border-color: var(--accent-blue); }
426
-
427
- #browser-view iframe {
428
- flex-grow: 1;
429
- width: 100%;
430
- height: 100%;
431
- border: none;
432
- }
433
-
434
  .modal-overlay {
435
  position: fixed;
436
  top: 0;
@@ -456,7 +439,7 @@ def index():
456
  }
457
  .modal-content h3 { margin-bottom: 20px; font-weight: 600; font-size: 1.3rem; }
458
  .modal-content label { display: block; margin-bottom: 8px; font-size: 0.9rem; color: var(--text-secondary); }
459
- .modal-content input:not([type="checkbox"]) {
460
  width: 100%;
461
  padding: 12px;
462
  margin-bottom: 16px;
@@ -467,38 +450,18 @@ def index():
467
  font-size: 1rem;
468
  }
469
  .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
470
- .modal-actions.column { flex-direction: column; align-items: stretch; }
471
  .modal-btn {
472
  padding: 10px 20px;
473
  border-radius: 6px;
474
  border: none;
475
  cursor: pointer;
476
  font-weight: 500;
477
- text-align: center;
478
  }
479
  .secondary-btn { background-color: var(--bg-hover); color: white; }
480
 
481
- #profile-modal .modal-content { text-align: center; }
482
- #profile-avatar-container { margin: 20px auto; display: inline-block; }
483
- #profile-username { font-size: 1.2rem; font-weight: 600; }
484
- #profile-address { color: var(--text-secondary); font-size: 0.9rem; word-break: break-all; margin-top: 8px; }
485
- #profile-qr-code { background: white; padding: 10px; margin: 20px auto; width: fit-content; border-radius: 8px; }
486
- #profile-modal .caption { text-align: center; color: var(--text-secondary); font-size: 0.8rem; margin-top: -10px; margin-bottom: 20px; }
487
- #profile-balance { margin-top: 16px; font-size: 1rem; color: var(--text-secondary); }
488
-
489
-
490
- #scanner-modal .modal-content { text-align: center; }
491
- #qr-reader {
492
- width: 100%;
493
- border: 1px solid var(--border-color);
494
- margin-top: 16px;
495
- border-radius: 8px;
496
- overflow: hidden;
497
- }
498
-
499
  #status-bar {
500
  position: fixed;
501
- bottom: calc(var(--navbar-height) + 20px); /* Position above navbar */
502
  left: 50%;
503
  transform: translateX(-50%);
504
  background-color: var(--bg-tertiary);
@@ -516,71 +479,65 @@ def index():
516
  #status-bar.error { background-color: var(--error-color); }
517
  #status-bar.visible { opacity: 1; visibility: visible; }
518
 
519
- .navbar {
520
  position: fixed;
521
  bottom: 0;
522
  left: 0;
523
  width: 100%;
524
- height: var(--navbar-height);
525
  background-color: var(--bg-secondary);
526
  border-top: 1px solid var(--border-color);
527
  display: flex;
528
  justify-content: space-around;
529
  align-items: center;
530
- z-index: 900;
531
  }
532
- .navbar-item {
533
  display: flex;
534
  flex-direction: column;
535
  align-items: center;
536
  justify-content: center;
537
- flex-grow: 1;
538
- height: 100%;
539
  color: var(--text-secondary);
540
- font-size: 0.75rem;
 
 
541
  cursor: pointer;
 
 
542
  transition: color 0.2s ease;
543
  }
544
- .navbar-item:hover {
545
- color: var(--text-primary);
546
- }
547
- .navbar-item.active {
548
- color: var(--accent-blue-light);
549
- }
550
- .navbar-item svg {
551
- width: 24px;
552
- height: 24px;
553
- margin-bottom: 4px;
554
- fill: currentColor; /* Use parent color */
555
- }
556
- .navbar-item.scan-qr-item {
557
- position: relative;
558
- top: -15px; /* Lift button slightly */
559
- width: 50px;
560
- height: 50px;
561
  background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
562
  border-radius: 50%;
563
- box-shadow: 0 5px 15px rgba(0, 136, 204, 0.3);
 
564
  color: white;
565
- display: flex;
566
- align-items: center;
567
- justify-content: center;
568
- font-size: 0; /* Hide text */
569
- flex-grow: 0;
570
  }
571
- .navbar-item.scan-qr-item svg {
572
- margin-bottom: 0;
573
- width: 28px;
574
- height: 28px;
575
  }
576
- .navbar-item.scan-qr-item:hover {
577
- color: white; /* Keep color white on hover */
578
- transform: translateY(-2px);
579
- box-shadow: 0 7px 20px rgba(0, 136, 204, 0.4);
 
 
 
 
 
 
 
 
580
  }
581
 
582
-
583
- /* Desktop Styles */
584
  @media (min-width: 768px) {
585
  .main-container {
586
  max-width: 1100px;
@@ -590,50 +547,41 @@ def index():
590
  box-shadow: 0 10px 40px rgba(0,0,0,0.3);
591
  border: 1px solid var(--border-color);
592
  }
593
-
594
  #app-view {
595
- flex-direction: column; /* Keep column for desktop */
596
  }
597
-
598
- .main-content-area {
599
- flex-direction: row; /* Desktop layout */
600
- height: 100%; /* Desktop uses split view, not offset by navbar */
601
  }
602
-
603
- #chatroom-list-view,
604
- #users-list-view {
605
- width: 320px;
606
- flex-shrink: 0;
607
  border-right: 1px solid var(--border-color);
608
- display: flex !important; /* Always show list on desktop */
609
  }
610
-
611
- #chat-window-view {
612
- width: auto;
613
- flex-grow: 1;
614
- display: flex !important; /* Always show chat on desktop */
615
  }
616
-
617
- #browser-view {
618
- width: auto;
619
  flex-grow: 1;
620
- display: flex !important; /* Always show browser when active */
621
  }
622
-
623
- .back-btn { display: none !important; } /* Hide back button on desktop */
624
-
625
- .navbar {
626
- position: static; /* Navbar is integrated into layout or hidden on desktop */
627
- height: 0; /* Make navbar height 0 to not take space */
628
- border-top: none;
629
  }
630
- .navbar-item {
631
- display: none; /* Hide navbar items on desktop */
 
632
  }
633
 
634
- #status-bar {
635
- bottom: 20px; /* Move status bar down on desktop */
636
- }
 
 
 
 
 
637
  }
638
  </style>
639
  </head>
@@ -647,102 +595,79 @@ def index():
647
  </div>
648
 
649
  <div id="app-view" class="main-container">
650
- <div class="main-content-area">
651
- <!-- Views go here -->
652
- <div id="chatroom-list-view" class="app-view-content">
653
  <div class="list-header">
654
- <div class="list-header-top">
655
- <h2>Чаты</h2>
656
- <div style="display: flex; align-items: center; gap: 8px;">
657
- <button id="create-room-show-modal" class="action-btn small">
658
- <svg xmlns="http://www.w3.org/2000/svg" height="16" viewBox="0 0 24 24" width="16" fill="white"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
659
- <span>Новый</span>
660
- </button>
661
- </div>
662
- </div>
663
- <div class="user-profile-summary">
664
- <div id="user-wallet"></div>
665
- <div id="user-nickname"></div>
666
- <form class="username-form" id="username-form">
667
- <input type="text" id="username-input" class="username-input" placeholder="Установить никнейм" autocomplete="off">
668
- <button type="submit" class="action-btn small">✓</button>
669
- </form>
670
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
671
  </div>
672
- <div id="chatroom-list"></div>
673
  </div>
674
 
675
- <div id="users-list-view" class="app-view-content" style="display: none;">
676
- <div class="list-header">
677
- <div class="list-header-top">
678
- <h2>Пользователи</h2>
679
- </div>
680
- </div>
681
- <div id="users-list">
682
- <!-- User items will be rendered here -->
683
- </div>
684
- </div>
685
-
686
- <div id="chat-window-view" class="app-view-content">
687
- <div id="chat-placeholder" class="chat-placeholder">
688
- <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
689
- <h2>Выберите чат</h2>
690
- <p>Начните общение в одном из существующих чатов или создайте свой собственный.</p>
691
- </div>
692
- <div id="active-chat" style="display: none; width: 100%; height: 100%; flex-direction: column;">
693
- <div class="chat-header">
694
- <button class="back-btn" id="back-to-list-btn">
695
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
696
- </button>
697
- <div id="chat-header-avatar" class="avatar"></div>
698
- <span id="chat-header-title"></span>
699
- </div>
700
- <div id="messages-container"></div>
701
- <form id="message-form" class="message-form">
702
- <input type="text" id="message-input" placeholder="Сообщение..." autocomplete="off">
703
- <button type="submit" class="action-btn send-btn" id="send-btn">
704
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24px" height="24px"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
705
- </button>
706
- </form>
707
- </div>
708
- </div>
709
-
710
- <div id="browser-view" class="app-view-content" style="display: none;">
711
- <div class="browser-url-bar">
712
- <input type="text" id="browser-url-input" class="browser-url-input" placeholder="Введите URL или поиск Google">
713
- <button id="browser-go-btn" class="action-btn small">Перейти</button>
714
  </div>
715
- <iframe id="browser-iframe" src="https://www.google.com" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation"></iframe>
716
  </div>
717
-
718
  </div>
 
719
 
720
- <!-- Navbar -->
721
- <div class="navbar">
722
- <div class="navbar-item active" data-view="chats">
723
- <svg viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/></svg>
724
- <span>Чаты</span>
725
- </div>
726
- <div class="navbar-item" data-view="users">
727
- <svg viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M16 7V3c-2.21 0-4-1.79-4-4H8c-2.21 0-4 1.79-4 4v4c-1.1 0-2 .9-2 2v5c0 .55.45 1 1 1h.18l-.68 3.85c-.19 1.09.9 2.17 2 1.92l1.46-.32L8.87 22h6.26l1.66.37 1.46.32c1.1.25 2.19-.83 2-1.92L20.82 15H21c.55 0 1-.45 1-1V9c0-1.1-.9-2-2-2zM6 15v-5h2v5H6zm7 0v-5h-3V7c0-.55-.45-1-1-1s-1 .45-1 1v8h-2v-5H9v5h2v-4h2v4h2v-5h2v5h-2z"/></svg>
728
- <span>Юзеры</span>
729
- </div>
730
- <div class="navbar-item scan-qr-item" data-view="scan">
731
- <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" fill="white"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M3 11h8V3H3v8zm2-6h4v4H5V5zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm8-12v8h8V3h-8zm6 6h-4V5h4v4zm-2 10a2 2 0 100-4 2 2 0 000 4z"/></svg>
732
- <span>QR</span>
733
- </div>
734
- <div class="navbar-item" data-view="browser">
735
- <svg viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14l-4-4 1.41-1.41L10 13.17l6.59-6.59L18 8l-8 8z"/></svg>
736
- <span>Браузер</span>
737
- </div>
738
- <div class="navbar-item" data-view="profile">
739
- <svg viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
740
- <span>Профиль</span>
741
- </div>
742
  </div>
 
 
 
 
 
743
 
744
- </div>
745
-
746
  <div id="create-room-modal" class="modal-overlay">
747
  <div class="modal-content">
748
  <h3>Создать новый чат</h3>
@@ -774,16 +699,16 @@ def index():
774
  </div>
775
 
776
  <div id="profile-modal" class="modal-overlay">
777
- <div class="modal-content">
778
  <h3 id="profile-modal-title">Профиль пользователя</h3>
779
  <div id="profile-avatar-container" style="margin: 20px auto; display: inline-block;"></div>
780
  <p id="profile-username" style="font-size: 1.2rem; font-weight: 600;"></p>
781
  <p id="profile-address" style="color: var(--text-secondary); font-size: 0.9rem; word-break: break-all; margin-top: 8px;"></p>
782
- <p id="profile-balance"></p>
783
  <div id="profile-qr-code" style="background: white; padding: 10px; margin: 20px auto; width: fit-content; border-radius: 8px;"></div>
784
- <p class="caption">Отсканируйте для открытия профиля</p>
785
- <div class="modal-actions column">
786
- <button id="send-ton-btn" class="modal-btn action-btn">Отправить TON</button>
787
  <button id="profile-close-btn" class="modal-btn secondary-btn">Закрыть</button>
788
  </div>
789
  </div>
@@ -792,7 +717,7 @@ def index():
792
  <div id="scanner-modal" class="modal-overlay">
793
  <div class="modal-content">
794
  <h3>Сканировать QR-код</h3>
795
- <div id="qr-reader"></div>
796
  <div class="modal-actions">
797
  <button id="scanner-close-btn" class="modal-btn secondary-btn">Отмена</button>
798
  </div>
@@ -805,30 +730,34 @@ def index():
805
  document.addEventListener('DOMContentLoaded', () => {
806
  const tonConnectUI = new TON_CONNECT_UI.TonConnectUI({
807
  manifestUrl: 'https://huggingface.co/spaces/Aleksmorshen/MorshenGroup/resolve/main/tonconnect-manifest.json',
808
- buttonRootId: 'ton-connect-button'
 
 
 
809
  });
810
 
811
  let currentUser = { address: null, username: null, balance: null };
812
  let activeChatroomId = null;
813
  let messagePollingInterval = null;
814
  let chatroomsData = {};
 
815
  let html5QrCode = null;
816
  let profileQrCode = null;
817
 
818
  const loginView = document.getElementById('login-view');
819
  const appView = document.getElementById('app-view');
820
- const mainContentArea = document.querySelector('.main-content-area');
821
- const chatroomListView = document.getElementById('chatroom-list-view');
822
- const usersListView = document.getElementById('users-list-view');
823
- const chatWindowView = document.getElementById('chat-window-view');
824
- const browserView = document.getElementById('browser-view');
825
  const chatPlaceholder = document.getElementById('chat-placeholder');
826
  const activeChat = document.getElementById('active-chat');
827
  const profileModal = document.getElementById('profile-modal');
828
  const scannerModal = document.getElementById('scanner-modal');
829
- const navbarItems = document.querySelectorAll('.navbar-item');
830
- const browserUrlInput = document.getElementById('browser-url-input');
831
- const browserIframe = document.getElementById('browser-iframe');
 
 
 
832
 
833
  const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292'];
834
 
@@ -859,7 +788,7 @@ def index():
859
  const response = await fetch(endpoint, options);
860
  if (!response.ok) {
861
  const errorData = await response.json().catch(() => ({ error: 'Request failed with status ' + response.status }));
862
- throw new Error(errorData.error || 'Unknown error');
863
  }
864
  if (response.status === 204) return null;
865
  return await response.json();
@@ -873,11 +802,26 @@ def index():
873
 
874
  const updateUserInfo = () => {
875
  document.getElementById('user-wallet').textContent = `Кошелек: ${truncateAddress(currentUser.address)}`;
 
876
  const nicknameEl = document.getElementById('user-nickname');
877
  const usernameInput = document.getElementById('username-input');
878
  nicknameEl.textContent = currentUser.username ? `Ник: ${currentUser.username}` : `Никнейм не установлен`;
879
  usernameInput.value = currentUser.username || '';
880
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
881
 
882
  document.getElementById('username-form').addEventListener('submit', async (e) => {
883
  e.preventDefault();
@@ -895,26 +839,15 @@ def index():
895
  currentUser.username = newUsername;
896
  updateUserInfo();
897
  showStatus('Никнейм успешно обновлен!', 'success');
898
- fetchChatrooms();
899
- fetchUsers(); // Update users list as well
900
- if (activeChatroomId) fetchMessages(activeChatroomId);
901
  } catch (err) {}
902
  });
903
 
904
  const initializeUser = async (address) => {
905
  currentUser.address = address;
906
- if (tonConnectUI.wallet && tonConnectUI.wallet.account) {
907
- try {
908
- const balance = await tonConnectUI.getBalance();
909
- currentUser.balance = balance / 1e9; // Convert nanoton to ton
910
- console.log("Balance:", currentUser.balance);
911
- } catch (e) {
912
- console.error("Failed to get balance:", e);
913
- currentUser.balance = null;
914
- }
915
- }
916
-
917
-
918
  try {
919
  const data = await apiCall('/api/user_data', {
920
  method: 'POST',
@@ -928,28 +861,29 @@ def index():
928
  updateUserInfo();
929
  loginView.style.display = 'none';
930
  appView.style.display = 'flex';
931
-
932
- switchView('chats'); // Default view after login
933
  fetchChatrooms();
934
- fetchUsers(); // Fetch users list on login
 
935
  };
936
 
937
  const renderChatrooms = (rooms) => {
938
- const list = document.getElementById('chatroom-list');
939
- list.innerHTML = '';
940
  chatroomsData = {};
 
 
 
941
  rooms.forEach(room => {
942
  chatroomsData[room.id] = room;
943
  const item = document.createElement('div');
944
- item.className = 'chatroom-item';
945
  item.dataset.id = room.id;
946
 
947
  item.appendChild(getAvatar(room.name));
948
 
949
  const infoDiv = document.createElement('div');
950
- infoDiv.className = 'chatroom-info';
951
  const nameSpan = document.createElement('div');
952
- nameSpan.className = 'chatroom-name';
953
  nameSpan.textContent = room.name;
954
  infoDiv.appendChild(nameSpan);
955
  item.appendChild(infoDiv);
@@ -961,46 +895,49 @@ def index():
961
  }
962
 
963
  item.addEventListener('click', () => selectChatroom(room.id, room.is_private));
964
- list.appendChild(item);
965
  });
966
  };
967
-
968
- const fetchChatrooms = async () => {
969
- try {
970
- const data = await apiCall('/api/chatrooms');
971
- renderChatrooms(data.chatrooms);
972
- } catch (err) {}
973
- };
974
 
975
  const renderUsers = (users) => {
976
- const list = document.getElementById('users-list');
977
- list.innerHTML = '';
 
 
 
978
  users.forEach(user => {
 
979
  const item = document.createElement('div');
980
- item.className = 'user-item';
981
  item.dataset.address = user.address;
982
 
983
- item.appendChild(getAvatar(user.username || truncateAddress(user.address)));
984
 
985
  const infoDiv = document.createElement('div');
986
- infoDiv.className = 'user-info';
987
-
988
- const nicknameSpan = document.createElement('div');
989
- nicknameSpan.className = 'user-nickname';
990
- nicknameSpan.textContent = user.username || `User ${truncateAddress(user.address)}`;
991
- infoDiv.appendChild(nicknameSpan);
992
 
993
  const addressSpan = document.createElement('div');
994
- addressSpan.className = 'user-address';
995
  addressSpan.textContent = user.address;
996
  infoDiv.appendChild(addressSpan);
997
 
998
  item.appendChild(infoDiv);
999
 
1000
  item.addEventListener('click', () => showProfile(user.address));
1001
- list.appendChild(item);
1002
  });
1003
  };
 
 
 
 
 
 
 
1004
 
1005
  const fetchUsers = async () => {
1006
  try {
@@ -1042,7 +979,7 @@ def index():
1042
  container.appendChild(msgDiv);
1043
  });
1044
 
1045
- if(shouldScroll) {
1046
  container.scrollTop = container.scrollHeight;
1047
  }
1048
  };
@@ -1053,142 +990,84 @@ def index():
1053
  const data = await apiCall(`/api/messages/${roomId}`);
1054
  renderMessages(data.messages);
1055
  } catch (err) {
1056
- if (messagePollingInterval) clearInterval(messagePollingInterval);
1057
  }
1058
  };
1059
 
1060
- const switchView = (viewName) => {
1061
- const views = {
1062
- chats: chatroomListView,
1063
- users: usersListView,
1064
- chat: chatWindowView, // This is a secondary view managed by selectChatroom
1065
- browser: browserView
1066
- };
1067
-
1068
- // Hide all main content views
1069
- Object.values(views).forEach(view => {
1070
- if (view) view.style.display = 'none';
1071
- });
1072
-
1073
- // Handle desktop layout where chat list is always visible
1074
- const isDesktop = window.innerWidth >= 768;
1075
-
1076
- if (viewName === 'chats') {
1077
- chatroomListView.style.display = 'flex';
1078
- if (!activeChatroomId) { // If no chat is selected, show placeholder in chat window
1079
- chatWindowView.style.display = 'flex'; // Ensure chat window area is visible
1080
- chatPlaceholder.style.display = 'flex';
1081
- activeChat.style.display = 'none';
1082
- } else { // If a chat is active, show it
1083
- selectChatroom(activeChatroomId, chatroomsData[activeChatroomId]?.is_private, false); // Re-show active chat without password prompt
1084
- }
1085
- // Stop message polling when leaving the active chat window view
1086
- if (messagePollingInterval) clearInterval(messagePollingInterval);
1087
- messagePollingInterval = null;
1088
-
1089
- } else if (viewName === 'users') {
1090
- usersListView.style.display = 'flex';
1091
- // On mobile, hide the chat window area
1092
- if (!isDesktop) chatWindowView.style.display = 'none';
1093
- if (messagePollingInterval) clearInterval(messagePollingInterval);
1094
- messagePollingInterval = null;
1095
-
1096
- } else if (viewName === 'browser') {
1097
- browserView.style.display = 'flex';
1098
- // On mobile, hide the chat window area
1099
- if (!isDesktop) chatWindowView.style.display = 'none';
1100
- if (messagePollingInterval) clearInterval(messagePollingInterval);
1101
- messagePollingInterval = null;
1102
-
1103
- } else if (viewName === 'scan') {
1104
- showScanner(); // Open scanner modal
1105
- // Revert navbar active state as modal is temporary
1106
- const activeNav = document.querySelector('.navbar-item.active');
1107
- if (activeNav) activeNav.classList.remove('active');
1108
- const currentViewName = document.querySelector('.main-content-area > .app-view-content[style*="display: flex"]').dataset.viewName;
1109
- const currentNavItem = document.querySelector(`.navbar-item[data-view="${currentViewName}"]`);
1110
- if(currentNavItem) currentNavItem.classList.add('active');
1111
- return; // Don't update active state below
1112
- } else if (viewName === 'profile') {
1113
- if (currentUser.address) showProfile(currentUser.address); // Open profile modal
1114
- // Revert navbar active state
1115
- const activeNav = document.querySelector('.navbar-item.active');
1116
- if (activeNav) activeNav.classList.remove('active');
1117
- const currentViewElement = document.querySelector('.main-content-area > .app-view-content[style*="display: flex"]');
1118
- const currentView = currentViewElement ? currentViewElement.dataset.viewName : 'chats'; // Default to chats if none active
1119
- const currentNavItem = document.querySelector(`.navbar-item[data-view="${currentView}"]`);
1120
- if(currentNavItem) currentNavItem.classList.add('active');
1121
- return; // Don't update active state below
1122
- } else {
1123
- // Handle switching FROM chat window back to list on mobile
1124
- if (isDesktop) {
1125
- // Do nothing, list is always visible
1126
- } else {
1127
- chatroomListView.style.display = 'flex';
1128
- chatWindowView.style.display = 'none';
1129
  }
1130
- viewName = 'chats'; // Ensure correct navbar item is active
1131
- if (messagePollingInterval) clearInterval(messagePollingInterval);
1132
- messagePollingInterval = null;
1133
- }
 
 
 
 
 
 
 
 
 
 
 
1134
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1135
 
1136
- // Update active navbar item
1137
- navbarItems.forEach(item => item.classList.remove('active'));
1138
- const targetNavItem = document.querySelector(`.navbar-item[data-view="${viewName}"]`);
1139
- if (targetNavItem) {
1140
- targetNavItem.classList.add('active');
1141
- mainContentArea.dataset.activeView = viewName; // Store active view name
1142
- } else {
1143
- // If we switched from chat window back to list, activate chats item
1144
- document.querySelector('.navbar-item[data-view="chats"]').classList.add('active');
1145
- mainContentArea.dataset.activeView = 'chats';
1146
- }
1147
- };
1148
 
1149
- const selectChatroom = (roomId, isPrivate, checkPassword = true) => {
1150
  const roomData = chatroomsData[roomId];
1151
- if (!roomData) {
1152
- showStatus('Чат не найден.', 'error');
1153
- return;
1154
- }
1155
 
1156
  const proceedToRoom = () => {
1157
- if (messagePollingInterval) clearInterval(messagePollingInterval);
1158
- activeChatroomId = roomId;
1159
-
1160
- // Ensure chat window view is visible
1161
- chatWindowView.style.display = 'flex';
1162
- chatPlaceholder.style.display = 'none';
1163
- activeChat.style.display = 'flex';
1164
-
1165
- // Hide list view on mobile
1166
- if (window.innerWidth < 768) {
1167
- chatroomListView.style.display = 'none';
1168
- }
1169
-
1170
-
1171
- document.getElementById('chat-header-title').textContent = roomData.name;
1172
- const headerAvatar = document.getElementById('chat-header-avatar');
1173
- headerAvatar.innerHTML = '';
1174
- headerAvatar.appendChild(getAvatar(roomData.name));
1175
 
1176
- fetchMessages(roomId);
1177
- messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000);
 
 
1178
 
1179
- // Ensure 'chats' navbar item is active if not already
1180
- const activeNav = document.querySelector('.navbar-item.active');
1181
- if (activeNav && activeNav.dataset.view !== 'chats') {
1182
- activeNav.classList.remove('active');
1183
- document.querySelector('.navbar-item[data-view="chats"]').classList.add('active');
1184
- mainContentArea.dataset.activeView = 'chats';
1185
- } else if (!activeNav) {
1186
- document.querySelector('.navbar-item[data-view="chats"]').classList.add('active');
1187
- mainContentArea.dataset.activeView = 'chats';
1188
- }
1189
  };
1190
 
1191
- if (isPrivate && checkPassword) {
1192
  const passwordModal = document.getElementById('password-modal');
1193
  const passwordForm = document.getElementById('password-form');
1194
  const passwordInput = document.getElementById('password-input');
@@ -1198,7 +1077,7 @@ def index():
1198
 
1199
  const formSubmitHandler = async (e) => {
1200
  e.preventDefault();
1201
- passwordForm.removeEventListener('submit', formSubmitHandler);
1202
  const password = passwordInput.value;
1203
  passwordModal.style.display = 'none';
1204
  try {
@@ -1209,14 +1088,15 @@ def index():
1209
  });
1210
  proceedToRoom();
1211
  } catch (err) {
1212
- showStatus('Неверный пароль.', 'error');
 
1213
  }
1214
  };
1215
  passwordForm.addEventListener('submit', formSubmitHandler);
1216
 
1217
  document.getElementById('password-cancel').onclick = () => {
1218
  passwordModal.style.display = 'none';
1219
- passwordForm.removeEventListener('submit', formSubmitHandler);
1220
  };
1221
  } else {
1222
  proceedToRoom();
@@ -1229,6 +1109,7 @@ def index():
1229
  const sendBtn = document.getElementById('send-btn');
1230
  const text = input.value.trim();
1231
  if (text && activeChatroomId && currentUser.address) {
 
1232
  input.value = '';
1233
  input.disabled = true;
1234
  sendBtn.disabled = true;
@@ -1239,12 +1120,22 @@ def index():
1239
  body: JSON.stringify({
1240
  chatroom_id: activeChatroomId,
1241
  sender_address: currentUser.address,
1242
- text: text
1243
  })
1244
  });
1245
- // Optimistic update or wait for poll
1246
- // await fetchMessages(activeChatroomId);
1247
- // document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight;
 
 
 
 
 
 
 
 
 
 
1248
  } finally {
1249
  input.disabled = false;
1250
  sendBtn.disabled = false;
@@ -1266,13 +1157,9 @@ def index():
1266
  const name = document.getElementById('room-name').value.trim();
1267
  const password = document.getElementById('room-password').value;
1268
  if (!name) {
1269
- showStatus('Название чата не может быть пустым.', 'error');
1270
- return;
1271
- }
1272
- if (!currentUser.address) {
1273
- showStatus('Необходимо подключить кошелек для создания чата.', 'error');
1274
  return;
1275
- }
1276
 
1277
  try {
1278
  await apiCall('/api/create_chatroom', {
@@ -1295,7 +1182,11 @@ def index():
1295
  body: JSON.stringify({ address: address })
1296
  });
1297
 
 
1298
  const username = userData.username || `User ${truncateAddress(address)}`;
 
 
 
1299
  const avatarContainer = document.getElementById('profile-avatar-container');
1300
  const usernameEl = document.getElementById('profile-username');
1301
  const addressEl = document.getElementById('profile-address');
@@ -1308,19 +1199,20 @@ def index():
1308
 
1309
  usernameEl.textContent = username;
1310
  addressEl.textContent = address;
1311
-
1312
- // Show balance only for current user's profile
1313
- if (address === currentUser.address && currentUser.balance !== null) {
1314
- balanceEl.textContent = `Баланс: ${currentUser.balance.toFixed(2)} TON`;
1315
- balanceEl.style.display = 'block';
 
1316
  } else {
1317
  balanceEl.style.display = 'none';
 
1318
  }
1319
-
1320
  qrCodeEl.innerHTML = '';
1321
  if (profileQrCode) {
1322
- profileQrCode.clear();
1323
- profileQrCode = null;
1324
  }
1325
  profileQrCode = new QRCode(qrCodeEl, {
1326
  text: address,
@@ -1332,11 +1224,15 @@ def index():
1332
  });
1333
 
1334
  sendTonBtn.onclick = async () => {
1335
- if (!tonConnectUI.connected) {
1336
- showStatus('Подключите кошелек для отправки TON.', 'error');
 
 
 
 
1337
  return;
1338
- }
1339
- const amountString = prompt("Введите сумму в TON для отправки:", "0.1");
1340
  if (amountString === null) return;
1341
 
1342
  const amount = parseFloat(amountString);
@@ -1354,14 +1250,15 @@ def index():
1354
 
1355
  try {
1356
  await tonConnectUI.sendTransaction(transaction);
1357
- showStatus(`Транзакция на ${amount} TON отправлена!`, 'success');
1358
  profileModal.style.display = 'none';
1359
  } catch (error) {
1360
- showStatus('Транзакция отклонена.', 'error');
 
1361
  }
1362
  };
1363
 
1364
- sendTonBtn.style.display = (address === currentUser.address || !tonConnectUI.connected) ? 'none' : 'block';
1365
  profileModal.style.display = 'flex';
1366
  } catch (err) {
1367
  showStatus('Не удалось загрузить профиль.', 'error');
@@ -1370,26 +1267,49 @@ def index():
1370
 
1371
  const showScanner = () => {
1372
  scannerModal.style.display = 'flex';
1373
- // Stop previous scanner if running
1374
- if (html5QrCode && html5QrCode.isScanning) {
1375
- html5QrCode.stop().catch(err => console.error("Failed to stop existing QR scanner:", err));
1376
- }
1377
  html5QrCode = new Html5Qrcode("qr-reader");
1378
  const qrCodeSuccessCallback = (decodedText, decodedResult) => {
1379
  hideScanner();
1380
- // Basic TON address validation
1381
- if (decodedText && decodedText.length > 40 && (decodedText.startsWith('EQ') || decodedText.startsWith('UQ'))) {
1382
- showProfile(decodedText);
1383
- } else {
1384
- showStatus('Отсканирован недействительный QR-код TON.', 'error');
1385
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1386
  };
1387
- const config = { fps: 10, qrbox: { width: 250, height: 250 }, aspectRatio: 1.0 };
 
1388
  html5QrCode.start({ facingMode: "environment" }, config, qrCodeSuccessCallback)
1389
  .catch(err => {
 
1390
  showStatus('Не удалось запустить сканер. Проверьте разрешения камеры.', 'error');
1391
- console.error("QR Scanner start error:", err);
1392
- hideScanner();
1393
  });
1394
  };
1395
 
@@ -1399,84 +1319,132 @@ def index():
1399
  }
1400
  scannerModal.style.display = 'none';
1401
  };
1402
-
1403
- const loadBrowserUrl = () => {
1404
- let url = browserUrlInput.value.trim();
1405
- if (!url) return;
1406
-
1407
- // Simple check if it looks like a domain, otherwise assume search
1408
- if (!url.includes('.') || url.startsWith(' ') || url.endsWith(' ')) {
1409
- url = 'https://www.google.com/search?q=' + encodeURIComponent(url);
1410
- } else if (!url.startsWith('http://') && !url.startsWith('https://')) {
1411
- url = 'https://' + url; // Default to https
1412
- }
1413
- browserIframe.src = url;
1414
- browserUrlInput.value = url; // Update input to full URL
1415
- };
1416
 
1417
- document.getElementById('profile-close-btn').addEventListener('click', () => profileModal.style.display = 'none');
1418
- document.getElementById('scanner-close-btn').addEventListener('click', hideScanner);
1419
- document.getElementById('back-to-list-btn').addEventListener('click', () => switchView('chats')); // Back button goes to chats view
1420
-
1421
- // Navbar event listeners
1422
- navbarItems.forEach(item => {
1423
- item.addEventListener('click', () => {
1424
- const viewName = item.dataset.view;
1425
- if (viewName) {
1426
- switchView(viewName);
1427
- }
1428
- });
1429
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1430
 
1431
- // Browser URL bar event listener
1432
- document.getElementById('browser-go-btn').addEventListener('click', loadBrowserUrl);
1433
- browserUrlInput.addEventListener('keypress', function(event) {
1434
- if (event.key === 'Enter') {
1435
- event.preventDefault();
1436
- loadBrowserUrl();
1437
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1438
  });
1439
 
1440
 
1441
  const handleResize = () => {
1442
- const isDesktop = window.innerWidth >= 768;
1443
- const isChatActive = activeChat.style.display === 'flex';
1444
- const currentViewName = mainContentArea.dataset.activeView || 'chats'; // Default to chats
1445
-
1446
- // Manage back button visibility
1447
- document.getElementById('back-to-list-btn').style.display = isDesktop ? 'none' : (isChatActive ? 'block' : 'none');
1448
-
1449
- // Manage view visibility based on desktop/mobile and active chat state
1450
- if (isDesktop) {
1451
- chatroomListView.style.display = 'flex';
1452
- usersListView.style.display = (currentViewName === 'users') ? 'flex' : 'none';
1453
- browserView.style.display = (currentViewName === 'browser') ? 'flex' : 'none';
1454
- chatWindowView.style.display = (currentViewName === 'chats') ? 'flex' : 'none';
1455
-
1456
- // On desktop, chat window is always visible when chats view is active, regardless of selected chat
1457
- if (currentViewName === 'chats') chatWindowView.style.display = 'flex';
1458
-
1459
- } else { // Mobile layout
1460
- // Hide all content views initially
1461
- document.querySelectorAll('.app-view-content').forEach(view => view.style.display = 'none');
1462
-
1463
- // Show the active view
1464
- if (isChatActive && currentViewName === 'chats') {
1465
- chatWindowView.style.display = 'flex';
1466
  } else {
1467
- switch (currentViewName) {
1468
- case 'chats': chatroomListView.style.display = 'flex'; break;
1469
- case 'users': usersListView.style.display = 'flex'; break;
1470
- case 'browser': browserView.style.display = 'flex'; break;
1471
- // Profile and Scan are modals, not main content views
1472
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1473
  }
1474
  }
1475
  };
1476
 
1477
- window.addEventListener('resize', handleResize);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1478
 
 
 
 
1479
 
 
1480
  tonConnectUI.onStatusChange(wallet => {
1481
  if (wallet) {
1482
  const address = TON_CONNECT_UI.toUserFriendlyAddress(wallet.account.address, false);
@@ -1486,33 +1454,39 @@ def index():
1486
  appView.style.display = 'none';
1487
  loginView.style.display = 'flex';
1488
  if (messagePollingInterval) clearInterval(messagePollingInterval);
1489
- messagePollingInterval = null;
1490
  activeChatroomId = null;
1491
- // Clear UI elements
1492
- document.getElementById('chatroom-list').innerHTML = '';
1493
- document.getElementById('users-list').innerHTML = '';
1494
- document.getElementById('messages-container').innerHTML = '';
1495
- chatPlaceholder.style.display = 'flex';
1496
- activeChat.style.display = 'none';
1497
- document.getElementById('user-wallet').textContent = '';
1498
- document.getElementById('user-nickname').textContent = '';
1499
- document.getElementById('username-input').value = '';
1500
-
1501
  }
1502
  });
1503
 
1504
- // Check initial TON Connect status
1505
- if (tonConnectUI.connected) {
1506
- const address = TON_CONNECT_UI.toUserFriendlyAddress(tonConnectUI.wallet.account.address, false);
1507
- initializeUser(address);
1508
- } else {
1509
- // Show login view by default if not connected
1510
- loginView.style.display = 'flex';
1511
- appView.style.display = 'none';
1512
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1513
 
1514
- // Initial view setup and resize handling
1515
- handleResize();
1516
  });
1517
  </script>
1518
  </body>
@@ -1520,6 +1494,7 @@ def index():
1520
  '''
1521
  return Response(html_content, mimetype='text/html')
1522
 
 
1523
  @app.route('/api/user_data', methods=['POST'])
1524
  def get_user_data():
1525
  data = request.get_json()
@@ -1527,22 +1502,27 @@ def get_user_data():
1527
  if not address:
1528
  return jsonify({'error': 'Address is required'}), 400
1529
  db = read_db()
1530
- user_info = db['users'].get(address)
1531
- username = user_info.get('username') if user_info else None
 
 
1532
  return jsonify({'username': username})
1533
 
1534
  @app.route('/api/users', methods=['GET'])
1535
  def get_users():
1536
  db = read_db()
1537
  users_list = []
1538
- for address, user_data in db.get('users', {}).items():
 
 
 
1539
  users_list.append({
1540
  'address': address,
1541
- 'username': user_data.get('username')
 
1542
  })
1543
- # Sort users alphabetically by username or address
1544
- users_list.sort(key=lambda x: x['username'] if x['username'] else x['address'])
1545
- return jsonify({'users': users_list})
1546
 
1547
 
1548
  @app.route('/api/set_username', methods=['POST'])
@@ -1566,13 +1546,13 @@ def set_username():
1566
  def get_chatrooms():
1567
  db = read_db()
1568
  chatrooms_list = []
1569
- for room_id, room_data in db.get('chatrooms', {}).items():
1570
  chatrooms_list.append({
1571
  'id': room_id,
1572
  'name': room_data['name'],
1573
- 'is_private': room_data.get('is_private', False)
1574
  })
1575
- return jsonify({'chatrooms': sorted(chatrooms_list, key=lambda x: x['name'])})
1576
 
1577
  @app.route('/api/create_chatroom', methods=['POST'])
1578
  def create_chatroom():
@@ -1582,15 +1562,16 @@ def create_chatroom():
1582
  creator_address = data.get('creator_address')
1583
  if not name or not creator_address:
1584
  return jsonify({'error': 'Name and creator address are required'}), 400
1585
- if not name.strip():
1586
- return jsonify({'error': 'Chatroom name cannot be empty'}), 400
 
 
1587
 
1588
 
1589
  db = read_db()
1590
  room_id = str(uuid.uuid4())
1591
  db['chatrooms'][room_id] = {
1592
- 'id': room_id,
1593
- 'name': name.strip(),
1594
  'creator': creator_address,
1595
  'is_private': bool(password),
1596
  'password_hash': generate_password_hash(password) if password else None
@@ -1608,22 +1589,29 @@ def join_chatroom():
1608
  chatroom = db['chatrooms'].get(chatroom_id)
1609
  if not chatroom:
1610
  return jsonify({'error': 'Chatroom not found'}), 404
1611
- if chatroom.get('is_private', False):
1612
- if not password or not chatroom.get('password_hash') or not check_password_hash(chatroom['password_hash'], password):
1613
  return jsonify({'error': 'Invalid password'}), 403
1614
  return jsonify({'success': True})
1615
 
1616
  @app.route('/api/messages/<chatroom_id>', methods=['GET'])
1617
  def get_messages(chatroom_id):
1618
  db = read_db()
1619
- room_messages = db['messages'].get(chatroom_id, [])
1620
-
 
1621
  messages_with_names = []
 
 
 
 
 
1622
  for msg in room_messages:
1623
  sender_address = msg['sender_address']
1624
- user_info = db['users'].get(sender_address)
1625
- display_name = (user_info.get('username') if user_info and user_info.get('username')
1626
- else f"{sender_address[:4]}...{sender_address[-4:]}")
 
1627
 
1628
  msg_copy = msg.copy()
1629
  msg_copy['display_name'] = display_name
@@ -1636,10 +1624,13 @@ def send_message():
1636
  data = request.get_json()
1637
  chatroom_id = data.get('chatroom_id')
1638
  sender_address = data.get('sender_address')
1639
- text = data.get('text')
 
 
 
 
 
1640
 
1641
- if not all([chatroom_id, sender_address, text]):
1642
- return jsonify({'error': 'Missing data'}), 400
1643
 
1644
  db = read_db()
1645
  if chatroom_id not in db['messages']:
@@ -1651,8 +1642,11 @@ def send_message():
1651
  'text': text,
1652
  'timestamp': datetime.utcnow().isoformat() + "Z"
1653
  }
1654
-
1655
- if len(db['messages'][chatroom_id]) >= 100:
 
 
 
1656
  db['messages'][chatroom_id].pop(0)
1657
 
1658
  db['messages'][chatroom_id].append(message)
@@ -1662,4 +1656,4 @@ def send_message():
1662
 
1663
  if __name__ == '__main__':
1664
  init_db()
1665
- app.run(host='0.0.0.0', port=7860)
 
22
  try:
23
  with open(DB_FILE, 'r') as f:
24
  return json.load(f)
25
+ except (FileNotFoundError, json.JSONDecodeError):
26
  init_db()
27
  return read_db()
 
 
 
 
 
 
28
 
29
  def write_db(data):
30
  with open(DB_FILE, 'w') as f:
 
59
  --success-color: #28a745;
60
  --error-color: #dc3545;
61
  --font-family: 'Inter', sans-serif;
62
+ --nav-height: 60px;
63
  }
64
 
65
  * {
 
83
  display: flex;
84
  align-items: center;
85
  justify-content: center;
86
+ position: relative; /* Needed for fixed bottom nav */
87
  }
88
 
89
  .main-container {
 
128
  display: none;
129
  width: 100%;
130
  height: 100%;
131
+ overflow: hidden; /* Prevent content overflow behind nav */
132
  }
133
+
134
+ #main-content-area {
135
  flex-grow: 1;
 
136
  display: flex;
137
+ overflow: hidden;
138
+ padding-bottom: var(--nav-height); /* Add padding for bottom nav */
139
  }
140
 
141
+ #left-panel {
 
142
  display: flex;
143
  flex-direction: column;
144
  height: 100%;
145
+ width: 100%;
 
146
  background-color: var(--bg-secondary);
147
+ flex-shrink: 0; /* Prevent shrinking on desktop */
148
  }
149
 
 
 
 
 
 
 
 
 
 
 
150
  .list-header {
151
  padding: 16px;
152
  border-bottom: 1px solid var(--border-color);
 
158
  align-items: center;
159
  margin-bottom: 16px;
160
  }
161
+ .list-header-title {
162
  font-size: 1.5rem;
163
  font-weight: 600;
164
  }
165
+
166
+ .header-tabs {
167
+ display: flex;
168
+ gap: 8px;
169
+ }
170
+ .header-tab-button {
171
+ background: none;
172
+ border: none;
173
+ color: var(--text-secondary);
174
+ padding: 8px 12px;
175
+ border-radius: 6px;
176
+ cursor: pointer;
177
+ font-weight: 500;
178
+ transition: color 0.2s ease, background-color 0.2s ease;
179
+ }
180
+ .header-tab-button.active {
181
+ color: var(--text-primary);
182
+ background-color: var(--bg-tertiary);
183
+ }
184
+
185
+ .user-profile {
186
  padding: 12px;
187
  background-color: var(--bg-tertiary);
188
  border-radius: 8px;
189
  margin-bottom: 16px;
 
190
  }
191
+ .user-profile div {
192
  font-size: 0.9rem;
193
  color: var(--text-secondary);
194
  margin-bottom: 8px;
195
+ word-break: break-all;
196
  }
197
+ .user-profile div:last-child { margin-bottom: 0; }
198
 
199
+ .username-form { display: flex; gap: 8px; margin-top: 12px;}
200
  .username-input {
201
  flex-grow: 1;
202
  background-color: var(--bg-primary);
 
231
  font-size: 0.9rem;
232
  }
233
 
234
+ #chatroom-list, #user-list {
 
235
  flex-grow: 1;
236
  overflow-y: auto;
237
+ -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
238
  }
239
+
240
+ #user-list { display: none; }
241
+
242
+ .list-item {
243
  display: flex;
244
  align-items: center;
245
  gap: 12px;
 
248
  border-bottom: 1px solid var(--border-color);
249
  transition: background-color 0.2s ease;
250
  }
251
+ .list-item:hover { background-color: var(--bg-hover); }
 
252
 
253
  .avatar {
254
  width: 40px;
 
260
  font-weight: 600;
261
  color: white;
262
  flex-shrink: 0;
263
+ background-color: #555; /* Default color */
 
 
 
 
 
264
  }
265
+ .avatar-content {
266
+ flex-grow: 1;
267
+ overflow: hidden;
268
+ }
269
+ .list-item-name {
270
  font-weight: 500;
271
  white-space: nowrap;
272
  overflow: hidden;
273
  text-overflow: ellipsis;
274
  }
275
+ .list-item-address {
276
+ font-size: 0.8rem;
277
  color: var(--text-secondary);
278
  white-space: nowrap;
279
  overflow: hidden;
 
286
  flex-shrink: 0;
287
  }
288
 
289
+ #right-panel {
290
+ display: none; /* Hidden by default on mobile */
291
+ flex-direction: column;
292
+ height: 100%;
293
+ width: 100%;
294
+ background-color: var(--bg-primary);
295
+ }
296
+
297
  .chat-header {
298
  display: flex;
299
  align-items: center;
 
307
  background: none;
308
  border: none;
309
  cursor: pointer;
310
+ padding: 0;
311
  }
312
  .back-btn svg {
313
  width: 24px;
 
326
  display: flex;
327
  flex-direction: column;
328
  gap: 12px;
329
+ -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
330
  }
331
 
332
  .message {
 
338
  width: 36px;
339
  height: 36px;
340
  align-self: flex-end;
 
341
  }
342
  .message-content {
343
  display: flex;
 
414
  }
415
  .send-btn svg { width: 20px; height: 20px; fill: white; }
416
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
  .modal-overlay {
418
  position: fixed;
419
  top: 0;
 
439
  }
440
  .modal-content h3 { margin-bottom: 20px; font-weight: 600; font-size: 1.3rem; }
441
  .modal-content label { display: block; margin-bottom: 8px; font-size: 0.9rem; color: var(--text-secondary); }
442
+ .modal-content input {
443
  width: 100%;
444
  padding: 12px;
445
  margin-bottom: 16px;
 
450
  font-size: 1rem;
451
  }
452
  .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
 
453
  .modal-btn {
454
  padding: 10px 20px;
455
  border-radius: 6px;
456
  border: none;
457
  cursor: pointer;
458
  font-weight: 500;
 
459
  }
460
  .secondary-btn { background-color: var(--bg-hover); color: white; }
461
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
  #status-bar {
463
  position: fixed;
464
+ bottom: calc(var(--nav-height) + 10px); /* Position above nav */
465
  left: 50%;
466
  transform: translateX(-50%);
467
  background-color: var(--bg-tertiary);
 
479
  #status-bar.error { background-color: var(--error-color); }
480
  #status-bar.visible { opacity: 1; visibility: visible; }
481
 
482
+ #bottom-nav {
483
  position: fixed;
484
  bottom: 0;
485
  left: 0;
486
  width: 100%;
487
+ height: var(--nav-height);
488
  background-color: var(--bg-secondary);
489
  border-top: 1px solid var(--border-color);
490
  display: flex;
491
  justify-content: space-around;
492
  align-items: center;
493
+ z-index: 999;
494
  }
495
+ .nav-item {
496
  display: flex;
497
  flex-direction: column;
498
  align-items: center;
499
  justify-content: center;
 
 
500
  color: var(--text-secondary);
501
+ font-size: 0.7rem;
502
+ font-weight: 500;
503
+ text-decoration: none;
504
  cursor: pointer;
505
+ flex-grow: 1;
506
+ height: 100%;
507
  transition: color 0.2s ease;
508
  }
509
+ .nav-item.scan-qr {
510
+ flex-grow: 0;
511
+ width: 60px;
512
+ height: 60px;
 
 
 
 
 
 
 
 
 
 
 
 
 
513
  background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
514
  border-radius: 50%;
515
+ margin-bottom: env(safe-area-inset-bottom); /* Push up from bottom edge */
516
+ box-shadow: 0 4px 10px rgba(0, 136, 204, 0.4);
517
  color: white;
518
+ position: relative;
519
+ top: -10px;
520
+ transition: box-shadow 0.2s ease;
 
 
521
  }
522
+ .nav-item.scan-qr svg {
523
+ width: 24px;
524
+ height: 24px;
525
+ fill: white;
526
  }
527
+ .nav-item svg {
528
+ width: 20px;
529
+ height: 20px;
530
+ margin-bottom: 4px;
531
+ fill: var(--text-secondary);
532
+ transition: fill 0.2s ease;
533
+ }
534
+ .nav-item.active {
535
+ color: var(--accent-blue-light);
536
+ }
537
+ .nav-item.active svg {
538
+ fill: var(--accent-blue-light);
539
  }
540
 
 
 
541
  @media (min-width: 768px) {
542
  .main-container {
543
  max-width: 1100px;
 
547
  box-shadow: 0 10px 40px rgba(0,0,0,0.3);
548
  border: 1px solid var(--border-color);
549
  }
 
550
  #app-view {
551
+ flex-direction: row;
552
  }
553
+ #main-content-area {
554
+ padding-bottom: 0; /* Remove padding on desktop */
 
 
555
  }
556
+ #left-panel {
557
+ width: 360px; /* Wider left panel on desktop */
 
 
 
558
  border-right: 1px solid var(--border-color);
559
+ display: flex !important; /* Always show left panel on desktop */
560
  }
561
+ .list-header-top {
562
+ margin-bottom: 0; /* No extra space above tabs */
 
 
 
563
  }
564
+ .header-tabs {
 
 
565
  flex-grow: 1;
 
566
  }
567
+ .header-tab-button {
568
+ flex-grow: 1;
569
+ text-align: center;
570
+ padding: 8px; /* Smaller padding for tabs */
 
 
 
571
  }
572
+ .list-header .user-profile { /* Move profile below tabs */
573
+ margin-top: 16px;
574
+ margin-bottom: 0;
575
  }
576
 
577
+ #right-panel {
578
+ width: auto; /* Takes remaining width */
579
+ flex-grow: 1;
580
+ display: flex !important; /* Always show right panel on desktop */
581
+ }
582
+ .back-btn { display: none !important; } /* Hide back button on desktop */
583
+ #bottom-nav { display: none !important; } /* Hide bottom nav on desktop */
584
+ #status-bar { bottom: 20px; } /* Restore status bar position */
585
  }
586
  </style>
587
  </head>
 
595
  </div>
596
 
597
  <div id="app-view" class="main-container">
598
+ <div id="main-content-area">
599
+ <div id="left-panel">
 
600
  <div class="list-header">
601
+ <div class="header-tabs">
602
+ <button id="chats-tab" class="header-tab-button active">Чаты</button>
603
+ <button id="users-tab" class="header-tab-button">Пользователи</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
604
  </div>
605
+ <div class="user-profile">
606
+ <div id="user-wallet"></div>
607
+ <div id="user-balance"></div>
608
+ <div id="user-nickname"></div>
609
+ <form class="username-form" id="username-form">
610
+ <input type="text" id="username-input" class="username-input" placeholder="Установить никнейм" autocomplete="off">
611
+ <button type="submit" class="action-btn small">✓</button>
612
+ </form>
613
+ </div>
614
+ <div style="display: flex; justify-content: flex-end; gap: 8px; padding: 0 16px 16px 16px; border-bottom: 1px solid var(--border-color); margin: 0 -16px 0 -16px;">
615
+ <button id="create-room-show-modal" class="action-btn small">
616
+ <svg xmlns="http://www.w3.org/2000/svg" height="16" viewBox="0 0 24 24" width="16" fill="white"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
617
+ <span>Новый чат</span>
618
+ </button>
619
+ </div>
620
+ </div>
621
+ <div id="list-content-container">
622
+ <div id="chatroom-list"></div>
623
+ <div id="user-list"></div>
624
  </div>
 
625
  </div>
626
 
627
+ <div id="right-panel">
628
+ <div id="chat-placeholder" class="chat-placeholder">
629
+ <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
630
+ <h2>Выберите чат</h2>
631
+ <p>Начните общение в одном из существующих чатов или создайте свой собственный.</p>
632
+ </div>
633
+ <div id="active-chat" style="display: none; width: 100%; height: 100%; flex-direction: column;">
634
+ <div class="chat-header">
635
+ <button class="back-btn" id="back-to-list-btn">
636
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
637
+ </button>
638
+ <div id="chat-header-avatar" class="avatar"></div>
639
+ <span id="chat-header-title"></span>
640
+ </div>
641
+ <div id="messages-container"></div>
642
+ <form id="message-form" class="message-form">
643
+ <input type="text" id="message-input" placeholder="Сообщение..." autocomplete="off">
644
+ <button type="submit" class="action-btn send-btn" id="send-btn">
645
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24px" height="24px"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
646
+ </button>
647
+ </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
648
  </div>
 
649
  </div>
 
650
  </div>
651
+ </div>
652
 
653
+ <nav id="bottom-nav">
654
+ <div class="nav-item" data-target="chats">
655
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12zM5 9h14v2H5zm0 4h8v2H5z"/></svg>
656
+ <span>Чаты</span>
657
+ </div>
658
+ <div class="nav-item" data-target="users">
659
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M16 7c0 1.66-1.34 3-3 3s-3-1.34-3-3 1.34-3 3-3 3 1.34 3 3zm-9 12c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1H7v-1z"/></svg>
660
+ <span>Пользователи</span>
661
+ </div>
662
+ <div class="nav-item scan-qr" data-target="scan">
663
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M3 11h8V3H3v8zm2-6h4v4H5V5zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm8-12v8h8V3h-8zm6 6h-4V5h4v4zm-2 10a2 2 0 100-4 2 2 0 000 4z"/></svg>
 
 
 
 
 
 
 
 
 
 
 
664
  </div>
665
+ <div class="nav-item" data-target="profile">
666
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
667
+ <span>Профиль</span>
668
+ </div>
669
+ </nav>
670
 
 
 
671
  <div id="create-room-modal" class="modal-overlay">
672
  <div class="modal-content">
673
  <h3>Создать новый чат</h3>
 
699
  </div>
700
 
701
  <div id="profile-modal" class="modal-overlay">
702
+ <div class="modal-content" style="text-align: center;">
703
  <h3 id="profile-modal-title">Профиль пользователя</h3>
704
  <div id="profile-avatar-container" style="margin: 20px auto; display: inline-block;"></div>
705
  <p id="profile-username" style="font-size: 1.2rem; font-weight: 600;"></p>
706
  <p id="profile-address" style="color: var(--text-secondary); font-size: 0.9rem; word-break: break-all; margin-top: 8px;"></p>
707
+ <p id="profile-balance" style="color: var(--accent-blue-light); font-size: 0.9rem; margin-top: 8px;"></p>
708
  <div id="profile-qr-code" style="background: white; padding: 10px; margin: 20px auto; width: fit-content; border-radius: 8px;"></div>
709
+ <p style="text-align: center; color: var(--text-secondary); font-size: 0.8rem; margin-top: -10px; margin-bottom: 20px;">Отсканируйте для открытия профиля</p>
710
+ <div class="modal-actions" style="flex-direction: column; gap: 12px; align-items: stretch;">
711
+ <button id="send-ton-btn" class="modal-btn action-btn" style="display: none;">Отправить TON</button>
712
  <button id="profile-close-btn" class="modal-btn secondary-btn">Закрыть</button>
713
  </div>
714
  </div>
 
717
  <div id="scanner-modal" class="modal-overlay">
718
  <div class="modal-content">
719
  <h3>Сканировать QR-код</h3>
720
+ <div id="qr-reader" style="width: 100%; border: 1px solid var(--border-color); margin-top: 16px; border-radius: 8px; overflow: hidden;"></div>
721
  <div class="modal-actions">
722
  <button id="scanner-close-btn" class="modal-btn secondary-btn">Отмена</button>
723
  </div>
 
730
  document.addEventListener('DOMContentLoaded', () => {
731
  const tonConnectUI = new TON_CONNECT_UI.TonConnectUI({
732
  manifestUrl: 'https://huggingface.co/spaces/Aleksmorshen/MorshenGroup/resolve/main/tonconnect-manifest.json',
733
+ buttonRootId: 'ton-connect-button',
734
+ actionsConfiguration: {
735
+ twaReturnUrl: 'https://huggingface.co/spaces/Aleksmorshen/MorshenGroup' // Replace with your actual TWA URL if deployed there
736
+ }
737
  });
738
 
739
  let currentUser = { address: null, username: null, balance: null };
740
  let activeChatroomId = null;
741
  let messagePollingInterval = null;
742
  let chatroomsData = {};
743
+ let usersData = {};
744
  let html5QrCode = null;
745
  let profileQrCode = null;
746
 
747
  const loginView = document.getElementById('login-view');
748
  const appView = document.getElementById('app-view');
749
+ const leftPanel = document.getElementById('left-panel');
750
+ const rightPanel = document.getElementById('right-panel');
 
 
 
751
  const chatPlaceholder = document.getElementById('chat-placeholder');
752
  const activeChat = document.getElementById('active-chat');
753
  const profileModal = document.getElementById('profile-modal');
754
  const scannerModal = document.getElementById('scanner-modal');
755
+ const bottomNav = document.getElementById('bottom-nav');
756
+ const chatroomList = document.getElementById('chatroom-list');
757
+ const userListEl = document.getElementById('user-list'); // Renamed to avoid conflict
758
+ const chatsTab = document.getElementById('chats-tab');
759
+ const usersTab = document.getElementById('users-tab');
760
+
761
 
762
  const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292'];
763
 
 
788
  const response = await fetch(endpoint, options);
789
  if (!response.ok) {
790
  const errorData = await response.json().catch(() => ({ error: 'Request failed with status ' + response.status }));
791
+ throw new Error(errorData.error || `Request failed with status ${response.status}`);
792
  }
793
  if (response.status === 204) return null;
794
  return await response.json();
 
802
 
803
  const updateUserInfo = () => {
804
  document.getElementById('user-wallet').textContent = `Кошелек: ${truncateAddress(currentUser.address)}`;
805
+ document.getElementById('user-balance').textContent = `Баланс: ${currentUser.balance !== null ? currentUser.balance.toFixed(2) + ' TON' : '...loading'}`;
806
  const nicknameEl = document.getElementById('user-nickname');
807
  const usernameInput = document.getElementById('username-input');
808
  nicknameEl.textContent = currentUser.username ? `Ник: ${currentUser.username}` : `Никнейм не установлен`;
809
  usernameInput.value = currentUser.username || '';
810
  };
811
+
812
+ const fetchBalance = async () => {
813
+ if (tonConnectUI.account) {
814
+ try {
815
+ const balance = await tonConnectUI.account.balance; // Balance in nanoton
816
+ currentUser.balance = balance / 1_000_000_000; // Convert to TON
817
+ updateUserInfo();
818
+ } catch (e) {
819
+ console.error("Failed to fetch balance:", e);
820
+ currentUser.balance = 'error';
821
+ updateUserInfo();
822
+ }
823
+ }
824
+ };
825
 
826
  document.getElementById('username-form').addEventListener('submit', async (e) => {
827
  e.preventDefault();
 
839
  currentUser.username = newUsername;
840
  updateUserInfo();
841
  showStatus('Никнейм успешно обновлен!', 'success');
842
+ fetchChatrooms(); // Update chat list in case username is used there
843
+ fetchUsers(); // Update user list
844
+ if (activeChatroomId) fetchMessages(activeChatroomId); // Update messages if current user sent them
845
  } catch (err) {}
846
  });
847
 
848
  const initializeUser = async (address) => {
849
  currentUser.address = address;
850
+ fetchBalance(); // Fetch balance on connection
 
 
 
 
 
 
 
 
 
 
 
851
  try {
852
  const data = await apiCall('/api/user_data', {
853
  method: 'POST',
 
861
  updateUserInfo();
862
  loginView.style.display = 'none';
863
  appView.style.display = 'flex';
 
 
864
  fetchChatrooms();
865
+ fetchUsers();
866
+ handleResize(); // Apply correct layout based on screen size
867
  };
868
 
869
  const renderChatrooms = (rooms) => {
870
+ chatroomList.innerHTML = '';
 
871
  chatroomsData = {};
872
+ if (rooms.length === 0) {
873
+ chatroomList.innerHTML = '<div class="chat-placeholder" style="height: auto; margin-top: 20px;"><p>Пока нет чатов. Создайте первый!</p></div>';
874
+ }
875
  rooms.forEach(room => {
876
  chatroomsData[room.id] = room;
877
  const item = document.createElement('div');
878
+ item.className = 'list-item chatroom-item';
879
  item.dataset.id = room.id;
880
 
881
  item.appendChild(getAvatar(room.name));
882
 
883
  const infoDiv = document.createElement('div');
884
+ infoDiv.className = 'avatar-content';
885
  const nameSpan = document.createElement('div');
886
+ nameSpan.className = 'list-item-name';
887
  nameSpan.textContent = room.name;
888
  infoDiv.appendChild(nameSpan);
889
  item.appendChild(infoDiv);
 
895
  }
896
 
897
  item.addEventListener('click', () => selectChatroom(room.id, room.is_private));
898
+ chatroomList.appendChild(item);
899
  });
900
  };
 
 
 
 
 
 
 
901
 
902
  const renderUsers = (users) => {
903
+ userListEl.innerHTML = '';
904
+ usersData = {};
905
+ if (users.length === 0) {
906
+ userListEl.innerHTML = '<div class="chat-placeholder" style="height: auto; margin-top: 20px;"><p>Пользователи не найдены.</p></div>';
907
+ }
908
  users.forEach(user => {
909
+ usersData[user.address] = user;
910
  const item = document.createElement('div');
911
+ item.className = 'list-item user-item';
912
  item.dataset.address = user.address;
913
 
914
+ item.appendChild(getAvatar(user.display_name));
915
 
916
  const infoDiv = document.createElement('div');
917
+ infoDiv.className = 'avatar-content';
918
+ const nameSpan = document.createElement('div');
919
+ nameSpan.className = 'list-item-name';
920
+ nameSpan.textContent = user.display_name;
921
+ infoDiv.appendChild(nameSpan);
 
922
 
923
  const addressSpan = document.createElement('div');
924
+ addressSpan.className = 'list-item-address';
925
  addressSpan.textContent = user.address;
926
  infoDiv.appendChild(addressSpan);
927
 
928
  item.appendChild(infoDiv);
929
 
930
  item.addEventListener('click', () => showProfile(user.address));
931
+ userListEl.appendChild(item);
932
  });
933
  };
934
+
935
+ const fetchChatrooms = async () => {
936
+ try {
937
+ const data = await apiCall('/api/chatrooms');
938
+ renderChatrooms(data.chatrooms);
939
+ } catch (err) {}
940
+ };
941
 
942
  const fetchUsers = async () => {
943
  try {
 
979
  container.appendChild(msgDiv);
980
  });
981
 
982
+ if(shouldScroll || messages.length > 0) { // Scroll to bottom on first load or if already near bottom
983
  container.scrollTop = container.scrollHeight;
984
  }
985
  };
 
990
  const data = await apiCall(`/api/messages/${roomId}`);
991
  renderMessages(data.messages);
992
  } catch (err) {
993
+ console.error("Failed to fetch messages:", err);
994
  }
995
  };
996
 
997
+ const showPanel = (panelId) => {
998
+ if (window.innerWidth < 768) {
999
+ // On mobile, panels switch
1000
+ leftPanel.style.display = 'none';
1001
+ rightPanel.style.display = 'none';
1002
+ if (panelId === 'left-panel') {
1003
+ leftPanel.style.display = 'flex';
1004
+ } else if (panelId === 'right-panel') {
1005
+ rightPanel.style.display = 'flex';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1006
  }
1007
+ } else {
1008
+ // On desktop, both are usually visible, only content changes
1009
+ leftPanel.style.display = 'flex';
1010
+ rightPanel.style.display = 'flex';
1011
+ }
1012
+ // Update active nav item
1013
+ document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active'));
1014
+ if (panelId === 'left-panel') {
1015
+ const activeTab = document.querySelector('.header-tab-button.active');
1016
+ const target = activeTab.dataset.target;
1017
+ document.querySelector(`.nav-item[data-target="${target}"]`).classList.add('active');
1018
+ } else if (panelId === 'right-panel' && activeChatroomId) {
1019
+ document.querySelector('.nav-item[data-target="chats"]').classList.add('active');
1020
+ }
1021
+ };
1022
 
1023
+ const switchLeftPanelContent = (contentType) => {
1024
+ chatroomList.style.display = 'none';
1025
+ userListEl.style.display = 'none';
1026
+
1027
+ chatsTab.classList.remove('active');
1028
+ usersTab.classList.remove('active');
1029
+
1030
+ if (contentType === 'chats') {
1031
+ chatroomList.style.display = 'flex';
1032
+ chatsTab.classList.add('active');
1033
+ fetchChatrooms();
1034
+ } else if (contentType === 'users') {
1035
+ userListEl.style.display = 'flex';
1036
+ usersTab.classList.add('active');
1037
+ fetchUsers();
1038
+ }
1039
+
1040
+ // Update bottom nav active state based on current left panel tab
1041
+ if (window.innerWidth < 768) {
1042
+ document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active'));
1043
+ document.querySelector(`.nav-item[data-target="${contentType}"]`).classList.add('active');
1044
+ }
1045
+ };
1046
 
 
 
 
 
 
 
 
 
 
 
 
 
1047
 
1048
+ const selectChatroom = (roomId, isPrivate) => {
1049
  const roomData = chatroomsData[roomId];
1050
+ if (!roomData) return;
 
 
 
1051
 
1052
  const proceedToRoom = () => {
1053
+ if (messagePollingInterval) clearInterval(messagePollingInterval);
1054
+ activeChatroomId = roomId;
1055
+
1056
+ document.getElementById('chat-header-title').textContent = roomData.name;
1057
+ const headerAvatar = document.getElementById('chat-header-avatar');
1058
+ headerAvatar.innerHTML = '';
1059
+ headerAvatar.appendChild(getAvatar(roomData.name));
 
 
 
 
 
 
 
 
 
 
 
1060
 
1061
+ chatPlaceholder.style.display = 'none';
1062
+ activeChat.style.display = 'flex';
1063
+
1064
+ showPanel('right-panel'); // Switch to chat view
1065
 
1066
+ fetchMessages(roomId);
1067
+ messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000);
 
 
 
 
 
 
 
 
1068
  };
1069
 
1070
+ if (isPrivate) {
1071
  const passwordModal = document.getElementById('password-modal');
1072
  const passwordForm = document.getElementById('password-form');
1073
  const passwordInput = document.getElementById('password-input');
 
1077
 
1078
  const formSubmitHandler = async (e) => {
1079
  e.preventDefault();
1080
+ passwordForm.removeEventListener('submit', formSubmitHandler); // Remove listener immediately
1081
  const password = passwordInput.value;
1082
  passwordModal.style.display = 'none';
1083
  try {
 
1088
  });
1089
  proceedToRoom();
1090
  } catch (err) {
1091
+ // Error handled by apiCall
1092
+ // Re-show password modal or handle error state if needed, for now just close
1093
  }
1094
  };
1095
  passwordForm.addEventListener('submit', formSubmitHandler);
1096
 
1097
  document.getElementById('password-cancel').onclick = () => {
1098
  passwordModal.style.display = 'none';
1099
+ passwordForm.removeEventListener('submit', formSubmitHandler); // Ensure listener is removed on cancel
1100
  };
1101
  } else {
1102
  proceedToRoom();
 
1109
  const sendBtn = document.getElementById('send-btn');
1110
  const text = input.value.trim();
1111
  if (text && activeChatroomId && currentUser.address) {
1112
+ const messageText = text;
1113
  input.value = '';
1114
  input.disabled = true;
1115
  sendBtn.disabled = true;
 
1120
  body: JSON.stringify({
1121
  chatroom_id: activeChatroomId,
1122
  sender_address: currentUser.address,
1123
+ text: messageText
1124
  })
1125
  });
1126
+ // Optimistically add message before fetching
1127
+ renderMessages([...document.getElementById('messages-container').children].map(el => {
1128
+ // Basic parsing for optimistic update - this is simplified
1129
+ return {
1130
+ sender_address: el.classList.contains('sent') ? currentUser.address : 'other', // Simplified check
1131
+ display_name: el.querySelector('.message-sender')?.textContent || '...',
1132
+ text: el.querySelector('.message-bubble')?.textContent || '...',
1133
+ timestamp: new Date().toISOString()
1134
+ };
1135
+ }), { sender_address: currentUser.address, display_name: currentUser.username || truncateAddress(currentUser.address), text: messageText, timestamp: new Date().toISOString() });
1136
+
1137
+ await fetchMessages(activeChatroomId); // Fetch to get official state and other messages
1138
+ document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight;
1139
  } finally {
1140
  input.disabled = false;
1141
  sendBtn.disabled = false;
 
1157
  const name = document.getElementById('room-name').value.trim();
1158
  const password = document.getElementById('room-password').value;
1159
  if (!name) {
1160
+ showStatus('Название чата не может быть пустым.', 'error');
 
 
 
 
1161
  return;
1162
+ };
1163
 
1164
  try {
1165
  await apiCall('/api/create_chatroom', {
 
1182
  body: JSON.stringify({ address: address })
1183
  });
1184
 
1185
+ const isCurrentUser = (address === currentUser.address);
1186
  const username = userData.username || `User ${truncateAddress(address)}`;
1187
+
1188
+ document.getElementById('profile-modal-title').textContent = isCurrentUser ? 'Мой профиль' : 'Профиль пользователя';
1189
+
1190
  const avatarContainer = document.getElementById('profile-avatar-container');
1191
  const usernameEl = document.getElementById('profile-username');
1192
  const addressEl = document.getElementById('profile-address');
 
1199
 
1200
  usernameEl.textContent = username;
1201
  addressEl.textContent = address;
1202
+
1203
+ // Show balance only for the connected user's profile
1204
+ if (isCurrentUser) {
1205
+ balanceEl.style.display = 'block';
1206
+ balanceEl.textContent = `Баланс: ${currentUser.balance !== null ? currentUser.balance.toFixed(2) + ' TON' : '...loading'}`;
1207
+ if (currentUser.balance === 'error') balanceEl.textContent = 'Баланс: ошибка загрузки';
1208
  } else {
1209
  balanceEl.style.display = 'none';
1210
+ balanceEl.textContent = ''; // Clear content
1211
  }
1212
+
1213
  qrCodeEl.innerHTML = '';
1214
  if (profileQrCode) {
1215
+ profileQrCode.clear(); // Clear previous QR code
 
1216
  }
1217
  profileQrCode = new QRCode(qrCodeEl, {
1218
  text: address,
 
1224
  });
1225
 
1226
  sendTonBtn.onclick = async () => {
1227
+ if (!tonConnectUI.connected) {
1228
+ showStatus('Подключите кошелек для отправки TON.', 'error');
1229
+ return;
1230
+ }
1231
+ if (isCurrentUser) { // Should not happen if button is hidden, but safety check
1232
+ showStatus('Вы не можете отправить TON себе.', 'error');
1233
  return;
1234
+ }
1235
+ const amountString = prompt(`Введите сумму в TON для отправки ${username} (${truncateAddress(address)}):`, "0.1");
1236
  if (amountString === null) return;
1237
 
1238
  const amount = parseFloat(amountString);
 
1250
 
1251
  try {
1252
  await tonConnectUI.sendTransaction(transaction);
1253
+ showStatus(`Транзакция отправлена успешно!`, 'success');
1254
  profileModal.style.display = 'none';
1255
  } catch (error) {
1256
+ showStatus('Транзакция отклонена или произошла ошибка.', 'error');
1257
+ console.error('Send transaction failed:', error);
1258
  }
1259
  };
1260
 
1261
+ sendTonBtn.style.display = isCurrentUser ? 'none' : 'block'; // Hide "Send TON" button for own profile
1262
  profileModal.style.display = 'flex';
1263
  } catch (err) {
1264
  showStatus('Не удалось загрузить профиль.', 'error');
 
1267
 
1268
  const showScanner = () => {
1269
  scannerModal.style.display = 'flex';
1270
+ const qrReaderEl = document.getElementById('qr-reader');
1271
+ qrReaderEl.innerHTML = ''; // Clear previous content
 
 
1272
  html5QrCode = new Html5Qrcode("qr-reader");
1273
  const qrCodeSuccessCallback = (decodedText, decodedResult) => {
1274
  hideScanner();
1275
+ // Attempt to parse as a TON address or a link containing one
1276
+ try {
1277
+ let address = decodedText;
1278
+ // Basic checks for common TON address prefixes and formats
1279
+ if (decodedText.startsWith('ton://transfer/')) {
1280
+ const url = new URL(decodedText);
1281
+ address = url.pathname.replace('/transfer/', '');
1282
+ } else if (decodedText.includes('ton://')) {
1283
+ const url = new URL(decodedText);
1284
+ address = url.pathname.replace('/', '');
1285
+ }
1286
+
1287
+ // Simple validation if it looks like a TON address
1288
+ if (typeof address === 'string' && address.length > 40 && (address.startsWith('EQ') || address.startsWith('UQ') || address.endsWith(':_'))) {
1289
+ // Convert to standard format if it's raw hex like ends with :_
1290
+ if (address.endsWith(':_')) {
1291
+ // This is a raw address hex. Need TON library to convert to user-friendly.
1292
+ // For this simple app, let's stick to user-friendly formats EQ/UQ
1293
+ showStatus('Отсканирован формат адреса, который пока не поддерживается (нужен пользовательский формат EQ/UQ).', 'error');
1294
+ return;
1295
+ }
1296
+ // Attempt to show profile for the scanned address
1297
+ showProfile(address);
1298
+ } else {
1299
+ showStatus('Отсканирован недействительный QR-код адреса TON.', 'error');
1300
+ }
1301
+ } catch (e) {
1302
+ console.error("QR scan error:", e);
1303
+ showStatus('Ошибка при обработке QR-кода.', 'error');
1304
+ }
1305
  };
1306
+ const config = { fps: 10, qrbox: { width: 250, height: 250 } };
1307
+ // Request camera permission and start scanning
1308
  html5QrCode.start({ facingMode: "environment" }, config, qrCodeSuccessCallback)
1309
  .catch(err => {
1310
+ console.error("QR scanner start failed:", err);
1311
  showStatus('Не удалось запустить сканер. Проверьте разрешения камеры.', 'error');
1312
+ hideScanner(); // Ensure modal closes on error
 
1313
  });
1314
  };
1315
 
 
1319
  }
1320
  scannerModal.style.display = 'none';
1321
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1322
 
1323
+ // Event listeners for tabs/buttons
1324
+ chatsTab.addEventListener('click', () => switchLeftPanelContent('chats'));
1325
+ usersTab.addEventListener('click', () => switchLeftPanelContent('users'));
1326
+ document.getElementById('back-to-list-btn').addEventListener('click', () => {
1327
+ activeChatroomId = null;
1328
+ if (messagePollingInterval) clearInterval(messagePollingInterval);
1329
+ activeChat.style.display = 'none';
1330
+ chatPlaceholder.style.display = 'flex';
1331
+ showPanel('left-panel');
1332
+ });
1333
+ document.getElementById('profile-close-btn').addEventListener('click', () => profileModal.style.display = 'none');
1334
+ document.getElementById('scanner-close-btn').addEventListener('click', hideScanner);
1335
+
1336
+ // Event listeners for bottom navigation
1337
+ document.querySelectorAll('.nav-item').forEach(item => {
1338
+ item.addEventListener('click', () => {
1339
+ const target = item.dataset.target;
1340
+ // Hide modals first if any are open
1341
+ createRoomModal.style.display = 'none';
1342
+ passwordModal.style.display = 'none';
1343
+ profileModal.style.display = 'none';
1344
+ hideScanner();
1345
+
1346
+ if (target === 'chats') {
1347
+ switchLeftPanelContent('chats');
1348
+ showPanel('left-panel');
1349
+ // On mobile, if chat is open, close it
1350
+ if (window.innerWidth < 768 && activeChat.style.display !== 'none') {
1351
+ document.getElementById('back-to-list-btn').click(); // Use existing back logic
1352
+ } else if (window.innerWidth >= 768) {
1353
+ // On desktop, just ensure list is visible and active
1354
+ showPanel('left-panel'); // Redundant on desktop but safe
1355
+ }
1356
 
1357
+ } else if (target === 'users') {
1358
+ switchLeftPanelContent('users');
1359
+ showPanel('left-panel');
1360
+ // On mobile, if chat is open, close it
1361
+ if (window.innerWidth < 768 && activeChat.style.display !== 'none') {
1362
+ document.getElementById('back-to-list-btn').click();
1363
+ } else if (window.innerWidth >= 768) {
1364
+ // On desktop, just ensure list is visible and active
1365
+ showPanel('left-panel'); // Redundant on desktop but safe
1366
+ }
1367
+
1368
+ } else if (target === 'scan') {
1369
+ showScanner();
1370
+ } else if (target === 'profile') {
1371
+ if (currentUser.address) {
1372
+ showProfile(currentUser.address);
1373
+ } else {
1374
+ showStatus('Подключите кошелек, чтобы увидеть профиль.', 'info');
1375
+ }
1376
+ }
1377
+ // Active state is updated within showPanel and switchLeftPanelContent
1378
+ });
1379
  });
1380
 
1381
 
1382
  const handleResize = () => {
1383
+ const isMobile = window.innerWidth < 768;
1384
+
1385
+ // Toggle back button visibility
1386
+ document.getElementById('back-to-list-btn').style.display = isMobile ? 'block' : 'none';
1387
+
1388
+ // Toggle bottom nav visibility
1389
+ bottomNav.style.display = isMobile ? 'flex' : 'none';
1390
+
1391
+ // Adjust panel visibility based on active state and screen size
1392
+ if (isMobile) {
1393
+ if (activeChat.style.display === 'flex') {
1394
+ // Mobile, chat is active
1395
+ leftPanel.style.display = 'none';
1396
+ rightPanel.style.display = 'flex';
1397
+ document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active'));
1398
+ document.querySelector('.nav-item[data-target="chats"]').classList.add('active'); // Chat is part of chats flow
 
 
 
 
 
 
 
 
1399
  } else {
1400
+ // Mobile, list (chats or users) is active
1401
+ rightPanel.style.display = 'none';
1402
+ leftPanel.style.display = 'flex';
1403
+ // Nav active state updated by switchLeftPanelContent
1404
+ }
1405
+ // Add padding to content containers to avoid being hidden by bottom nav
1406
+ chatroomList.style.paddingBottom = userListEl.style.paddingBottom = messagesContainer.style.paddingBottom = `${varNames.navHeightValue}px`;
1407
+ document.getElementById('status-bar').style.bottom = `calc(${varNames.navHeightValue}px + 10px)`;
1408
+
1409
+ } else {
1410
+ // Desktop
1411
+ leftPanel.style.display = 'flex';
1412
+ rightPanel.style.display = 'flex';
1413
+ // Remove padding added for mobile nav
1414
+ chatroomList.style.paddingBottom = userListEl.style.paddingBottom = messagesContainer.style.paddingBottom = '0';
1415
+ document.getElementById('status-bar').style.bottom = '20px';
1416
+
1417
+ // Ensure correct left panel content is shown based on tabs
1418
+ if(chatsTab.classList.contains('active')) {
1419
+ switchLeftPanelContent('chats');
1420
+ } else if (usersTab.classList.contains('active')) {
1421
+ switchLeftPanelContent('users');
1422
  }
1423
  }
1424
  };
1425
 
1426
+ // Get CSS variable value for nav height
1427
+ const getCssVariable = (name) => getComputedStyle(document.documentElement).getPropertyValue(name).trim();
1428
+ const varNames = {
1429
+ navHeight: '--nav-height'
1430
+ };
1431
+ const varValues = {};
1432
+ const updateCssVariables = () => {
1433
+ varValues.navHeightValue = parseFloat(getCssVariable(varNames.navHeight));
1434
+ };
1435
+ updateCssVariables();
1436
+
1437
+
1438
+ window.addEventListener('resize', () => {
1439
+ updateCssVariables(); // Recalculate nav height on resize
1440
+ handleResize();
1441
+ });
1442
 
1443
+ // Initial setup
1444
+ handleResize();
1445
+ switchLeftPanelContent('chats'); // Start by showing chats list
1446
 
1447
+ // TON Connect UI status change listener
1448
  tonConnectUI.onStatusChange(wallet => {
1449
  if (wallet) {
1450
  const address = TON_CONNECT_UI.toUserFriendlyAddress(wallet.account.address, false);
 
1454
  appView.style.display = 'none';
1455
  loginView.style.display = 'flex';
1456
  if (messagePollingInterval) clearInterval(messagePollingInterval);
 
1457
  activeChatroomId = null;
1458
+ activeChat.style.display = 'none';
1459
+ chatPlaceholder.style.display = 'flex';
1460
+ // Reset to default view state on disconnect
1461
+ switchLeftPanelContent('chats');
1462
+ handleResize();
 
 
 
 
 
1463
  }
1464
  });
1465
 
1466
+ // Listen for account change (e.g., balance update from wallet)
1467
+ // TonConnectUI doesn't provide a direct balance update event for the *connected* account
1468
+ // The balance property on `tonConnectUI.account` is the most reliable source.
1469
+ // We can periodically fetch it or rely on UI showing latest when modal opens.
1470
+ // Let's add a periodic fetch while connected.
1471
+ let balancePollingInterval = null;
1472
+ tonConnectUI.onStatusChange(wallet => {
1473
+ if (wallet) {
1474
+ // Start balance polling when connected
1475
+ if (!balancePollingInterval) {
1476
+ balancePollingInterval = setInterval(fetchBalance, 15000); // Poll balance every 15 seconds
1477
+ }
1478
+ } else {
1479
+ // Stop balance polling when disconnected
1480
+ if (balancePollingInterval) {
1481
+ clearInterval(balancePollingInterval);
1482
+ balancePollingInterval = null;
1483
+ }
1484
+ currentUser.balance = null; // Clear balance state
1485
+ updateUserInfo(); // Update UI
1486
+ }
1487
+ }, err => console.error("TON Connect status change error:", err));
1488
+
1489
 
 
 
1490
  });
1491
  </script>
1492
  </body>
 
1494
  '''
1495
  return Response(html_content, mimetype='text/html')
1496
 
1497
+
1498
  @app.route('/api/user_data', methods=['POST'])
1499
  def get_user_data():
1500
  data = request.get_json()
 
1502
  if not address:
1503
  return jsonify({'error': 'Address is required'}), 400
1504
  db = read_db()
1505
+ user_info = db['users'].get(address, {})
1506
+ username = user_info.get('username')
1507
+ # Balance is fetched on frontend via TON Connect UI for the connected user
1508
+ # We don't need to return balance here for any arbitrary user
1509
  return jsonify({'username': username})
1510
 
1511
  @app.route('/api/users', methods=['GET'])
1512
  def get_users():
1513
  db = read_db()
1514
  users_list = []
1515
+ # Iterate through all users who have an entry in the db
1516
+ for address, user_data in db['users'].items():
1517
+ # Use username if available, otherwise use truncated address
1518
+ display_name = user_data.get('username') if user_data.get('username') else f"{address[:6]}...{address[-6:]}" # Slightly longer truncation for clarity
1519
  users_list.append({
1520
  'address': address,
1521
+ 'display_name': display_name,
1522
+ 'has_username': bool(user_data.get('username'))
1523
  })
1524
+ # Sort alphabetically by display name
1525
+ return jsonify({'users': sorted(users_list, key=lambda x: x['display_name'].lower())})
 
1526
 
1527
 
1528
  @app.route('/api/set_username', methods=['POST'])
 
1546
  def get_chatrooms():
1547
  db = read_db()
1548
  chatrooms_list = []
1549
+ for room_id, room_data in db['chatrooms'].items():
1550
  chatrooms_list.append({
1551
  'id': room_id,
1552
  'name': room_data['name'],
1553
+ 'is_private': room_data.get('is_private', False) # Default to false
1554
  })
1555
+ return jsonify({'chatrooms': sorted(chatrooms_list, key=lambda x: x['name'].lower())})
1556
 
1557
  @app.route('/api/create_chatroom', methods=['POST'])
1558
  def create_chatroom():
 
1562
  creator_address = data.get('creator_address')
1563
  if not name or not creator_address:
1564
  return jsonify({'error': 'Name and creator address are required'}), 400
1565
+ if len(name) < 3 or len(name) > 30: # Add name length validation
1566
+ return jsonify({'error': 'Chatroom name must be between 3 and 30 characters'}), 400
1567
+ if password and len(password) < 4: # Add password length hint/validation
1568
+ return jsonify({'error': 'Password should be at least 4 characters'}), 400
1569
 
1570
 
1571
  db = read_db()
1572
  room_id = str(uuid.uuid4())
1573
  db['chatrooms'][room_id] = {
1574
+ 'name': name,
 
1575
  'creator': creator_address,
1576
  'is_private': bool(password),
1577
  'password_hash': generate_password_hash(password) if password else None
 
1589
  chatroom = db['chatrooms'].get(chatroom_id)
1590
  if not chatroom:
1591
  return jsonify({'error': 'Chatroom not found'}), 404
1592
+ if chatroom.get('is_private', False): # Default to false if key missing
1593
+ if not password or not check_password_hash(chatroom['password_hash'], password):
1594
  return jsonify({'error': 'Invalid password'}), 403
1595
  return jsonify({'success': True})
1596
 
1597
  @app.route('/api/messages/<chatroom_id>', methods=['GET'])
1598
  def get_messages(chatroom_id):
1599
  db = read_db()
1600
+ if chatroom_id not in db['messages']:
1601
+ return jsonify({'error': 'Chatroom not found'}), 404
1602
+
1603
  messages_with_names = []
1604
+ room_messages = db['messages'].get(chatroom_id, [])
1605
+
1606
+ # Fetch all necessary user data once
1607
+ user_data_map = db['users']
1608
+
1609
  for msg in room_messages:
1610
  sender_address = msg['sender_address']
1611
+ user_info = user_data_map.get(sender_address, {})
1612
+ # Use username if available, otherwise use truncated address
1613
+ display_name = (user_info.get('username') if user_info.get('username')
1614
+ else f"{sender_address[:6]}...{sender_address[-6:]}") # Consistent truncation
1615
 
1616
  msg_copy = msg.copy()
1617
  msg_copy['display_name'] = display_name
 
1624
  data = request.get_json()
1625
  chatroom_id = data.get('chatroom_id')
1626
  sender_address = data.get('sender_address')
1627
+ text = data.get('text', '').strip() # Strip whitespace
1628
+
1629
+ if not all([chatroom_id, sender_address]) or not text: # Text cannot be empty after strip
1630
+ return jsonify({'error': 'Missing data or empty message'}), 400
1631
+ if len(text) > 1000: # Add message length limit
1632
+ return jsonify({'error': 'Message is too long'}), 400
1633
 
 
 
1634
 
1635
  db = read_db()
1636
  if chatroom_id not in db['messages']:
 
1642
  'text': text,
1643
  'timestamp': datetime.utcnow().isoformat() + "Z"
1644
  }
1645
+
1646
+ # Keep message list size reasonable
1647
+ MAX_MESSAGES_PER_CHAT = 200 # Increased limit slightly
1648
+ if len(db['messages'][chatroom_id]) >= MAX_MESSAGES_PER_CHAT:
1649
+ # Remove oldest message
1650
  db['messages'][chatroom_id].pop(0)
1651
 
1652
  db['messages'][chatroom_id].append(message)
 
1656
 
1657
  if __name__ == '__main__':
1658
  init_db()
1659
+ app.run(host='0.0.0.0', port=7860, debug=True) # Debug mode for development