Aleksmorshen commited on
Commit
73ce5d8
·
verified ·
1 Parent(s): 58c8953

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +658 -649
app.py CHANGED
@@ -19,8 +19,12 @@ def init_db():
19
  }, f, indent=4)
20
 
21
  def read_db():
22
- with open(DB_FILE, 'r') as f:
23
- return json.load(f)
 
 
 
 
24
 
25
  def write_db(data):
26
  with open(DB_FILE, 'w') as f:
@@ -86,6 +90,7 @@ def index():
86
  display: flex;
87
  flex-direction: column;
88
  transition: opacity 0.3s ease;
 
89
  }
90
 
91
  #login-view {
@@ -123,32 +128,55 @@ def index():
123
  width: 100%;
124
  height: 100%;
125
  flex-direction: column;
126
- position: relative;
127
  }
128
 
129
- .app-content {
130
  flex-grow: 1;
131
- overflow-y: auto;
132
- padding-bottom: 60px; /* Space for nav bar */
 
133
  }
134
 
135
- #chatroom-list-view, #user-list-view, #browser-view, #profile-details-view {
136
- display: none;
137
  width: 100%;
138
  height: 100%;
139
- flex-direction: column;
140
- background-color: var(--bg-secondary);
 
 
 
 
 
 
 
 
141
  }
142
 
143
- .view-header {
 
 
 
 
 
 
 
 
 
 
 
 
144
  padding: 16px;
145
  border-bottom: 1px solid var(--border-color);
146
  flex-shrink: 0;
 
 
 
147
  display: flex;
148
  justify-content: space-between;
149
  align-items: center;
 
150
  }
151
- .view-header h2 {
152
  font-size: 1.5rem;
153
  font-weight: 600;
154
  }
@@ -157,7 +185,7 @@ def index():
157
  padding: 12px;
158
  background-color: var(--bg-tertiary);
159
  border-radius: 8px;
160
- margin: 16px;
161
  }
162
  #user-wallet, #user-nickname {
163
  font-size: 0.9rem;
@@ -192,9 +220,13 @@ def index():
192
  justify-content: center;
193
  gap: 8px;
194
  }
195
- .action-btn:hover {
196
  transform: translateY(-2px);
197
  box-shadow: 0 4px 15px rgba(0, 136, 204, 0.3);
 
 
 
 
198
  }
199
  .action-btn.small {
200
  padding: 8px 12px;
@@ -226,6 +258,7 @@ def index():
226
  font-weight: 600;
227
  color: white;
228
  flex-shrink: 0;
 
229
  font-size: 1.1rem;
230
  }
231
  .item-info {
@@ -238,8 +271,8 @@ def index():
238
  overflow: hidden;
239
  text-overflow: ellipsis;
240
  }
241
- .item-description {
242
- font-size: 0.9rem;
243
  color: var(--text-secondary);
244
  white-space: nowrap;
245
  overflow: hidden;
@@ -252,21 +285,12 @@ def index():
252
  flex-shrink: 0;
253
  }
254
 
 
255
  #chat-window-view {
256
- display: none;
257
  flex-direction: column;
258
- height: 100%;
259
- width: 100%;
260
- background-color: var(--bg-primary);
261
- position: absolute;
262
- top: 0;
263
- left: 0;
264
- z-index: 10;
265
- }
266
- #chat-window-view.visible-on-mobile {
267
- display: flex !important;
268
  }
269
-
270
  .chat-header {
271
  display: flex;
272
  align-items: center;
@@ -280,7 +304,8 @@ def index():
280
  background: none;
281
  border: none;
282
  cursor: pointer;
283
- display: none; /* Hidden by default, shown on mobile */
 
284
  }
285
  .back-btn svg {
286
  width: 24px;
@@ -345,7 +370,7 @@ def index():
345
  border-bottom-left-radius: 4px;
346
  }
347
 
348
- .chat-placeholder, .view-placeholder {
349
  display: flex;
350
  flex-direction: column;
351
  align-items: center;
@@ -355,11 +380,11 @@ def index():
355
  color: var(--text-secondary);
356
  padding: 20px;
357
  }
358
- .chat-placeholder img, .view-placeholder img { width: 80px; margin-bottom: 20px; opacity: 0.5; }
359
 
360
  .message-form {
361
  display: flex;
362
- padding: 12px 16px;
363
  gap: 12px;
364
  background-color: var(--bg-secondary);
365
  border-top: 1px solid var(--border-color);
@@ -387,153 +412,114 @@ def index():
387
  }
388
  .send-btn svg { width: 20px; height: 20px; fill: white; }
389
 
390
- /* Browser View Specific Styles */
391
- .browser-controls {
392
- display: flex;
393
- padding: 12px 16px;
394
- gap: 8px;
395
- background-color: var(--bg-tertiary);
396
- flex-shrink: 0;
397
- }
398
- .browser-input {
399
- flex-grow: 1;
400
- padding: 8px 12px;
401
- border: 1px solid var(--border-color);
402
- background-color: var(--bg-primary);
403
- color: var(--text-primary);
404
- border-radius: 6px;
405
- font-size: 0.9rem;
406
- outline: none;
407
  }
408
- .browser-input:focus { border-color: var(--accent-blue); }
409
- .browser-iframe {
410
  flex-grow: 1;
411
- width: 100%;
412
- border: none;
413
  }
414
 
415
- /* Profile Details View Specific Styles */
416
- #profile-details-view .profile-content {
417
- padding: 24px;
418
- text-align: center;
419
- }
420
- #profile-details-view .avatar {
421
- width: 80px;
422
- height: 80px;
423
- font-size: 2rem;
424
- margin: 20px auto;
425
- }
426
- #profile-details-view #my-profile-username {
427
- font-size: 1.5rem;
428
- font-weight: 600;
429
- margin-bottom: 8px;
430
- }
431
- #profile-details-view #my-profile-address {
432
- color: var(--text-secondary);
433
- font-size: 1rem;
434
- word-break: break-all;
435
- margin-bottom: 8px;
436
  }
437
- #profile-details-view #my-profile-balance {
438
- font-size: 1rem;
439
- color: var(--accent-blue-light);
440
- margin-bottom: 20px;
 
 
 
 
441
  }
442
- #profile-details-view .username-form {
443
- max-width: 300px;
444
- margin: 0 auto 20px auto;
 
445
  }
446
- #profile-details-view #my-profile-qr-code {
447
- background: white;
448
- padding: 10px;
449
- margin: 20px auto;
450
- width: fit-content;
451
- border-radius: 8px;
 
 
 
452
  }
453
- #profile-details-view .profile-actions {
454
- display: flex;
455
- flex-direction: column;
456
- gap: 12px;
457
- align-items: stretch;
458
- max-width: 300px;
459
- margin: 0 auto;
460
  }
461
 
462
-
463
- /* Navigation Bar Styles */
464
- .nav-bar {
465
- position: fixed;
466
- bottom: 0;
467
- left: 0;
468
- width: 100%;
469
- height: 56px; /* Standard mobile nav height */
470
- background-color: var(--bg-tertiary);
471
  display: flex;
472
  justify-content: space-around;
473
  align-items: center;
 
 
474
  border-top: 1px solid var(--border-color);
475
- z-index: 100;
476
  }
477
- .nav-button {
478
- flex-grow: 1;
479
  display: flex;
480
  flex-direction: column;
481
  align-items: center;
482
  justify-content: center;
483
- font-size: 0.7rem;
484
- color: var(--text-secondary);
485
- background: none;
486
- border: none;
487
- cursor: pointer;
488
  padding: 8px 0;
 
 
 
489
  transition: color 0.2s ease;
490
  }
491
- .nav-button svg {
492
- width: 24px;
493
- height: 24px;
494
  fill: var(--text-secondary);
495
- margin-bottom: 2px;
496
  transition: fill 0.2s ease;
497
  }
498
- .nav-button.active {
499
  color: var(--accent-blue-light);
500
  }
501
- .nav-button.active svg {
502
  fill: var(--accent-blue-light);
503
  }
504
- .nav-button:hover {
505
- color: var(--text-primary);
506
- }
507
- .nav-button:hover svg {
508
- fill: var(--text-primary);
509
- }
510
-
511
- .nav-button#nav-scan {
512
- position: relative;
513
- top: -16px; /* Lift the middle button */
514
- width: 56px;
515
- height: 56px;
516
- border-radius: 50%;
517
- background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
518
- color: white;
519
- box-shadow: 0 4px 15px rgba(0, 136, 204, 0.3);
520
- transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
521
- padding: 0;
522
- justify-content: center;
523
- }
524
- .nav-button#nav-scan svg {
525
- width: 28px;
526
- height: 28px;
527
- fill: white;
528
- margin: 0;
529
- }
530
- .nav-button#nav-scan:hover {
531
- transform: scale(1.05);
532
- box-shadow: 0 6px 20px rgba(0, 136, 204, 0.4);
533
- }
534
-
535
 
536
- /* Modal Styles */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
  .modal-overlay {
538
  position: fixed;
539
  top: 0;
@@ -557,7 +543,7 @@ def index():
557
  border: 1px solid var(--border-color);
558
  box-shadow: 0 10px 30px rgba(0,0,0,0.5);
559
  }
560
- .modal-content h3 { margin-bottom: 20px; font-weight: 600; font-size: 1.3rem; }
561
  .modal-content label { display: block; margin-bottom: 8px; font-size: 0.9rem; color: var(--text-secondary); }
562
  .modal-content input {
563
  width: 100%;
@@ -565,7 +551,7 @@ def index():
565
  margin-bottom: 16px;
566
  background-color: var(--bg-tertiary);
567
  border: 1px solid var(--border-color);
568
- color: white;
569
  border-radius: 6px;
570
  font-size: 1rem;
571
  }
@@ -576,12 +562,34 @@ def index():
576
  border: none;
577
  cursor: pointer;
578
  font-weight: 500;
 
579
  }
 
580
  .secondary-btn { background-color: var(--bg-hover); color: white; }
581
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
582
  #status-bar {
583
  position: fixed;
584
- bottom: calc(56px + 20px); /* Above nav bar */
585
  left: 50%;
586
  transform: translateX(-50%);
587
  background-color: var(--bg-tertiary);
@@ -594,18 +602,14 @@ def index():
594
  transition: opacity 0.3s, visibility 0.3s;
595
  z-index: 2000;
596
  box-shadow: 0 5px 15px rgba(0,0,0,0.3);
597
- max-width: 90%;
598
- text-align: center;
599
  }
600
  #status-bar.success { background-color: var(--success-color); }
601
  #status-bar.error { background-color: var(--error-color); }
602
  #status-bar.visible { opacity: 1; visibility: visible; }
603
 
 
604
  @media (min-width: 768px) {
605
- body {
606
- background-color: var(--bg-secondary); /* More subtle bg on desktop */
607
- }
608
- .main-container {
609
  max-width: 1100px;
610
  max-height: 800px;
611
  border-radius: 12px;
@@ -613,81 +617,52 @@ def index():
613
  box-shadow: 0 10px 40px rgba(0,0,0,0.3);
614
  border: 1px solid var(--border-color);
615
  }
616
- #app-view {
 
 
 
617
  flex-direction: row;
618
  }
619
 
620
- .app-content {
 
 
621
  flex-grow: 1;
622
- display: flex;
623
- padding-bottom: 0; /* No nav bar padding */
624
  }
625
 
626
- #chatroom-list-view, #user-list-view, #browser-view, #profile-details-view {
627
- width: 320px; /* Fixed sidebar width */
628
  flex-shrink: 0;
629
  border-right: 1px solid var(--border-color);
630
- display: flex !important; /* Always visible sections on desktop */
631
- position: static; /* Remove absolute positioning */
632
- z-index: 1;
633
  }
634
 
635
- #user-list-view, #browser-view, #profile-details-view {
636
- display: none; /* Hide non-active content on desktop by default */
637
- }
638
-
639
- #chat-window-view {
640
- width: auto; /* Take remaining width */
641
- flex-grow: 1;
642
- position: static; /* Remove absolute positioning */
643
- z-index: 1;
644
- display: flex; /* Always show chat area on desktop */
645
- }
646
- #chat-window-view.hidden-on-desktop {
647
- display: flex; /* Ensure it's visible */
648
  }
649
 
650
- .back-btn { display: none !important; } /* Hide back button on desktop */
651
-
652
- .nav-bar {
653
- position: static; /* No fixed nav bar */
654
- height: auto;
655
- background-color: transparent;
656
- border-top: none;
657
- flex-direction: column;
658
- padding: 16px 0;
659
- gap: 8px;
660
- width: 60px; /* Narrow sidebar for navigation */
661
- border-right: 1px solid var(--border-color);
662
- flex-shrink: 0;
663
- justify-content: flex-start;
664
- }
665
- .nav-button {
666
- flex-grow: 0;
667
- font-size: 0.6rem;
668
- padding: 8px 4px;
669
- width: 100%;
670
- gap: 4px;
671
- }
672
- .nav-button svg {
673
- width: 20px;
674
- height: 20px;
675
  }
676
- .nav-button#nav-scan {
677
- position: static; /* Remove lifted effect */
678
- width: 40px;
679
- height: 40px;
680
- top: auto;
681
- margin: 8px auto; /* Center small icon */
682
  }
683
- .nav-button#nav-scan svg {
684
- width: 20px;
685
- height: 20px;
686
- }
687
 
 
 
 
 
 
 
688
  #status-bar {
689
- bottom: 20px; /* Standard bottom position */
690
  }
 
 
691
  }
692
  </style>
693
  </head>
@@ -701,114 +676,91 @@ def index():
701
  </div>
702
 
703
  <div id="app-view" class="main-container">
704
- <div class="nav-bar">
705
- <button class="nav-button" data-view="chatroom-list-view" id="nav-chats">
706
- <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 2V4h16v12z"/></svg>
707
- <span>Чаты</span>
708
- </button>
709
- <button class="nav-button" data-view="user-list-view" id="nav-users">
710
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5s-3 1.34-3 3 1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5s-3 1.34-3 3 1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>
711
- <span>Пользователи</span>
712
- </button>
713
- <button class="nav-button" id="nav-scan">
714
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 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>
715
- </button>
716
- <button class="nav-button" data-view="browser-view" id="nav-browser">
717
- <svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24"><rect fill="none" height="24" width="24"/><g><g><path d="M20,4H4C2.9,4,2,4.9,2,6v12c0,1.1,0.9,2,2,2h16c1.1,0,2-0.9,2-2V6C22,4.9,21.1,4,20,4z M4,6h16v2H4V6z M20,18H4V10h16V18 z"/><circle cx="8.5" cy="14.5" r="1.5"/><circle cx="15.5" cy="14.5" r="1.5"/></g></g></svg>
718
- <span>Браузер</span>
719
- </button>
720
- <button class="nav-button" data-view="profile-details-view" id="nav-profile">
721
- <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>
722
- <span>Профиль</span>
723
- </button>
724
- </div>
725
-
726
- <div class="app-content">
727
- <div id="chatroom-list-view">
728
- <div class="view-header">
729
- <h2>Чаты</h2>
730
- <div style="display: flex; align-items: center; gap: 8px;">
731
- <button id="create-room-show-modal" class="action-btn small">
732
  <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>
733
  <span>Новый</span>
734
  </button>
735
  </div>
 
 
 
 
 
 
 
 
736
  </div>
737
  <div id="chatroom-list"></div>
738
- <div class="view-placeholder" id="chat-list-placeholder">
739
- <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
740
- <h2>Начните общение</h2>
741
- <p>Создайте новый чат или дождитесь, пока вас кто-то пригласит.</p>
742
- </div>
743
  </div>
744
 
745
- <div id="user-list-view">
746
- <div class="view-header">
747
  <h2>Пользователи</h2>
748
  </div>
749
  <div id="user-list"></div>
750
- <div class="view-placeholder" id="user-list-placeholder">
751
- <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
752
- <h2>Загрузка пользователей...</h2>
753
- <p>Список зарегистрированных пользователей появится здесь.</p>
754
- </div>
755
  </div>
756
 
757
- <div id="browser-view">
758
- <div class="view-header">
759
- <h2>Браузер</h2>
 
 
760
  </div>
761
- <div class="browser-controls">
762
- <input type="text" id="browser-url" class="browser-input" placeholder="https://google.com" value="https://google.com">
763
- <button id="browser-go" class="action-btn small">Go</button>
 
 
 
 
 
 
 
 
 
 
 
 
764
  </div>
765
- <iframe id="browser-iframe" class="browser-iframe" src="https://google.com"></iframe>
766
  </div>
767
 
768
- <div id="profile-details-view">
769
- <div class="view-header">
770
- <h2>Мой Профиль</h2>
771
- </div>
772
- <div class="profile-content">
773
- <div id="my-profile-avatar" class="avatar"></div>
774
- <p id="my-profile-username"></p>
775
- <p id="my-profile-address"></p>
776
- <p id="my-profile-balance"></p>
777
- <form class="username-form" id="my-username-form">
778
- <input type="text" id="my-username-input" class="username-input" placeholder="Установить никнейм" autocomplete="off">
779
- <button type="submit" class="action-btn small">✓</button>
780
- </form>
781
- <p style="text-align: center; color: var(--text-secondary); font-size: 0.8rem; margin-bottom: 12px;">Мой QR-код:</p>
782
- <div id="my-profile-qr-code" style="background: white; padding: 10px; margin: 0 auto 20px auto; width: fit-content; border-radius: 8px;"></div>
783
- <div class="profile-actions">
784
- <!-- Add more profile actions here if needed -->
785
- </div>
786
  </div>
 
787
  </div>
788
 
789
  </div>
790
 
791
- <div id="chat-window-view">
792
- <div id="chat-placeholder" class="chat-placeholder">
793
- <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
794
- <h2>Выберите чат</h2>
795
- <p>Начните общение в одном из существующих чатов или создайте свой собственный.</p>
796
  </div>
797
- <div id="active-chat" style="display: none; width: 100%; height: 100%; flex-direction: column;">
798
- <div class="chat-header">
799
- <button class="back-btn" id="back-to-list-btn">
800
- <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>
801
- </button>
802
- <div id="chat-header-avatar" class="avatar"></div>
803
- <span id="chat-header-title"></span>
804
- </div>
805
- <div id="messages-container"></div>
806
- <form id="message-form" class="message-form">
807
- <input type="text" id="message-input" placeholder="Сообщение..." autocomplete="off">
808
- <button type="submit" class="action-btn send-btn" id="send-btn">
809
- <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>
810
- </button>
811
- </form>
812
  </div>
813
  </div>
814
 
@@ -824,7 +776,7 @@ def index():
824
  <input type="password" id="room-password">
825
  <div class="modal-actions">
826
  <button type="button" id="create-room-cancel" class="modal-btn secondary-btn">Отмена</button>
827
- <button type="submit" class="modal-btn action-btn">Создать</button>
828
  </div>
829
  </form>
830
  </div>
@@ -838,22 +790,22 @@ def index():
838
  <input type="password" id="password-input" required>
839
  <div class="modal-actions">
840
  <button type="button" id="password-cancel" class="modal-btn secondary-btn">Отмена</button>
841
- <button type="submit" class="modal-btn action-btn">Войти</button>
842
  </div>
843
  </form>
844
  </div>
845
  </div>
846
 
847
  <div id="profile-modal" class="modal-overlay">
848
- <div class="modal-content" style="text-align: center;">
849
  <h3 id="profile-modal-title">Профиль пользователя</h3>
850
  <div id="profile-avatar-container" style="margin: 20px auto; display: inline-block;"></div>
851
- <p id="profile-username-modal" style="font-size: 1.2rem; font-weight: 600;"></p>
852
- <p id="profile-address-modal" style="color: var(--text-secondary); font-size: 0.9rem; word-break: break-all; margin-top: 8px;"></p>
853
- <div id="profile-qr-code-modal" style="background: white; padding: 10px; margin: 20px auto; width: fit-content; border-radius: 8px;"></div>
854
  <p style="text-align: center; color: var(--text-secondary); font-size: 0.8rem; margin-top: -10px; margin-bottom: 20px;">Отсканируйте для открытия профиля</p>
855
  <div class="modal-actions" style="flex-direction: column; gap: 12px; align-items: stretch;">
856
- <button id="send-ton-btn" class="modal-btn action-btn">Отправить TON</button>
857
  <button id="profile-close-btn" class="modal-btn secondary-btn">Закрыть</button>
858
  </div>
859
  </div>
@@ -862,7 +814,7 @@ def index():
862
  <div id="scanner-modal" class="modal-overlay">
863
  <div class="modal-content">
864
  <h3>Сканировать QR-код</h3>
865
- <div id="qr-reader" style="width: 100%; border: 1px solid var(--border-color); margin-top: 16px; border-radius: 8px; overflow: hidden;"></div>
866
  <div class="modal-actions">
867
  <button id="scanner-close-btn" class="modal-btn secondary-btn">Отмена</button>
868
  </div>
@@ -882,23 +834,21 @@ def index():
882
  let activeChatroomId = null;
883
  let messagePollingInterval = null;
884
  let chatroomsData = {};
885
- let usersData = {};
886
  let html5QrCode = null;
887
- let profileQrCodeInstance = null;
888
- let myProfileQrCodeInstance = null;
889
-
890
  const loginView = document.getElementById('login-view');
891
  const appView = document.getElementById('app-view');
892
  const chatroomListView = document.getElementById('chatroom-list-view');
893
  const userListView = document.getElementById('user-list-view');
894
- const browserView = document.getElementById('browser-view');
895
- const profileDetailsView = document.getElementById('profile-details-view');
896
  const chatWindowView = document.getElementById('chat-window-view');
 
897
  const chatPlaceholder = document.getElementById('chat-placeholder');
898
  const activeChat = document.getElementById('active-chat');
899
  const profileModal = document.getElementById('profile-modal');
900
  const scannerModal = document.getElementById('scanner-modal');
901
- const navButtons = document.querySelectorAll('.nav-button');
902
 
903
  const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292'];
904
 
@@ -919,9 +869,13 @@ def index():
919
  statusBar.className = 'status-bar';
920
  if (type === 'success') statusBar.classList.add('success');
921
  else if (type === 'error') statusBar.classList.add('error');
922
-
923
  statusBar.classList.add('visible');
924
- setTimeout(() => statusBar.classList.remove('visible'), duration);
 
 
 
 
925
  };
926
 
927
  const apiCall = async (endpoint, options = {}) => {
@@ -938,51 +892,20 @@ def index():
938
  throw error;
939
  }
940
  };
941
-
942
  const truncateAddress = (address) => address ? `${address.substring(0, 4)}...${address.substring(address.length - 4)}` : '';
943
 
944
- const updateMyProfileView = async () => {
945
- if (!currentUser.address) return;
946
-
947
- const username = currentUser.username || `User ${truncateAddress(currentUser.address)}`;
948
- const avatarContainer = document.getElementById('my-profile-avatar');
949
- const usernameEl = document.getElementById('my-profile-username');
950
- const addressEl = document.getElementById('my-profile-address');
951
- const balanceEl = document.getElementById('my-profile-balance');
952
- const qrCodeEl = document.getElementById('my-profile-qr-code');
953
- const usernameInput = document.getElementById('my-username-input');
954
-
955
- avatarContainer.innerHTML = '';
956
- avatarContainer.appendChild(getAvatar(username));
957
- usernameEl.textContent = username;
958
- addressEl.textContent = currentUser.address;
959
  usernameInput.value = currentUser.username || '';
960
-
961
- if (tonConnectUI.wallet?.account?.balance) {
962
- const balanceNano = tonConnectUI.wallet.account.balance;
963
- const balanceTon = (parseInt(balanceNano) / 1_000_000_000).toFixed(2);
964
- balanceEl.textContent = `Баланс: ${balanceTon} TON`;
965
- } else {
966
- balanceEl.textContent = `Баланс: Недоступно`;
967
- }
968
-
969
- qrCodeEl.innerHTML = '';
970
- if (myProfileQrCodeInstance) {
971
- myProfileQrCodeInstance.clear();
972
- }
973
- myProfileQrCodeInstance = new QRCode(qrCodeEl, {
974
- text: currentUser.address,
975
- width: 150,
976
- height: 150,
977
- colorDark : "#000000",
978
- colorLight : "#ffffff",
979
- correctLevel : QRCode.CorrectLevel.H
980
- });
981
  };
982
-
983
- document.getElementById('my-username-form').addEventListener('submit', async (e) => {
984
  e.preventDefault();
985
- const newUsername = document.getElementById('my-username-input').value.trim();
986
  if (!newUsername || newUsername.length < 3) {
987
  showStatus('Никнейм должен быть не короче 3 символов.', 'error');
988
  return;
@@ -998,11 +921,11 @@ def index():
998
  body: JSON.stringify({ address: currentUser.address, username: newUsername })
999
  });
1000
  currentUser.username = newUsername;
1001
- updateMyProfileView();
1002
  showStatus('Никнейм успешно обновлен!', 'success');
1003
- fetchChatrooms();
1004
- fetchUsers();
1005
- if (activeChatroomId) fetchMessages(activeChatroomId);
1006
  } catch (err) {}
1007
  });
1008
 
@@ -1018,24 +941,109 @@ def index():
1018
  } catch (err) {
1019
  currentUser.username = null;
1020
  }
 
1021
  loginView.style.display = 'none';
1022
  appView.style.display = 'flex';
1023
- showView('chatroom-list-view'); // Default view after login
1024
  fetchChatrooms();
1025
- // Users and Profile views fetch data when selected
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1026
  };
1027
 
1028
  const renderChatrooms = (rooms) => {
1029
  const list = document.getElementById('chatroom-list');
1030
  list.innerHTML = '';
1031
- document.getElementById('chat-list-placeholder').style.display = rooms.length === 0 ? 'flex' : 'none';
1032
  chatroomsData = {};
1033
  rooms.forEach(room => {
1034
  chatroomsData[room.id] = room;
1035
  const item = document.createElement('div');
1036
  item.className = 'chatroom-item';
1037
  item.dataset.id = room.id;
1038
-
1039
  item.appendChild(getAvatar(room.name));
1040
 
1041
  const infoDiv = document.createElement('div');
@@ -1043,13 +1051,7 @@ def index():
1043
  const nameSpan = document.createElement('div');
1044
  nameSpan.className = 'item-name';
1045
  nameSpan.textContent = room.name;
1046
- const descSpan = document.createElement('div');
1047
- descSpan.className = 'item-description';
1048
- // Could add last message preview here if backend supported it
1049
- descSpan.textContent = room.is_private ? 'Приватный чат' : 'Открытый чат';
1050
-
1051
  infoDiv.appendChild(nameSpan);
1052
- infoDiv.appendChild(descSpan);
1053
  item.appendChild(infoDiv);
1054
 
1055
  if (room.is_private) {
@@ -1062,59 +1064,48 @@ def index():
1062
  list.appendChild(item);
1063
  });
1064
  };
1065
-
1066
  const fetchChatrooms = async () => {
1067
- if (!currentUser.address) return;
1068
  try {
1069
  const data = await apiCall('/api/chatrooms');
1070
  renderChatrooms(data.chatrooms);
1071
  } catch (err) {}
1072
  };
1073
 
1074
- const renderUsers = (users) => {
1075
- const list = document.getElementById('user-list');
1076
- list.innerHTML = '';
1077
- document.getElementById('user-list-placeholder').style.display = users.length === 0 ? 'flex' : 'none';
1078
- usersData = {};
1079
  users.forEach(user => {
1080
- usersData[user.address] = user;
1081
  const item = document.createElement('div');
1082
  item.className = 'user-item';
1083
  item.dataset.address = user.address;
1084
 
1085
- const displayName = user.username || `User ${truncateAddress(user.address)}`;
1086
- item.appendChild(getAvatar(displayName));
1087
 
1088
  const infoDiv = document.createElement('div');
1089
  infoDiv.className = 'item-info';
1090
  const nameSpan = document.createElement('div');
1091
  nameSpan.className = 'item-name';
1092
- nameSpan.textContent = displayName;
 
1093
  const addressSpan = document.createElement('div');
1094
- addressSpan.className = 'item-description';
1095
  addressSpan.textContent = truncateAddress(user.address);
1096
-
1097
- infoDiv.appendChild(nameSpan);
1098
  infoDiv.appendChild(addressSpan);
 
1099
  item.appendChild(infoDiv);
1100
 
1101
  item.addEventListener('click', () => showProfile(user.address));
1102
  list.appendChild(item);
1103
  });
1104
- };
1105
 
1106
- const fetchUsers = async () => {
1107
- if (!currentUser.address) return;
1108
- document.getElementById('user-list-placeholder').style.display = 'flex';
1109
  try {
1110
  const data = await apiCall('/api/users');
1111
  renderUsers(data.users);
1112
- } catch (err) {
1113
- renderUsers([]); // Clear list on error
1114
- } finally {
1115
- document.getElementById('user-list-placeholder').style.display = 'none';
1116
- }
1117
- };
1118
 
1119
 
1120
  const renderMessages = (messages) => {
@@ -1129,7 +1120,7 @@ def index():
1129
  avatar.classList.add('message-avatar');
1130
  avatar.style.cursor = 'pointer';
1131
  avatar.onclick = () => showProfile(msg.sender_address);
1132
-
1133
  const contentDiv = document.createElement('div');
1134
  contentDiv.className = 'message-content';
1135
 
@@ -1137,7 +1128,7 @@ def index():
1137
  senderDiv.className = 'message-sender';
1138
  senderDiv.textContent = msg.display_name;
1139
  senderDiv.onclick = () => showProfile(msg.sender_address);
1140
-
1141
  const bubbleDiv = document.createElement('div');
1142
  bubbleDiv.className = 'message-bubble';
1143
  bubbleDiv.textContent = msg.text;
@@ -1150,83 +1141,38 @@ def index():
1150
  container.appendChild(msgDiv);
1151
  });
1152
 
1153
- if(shouldScroll) {
1154
  container.scrollTop = container.scrollHeight;
1155
  }
1156
  };
1157
-
1158
  const fetchMessages = async (roomId) => {
1159
- if (!currentUser.address || !roomId) return;
1160
  try {
1161
  const data = await apiCall(`/api/messages/${roomId}`);
1162
  renderMessages(data.messages);
1163
  } catch (err) {
1164
- if (messagePollingInterval) clearInterval(messagePollingInterval);
1165
  }
1166
  };
1167
 
1168
- const showView = (viewId) => {
1169
- const views = [chatroomListView, userListView, browserView, profileDetailsView];
1170
- views.forEach(view => {
1171
- view.style.display = 'none';
1172
- view.classList.remove('active');
1173
- });
1174
- document.getElementById(viewId).style.display = 'flex';
1175
- document.getElementById(viewId).classList.add('active');
1176
-
1177
- navButtons.forEach(button => button.classList.remove('active'));
1178
- document.querySelector(`.nav-button[data-view="${viewId}"]`)?.classList.add('active');
1179
-
1180
- if (messagePollingInterval) {
1181
- clearInterval(messagePollingInterval);
1182
- messagePollingInterval = null;
1183
- }
1184
- activeChat.style.display = 'none';
1185
- chatPlaceholder.style.display = 'flex';
1186
- chatWindowView.classList.remove('visible-on-mobile');
1187
-
1188
-
1189
- if (viewId === 'user-list-view' && currentUser.address) {
1190
- fetchUsers();
1191
- } else if (viewId === 'chatroom-list-view' && currentUser.address) {
1192
- fetchChatrooms();
1193
- } else if (viewId === 'profile-details-view' && currentUser.address) {
1194
- updateMyProfileView();
1195
- }
1196
-
1197
- // Handle desktop split view state
1198
- handleResize();
1199
- };
1200
-
1201
  const selectChatroom = (roomId, isPrivate) => {
1202
  const roomData = chatroomsData[roomId];
1203
- if (!roomData || !currentUser.address) return;
1204
 
1205
  const proceedToRoom = () => {
1206
- if (messagePollingInterval) clearInterval(messagePollingInterval);
1207
  activeChatroomId = roomId;
1208
-
1209
  document.getElementById('chat-header-title').textContent = roomData.name;
1210
  const headerAvatar = document.getElementById('chat-header-avatar');
1211
  headerAvatar.innerHTML = '';
1212
  headerAvatar.appendChild(getAvatar(roomData.name));
1213
 
1214
- chatPlaceholder.style.display = 'none';
1215
- activeChat.style.display = 'flex';
1216
-
1217
- if (window.innerWidth < 768) {
1218
- chatroomListView.style.display = 'none';
1219
- chatWindowView.classList.add('visible-on-mobile');
1220
- } else {
1221
- // On desktop, chat window is always visible next to the list
1222
- chatWindowView.style.display = 'flex';
1223
- chatroomListView.style.display = 'flex'; // Ensure list is visible
1224
- }
1225
-
1226
  fetchMessages(roomId);
1227
- messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000);
1228
  };
1229
-
1230
  if (isPrivate) {
1231
  const passwordModal = document.getElementById('password-modal');
1232
  const passwordForm = document.getElementById('password-form');
@@ -1237,9 +1183,11 @@ def index():
1237
 
1238
  const formSubmitHandler = async (e) => {
1239
  e.preventDefault();
1240
- passwordForm.removeEventListener('submit', formSubmitHandler);
1241
  const password = passwordInput.value;
1242
  passwordModal.style.display = 'none';
 
 
 
1243
  try {
1244
  await apiCall('/api/join_chatroom', {
1245
  method: 'POST',
@@ -1247,14 +1195,18 @@ def index():
1247
  body: JSON.stringify({ chatroom_id: roomId, password })
1248
  });
1249
  proceedToRoom();
1250
- } catch (err) {}
 
 
1251
  };
1252
  passwordForm.addEventListener('submit', formSubmitHandler);
1253
 
1254
  document.getElementById('password-cancel').onclick = () => {
1255
  passwordModal.style.display = 'none';
1256
- passwordForm.removeEventListener('submit', formSubmitHandler);
 
1257
  };
 
1258
  } else {
1259
  proceedToRoom();
1260
  }
@@ -1279,8 +1231,9 @@ def index():
1279
  text: text
1280
  })
1281
  });
 
1282
  await fetchMessages(activeChatroomId);
1283
- document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight;
1284
  } finally {
1285
  input.disabled = false;
1286
  sendBtn.disabled = false;
@@ -1301,7 +1254,14 @@ def index():
1301
  e.preventDefault();
1302
  const name = document.getElementById('room-name').value.trim();
1303
  const password = document.getElementById('room-password').value;
1304
- if (!name || !currentUser.address) return;
 
 
 
 
 
 
 
1305
 
1306
  try {
1307
  await apiCall('/api/create_chatroom', {
@@ -1315,64 +1275,64 @@ def index():
1315
  } catch (err) {}
1316
  });
1317
 
1318
- const showProfile = async (address) => {
1319
- if (!address) return;
 
 
 
1320
  try {
1321
  const userData = await apiCall('/api/user_data', {
1322
  method: 'POST',
1323
  headers: { 'Content-Type': 'application/json' },
1324
- body: JSON.stringify({ address: address })
1325
  });
1326
-
1327
- const username = userData.username || `User ${truncateAddress(address)}`;
1328
  const avatarContainer = document.getElementById('profile-avatar-container');
1329
- const usernameEl = document.getElementById('profile-username-modal');
1330
- const addressEl = document.getElementById('profile-address-modal');
1331
- const qrCodeEl = document.getElementById('profile-qr-code-modal');
1332
  const sendTonBtn = document.getElementById('send-ton-btn');
1333
-
1334
  avatarContainer.innerHTML = '';
1335
  avatarContainer.appendChild(getAvatar(username));
1336
-
1337
  usernameEl.textContent = username;
1338
- addressEl.textContent = address;
1339
-
1340
  qrCodeEl.innerHTML = '';
1341
- if (profileQrCodeInstance) {
1342
- profileQrCodeInstance.clear();
 
1343
  }
1344
- profileQrCodeInstance = new QRCode(qrCodeEl, {
1345
- text: address,
1346
  width: 150,
1347
  height: 150,
1348
  colorDark : "#000000",
1349
  colorLight : "#ffffff",
1350
  correctLevel : QRCode.CorrectLevel.H
1351
  });
1352
-
1353
  sendTonBtn.onclick = async () => {
1354
  if (!tonConnectUI.connected) {
1355
  showStatus('Подключите кошелек для отправки TON.', 'error');
1356
  return;
1357
  }
1358
- if (address === currentUser.address) {
1359
- showStatus('Нельзя отправить TON себе.', 'error');
1360
- return;
1361
- }
1362
  const amountString = prompt("Введите сумму в TON для отправки:", "0.1");
1363
  if (amountString === null) return;
1364
-
1365
  const amount = parseFloat(amountString);
1366
  if (isNaN(amount) || amount <= 0) {
1367
  showStatus('Неверная сумма.', 'error');
1368
  return;
1369
  }
1370
-
1371
  const amountInNanoTon = Math.floor(amount * 1_000_000_000).toString();
1372
 
1373
  const transaction = {
1374
- validUntil: Math.floor(Date.now() / 1000) + 600,
1375
- messages: [ { address: address, amount: amountInNanoTon } ]
1376
  };
1377
 
1378
  try {
@@ -1383,8 +1343,8 @@ def index():
1383
  showStatus('Транзакция отклонена.', 'error');
1384
  }
1385
  };
1386
-
1387
- sendTonBtn.style.display = (address === currentUser.address || !tonConnectUI.connected) ? 'none' : 'block';
1388
  profileModal.style.display = 'flex';
1389
  } catch (err) {
1390
  showStatus('Не удалось загрузить профиль.', 'error');
@@ -1393,20 +1353,29 @@ def index():
1393
 
1394
  const showScanner = () => {
1395
  scannerModal.style.display = 'flex';
1396
- html5QrCode = new Html5Qrcode("qr-reader");
 
 
 
1397
  const qrCodeSuccessCallback = (decodedText, decodedResult) => {
1398
  hideScanner();
 
1399
  if (decodedText && decodedText.length > 40 && (decodedText.startsWith('EQ') || decodedText.startsWith('UQ'))) {
1400
  showProfile(decodedText);
1401
  } else {
1402
- showStatus('Отсканирован недействительный QR-код адреса TON.', 'error');
1403
  }
1404
  };
1405
- const config = { fps: 10, qrbox: { width: 250, height: 250 } };
1406
- html5QrCode.start({ facingMode: "environment" }, config, qrCodeSuccessCallback)
 
 
 
 
 
1407
  .catch(err => {
1408
- showStatus('Не удалось запустить сканер.', 'error');
1409
- hideScanner();
1410
  });
1411
  };
1412
 
@@ -1417,114 +1386,143 @@ def index():
1417
  scannerModal.style.display = 'none';
1418
  };
1419
 
1420
- document.getElementById('browser-go').addEventListener('click', () => {
1421
- const urlInput = document.getElementById('browser-url');
1422
- let url = urlInput.value.trim();
1423
- if (!url) return;
1424
- if (!url.startsWith('http://') && !url.startsWith('https://')) {
1425
- url = 'https://' + url; // Default to https
1426
- urlInput.value = url;
1427
- }
1428
- document.getElementById('browser-iframe').src = url;
1429
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1430
 
1431
- navButtons.forEach(button => {
1432
- if (button.id === 'nav-scan') {
1433
- button.addEventListener('click', showScanner);
1434
- } else {
1435
- button.addEventListener('click', () => {
1436
- const viewId = button.dataset.view;
1437
- if (viewId) {
1438
- if (viewId === 'chatroom-list-view') {
1439
- // Reset chat window on mobile when navigating away from chat
1440
- if (window.innerWidth < 768) {
1441
- activeChatroomId = null;
1442
- activeChat.style.display = 'none';
1443
- chatPlaceholder.style.display = 'flex';
1444
- chatWindowView.classList.remove('visible-on-mobile');
1445
- }
1446
- }
1447
- showView(viewId);
1448
- }
1449
- });
1450
- }
1451
- });
1452
 
 
1453
  document.getElementById('profile-close-btn').addEventListener('click', () => profileModal.style.display = 'none');
1454
  document.getElementById('scanner-close-btn').addEventListener('click', hideScanner);
1455
-
1456
- document.getElementById('back-to-list-btn').addEventListener('click', () => {
1457
- activeChatroomId = null;
1458
- activeChat.style.display = 'none';
1459
- chatPlaceholder.style.display = 'flex';
1460
- chatWindowView.classList.remove('visible-on-mobile');
1461
- chatroomListView.style.display = 'flex'; // Show the list again
1462
- // No need to call showView because we are staying within the chatroom list context
1463
- });
1464
-
1465
- const handleResize = () => {
1466
- const isMobile = window.innerWidth < 768;
1467
-
1468
- // Navigation bar display
1469
- const navBar = document.querySelector('.nav-bar');
1470
- if (isMobile) {
1471
- navBar.style.flexDirection = 'row';
1472
- navBar.style.position = 'fixed';
1473
- navBar.style.width = '100%';
1474
- navBar.style.height = '56px';
1475
- navBar.style.borderRight = 'none';
1476
- navBar.style.borderTop = '1px solid var(--border-color)';
1477
- navBar.style.padding = '0';
1478
- } else {
1479
- navBar.style.flexDirection = 'column';
1480
- navBar.style.position = 'static';
1481
- navBar.style.width = '60px';
1482
- navBar.style.height = 'auto';
1483
- navBar.style.borderRight = '1px solid var(--border-color)';
1484
- navBar.style.borderTop = 'none';
1485
- navBar.style.padding = '16px 0';
1486
  }
 
1487
 
1488
- // Back button visibility
1489
- document.getElementById('back-to-list-btn').style.display = isMobile && activeChatroomId ? 'block' : 'none';
1490
 
1491
- // Main content views visibility
1492
- const activeViewId = document.querySelector('.app-content > div.active')?.id;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1493
 
1494
- // On desktop, show the active view AND the chat window (if active)
1495
- if (!isMobile) {
1496
- [chatroomListView, userListView, browserView, profileDetailsView].forEach(view => {
1497
- view.style.display = view.id === activeViewId ? 'flex' : 'none';
1498
- });
1499
- // Always show chat window area on desktop, hide placeholder if chat is active
1500
- chatWindowView.style.display = 'flex';
1501
- chatPlaceholder.style.display = activeChatroomId ? 'none' : 'flex';
1502
- activeChat.style.display = activeChatroomId ? 'flex' : 'none';
1503
-
1504
- // Special case: if chat is active AND chatrooms is the active view, show both
1505
- if (activeChatroomId && activeViewId === 'chatroom-list-view') {
1506
- chatroomListView.style.display = 'flex';
1507
- chatWindowView.style.display = 'flex';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1508
  }
1509
 
1510
-
1511
- } else {
1512
- // On mobile, only show either the active view or the chat window if active
1513
- [chatroomListView, userListView, browserView, profileDetailsView].forEach(view => {
1514
- view.style.display = 'none';
1515
- });
1516
-
1517
- if (activeChatroomId) {
1518
- chatWindowView.classList.add('visible-on-mobile');
1519
- } else {
1520
- chatWindowView.classList.remove('visible-on-mobile');
1521
- document.getElementById(activeViewId || 'chatroom-list-view').style.display = 'flex'; // Show the currently selected main view
1522
- }
1523
- }
1524
  };
1525
 
1526
  window.addEventListener('resize', handleResize);
1527
-
 
 
1528
  tonConnectUI.onStatusChange(wallet => {
1529
  if (wallet) {
1530
  const address = TON_CONNECT_UI.toUserFriendlyAddress(wallet.account.address, false);
@@ -1533,37 +1531,22 @@ def index():
1533
  currentUser = { address: null, username: null };
1534
  appView.style.display = 'none';
1535
  loginView.style.display = 'flex';
1536
- if (messagePollingInterval) clearInterval(messagePollingInterval);
1537
  activeChatroomId = null;
1538
- chatroomsData = {};
1539
- usersData = {};
1540
- document.getElementById('chatroom-list').innerHTML = '';
1541
- document.getElementById('user-list').innerHTML = '';
1542
- document.getElementById('messages-container').innerHTML = '';
1543
- activeChat.style.display = 'none';
1544
- chatPlaceholder.style.display = 'flex';
1545
- chatWindowView.classList.remove('visible-on-mobile');
1546
- // Ensure chatroom list view is visible again on mobile after logout
1547
- if (window.innerWidth < 768) {
1548
- chatroomListView.style.display = 'flex';
1549
- }
1550
- }
1551
- // Update profile view and button states based on connection status
1552
- if (document.querySelector('.app-content > div.active')?.id === 'profile-details-view') {
1553
- updateMyProfileView();
1554
  }
1555
  });
1556
 
1557
- // Initial check on load
1558
- if (tonConnectUI.connected) {
1559
- const address = TON_CONNECT_UI.toUserFriendlyAddress(tonConnectUI.wallet.account.address, false);
1560
- initializeUser(address);
1561
- } else {
1562
  loginView.style.display = 'flex';
1563
  appView.style.display = 'none';
1564
- }
 
 
 
 
1565
 
1566
- handleResize(); // Apply correct layout on initial load
1567
  });
1568
  </script>
1569
  </body>
@@ -1571,7 +1554,6 @@ def index():
1571
  '''
1572
  return Response(html_content, mimetype='text/html')
1573
 
1574
-
1575
  @app.route('/api/user_data', methods=['POST'])
1576
  def get_user_data():
1577
  data = request.get_json()
@@ -1584,30 +1566,40 @@ def get_user_data():
1584
  return jsonify({'username': username})
1585
 
1586
  @app.route('/api/users', methods=['GET'])
1587
- def get_all_users():
1588
  db = read_db()
1589
  users_list = []
1590
  for address, user_info in db['users'].items():
1591
- users_list.append({
1592
- 'address': address,
1593
- 'username': user_info.get('username')
1594
- })
1595
- # Add users from messages who don't have a username registered
1596
- all_addresses = set(db['users'].keys())
1597
- for room_messages in db['messages'].values():
1598
- for msg in room_messages:
1599
- sender_address = msg['sender_address']
1600
- if sender_address not in all_addresses:
1601
- all_addresses.add(sender_address)
1602
- users_list.append({
1603
- 'address': sender_address,
1604
- 'username': None # No registered username
1605
- })
1606
-
1607
- # Sort users by username or address prefix
1608
- sorted_users = sorted(users_list, key=lambda x: (x['username'] is None, x['username'] or x['address']))
1609
-
1610
- return jsonify({'users': sorted_users})
 
 
 
 
 
 
 
 
 
 
1611
 
1612
 
1613
  @app.route('/api/set_username', methods=['POST'])
@@ -1678,25 +1670,31 @@ def join_chatroom():
1678
  def get_messages(chatroom_id):
1679
  db = read_db()
1680
  if chatroom_id not in db['messages']:
 
 
 
1681
  return jsonify({'error': 'Chatroom not found'}), 404
1682
-
1683
  messages_with_names = []
1684
  room_messages = db['messages'].get(chatroom_id, [])
1685
-
1686
- # Collect all sender addresses in this chat
1687
- sender_addresses_in_chat = {msg['sender_address'] for msg in room_messages}
1688
-
1689
- # Fetch usernames for all relevant addresses in one go
1690
- user_display_names = {}
1691
- for address in sender_addresses_in_chat:
1692
- user_info = db['users'].get(address)
1693
- display_name = (user_info.get('username') if user_info and user_info.get('username')
1694
- else f"{address[:4]}...{address[-4:]}")
1695
- user_display_names[address] = display_name
1696
 
1697
  for msg in room_messages:
 
 
 
 
1698
  msg_copy = msg.copy()
1699
- msg_copy['display_name'] = user_display_names.get(msg['sender_address'], f"{msg['sender_address'][:4]}...{msg['sender_address'][-4:]}") # Fallback just in case
1700
  messages_with_names.append(msg_copy)
1701
 
1702
  return jsonify({'messages': messages_with_names})
@@ -1710,10 +1708,20 @@ def send_message():
1710
 
1711
  if not all([chatroom_id, sender_address, text]):
1712
  return jsonify({'error': 'Missing data'}), 400
 
 
1713
 
1714
  db = read_db()
1715
  if chatroom_id not in db['messages']:
1716
- return jsonify({'error': 'Chatroom not found'}), 404
 
 
 
 
 
 
 
 
1717
 
1718
  message = {
1719
  'id': str(uuid.uuid4()),
@@ -1721,8 +1729,9 @@ def send_message():
1721
  'text': text,
1722
  'timestamp': datetime.utcnow().isoformat() + "Z"
1723
  }
1724
-
1725
- if len(db['messages'][chatroom_id]) >= 100: # Keep last 100 messages
 
1726
  db['messages'][chatroom_id].pop(0)
1727
 
1728
  db['messages'][chatroom_id].append(message)
@@ -1732,4 +1741,4 @@ def send_message():
1732
 
1733
  if __name__ == '__main__':
1734
  init_db()
1735
- app.run(host='0.0.0.0', port=7860, debug=True) # debug=True for easier development
 
19
  }, f, indent=4)
20
 
21
  def read_db():
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:
 
90
  display: flex;
91
  flex-direction: column;
92
  transition: opacity 0.3s ease;
93
+ position: relative;
94
  }
95
 
96
  #login-view {
 
128
  width: 100%;
129
  height: 100%;
130
  flex-direction: column;
 
131
  }
132
 
133
+ .content-area {
134
  flex-grow: 1;
135
+ display: flex;
136
+ overflow: hidden; /* Allows individual views to scroll */
137
+ position: relative; /* Needed for absolute positioning of views on mobile */
138
  }
139
 
140
+ .app-view-section {
 
141
  width: 100%;
142
  height: 100%;
143
+ flex-shrink: 0;
144
+ overflow-y: auto;
145
+ display: none; /* Managed by JS */
146
+ flex-direction: column; /* Default for views */
147
+ background-color: var(--bg-primary); /* Default */
148
+ position: absolute; /* Mobile: Stack views */
149
+ top: 0;
150
+ left: 0;
151
+ right: 0;
152
+ bottom: 0;
153
  }
154
 
155
+ .app-view-section.active {
156
+ display: flex;
157
+ position: relative; /* Active view takes up space */
158
+ }
159
+
160
+ #chatroom-list-view {
161
+ background-color: var(--bg-secondary);
162
+ }
163
+ #chat-window-view {
164
+ background-color: var(--bg-primary);
165
+ }
166
+
167
+ .list-header {
168
  padding: 16px;
169
  border-bottom: 1px solid var(--border-color);
170
  flex-shrink: 0;
171
+ background-color: var(--bg-secondary);
172
+ }
173
+ .list-header-top {
174
  display: flex;
175
  justify-content: space-between;
176
  align-items: center;
177
+ margin-bottom: 16px;
178
  }
179
+ .list-header-top h2 {
180
  font-size: 1.5rem;
181
  font-weight: 600;
182
  }
 
185
  padding: 12px;
186
  background-color: var(--bg-tertiary);
187
  border-radius: 8px;
188
+ margin-bottom: 0; /* Adjusted for list header */
189
  }
190
  #user-wallet, #user-nickname {
191
  font-size: 0.9rem;
 
220
  justify-content: center;
221
  gap: 8px;
222
  }
223
+ .action-btn:hover:not(:disabled) {
224
  transform: translateY(-2px);
225
  box-shadow: 0 4px 15px rgba(0, 136, 204, 0.3);
226
+ }
227
+ .action-btn:disabled {
228
+ opacity: 0.5;
229
+ cursor: not-allowed;
230
  }
231
  .action-btn.small {
232
  padding: 8px 12px;
 
258
  font-weight: 600;
259
  color: white;
260
  flex-shrink: 0;
261
+ background-color: #555; /* Fallback */
262
  font-size: 1.1rem;
263
  }
264
  .item-info {
 
271
  overflow: hidden;
272
  text-overflow: ellipsis;
273
  }
274
+ .item-address {
275
+ font-size: 0.85rem;
276
  color: var(--text-secondary);
277
  white-space: nowrap;
278
  overflow: hidden;
 
285
  flex-shrink: 0;
286
  }
287
 
288
+ /* Chat Window */
289
  #chat-window-view {
290
+ display: flex; /* Always flex container */
291
  flex-direction: column;
 
 
 
 
 
 
 
 
 
 
292
  }
293
+
294
  .chat-header {
295
  display: flex;
296
  align-items: center;
 
304
  background: none;
305
  border: none;
306
  cursor: pointer;
307
+ padding: 0;
308
+ display: none; /* Managed by JS/Media Query */
309
  }
310
  .back-btn svg {
311
  width: 24px;
 
370
  border-bottom-left-radius: 4px;
371
  }
372
 
373
+ .chat-placeholder {
374
  display: flex;
375
  flex-direction: column;
376
  align-items: center;
 
380
  color: var(--text-secondary);
381
  padding: 20px;
382
  }
383
+ .chat-placeholder img { width: 80px; margin-bottom: 20px; opacity: 0.5; }
384
 
385
  .message-form {
386
  display: flex;
387
+ padding: 16px;
388
  gap: 12px;
389
  background-color: var(--bg-secondary);
390
  border-top: 1px solid var(--border-color);
 
412
  }
413
  .send-btn svg { width: 20px; height: 20px; fill: white; }
414
 
415
+ /* User List View */
416
+ #user-list-view {
417
+ background-color: var(--bg-secondary);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  }
419
+ #user-list-view .list-header h2 { margin-bottom: 0; } /* No user profile in this header */
420
+ #user-list {
421
  flex-grow: 1;
422
+ overflow-y: auto;
 
423
  }
424
 
425
+ /* Browser View */
426
+ #browser-view {
427
+ display: flex;
428
+ flex-direction: column;
429
+ background-color: var(--bg-primary);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  }
431
+ .browser-header {
432
+ display: flex;
433
+ align-items: center;
434
+ gap: 8px;
435
+ padding: 8px 16px;
436
+ background-color: var(--bg-secondary);
437
+ border-bottom: 1px solid var(--border-color);
438
+ flex-shrink: 0;
439
  }
440
+ .browser-header form {
441
+ flex-grow: 1;
442
+ display: flex;
443
+ gap: 8px;
444
  }
445
+ .url-input {
446
+ flex-grow: 1;
447
+ padding: 6px 10px;
448
+ border: 1px solid var(--border-color);
449
+ background-color: var(--bg-tertiary);
450
+ color: var(--text-primary);
451
+ border-radius: 4px;
452
+ font-size: 0.9rem;
453
+ outline: none;
454
  }
455
+ .browser-iframe {
456
+ flex-grow: 1;
457
+ width: 100%;
458
+ height: 100%;
459
+ border: none;
 
 
460
  }
461
 
462
+ /* Bottom Navigation */
463
+ .bottom-nav {
 
 
 
 
 
 
 
464
  display: flex;
465
  justify-content: space-around;
466
  align-items: center;
467
+ height: 60px; /* Fixed height */
468
+ background-color: var(--bg-secondary);
469
  border-top: 1px solid var(--border-color);
470
+ flex-shrink: 0; /* Prevents shrinking */
471
  }
472
+
473
+ .nav-item {
474
  display: flex;
475
  flex-direction: column;
476
  align-items: center;
477
  justify-content: center;
478
+ flex-grow: 1;
 
 
 
 
479
  padding: 8px 0;
480
+ cursor: pointer;
481
+ color: var(--text-secondary);
482
+ font-size: 0.75rem;
483
  transition: color 0.2s ease;
484
  }
485
+ .nav-item svg {
486
+ width: 20px;
487
+ height: 20px;
488
  fill: var(--text-secondary);
489
+ margin-bottom: 4px;
490
  transition: fill 0.2s ease;
491
  }
492
+ .nav-item.active {
493
  color: var(--accent-blue-light);
494
  }
495
+ .nav-item.active svg {
496
  fill: var(--accent-blue-light);
497
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
 
499
+ .nav-item.qr-scan {
500
+ flex-grow: 0;
501
+ width: 56px;
502
+ height: 56px;
503
+ border-radius: 50%;
504
+ background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
505
+ color: white;
506
+ margin-top: -20px; /* Visually lift the button */
507
+ box-shadow: 0 -4px 15px rgba(0, 136, 204, 0.4);
508
+ display: flex;
509
+ align-items: center;
510
+ justify-content: center;
511
+ font-size: 0.8rem;
512
+ font-weight: 500;
513
+ }
514
+ .nav-item.qr-scan svg {
515
+ width: 24px;
516
+ height: 24px;
517
+ fill: white;
518
+ margin: 0;
519
+ }
520
+ .nav-item.qr-scan span { display: none; } /* Hide text on QR button */
521
+
522
+ /* Modals (Existing styles refined) */
523
  .modal-overlay {
524
  position: fixed;
525
  top: 0;
 
543
  border: 1px solid var(--border-color);
544
  box-shadow: 0 10px 30px rgba(0,0,0,0.5);
545
  }
546
+ .modal-content h3 { margin-bottom: 20px; font-weight: 600; font-size: 1.3rem; color: var(--text-primary); }
547
  .modal-content label { display: block; margin-bottom: 8px; font-size: 0.9rem; color: var(--text-secondary); }
548
  .modal-content input {
549
  width: 100%;
 
551
  margin-bottom: 16px;
552
  background-color: var(--bg-tertiary);
553
  border: 1px solid var(--border-color);
554
+ color: var(--text-primary);
555
  border-radius: 6px;
556
  font-size: 1rem;
557
  }
 
562
  border: none;
563
  cursor: pointer;
564
  font-weight: 500;
565
+ transition: opacity 0.2s ease;
566
  }
567
+ .modal-btn:hover { opacity: 0.9; }
568
  .secondary-btn { background-color: var(--bg-hover); color: white; }
569
+ .action-modal-btn { /* Reuse action-btn styles */
570
+ background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
571
+ color: white;
572
+ }
573
+
574
+ #profile-modal .modal-content { text-align: center; }
575
+ #profile-avatar-container .avatar {
576
+ width: 80px;
577
+ height: 80px;
578
+ font-size: 2.5rem;
579
+ }
580
+ #profile-username { font-size: 1.2rem; font-weight: 600; color: var(--text-primary); margin-top: 10px; }
581
+ #profile-address { color: var(--text-secondary); font-size: 0.9rem; word-break: break-all; margin-top: 8px; }
582
+ #profile-qr-code { background: white; padding: 10px; margin: 20px auto; width: fit-content; border-radius: 8px; }
583
+ #profile-modal p { text-align: center; color: var(--text-secondary); font-size: 0.8rem; margin-top: -10px; margin-bottom: 20px; }
584
+ #profile-modal .modal-actions { flex-direction: column; gap: 12px; align-items: stretch; }
585
+
586
+ #scanner-modal .modal-content { max-width: 450px; }
587
+ #qr-reader { width: 100%; border: 1px solid var(--border-color); margin-top: 16px; border-radius: 8px; overflow: hidden; }
588
+ #qr-reader__dashboard_section_swaplink { display: none; } /* Hide camera swap link */
589
+
590
  #status-bar {
591
  position: fixed;
592
+ bottom: 80px; /* Above the bottom nav */
593
  left: 50%;
594
  transform: translateX(-50%);
595
  background-color: var(--bg-tertiary);
 
602
  transition: opacity 0.3s, visibility 0.3s;
603
  z-index: 2000;
604
  box-shadow: 0 5px 15px rgba(0,0,0,0.3);
 
 
605
  }
606
  #status-bar.success { background-color: var(--success-color); }
607
  #status-bar.error { background-color: var(--error-color); }
608
  #status-bar.visible { opacity: 1; visibility: visible; }
609
 
610
+ /* Desktop styles */
611
  @media (min-width: 768px) {
612
+ .main-container {
 
 
 
613
  max-width: 1100px;
614
  max-height: 800px;
615
  border-radius: 12px;
 
617
  box-shadow: 0 10px 40px rgba(0,0,0,0.3);
618
  border: 1px solid var(--border-color);
619
  }
620
+ #app-view {
621
+ flex-direction: row;
622
+ }
623
+ .content-area {
624
  flex-direction: row;
625
  }
626
 
627
+ .app-view-section {
628
+ position: relative; /* No stacking on desktop */
629
+ width: auto; /* Width managed by flex/specific rules */
630
  flex-grow: 1;
631
+ display: flex !important; /* Always visible in its container */
632
+ flex-direction: column;
633
  }
634
 
635
+ #chatroom-list-view, #user-list-view {
636
+ width: 320px;
637
  flex-shrink: 0;
638
  border-right: 1px solid var(--border-color);
 
 
 
639
  }
640
 
641
+ #chat-window-view, #browser-view {
642
+ flex-grow: 1;
 
 
 
 
 
 
 
 
 
 
 
643
  }
644
 
645
+ /* Hide non-active views from taking up space, but keep them display:flex for layout */
646
+ #chatroom-list-view:not(.active),
647
+ #user-list-view:not(.active),
648
+ #browser-view:not(.active) {
649
+ display: none !important; /* Force hide if not active */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
650
  }
651
+ #chat-window-view:not(.active) {
652
+ /* On desktop, chat view can be visible alongside list view */
 
 
 
 
653
  }
 
 
 
 
654
 
655
+
656
+ .back-btn { display: none !important; } /* No back button on desktop */
657
+
658
+ .bottom-nav {
659
+ display: none; /* Hide bottom nav on desktop */
660
+ }
661
  #status-bar {
662
+ bottom: 20px; /* Move status bar back down */
663
  }
664
+ .user-profile { margin-bottom: 16px;} /* Restore margin */
665
+ .list-header-top { margin-bottom: 16px;} /* Restore margin */
666
  }
667
  </style>
668
  </head>
 
676
  </div>
677
 
678
  <div id="app-view" class="main-container">
679
+ <div class="content-area">
680
+ <div id="chatroom-list-view" class="app-view-section">
681
+ <div class="list-header">
682
+ <div class="list-header-top">
683
+ <h2>Чаты</h2>
684
+ <button id="create-room-show-modal" class="action-btn small">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
685
  <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>
686
  <span>Новый</span>
687
  </button>
688
  </div>
689
+ <div class="user-profile">
690
+ <div id="user-wallet"></div>
691
+ <div id="user-nickname"></div>
692
+ <form class="username-form" id="username-form">
693
+ <input type="text" id="username-input" class="username-input" placeholder="Установить никнейм" autocomplete="off">
694
+ <button type="submit" class="action-btn small">✓</button>
695
+ </form>
696
+ </div>
697
  </div>
698
  <div id="chatroom-list"></div>
 
 
 
 
 
699
  </div>
700
 
701
+ <div id="user-list-view" class="app-view-section">
702
+ <div class="list-header">
703
  <h2>Пользователи</h2>
704
  </div>
705
  <div id="user-list"></div>
 
 
 
 
 
706
  </div>
707
 
708
+ <div id="chat-window-view" class="app-view-section">
709
+ <div id="chat-placeholder" class="chat-placeholder">
710
+ <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
711
+ <h2>Выберите чат</h2>
712
+ <p>Начните общение в одном из существующих чатов или создайте свой собственный.</p>
713
  </div>
714
+ <div id="active-chat" style="display: none; width: 100%; height: 100%; flex-direction: column;">
715
+ <div class="chat-header">
716
+ <button class="back-btn" id="back-to-list-btn">
717
+ <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>
718
+ </button>
719
+ <div id="chat-header-avatar" class="avatar"></div>
720
+ <span id="chat-header-title"></span>
721
+ </div>
722
+ <div id="messages-container"></div>
723
+ <form id="message-form" class="message-form">
724
+ <input type="text" id="message-input" placeholder="Сообщение..." autocomplete="off">
725
+ <button type="submit" class="action-btn send-btn" id="send-btn">
726
+ <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>
727
+ </button>
728
+ </form>
729
  </div>
 
730
  </div>
731
 
732
+ <div id="browser-view" class="app-view-section">
733
+ <div class="browser-header">
734
+ <form id="browser-form">
735
+ <input type="text" id="url-input" class="url-input" placeholder="Введите URL или поисковый запрос" autocomplete="off">
736
+ <button type="submit" class="action-btn small">Перейти</button>
737
+ </form>
 
 
 
 
 
 
 
 
 
 
 
 
738
  </div>
739
+ <iframe id="browser-iframe" class="browser-iframe" src="https://www.google.com"></iframe>
740
  </div>
741
 
742
  </div>
743
 
744
+ <div class="bottom-nav">
745
+ <div class="nav-item" data-view="chats">
746
+ <svg xmlns="http://www.w3.org/2000/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>
747
+ <span>Чаты</span>
 
748
  </div>
749
+ <div class="nav-item" data-view="users">
750
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.55 1.26 2.78 2.83 3.46 4.45.1-.01.2-.02.31-.05V19h4v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>
751
+ <span>Пользователи</span>
752
+ </div>
753
+ <div class="nav-item qr-scan" data-view="scanner">
754
+ <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>
755
+ <span>QR</span>
756
+ </div>
757
+ <div class="nav-item" data-view="browser">
758
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-5 14H4V6h11v12zm-1-7l-1.25 2.75L10 15l2.75-1.25L15 10l-1 1z"/></svg>
759
+ <span>Браузер</span>
760
+ </div>
761
+ <div class="nav-item" data-view="profile">
762
+ <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="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>
763
+ <span>Профиль</span>
764
  </div>
765
  </div>
766
 
 
776
  <input type="password" id="room-password">
777
  <div class="modal-actions">
778
  <button type="button" id="create-room-cancel" class="modal-btn secondary-btn">Отмена</button>
779
+ <button type="submit" class="modal-btn action-modal-btn">Создать</button>
780
  </div>
781
  </form>
782
  </div>
 
790
  <input type="password" id="password-input" required>
791
  <div class="modal-actions">
792
  <button type="button" id="password-cancel" class="modal-btn secondary-btn">Отмена</button>
793
+ <button type="submit" class="modal-btn action-modal-btn">Войти</button>
794
  </div>
795
  </form>
796
  </div>
797
  </div>
798
 
799
  <div id="profile-modal" class="modal-overlay">
800
+ <div class="modal-content">
801
  <h3 id="profile-modal-title">Профиль пользователя</h3>
802
  <div id="profile-avatar-container" style="margin: 20px auto; display: inline-block;"></div>
803
+ <p id="profile-username" style="font-size: 1.2rem; font-weight: 600;"></p>
804
+ <p id="profile-address" style="color: var(--text-secondary); font-size: 0.9rem; word-break: break-all; margin-top: 8px;"></p>
805
+ <div id="profile-qr-code" style="background: white; padding: 10px; margin: 20px auto; width: fit-content; border-radius: 8px;"></div>
806
  <p style="text-align: center; color: var(--text-secondary); font-size: 0.8rem; margin-top: -10px; margin-bottom: 20px;">Отсканируйте для открытия профиля</p>
807
  <div class="modal-actions" style="flex-direction: column; gap: 12px; align-items: stretch;">
808
+ <button id="send-ton-btn" class="modal-btn action-modal-btn">Отправить TON</button>
809
  <button id="profile-close-btn" class="modal-btn secondary-btn">Закрыть</button>
810
  </div>
811
  </div>
 
814
  <div id="scanner-modal" class="modal-overlay">
815
  <div class="modal-content">
816
  <h3>Сканировать QR-код</h3>
817
+ <div id="qr-reader"></div>
818
  <div class="modal-actions">
819
  <button id="scanner-close-btn" class="modal-btn secondary-btn">Отмена</button>
820
  </div>
 
834
  let activeChatroomId = null;
835
  let messagePollingInterval = null;
836
  let chatroomsData = {};
 
837
  let html5QrCode = null;
838
+ let profileQrCode = null;
839
+ let currentView = 'chats'; // Default view
840
+
841
  const loginView = document.getElementById('login-view');
842
  const appView = document.getElementById('app-view');
843
  const chatroomListView = document.getElementById('chatroom-list-view');
844
  const userListView = document.getElementById('user-list-view');
 
 
845
  const chatWindowView = document.getElementById('chat-window-view');
846
+ const browserView = document.getElementById('browser-view');
847
  const chatPlaceholder = document.getElementById('chat-placeholder');
848
  const activeChat = document.getElementById('active-chat');
849
  const profileModal = document.getElementById('profile-modal');
850
  const scannerModal = document.getElementById('scanner-modal');
851
+ const bottomNavItems = document.querySelectorAll('.bottom-nav .nav-item');
852
 
853
  const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292'];
854
 
 
869
  statusBar.className = 'status-bar';
870
  if (type === 'success') statusBar.classList.add('success');
871
  else if (type === 'error') statusBar.classList.add('error');
872
+
873
  statusBar.classList.add('visible');
874
+ setTimeout(() => {
875
+ statusBar.classList.remove('visible');
876
+ // Allow time for transition before removing class
877
+ setTimeout(() => statusBar.className = 'status-bar', 300);
878
+ }, duration);
879
  };
880
 
881
  const apiCall = async (endpoint, options = {}) => {
 
892
  throw error;
893
  }
894
  };
895
+
896
  const truncateAddress = (address) => address ? `${address.substring(0, 4)}...${address.substring(address.length - 4)}` : '';
897
 
898
+ const updateUserInfo = () => {
899
+ document.getElementById('user-wallet').textContent = `Кошелек: ${truncateAddress(currentUser.address)}`;
900
+ const nicknameEl = document.getElementById('user-nickname');
901
+ const usernameInput = document.getElementById('username-input');
902
+ nicknameEl.textContent = currentUser.username ? `Ник: ${currentUser.username}` : `Никнейм не установлен`;
 
 
 
 
 
 
 
 
 
 
903
  usernameInput.value = currentUser.username || '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
904
  };
905
+
906
+ document.getElementById('username-form').addEventListener('submit', async (e) => {
907
  e.preventDefault();
908
+ const newUsername = document.getElementById('username-input').value.trim();
909
  if (!newUsername || newUsername.length < 3) {
910
  showStatus('Никнейм должен быть не короче 3 символов.', 'error');
911
  return;
 
921
  body: JSON.stringify({ address: currentUser.address, username: newUsername })
922
  });
923
  currentUser.username = newUsername;
924
+ updateUserInfo();
925
  showStatus('Никнейм успешно обновлен!', 'success');
926
+ fetchChatrooms(); // Update chat list names
927
+ fetchUsers(); // Update user list names
928
+ if (activeChatroomId) fetchMessages(activeChatroomId); // Update chat messages names
929
  } catch (err) {}
930
  });
931
 
 
941
  } catch (err) {
942
  currentUser.username = null;
943
  }
944
+ updateUserInfo();
945
  loginView.style.display = 'none';
946
  appView.style.display = 'flex';
947
+ showView('chats'); // Show default view after login
948
  fetchChatrooms();
949
+ fetchUsers();
950
+ };
951
+
952
+ const stopMessagePolling = () => {
953
+ if (messagePollingInterval) {
954
+ clearInterval(messagePollingInterval);
955
+ messagePollingInterval = null;
956
+ }
957
+ };
958
+
959
+ const startMessagePolling = (roomId) => {
960
+ stopMessagePolling(); // Ensure no double polling
961
+ messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000);
962
+ };
963
+
964
+ const showView = (viewName) => {
965
+ stopMessagePolling(); // Stop polling when switching views
966
+
967
+ const views = {
968
+ 'chats': chatroomListView,
969
+ 'users': userListView,
970
+ 'chat': chatWindowView, // Special case for chat window on mobile
971
+ 'browser': browserView,
972
+ 'profile': profileModal, // Profile is a modal, handle separately
973
+ 'scanner': scannerModal // Scanner is a modal, handle separately
974
+ };
975
+
976
+ // Handle modals first
977
+ if (viewName === 'profile') {
978
+ if (currentUser.address) showProfile(currentUser.address);
979
+ return; // Don't change main content view
980
+ }
981
+ if (viewName === 'scanner') {
982
+ showScanner();
983
+ return; // Don't change main content view
984
+ }
985
+
986
+ // Handle main content views
987
+ currentView = viewName;
988
+ const isMobile = window.innerWidth < 768;
989
+
990
+ document.querySelectorAll('.app-view-section').forEach(view => {
991
+ view.classList.remove('active');
992
+ });
993
+
994
+ bottomNavItems.forEach(item => {
995
+ item.classList.remove('active');
996
+ if (item.dataset.view === viewName) {
997
+ item.classList.add('active');
998
+ }
999
+ });
1000
+
1001
+ if (isMobile) {
1002
+ // On mobile, only one main view is visible at a time
1003
+ if (viewName === 'chat') {
1004
+ views[viewName].classList.add('active');
1005
+ document.getElementById('back-to-list-btn').style.display = 'block';
1006
+ activeChat.style.display = 'flex'; // Ensure active chat is visible
1007
+ chatPlaceholder.style.display = 'none'; // Hide placeholder
1008
+ if (activeChatroomId) startMessagePolling(activeChatroomId); // Restart polling if in chat
1009
+ } else {
1010
+ views[viewName].classList.add('active');
1011
+ document.getElementById('back-to-list-btn').style.display = 'none';
1012
+ activeChat.style.display = 'none'; // Hide active chat content
1013
+ chatPlaceholder.style.display = 'flex'; // Show placeholder
1014
+ }
1015
+ } else {
1016
+ // On desktop, chat/browser appears next to list
1017
+ // Show the requested view (chats/users/browser)
1018
+ if (viewName !== 'chat') { // 'chat' view is handled by selecting a room
1019
+ views[viewName].classList.add('active');
1020
+ }
1021
+
1022
+ // Ensure the chat window is visible *if* a room is selected, regardless of the button clicked
1023
+ // But hide its content if we're not in the 'chat' context
1024
+ chatWindowView.classList.add('active'); // Chat window area is always visible on desktop
1025
+
1026
+ if (activeChatroomId && viewName === 'chats') {
1027
+ activeChat.style.display = 'flex';
1028
+ chatPlaceholder.style.display = 'none';
1029
+ startMessagePolling(activeChatroomId);
1030
+ } else {
1031
+ activeChat.style.display = 'none';
1032
+ chatPlaceholder.style.display = 'flex';
1033
+ }
1034
+ }
1035
  };
1036
 
1037
  const renderChatrooms = (rooms) => {
1038
  const list = document.getElementById('chatroom-list');
1039
  list.innerHTML = '';
 
1040
  chatroomsData = {};
1041
  rooms.forEach(room => {
1042
  chatroomsData[room.id] = room;
1043
  const item = document.createElement('div');
1044
  item.className = 'chatroom-item';
1045
  item.dataset.id = room.id;
1046
+
1047
  item.appendChild(getAvatar(room.name));
1048
 
1049
  const infoDiv = document.createElement('div');
 
1051
  const nameSpan = document.createElement('div');
1052
  nameSpan.className = 'item-name';
1053
  nameSpan.textContent = room.name;
 
 
 
 
 
1054
  infoDiv.appendChild(nameSpan);
 
1055
  item.appendChild(infoDiv);
1056
 
1057
  if (room.is_private) {
 
1064
  list.appendChild(item);
1065
  });
1066
  };
1067
+
1068
  const fetchChatrooms = async () => {
 
1069
  try {
1070
  const data = await apiCall('/api/chatrooms');
1071
  renderChatrooms(data.chatrooms);
1072
  } catch (err) {}
1073
  };
1074
 
1075
+ const renderUsers = (users) => {
1076
+ const list = document.getElementById('user-list');
1077
+ list.innerHTML = '';
 
 
1078
  users.forEach(user => {
 
1079
  const item = document.createElement('div');
1080
  item.className = 'user-item';
1081
  item.dataset.address = user.address;
1082
 
1083
+ item.appendChild(getAvatar(user.username || user.address));
 
1084
 
1085
  const infoDiv = document.createElement('div');
1086
  infoDiv.className = 'item-info';
1087
  const nameSpan = document.createElement('div');
1088
  nameSpan.className = 'item-name';
1089
+ nameSpan.textContent = user.username || 'Не установлен';
1090
+ infoDiv.appendChild(nameSpan);
1091
  const addressSpan = document.createElement('div');
1092
+ addressSpan.className = 'item-address';
1093
  addressSpan.textContent = truncateAddress(user.address);
 
 
1094
  infoDiv.appendChild(addressSpan);
1095
+
1096
  item.appendChild(infoDiv);
1097
 
1098
  item.addEventListener('click', () => showProfile(user.address));
1099
  list.appendChild(item);
1100
  });
1101
+ };
1102
 
1103
+ const fetchUsers = async () => {
 
 
1104
  try {
1105
  const data = await apiCall('/api/users');
1106
  renderUsers(data.users);
1107
+ } catch (err) {}
1108
+ };
 
 
 
 
1109
 
1110
 
1111
  const renderMessages = (messages) => {
 
1120
  avatar.classList.add('message-avatar');
1121
  avatar.style.cursor = 'pointer';
1122
  avatar.onclick = () => showProfile(msg.sender_address);
1123
+
1124
  const contentDiv = document.createElement('div');
1125
  contentDiv.className = 'message-content';
1126
 
 
1128
  senderDiv.className = 'message-sender';
1129
  senderDiv.textContent = msg.display_name;
1130
  senderDiv.onclick = () => showProfile(msg.sender_address);
1131
+
1132
  const bubbleDiv = document.createElement('div');
1133
  bubbleDiv.className = 'message-bubble';
1134
  bubbleDiv.textContent = msg.text;
 
1141
  container.appendChild(msgDiv);
1142
  });
1143
 
1144
+ if(shouldScroll || messages.length <= 5) { // Scroll to bottom on initial load or if already there
1145
  container.scrollTop = container.scrollHeight;
1146
  }
1147
  };
1148
+
1149
  const fetchMessages = async (roomId) => {
1150
+ if (!roomId) return;
1151
  try {
1152
  const data = await apiCall(`/api/messages/${roomId}`);
1153
  renderMessages(data.messages);
1154
  } catch (err) {
1155
+ // Error fetching messages, polling will stop automatically if apiCall throws
1156
  }
1157
  };
1158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1159
  const selectChatroom = (roomId, isPrivate) => {
1160
  const roomData = chatroomsData[roomId];
1161
+ if (!roomData) return;
1162
 
1163
  const proceedToRoom = () => {
 
1164
  activeChatroomId = roomId;
1165
+
1166
  document.getElementById('chat-header-title').textContent = roomData.name;
1167
  const headerAvatar = document.getElementById('chat-header-avatar');
1168
  headerAvatar.innerHTML = '';
1169
  headerAvatar.appendChild(getAvatar(roomData.name));
1170
 
1171
+ showView('chat'); // Switch view to chat window
 
 
 
 
 
 
 
 
 
 
 
1172
  fetchMessages(roomId);
1173
+ startMessagePolling(roomId);
1174
  };
1175
+
1176
  if (isPrivate) {
1177
  const passwordModal = document.getElementById('password-modal');
1178
  const passwordForm = document.getElementById('password-form');
 
1183
 
1184
  const formSubmitHandler = async (e) => {
1185
  e.preventDefault();
 
1186
  const password = passwordInput.value;
1187
  passwordModal.style.display = 'none';
1188
+ passwordForm.removeEventListener('submit', formSubmitHandler); // Remove listener immediately
1189
+ document.getElementById('password-cancel').onclick = null; // Remove cancel handler
1190
+
1191
  try {
1192
  await apiCall('/api/join_chatroom', {
1193
  method: 'POST',
 
1195
  body: JSON.stringify({ chatroom_id: roomId, password })
1196
  });
1197
  proceedToRoom();
1198
+ } catch (err) {
1199
+ // Error message shown by apiCall
1200
+ }
1201
  };
1202
  passwordForm.addEventListener('submit', formSubmitHandler);
1203
 
1204
  document.getElementById('password-cancel').onclick = () => {
1205
  passwordModal.style.display = 'none';
1206
+ passwordForm.removeEventListener('submit', formSubmitHandler); // Clean up
1207
+ document.getElementById('password-cancel').onclick = null;
1208
  };
1209
+
1210
  } else {
1211
  proceedToRoom();
1212
  }
 
1231
  text: text
1232
  })
1233
  });
1234
+ // Fetch messages immediately after sending
1235
  await fetchMessages(activeChatroomId);
1236
+ // No need to scroll manually, renderMessages handles it
1237
  } finally {
1238
  input.disabled = false;
1239
  sendBtn.disabled = false;
 
1254
  e.preventDefault();
1255
  const name = document.getElementById('room-name').value.trim();
1256
  const password = document.getElementById('room-password').value;
1257
+ if (!name) {
1258
+ showStatus('Название чата не может быть пустым.', 'error');
1259
+ return;
1260
+ }
1261
+ if (!currentUser.address) {
1262
+ showStatus('Ошибка: Пользователь не авторизован.', 'error');
1263
+ return;
1264
+ }
1265
 
1266
  try {
1267
  await apiCall('/api/create_chatroom', {
 
1275
  } catch (err) {}
1276
  });
1277
 
1278
+ const showProfile = async (addressToShow) => {
1279
+ if (!addressToShow) {
1280
+ showStatus('Ошибка: Не указан адрес пользователя.', 'error');
1281
+ return;
1282
+ }
1283
  try {
1284
  const userData = await apiCall('/api/user_data', {
1285
  method: 'POST',
1286
  headers: { 'Content-Type': 'application/json' },
1287
+ body: JSON.stringify({ address: addressToShow })
1288
  });
1289
+
1290
+ const username = userData.username || `User ${truncateAddress(addressToShow)}`;
1291
  const avatarContainer = document.getElementById('profile-avatar-container');
1292
+ const usernameEl = document.getElementById('profile-username');
1293
+ const addressEl = document.getElementById('profile-address');
1294
+ const qrCodeEl = document.getElementById('profile-qr-code');
1295
  const sendTonBtn = document.getElementById('send-ton-btn');
1296
+
1297
  avatarContainer.innerHTML = '';
1298
  avatarContainer.appendChild(getAvatar(username));
1299
+
1300
  usernameEl.textContent = username;
1301
+ addressEl.textContent = addressToShow;
1302
+
1303
  qrCodeEl.innerHTML = '';
1304
+ if (profileQrCode) {
1305
+ profileQrCode.clear(); // Clear previous QR code
1306
+ profileQrCode = null;
1307
  }
1308
+ profileQrCode = new QRCode(qrCodeEl, {
1309
+ text: addressToShow,
1310
  width: 150,
1311
  height: 150,
1312
  colorDark : "#000000",
1313
  colorLight : "#ffffff",
1314
  correctLevel : QRCode.CorrectLevel.H
1315
  });
1316
+
1317
  sendTonBtn.onclick = async () => {
1318
  if (!tonConnectUI.connected) {
1319
  showStatus('Подключите кошелек для отправки TON.', 'error');
1320
  return;
1321
  }
 
 
 
 
1322
  const amountString = prompt("Введите сумму в TON для отправки:", "0.1");
1323
  if (amountString === null) return;
1324
+
1325
  const amount = parseFloat(amountString);
1326
  if (isNaN(amount) || amount <= 0) {
1327
  showStatus('Неверная сумма.', 'error');
1328
  return;
1329
  }
1330
+
1331
  const amountInNanoTon = Math.floor(amount * 1_000_000_000).toString();
1332
 
1333
  const transaction = {
1334
+ validUntil: Math.floor(Date.now() / 1000) + 600, // 10 minutes
1335
+ messages: [ { address: addressToShow, amount: amountInNanoTon } ]
1336
  };
1337
 
1338
  try {
 
1343
  showStatus('Транзакция отклонена.', 'error');
1344
  }
1345
  };
1346
+
1347
+ sendTonBtn.style.display = (addressToShow === currentUser.address) ? 'none' : 'block';
1348
  profileModal.style.display = 'flex';
1349
  } catch (err) {
1350
  showStatus('Не удалось загрузить профиль.', 'error');
 
1353
 
1354
  const showScanner = () => {
1355
  scannerModal.style.display = 'flex';
1356
+ if (!html5QrCode) {
1357
+ html5QrCode = new Html5Qrcode("qr-reader");
1358
+ }
1359
+
1360
  const qrCodeSuccessCallback = (decodedText, decodedResult) => {
1361
  hideScanner();
1362
+ // Basic validation for TON addresses
1363
  if (decodedText && decodedText.length > 40 && (decodedText.startsWith('EQ') || decodedText.startsWith('UQ'))) {
1364
  showProfile(decodedText);
1365
  } else {
1366
+ showStatus('Отсканирован недействительный QR-код (не похож на адрес TON).', 'error');
1367
  }
1368
  };
1369
+ const config = { fps: 10, qrbox: { width: 250, height: 250 }, aspectRatio: 1.0 };
1370
+
1371
+ // Check for camera permissions and start scanning
1372
+ html5QrCode.start({ facingMode: "environment" }, config, qrCodeSuccessCallback, (errorMessage) => {
1373
+ // Optional: report errors
1374
+ // console.warn(`QR code no match: ${errorMessage}`);
1375
+ })
1376
  .catch(err => {
1377
+ showStatus(`Не удалось запустить сканер: ${err}`, 'error');
1378
+ hideScanner(); // Hide modal if start fails
1379
  });
1380
  };
1381
 
 
1386
  scannerModal.style.display = 'none';
1387
  };
1388
 
1389
+ // Browser View Logic
1390
+ document.getElementById('browser-form').addEventListener('submit', (e) => {
1391
+ e.preventDefault();
1392
+ const urlInput = document.getElementById('url-input');
1393
+ let url = urlInput.value.trim();
1394
+ const iframe = document.getElementById('browser-iframe');
1395
+
1396
+ if (!url) return;
1397
+
1398
+ // Simple URL sanitization/prefixing
1399
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
1400
+ // Assume it's a search query or missing protocol
1401
+ if (url.includes('.')) { // Looks like a domain
1402
+ url = 'https://' + url;
1403
+ } else { // Assume it's a search query
1404
+ url = `https://www.google.com/search?q=${encodeURIComponent(url)}`;
1405
+ }
1406
+ }
1407
+
1408
+ try {
1409
+ iframe.src = url;
1410
+ // Update input value to the potentially corrected URL
1411
+ urlInput.value = url;
1412
+ } catch (e) {
1413
+ showStatus('Не удалось загрузить URL. Проверьте формат.', 'error');
1414
+ }
1415
+ });
1416
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1417
 
1418
+ // Event listeners for modals and navigation
1419
  document.getElementById('profile-close-btn').addEventListener('click', () => profileModal.style.display = 'none');
1420
  document.getElementById('scanner-close-btn').addEventListener('click', hideScanner);
1421
+ document.getElementById('back-to-list-btn').addEventListener('click', () => showView('chats')); // Go back to chats list
1422
+
1423
+
1424
+ bottomNavItems.forEach(item => {
1425
+ if (item.dataset.view) {
1426
+ item.addEventListener('click', () => {
1427
+ // Special handling for chat window on mobile back button
1428
+ if (window.innerWidth < 768 && currentView === 'chat' && item.dataset.view !== 'chat') {
1429
+ // If currently in chat on mobile and clicking another nav item,
1430
+ // hide chat content first, then show the new view.
1431
+ activeChatroomId = null; // Effectively leave chat state
1432
+ activeChat.style.display = 'none';
1433
+ chatPlaceholder.style.display = 'flex';
1434
+ }
1435
+ showView(item.dataset.view);
1436
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1437
  }
1438
+ });
1439
 
 
 
1440
 
1441
+ const handleResize = () => {
1442
+ // Re-apply view logic on resize
1443
+ showView(currentView);
1444
+
1445
+ const isMobile = window.innerWidth < 768;
1446
+ const backBtn = document.getElementById('back-to-list-btn');
1447
+
1448
+ if (isMobile) {
1449
+ // On mobile, only show back button if current view is 'chat'
1450
+ if (currentView === 'chat' && activeChat.style.display === 'flex') {
1451
+ backBtn.style.display = 'block';
1452
+ chatroomListView.classList.remove('active');
1453
+ userListView.classList.remove('active');
1454
+ browserView.classList.remove('active');
1455
+ chatWindowView.classList.add('active');
1456
+ } else {
1457
+ backBtn.style.display = 'none';
1458
+ // Ensure correct view is active based on currentView
1459
+ document.querySelectorAll('.app-view-section').forEach(view => {
1460
+ view.classList.remove('active');
1461
+ });
1462
+ if (currentView === 'chats') chatroomListView.classList.add('active');
1463
+ if (currentView === 'users') userListView.classList.add('active');
1464
+ if (currentView === 'browser') browserView.classList.add('active');
1465
 
1466
+ activeChat.style.display = 'none';
1467
+ chatPlaceholder.style.display = 'flex';
1468
+ }
1469
+ } else {
1470
+ // On desktop, back button is never visible
1471
+ backBtn.style.display = 'none';
1472
+
1473
+ // On desktop, multiple sections can be displayed via flex
1474
+ chatroomListView.classList.remove('active'); // No need for .active class on desktop views
1475
+ userListView.classList.remove('active');
1476
+ chatWindowView.classList.remove('active');
1477
+ browserView.classList.remove('active');
1478
+
1479
+
1480
+ // Ensure the selected view and chat window are displayed side-by-side
1481
+ document.querySelectorAll('.app-view-section').forEach(view => view.style.display = 'none');
1482
+
1483
+
1484
+ if (currentView === 'chats') {
1485
+ chatroomListView.style.display = 'flex';
1486
+ chatWindowView.style.display = 'flex';
1487
+ if (activeChatroomId) {
1488
+ activeChat.style.display = 'flex';
1489
+ chatPlaceholder.style.display = 'none';
1490
+ } else {
1491
+ activeChat.style.display = 'none';
1492
+ chatPlaceholder.style.display = 'flex';
1493
+ }
1494
+ } else if (currentView === 'users') {
1495
+ userListView.style.display = 'flex';
1496
+ chatWindowView.style.display = 'flex'; // Keep chat window area visible
1497
+ activeChat.style.display = 'none'; // But hide chat content
1498
+ chatPlaceholder.style.display = 'flex';
1499
+ } else if (currentView === 'browser') {
1500
+ browserView.style.display = 'flex';
1501
+ chatWindowView.style.display = 'flex'; // Keep chat window area visible
1502
+ activeChat.style.display = 'none'; // But hide chat content
1503
+ chatPlaceholder.style.display = 'flex';
1504
+ } else if (currentView === 'chat') { // If we were in chat on mobile and resized to desktop
1505
+ chatroomListView.style.display = 'flex';
1506
+ chatWindowView.style.display = 'flex';
1507
+ if (activeChatroomId) {
1508
+ activeChat.style.display = 'flex';
1509
+ chatPlaceholder.style.display = 'none';
1510
+ startMessagePolling(activeChatroomId); // Resume polling if active
1511
+ } else {
1512
+ // Should not happen if currentView is 'chat' but activeChatroomId is null
1513
+ activeChat.style.display = 'none';
1514
+ chatPlaceholder.style.display = 'flex';
1515
+ showView('chats'); // Default back to chats view
1516
+ }
1517
  }
1518
 
1519
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
1520
  };
1521
 
1522
  window.addEventListener('resize', handleResize);
1523
+
1524
+
1525
+ // Initial TON Connect setup
1526
  tonConnectUI.onStatusChange(wallet => {
1527
  if (wallet) {
1528
  const address = TON_CONNECT_UI.toUserFriendlyAddress(wallet.account.address, false);
 
1531
  currentUser = { address: null, username: null };
1532
  appView.style.display = 'none';
1533
  loginView.style.display = 'flex';
1534
+ stopMessagePolling();
1535
  activeChatroomId = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1536
  }
1537
  });
1538
 
1539
+ // Initial view setup
1540
+ handleResize(); // Apply initial layout based on screen size
1541
+ if (!tonConnectUI.connected) {
 
 
1542
  loginView.style.display = 'flex';
1543
  appView.style.display = 'none';
1544
+ } else {
1545
+ // If already connected on page load, initialize user and show app
1546
+ const address = TON_CONNECT_UI.toUserFriendlyAddress(tonConnectUI.wallet.account.address, false);
1547
+ initializeUser(address);
1548
+ }
1549
 
 
1550
  });
1551
  </script>
1552
  </body>
 
1554
  '''
1555
  return Response(html_content, mimetype='text/html')
1556
 
 
1557
  @app.route('/api/user_data', methods=['POST'])
1558
  def get_user_data():
1559
  data = request.get_json()
 
1566
  return jsonify({'username': username})
1567
 
1568
  @app.route('/api/users', methods=['GET'])
1569
+ def get_users():
1570
  db = read_db()
1571
  users_list = []
1572
  for address, user_info in db['users'].items():
1573
+ users_list.append({
1574
+ 'address': address,
1575
+ 'username': user_info.get('username')
1576
+ })
1577
+ # Add users from messages who aren't in the users dict yet
1578
+ all_sender_addresses = set()
1579
+ for chatroom_messages in db['messages'].values():
1580
+ for msg in chatroom_messages:
1581
+ all_sender_addresses.add(msg['sender_address'])
1582
+
1583
+ for address in all_sender_addresses:
1584
+ if address not in db['users']:
1585
+ users_list.append({
1586
+ 'address': address,
1587
+ 'username': None
1588
+ })
1589
+
1590
+ # Remove duplicates based on address (though set usage above should prevent most)
1591
+ seen_addresses = set()
1592
+ unique_users = []
1593
+ for user in users_list:
1594
+ if user['address'] not in seen_addresses:
1595
+ unique_users.append(user)
1596
+ seen_addresses.add(user['address'])
1597
+
1598
+
1599
+ # Sort by username or address
1600
+ unique_users.sort(key=lambda x: (x['username'] is None, x['username'] or x['address']))
1601
+
1602
+ return jsonify({'users': unique_users})
1603
 
1604
 
1605
  @app.route('/api/set_username', methods=['POST'])
 
1670
  def get_messages(chatroom_id):
1671
  db = read_db()
1672
  if chatroom_id not in db['messages']:
1673
+ # Check if chatroom exists but has no messages yet
1674
+ if chatroom_id in db['chatrooms']:
1675
+ return jsonify({'messages': []})
1676
  return jsonify({'error': 'Chatroom not found'}), 404
1677
+
1678
  messages_with_names = []
1679
  room_messages = db['messages'].get(chatroom_id, [])
1680
+
1681
+ # Collect all sender addresses in this chatroom
1682
+ sender_addresses_in_room = {msg['sender_address'] for msg in room_messages}
1683
+
1684
+ # Fetch user data for all senders in the room efficiently
1685
+ users_in_room = {}
1686
+ for address in sender_addresses_in_room:
1687
+ user_info = db['users'].get(address)
1688
+ users_in_room[address] = user_info.get('username') if user_info else None
1689
+
 
1690
 
1691
  for msg in room_messages:
1692
+ sender_address = msg['sender_address']
1693
+ username = users_in_room.get(sender_address)
1694
+ display_name = username if username else f"{sender_address[:4]}...{sender_address[-4:]}"
1695
+
1696
  msg_copy = msg.copy()
1697
+ msg_copy['display_name'] = display_name
1698
  messages_with_names.append(msg_copy)
1699
 
1700
  return jsonify({'messages': messages_with_names})
 
1708
 
1709
  if not all([chatroom_id, sender_address, text]):
1710
  return jsonify({'error': 'Missing data'}), 400
1711
+ if not sender_address: # Double check sender address exists
1712
+ return jsonify({'error': 'Sender address is required'}), 400
1713
 
1714
  db = read_db()
1715
  if chatroom_id not in db['messages']:
1716
+ # Check if chatroom exists but message list is missing (shouldn't happen with init_db)
1717
+ if chatroom_id in db['chatrooms']:
1718
+ db['messages'][chatroom_id] = []
1719
+ else:
1720
+ return jsonify({'error': 'Chatroom not found'}), 404
1721
+
1722
+ # Ensure sender is in users list if not already
1723
+ if sender_address not in db['users']:
1724
+ db['users'][sender_address] = {} # Add with default empty info
1725
 
1726
  message = {
1727
  'id': str(uuid.uuid4()),
 
1729
  'text': text,
1730
  'timestamp': datetime.utcnow().isoformat() + "Z"
1731
  }
1732
+
1733
+ # Keep only the last 100 messages per chatroom
1734
+ if len(db['messages'][chatroom_id]) >= 100:
1735
  db['messages'][chatroom_id].pop(0)
1736
 
1737
  db['messages'][chatroom_id].append(message)
 
1741
 
1742
  if __name__ == '__main__':
1743
  init_db()
1744
+ app.run(host='0.0.0.0', port=7860)