Aleksmorshen commited on
Commit
b56109b
·
verified ·
1 Parent(s): 019dc5f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +617 -648
app.py CHANGED
@@ -47,22 +47,21 @@ def index():
47
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
48
  <style>
49
  :root {
50
- --bg-primary: #0E0E11;
51
- --bg-secondary: #17171A;
52
- --bg-tertiary: #222227;
53
- --bg-hover: #2B2B33;
54
- --text-primary: #EAECEF;
55
- --text-secondary: #8E9297;
56
- --accent-blue: #007AFF; /* iOS Blue */
57
- --accent-blue-dark: #005BBB; /* Deeper Blue */
58
- --accent-blue-light: #34C759; /* Greenish Blue */
59
- --ton-blue: #0088CC;
60
- --ton-teal: #00BFFF;
61
- --border-color: #323239;
62
- --success-color: #34C759;
63
- --error-color: #FF3B30;
64
  --font-family: 'Inter', sans-serif;
65
- --nav-height: 60px;
 
66
  }
67
 
68
  * {
@@ -86,13 +85,13 @@ def index():
86
  display: flex;
87
  flex-direction: column;
88
  }
89
-
90
  .main-container {
91
- flex-grow: 1;
92
  width: 100%;
93
- overflow: hidden;
94
  display: flex;
95
  flex-direction: column;
 
96
  }
97
 
98
  #login-view {
@@ -102,59 +101,59 @@ def index():
102
  align-items: center;
103
  justify-content: center;
104
  text-align: center;
105
- background: radial-gradient(circle, rgba(0, 136, 204, 0.1) 0%, var(--bg-primary) 70%);
106
  padding: 20px;
 
107
  }
108
  #login-view img {
109
- width: 120px;
110
- height: 120px;
111
- margin-bottom: 32px;
112
  filter: drop-shadow(0 0 20px rgba(0, 136, 204, 0.6));
113
  }
114
  #login-view h1 {
115
- font-size: 3.5rem;
116
  font-weight: 700;
117
- background: linear-gradient(45deg, var(--ton-blue), var(--ton-teal));
118
  -webkit-background-clip: text;
119
  -webkit-text-fill-color: transparent;
120
- margin-bottom: 16px;
121
  }
122
  #login-view p {
123
  font-size: 1.2rem;
124
  color: var(--text-secondary);
125
- margin-bottom: 50px;
126
- }
127
- #ton-connect-button {
128
- --tc-button-height: 50px;
129
- --tc-button-border-radius: 12px;
130
  }
131
 
132
  #app-view {
133
- display: none;
134
- flex-direction: column;
135
  flex-grow: 1;
136
- height: calc(100vh - var(--nav-height)); /* Adjust for nav bar */
137
  }
138
-
139
- #content-container {
140
- flex-grow: 1;
141
- display: flex;
142
- overflow: hidden;
143
  }
144
 
145
- #chat-container {
146
- display: flex;
 
 
 
 
 
147
  flex-direction: column;
148
- flex-grow: 1;
149
- width: 100%; /* Start with full width, media query changes */
150
  }
151
 
 
 
 
 
152
  #chatroom-list-view {
153
- display: none; /* Hidden by default on mobile */
154
- flex-direction: column;
155
- height: 100%;
156
  background-color: var(--bg-secondary);
157
- width: 100%;
158
  }
159
 
160
  .list-header {
@@ -199,7 +198,7 @@ def index():
199
  .username-input:focus { outline: none; border-color: var(--accent-blue); }
200
 
201
  .action-btn {
202
- background: linear-gradient(45deg, var(--ton-blue), var(--ton-teal));
203
  color: white;
204
  border: none;
205
  padding: 10px 16px;
@@ -211,17 +210,22 @@ def index():
211
  align-items: center;
212
  justify-content: center;
213
  gap: 8px;
214
- -webkit-user-select: none;
215
- -ms-user-select: none;
216
- user-select: none;
217
  }
218
- .action-btn:active {
219
- transform: scale(0.98);
 
220
  }
221
  .action-btn.small {
222
  padding: 8px 12px;
223
  font-size: 0.9rem;
224
  }
 
 
 
 
 
 
225
 
226
  #chatroom-list {
227
  flex-grow: 1;
@@ -239,8 +243,8 @@ def index():
239
  .chatroom-item:hover { background-color: var(--bg-hover); }
240
 
241
  .avatar {
242
- width: 44px;
243
- height: 44px;
244
  border-radius: 50%;
245
  display: flex;
246
  align-items: center;
@@ -248,7 +252,8 @@ def index():
248
  font-weight: 600;
249
  color: white;
250
  flex-shrink: 0;
251
- font-size: 1.2rem;
 
252
  }
253
  .chatroom-info {
254
  flex-grow: 1;
@@ -259,7 +264,6 @@ def index():
259
  white-space: nowrap;
260
  overflow: hidden;
261
  text-overflow: ellipsis;
262
- font-size: 1.1rem;
263
  }
264
  .lock-icon {
265
  width: 16px;
@@ -269,11 +273,7 @@ def index():
269
  }
270
 
271
  #chat-window-view {
272
- display: none; /* Hidden by default on mobile */
273
- flex-direction: column;
274
- flex-grow: 1;
275
- width: 100%;
276
- background-color: var(--bg-primary);
277
  }
278
 
279
  .chat-header {
@@ -289,7 +289,10 @@ def index():
289
  background: none;
290
  border: none;
291
  cursor: pointer;
292
- display: none; /* Hidden by default, shown on mobile chat */
 
 
 
293
  }
294
  .back-btn svg {
295
  width: 24px;
@@ -308,19 +311,17 @@ def index():
308
  display: flex;
309
  flex-direction: column;
310
  gap: 12px;
311
- -webkit-overflow-scrolling: touch;
312
  }
313
 
314
  .message {
315
  display: flex;
316
  gap: 10px;
317
- max-width: 85%;
318
  }
319
  .message .avatar {
320
- width: 36px;
321
- height: 36px;
322
  align-self: flex-end;
323
- font-size: 1rem;
324
  }
325
  .message-content {
326
  display: flex;
@@ -328,7 +329,7 @@ def index():
328
  gap: 4px;
329
  }
330
  .message-sender {
331
- font-size: 0.85rem;
332
  font-weight: 500;
333
  color: var(--text-secondary);
334
  word-break: break-all;
@@ -345,7 +346,7 @@ def index():
345
  .message.sent { align-self: flex-end; flex-direction: row-reverse; }
346
  .message.sent .message-sender { text-align: right; color: var(--accent-blue-light); }
347
  .message.sent .message-bubble {
348
- background: linear-gradient(to right bottom, var(--accent-blue), var(--accent-blue-dark));
349
  color: white;
350
  border-bottom-right-radius: 4px;
351
  }
@@ -357,11 +358,11 @@ def index():
357
  }
358
 
359
  .chat-placeholder {
 
360
  display: flex;
361
  flex-direction: column;
362
  align-items: center;
363
  justify-content: center;
364
- height: 100%;
365
  text-align: center;
366
  color: var(--text-secondary);
367
  padding: 20px;
@@ -370,7 +371,7 @@ def index():
370
 
371
  .message-form {
372
  display: flex;
373
- padding: 8px 16px;
374
  gap: 12px;
375
  background-color: var(--bg-secondary);
376
  border-top: 1px solid var(--border-color);
@@ -378,7 +379,7 @@ def index():
378
  }
379
  #message-input {
380
  flex-grow: 1;
381
- padding: 10px 16px;
382
  border: 1px solid var(--border-color);
383
  background-color: var(--bg-tertiary);
384
  color: var(--text-primary);
@@ -390,131 +391,92 @@ def index():
390
  #message-input:focus { border-color: var(--accent-blue); }
391
 
392
  .send-btn {
393
- width: 44px;
394
- height: 44px;
395
  border-radius: 50%;
396
  flex-shrink: 0;
397
  padding: 0;
398
- background: linear-gradient(45deg, var(--accent-blue-dark), var(--accent-blue-light));
399
  }
400
- .send-btn svg { width: 20px; height: 20px; fill: white; }
401
 
402
- /* Browser View */
403
  #browser-view {
404
- display: none; /* Hidden by default */
405
- flex-direction: column;
406
- flex-grow: 1;
407
- width: 100%;
408
- }
409
- .browser-bar {
410
- display: flex;
411
- padding: 8px 16px;
412
- gap: 8px;
413
- background-color: var(--bg-secondary);
414
- border-bottom: 1px solid var(--border-color);
415
- align-items: center;
416
- flex-shrink: 0;
417
- }
418
- .browser-bar input[type="text"] {
419
- flex-grow: 1;
420
- padding: 8px 12px;
421
- border: 1px solid var(--border-color);
422
- background-color: var(--bg-tertiary);
423
- color: var(--text-primary);
424
- border-radius: 6px;
425
- font-size: 0.9rem;
426
  }
427
- .browser-bar input[type="text"]:focus { outline: none; border-color: var(--accent-blue); }
428
- .browser-bar button {
429
- background: var(--bg-hover);
430
- border: none;
431
- color: var(--text-secondary);
432
- padding: 8px;
433
- border-radius: 6px;
434
- cursor: pointer;
435
- transition: background-color 0.2s ease;
436
- }
437
- .browser-bar button:active { transform: scale(0.95); }
438
- .browser-bar button svg { width: 20px; height: 20px; fill: currentColor; }
439
- #browser-iframe {
440
- flex-grow: 1;
441
- width: 100%;
442
- border: none;
443
  }
444
 
445
-
446
- /* Bottom Navigation */
447
  #bottom-nav {
448
- position: fixed;
449
- bottom: 0;
450
- left: 0;
451
- width: 100%;
452
- height: var(--nav-height);
453
- background-color: var(--bg-secondary);
454
  display: flex;
455
  justify-content: space-around;
456
  align-items: center;
 
 
457
  border-top: 1px solid var(--border-color);
458
- z-index: 500; /* Below modals, above main content */
459
- padding-bottom: env(safe-area-inset-bottom); /* Handle iPhone notches */
460
  }
461
- .nav-btn {
462
- flex-grow: 1;
463
  display: flex;
464
  flex-direction: column;
465
  align-items: center;
466
  justify-content: center;
467
- color: var(--text-secondary);
468
- font-size: 0.7rem;
469
  cursor: pointer;
470
- padding: 4px 0;
 
 
 
471
  transition: color 0.2s ease;
472
- -webkit-user-select: none;
473
- -ms-user-select: none;
474
- user-select: none;
475
  }
476
- .nav-btn svg {
477
- width: 24px;
478
- height: 24px;
479
- margin-bottom: 2px;
480
- fill: currentColor;
481
  transition: fill 0.2s ease;
482
  }
483
- .nav-btn:active { transform: scale(0.95); }
484
-
485
- .nav-btn.active {
486
- color: var(--ton-blue);
 
 
 
 
487
  }
488
- .nav-btn.active svg {
489
- fill: var(--ton-blue);
490
  }
491
-
492
- #nav-scan-btn {
493
- background: linear-gradient(45deg, var(--ton-blue), var(--ton-teal));
494
- border-radius: 50%;
495
- width: 56px;
496
  height: 56px;
 
 
497
  display: flex;
498
  align-items: center;
499
  justify-content: center;
500
- position: relative;
501
- bottom: 8px;
502
- box-shadow: 0 4px 10px rgba(0, 136, 204, 0.5);
503
- flex-shrink: 0;
504
- -webkit-user-select: none;
505
- -ms-user-select: none;
506
- user-select: none;
507
  }
508
- #nav-scan-btn:active { transform: scale(0.9); }
509
- #nav-scan-btn svg {
510
- width: 28px;
511
- height: 28px;
512
- fill: white;
513
- margin-bottom: 0;
 
 
 
514
  }
515
 
516
-
517
- /* Modals */
518
  .modal-overlay {
519
  position: fixed;
520
  top: 0;
@@ -526,19 +488,20 @@ def index():
526
  align-items: center;
527
  justify-content: center;
528
  z-index: 1000;
529
- -webkit-backdrop-filter: blur(5px);
530
- backdrop-filter: blur(5px);
 
531
  }
532
  .modal-content {
533
  background-color: var(--bg-secondary);
534
  padding: 24px;
535
  border-radius: 12px;
536
- width: 90%;
537
  max-width: 400px;
538
  border: 1px solid var(--border-color);
539
- box-shadow: 0 10px 30px rgba(0,0,0,0.5);
540
  }
541
- .modal-content h3 { margin-bottom: 20px; font-weight: 600; font-size: 1.3rem; }
542
  .modal-content label { display: block; margin-bottom: 8px; font-size: 0.9rem; color: var(--text-secondary); }
543
  .modal-content input {
544
  width: 100%;
@@ -550,7 +513,6 @@ def index():
550
  border-radius: 6px;
551
  font-size: 1rem;
552
  }
553
- .modal-content input:focus { outline: none; border-color: var(--accent-blue); }
554
  .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
555
  .modal-btn {
556
  padding: 10px 20px;
@@ -558,28 +520,16 @@ def index():
558
  border: none;
559
  cursor: pointer;
560
  font-weight: 500;
561
- -webkit-user-select: none;
562
- -ms-user-select: none;
563
- user-select: none;
 
564
  }
565
- .modal-btn:active { transform: scale(0.95); }
566
- .secondary-btn { background-color: var(--bg-hover); color: white; }
567
- .modal-btn.action-btn { background: linear-gradient(45deg, var(--ton-blue), var(--ton-teal)); color: white; }
568
 
569
- #profile-modal .modal-content { text-align: center; }
570
- #profile-modal .avatar { margin: 20px auto; width: 60px; height: 60px; font-size: 1.8rem; }
571
- #profile-modal #profile-username { font-size: 1.2rem; font-weight: 600; margin-bottom: 8px; }
572
- #profile-modal #profile-address { color: var(--text-secondary); font-size: 0.9rem; word-break: break-all; margin-top: 0; }
573
- #profile-modal #profile-qr-code { background: white; padding: 10px; margin: 20px auto; width: fit-content; border-radius: 8px; }
574
- #profile-modal .qr-label { text-align: center; color: var(--text-secondary); font-size: 0.8rem; margin-top: -10px; margin-bottom: 20px; }
575
- #profile-modal .modal-actions { flex-direction: column; gap: 12px; align-items: stretch; }
576
-
577
-
578
- #scanner-modal #qr-reader { width: 100%; border: 1px solid var(--border-color); margin-top: 16px; border-radius: 8px; overflow: hidden; }
579
-
580
  #status-bar {
581
  position: fixed;
582
- bottom: calc(var(--nav-height) + 10px); /* Position above nav bar */
583
  left: 50%;
584
  transform: translateX(-50%);
585
  background-color: var(--bg-tertiary);
@@ -599,83 +549,82 @@ def index():
599
  #status-bar.error { background-color: var(--error-color); }
600
  #status-bar.visible { opacity: 1; visibility: visible; }
601
 
 
602
  @media (min-width: 768px) {
603
- body {
604
- justify-content: center;
605
- align-items: center;
606
- }
607
  .main-container {
608
- max-width: 1100px;
609
- max-height: 800px;
 
 
610
  border-radius: 12px;
611
  overflow: hidden;
612
- box-shadow: 0 10px 40px rgba(0,0,0,0.3);
613
  border: 1px solid var(--border-color);
614
- height: 100%;
615
- flex-grow: 0;
616
  }
617
-
618
  #app-view {
619
- height: 100%;
620
  }
621
-
622
- #content-container {
623
- flex-direction: row;
 
 
 
 
 
 
 
624
  }
625
-
626
  #chatroom-list-view {
627
- width: 320px;
628
- flex-shrink: 0;
629
  border-right: 1px solid var(--border-color);
630
- display: flex !important; /* Always show on desktop */
631
  }
632
- #chat-window-view {
633
- width: auto;
634
- flex-grow: 1;
635
- display: flex !important; /* Always show on desktop */
636
- }
637
-
638
  #browser-view {
639
- display: flex; /* Always show on desktop when active */
 
 
 
640
  }
641
 
642
- #chat-container {
643
- flex-direction: row; /* Side-by-side on desktop */
 
 
 
 
 
 
 
644
  }
645
 
 
 
 
646
  #bottom-nav {
647
- position: static; /* Not fixed */
648
- width: auto;
649
- height: auto;
650
- background-color: transparent;
651
- border-top: none;
652
- z-index: auto;
653
- padding-bottom: 0;
654
- margin-top: 20px; /* Example spacing if needed */
 
 
 
 
 
 
 
 
 
 
655
  }
656
- .nav-btn {
657
- display: none; /* Hide text nav buttons */
658
- }
659
- #nav-scan-btn {
660
- display: none !important; /* Hide the large central scan button */
661
- }
662
- .list-header-top {
663
- margin-bottom: 12px; /* Less space needed */
664
- }
665
- .list-header-top h2 { font-size: 1.8rem; }
666
- .user-profile { margin-bottom: 20px; }
667
- .chat-header { padding: 16px; }
668
- #chat-header-title { font-size: 1.4rem; }
669
- .message-form { padding: 12px 16px; }
670
- #message-input { padding: 14px 18px; }
671
- .send-btn { width: 48px; height: 48px; }
672
- .send-btn svg { width: 24px; height: 24px; }
673
-
674
- .back-btn { display: none !important; } /* Hide back button on desktop */
675
-
676
- #status-bar {
677
- bottom: 20px; /* Back to standard bottom position */
678
- }
679
  }
680
  </style>
681
  </head>
@@ -689,95 +638,77 @@ def index():
689
  </div>
690
 
691
  <div id="app-view" class="main-container">
692
- <div id="content-container">
693
-
694
- <!-- Chat Views -->
695
- <div id="chat-container">
696
- <div id="chatroom-list-view">
697
- <div class="list-header">
698
- <div class="list-header-top">
699
- <h2>Чаты</h2>
700
- <div style="display: flex; align-items: center; gap: 8px;">
701
- <button id="my-profile-btn" class="action-btn small" title="Мой профиль">
702
- <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="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>
703
- </button>
704
- <!-- QR Scan button moved to bottom nav -->
705
- <button id="create-room-show-modal" class="action-btn small">
706
- <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>
707
- <span>Новый</span>
708
- </button>
709
- </div>
710
- </div>
711
- <div class="user-profile">
712
- <div id="user-wallet"></div>
713
- <div id="user-nickname"></div>
714
- <form class="username-form" id="username-form">
715
- <input type="text" id="username-input" class="username-input" placeholder="Установить никнейм" autocomplete="off">
716
- <button type="submit" class="action-btn small">✓</button>
717
- </form>
718
- </div>
719
- </div>
720
- <div id="chatroom-list"></div>
721
- </div>
722
-
723
- <div id="chat-window-view">
724
- <div id="chat-placeholder" class="chat-placeholder">
725
- <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
726
- <h2>Выберите чат</h2>
727
- <p>Начните общение в одном из существующих чатов или создайте свой собственный.</p>
728
- </div>
729
- <div id="active-chat" style="display: none; width: 100%; height: 100%; flex-direction: column;">
730
- <div class="chat-header">
731
- <button class="back-btn" id="back-to-list-btn">
732
- <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>
733
  </button>
734
- <div id="chat-header-avatar" class="avatar"></div>
735
- <span id="chat-header-title"></span>
736
- </div>
737
- <div id="messages-container"></div>
738
- <form id="message-form" class="message-form">
739
- <input type="text" id="message-input" placeholder="Сообщение..." autocomplete="off">
740
- <button type="submit" class="action-btn send-btn" id="send-btn">
741
- <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>
742
  </button>
 
 
 
 
 
 
 
 
743
  </form>
744
  </div>
745
  </div>
 
746
  </div>
747
 
748
- <!-- Browser View -->
749
- <div id="browser-view">
750
- <div class="browser-bar">
751
- <button id="browser-back"><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 13H20z"/></svg></button>
752
- <button id="browser-forward"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 11h12.17l-5.58-5.59L12 4l8 8-8 8-1.41-1.41L16.17 13H4z"/></svg></button>
753
- <button id="browser-home"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg></button>
754
- <form id="browser-form" style="flex-grow: 1; display: flex; gap: 8px;">
755
- <input type="text" id="browser-url" value="https://www.google.com/" placeholder="Введите адрес или запрос">
756
- <button type="submit"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg></button>
757
- </form>
 
 
 
 
 
 
 
 
 
 
 
758
  </div>
759
- <iframe id="browser-iframe" src="https://www.google.com/"></iframe>
760
  </div>
761
 
762
- </div>
763
-
764
- <!-- Bottom Navigation Bar -->
765
- <div id="bottom-nav">
766
- <div class="nav-btn active" data-view="chat">
767
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><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-2zM6 9h12v2H6V9zm8 6H6v-2h8v2zm4-4H6V7h12v4z"/></svg>
768
- <span>Чаты</span>
769
  </div>
770
- <div id="nav-scan-btn">
 
 
 
 
 
 
 
771
  <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>
772
- </div>
773
- <div class="nav-btn" data-view="browser">
774
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93s3.05-7.44 7-7.93v15.86zm2 0V4.07c3.95.49 7 3.85 7 7.93s-3.05 7.44-7 7.93z"/></svg>
775
- <span>Браузер</span>
776
- </div>
777
- </div>
778
  </div>
779
 
780
- <!-- Modals (outside main-container flow) -->
781
  <div id="create-room-modal" class="modal-overlay">
782
  <div class="modal-content">
783
  <h3>Создать новый чат</h3>
@@ -809,14 +740,14 @@ def index():
809
  </div>
810
 
811
  <div id="profile-modal" class="modal-overlay">
812
- <div class="modal-content">
813
  <h3 id="profile-modal-title">Профиль пользователя</h3>
814
- <div id="profile-avatar-container"></div>
815
- <p id="profile-username"></p>
816
- <p id="profile-address"></p>
817
- <div id="profile-qr-code"></div>
818
- <p class="qr-label">Отсканируйте для открытия профиля</p>
819
- <div class="modal-actions">
820
  <button id="send-ton-btn" class="modal-btn action-btn">Отправить TON</button>
821
  <button id="profile-close-btn" class="modal-btn secondary-btn">Закрыть</button>
822
  </div>
@@ -826,7 +757,7 @@ def index():
826
  <div id="scanner-modal" class="modal-overlay">
827
  <div class="modal-content">
828
  <h3>Сканировать QR-код</h3>
829
- <div id="qr-reader"></div>
830
  <div class="modal-actions">
831
  <button id="scanner-close-btn" class="modal-btn secondary-btn">Отмена</button>
832
  </div>
@@ -848,36 +779,29 @@ def index():
848
  let chatroomsData = {};
849
  let html5QrCode = null;
850
  let profileQrCode = null;
851
-
 
852
  const loginView = document.getElementById('login-view');
853
  const appView = document.getElementById('app-view');
854
- const chatContainer = document.getElementById('chat-container');
855
  const chatroomListView = document.getElementById('chatroom-list-view');
856
  const chatWindowView = document.getElementById('chat-window-view');
857
  const browserView = document.getElementById('browser-view');
858
  const chatPlaceholder = document.getElementById('chat-placeholder');
859
  const activeChat = document.getElementById('active-chat');
860
- const bottomNav = document.getElementById('bottom-nav');
861
- const navButtons = bottomNav.querySelectorAll('.nav-btn');
862
- const navScanBtn = document.getElementById('nav-scan-btn');
863
-
864
- const createRoomModal = document.getElementById('create-room-modal');
865
- const passwordModal = document.getElementById('password-modal');
866
  const profileModal = document.getElementById('profile-modal');
867
  const scannerModal = document.getElementById('scanner-modal');
 
 
 
 
 
 
868
 
869
- const browserIframe = document.getElementById('browser-iframe');
870
- const browserUrlInput = document.getElementById('browser-url');
871
- const browserForm = document.getElementById('browser-form');
872
- const browserBackBtn = document.getElementById('browser-back');
873
- const browserForwardBtn = document.getElementById('browser-forward');
874
- const browserHomeBtn = document.getElementById('browser-home');
875
-
876
-
877
- const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292', '#a1887f', '#ff8a65'];
878
 
879
  const getAvatar = (name) => {
880
- const initial = (name ? name[0] : '?').toUpperCase();
881
  const charCode = initial.charCodeAt(0);
882
  const color = AVATAR_COLORS[charCode % AVATAR_COLORS.length];
883
  const avatar = document.createElement('div');
@@ -906,7 +830,9 @@ def index():
906
  throw new Error(errorData.error || 'Unknown error');
907
  }
908
  if (response.status === 204) return null;
909
- return await response.json();
 
 
910
  } catch (error) {
911
  showStatus(`Ошибка: ${error.message}`, 'error');
912
  throw error;
@@ -918,18 +844,31 @@ def index():
918
  const updateUserInfo = () => {
919
  document.getElementById('user-wallet').textContent = `Кошелек: ${truncateAddress(currentUser.address)}`;
920
  const nicknameEl = document.getElementById('user-nickname');
921
- const usernameInput = document.getElementById('username-input');
922
  nicknameEl.textContent = currentUser.username ? `Ник: ${currentUser.username}` : `Никнейм не установлен`;
923
  usernameInput.value = currentUser.username || '';
 
 
924
  };
925
 
926
  document.getElementById('username-form').addEventListener('submit', async (e) => {
927
  e.preventDefault();
928
- const newUsername = document.getElementById('username-input').value.trim();
929
- if (!newUsername || newUsername.length < 3) {
930
- showStatus('Никнейм должен быть не короче 3 символов.', 'error');
 
 
 
 
931
  return;
932
  }
 
 
 
 
 
 
 
 
933
  try {
934
  await apiCall('/api/set_username', {
935
  method: 'POST',
@@ -939,9 +878,72 @@ def index():
939
  currentUser.username = newUsername;
940
  updateUserInfo();
941
  showStatus('Никнейм успешно обновлен!', 'success');
942
- fetchChatrooms();
943
- if (activeChatroomId) fetchMessages(activeChatroomId);
944
- } catch (err) {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
945
  });
946
 
947
  const initializeUser = async (address) => {
@@ -959,15 +961,17 @@ def index():
959
  updateUserInfo();
960
  loginView.style.display = 'none';
961
  appView.style.display = 'flex';
962
- bottomNav.style.display = 'flex';
963
- switchView('chat'); // Default view is chat
964
  fetchChatrooms();
 
965
  };
966
 
967
  const renderChatrooms = (rooms) => {
968
  const list = document.getElementById('chatroom-list');
969
  list.innerHTML = '';
970
  chatroomsData = {};
 
 
 
971
  rooms.forEach(room => {
972
  chatroomsData[room.id] = room;
973
  const item = document.createElement('div');
@@ -1004,7 +1008,8 @@ def index():
1004
 
1005
  const renderMessages = (messages) => {
1006
  const container = document.getElementById('messages-container');
1007
- const shouldScroll = container.scrollTop + container.clientHeight >= container.scrollHeight - 30;
 
1008
  container.innerHTML = '';
1009
  messages.forEach(msg => {
1010
  const msgDiv = document.createElement('div');
@@ -1031,78 +1036,51 @@ def index():
1031
  contentDiv.appendChild(bubbleDiv);
1032
 
1033
  msgDiv.appendChild(contentDiv);
1034
- msgDiv.insertBefore(avatar, contentDiv);
 
 
 
 
 
 
1035
  container.appendChild(msgDiv);
1036
  });
1037
 
1038
- if(shouldScroll || messages.length === 0) {
1039
  container.scrollTop = container.scrollHeight;
1040
  }
1041
  };
1042
 
1043
  const fetchMessages = async (roomId) => {
1044
- if (!currentUser.address) return;
 
 
 
1045
  try {
1046
  const data = await apiCall(`/api/messages/${roomId}`);
1047
  renderMessages(data.messages);
1048
  } catch (err) {
1049
- console.error("Failed to fetch messages:", err);
 
 
 
 
 
 
 
1050
  }
1051
  };
1052
 
1053
- const showChatView = () => {
1054
- chatContainer.style.display = 'flex';
1055
- browserView.style.display = 'none';
1056
-
1057
- if (window.innerWidth < 768) {
1058
- // On mobile, determine whether to show list or window
1059
- if (activeChatroomId && activeChat.style.display === 'flex') {
1060
- chatroomListView.style.display = 'none';
1061
- chatWindowView.style.display = 'flex';
1062
- document.getElementById('back-to-list-btn').style.display = 'block';
1063
- } else {
1064
- chatroomListView.style.display = 'flex';
1065
- chatWindowView.style.display = 'none';
1066
- document.getElementById('back-to-list-btn').style.display = 'none';
1067
- }
1068
- } else {
1069
- // On desktop, always show both
1070
- chatroomListView.style.display = 'flex';
1071
- chatWindowView.style.display = 'flex';
1072
- document.getElementById('back-to-list-btn').style.display = 'none';
1073
- }
1074
- };
1075
-
1076
- const showBrowserView = () => {
1077
- chatContainer.style.display = 'none';
1078
- browserView.style.display = 'flex';
1079
- if (messagePollingInterval) clearInterval(messagePollingInterval);
1080
- activeChatroomId = null;
1081
- };
1082
-
1083
- const switchView = (viewId) => {
1084
- navButtons.forEach(btn => btn.classList.remove('active'));
1085
- let targetView = null;
1086
-
1087
- if (viewId === 'chat') {
1088
- showChatView();
1089
- targetView = document.querySelector('.nav-btn[data-view="chat"]');
1090
- } else if (viewId === 'browser') {
1091
- showBrowserView();
1092
- targetView = document.querySelector('.nav-btn[data-view="browser"]');
1093
- }
1094
-
1095
- if (targetView) {
1096
- targetView.classList.add('active');
1097
- }
1098
- };
1099
-
1100
-
1101
  const selectChatroom = (roomId, isPrivate) => {
1102
  const roomData = chatroomsData[roomId];
1103
- if (!roomData) return;
 
 
 
 
1104
 
1105
  const proceedToRoom = () => {
 
1106
  if (messagePollingInterval) clearInterval(messagePollingInterval);
1107
  activeChatroomId = roomId;
1108
 
@@ -1114,27 +1092,26 @@ def index():
1114
  chatPlaceholder.style.display = 'none';
1115
  activeChat.style.display = 'flex';
1116
 
1117
- showChatView(); // Ensure chat view is active and handle mobile/desktop split
1118
-
1119
- fetchMessages(roomId);
1120
- messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000);
1121
  };
1122
 
1123
  if (isPrivate) {
 
1124
  const passwordForm = document.getElementById('password-form');
1125
  const passwordInput = document.getElementById('password-input');
1126
-
1127
  passwordModal.style.display = 'flex';
1128
  passwordInput.value = '';
1129
  passwordInput.focus();
1130
 
1131
- // Use a flag to prevent multiple listeners if modal is shown multiple times
1132
- let passwordFormListener = null;
1133
- passwordFormListener = async (e) => {
1134
  e.preventDefault();
1135
- passwordForm.removeEventListener('submit', passwordFormListener); // Remove listener after submit
1136
  const password = passwordInput.value;
1137
- passwordModal.style.display = 'none';
 
 
1138
  try {
1139
  await apiCall('/api/join_chatroom', {
1140
  method: 'POST',
@@ -1143,33 +1120,40 @@ def index():
1143
  });
1144
  proceedToRoom();
1145
  } catch (err) {
1146
- showStatus('Неверный пароль', 'error');
1147
- // Re-attach listener if join failed and modal might be shown again
1148
- // This part is tricky; a better approach might be a dedicated join attempt handler
1149
  }
1150
  };
1151
- passwordForm.addEventListener('submit', passwordFormListener);
1152
 
1153
  document.getElementById('password-cancel').onclick = () => {
1154
  passwordModal.style.display = 'none';
1155
- passwordForm.removeEventListener('submit', passwordFormListener); // Clean up listener on cancel
1156
  };
1157
-
1158
  } else {
1159
- // Auto-join public rooms
1160
  proceedToRoom();
1161
  }
1162
  };
1163
 
 
 
 
 
 
 
 
 
 
 
 
 
1164
  document.getElementById('message-form').addEventListener('submit', async (e) => {
1165
  e.preventDefault();
1166
- const input = document.getElementById('message-input');
1167
- const sendBtn = document.getElementById('send-btn');
1168
- const text = input.value.trim();
1169
  if (text && activeChatroomId && currentUser.address) {
1170
- input.value = '';
1171
- input.disabled = true;
1172
- sendBtn.disabled = true;
1173
  try {
1174
  await apiCall('/api/send_message', {
1175
  method: 'POST',
@@ -1180,44 +1164,17 @@ def index():
1180
  text: text
1181
  })
1182
  });
1183
- // Don't wait for poll, add message locally for instant feedback
1184
- const tempMsg = {
1185
- id: 'temp-' + Date.now(), // Temporary ID
1186
- sender_address: currentUser.address,
1187
- text: text,
1188
- timestamp: new Date().toISOString(),
1189
- display_name: currentUser.username || `${currentUser.address[:4]}...${currentUser.address[-4:]}`
1190
- };
1191
- const messagesContainer = document.getElementById('messages-container');
1192
- // Check if the last message is from the current user to group bubbles
1193
- const lastMsgElement = messagesContainer.lastElementChild;
1194
- if (lastMsgElement && lastMsgElement.classList.contains('sent')) {
1195
- // Append to existing sent bubble structure (simplified)
1196
- // For real grouping, you'd restructure the message HTML
1197
- // For now, just adding as a new message item
1198
- }
1199
- renderMessages([...Array.from(messagesContainer.children).map(el => {
1200
- // Basic extraction for temp rendering
1201
- const senderEl = el.querySelector('.message-sender');
1202
- const bubbleEl = el.querySelector('.message-bubble');
1203
- return {
1204
- sender_address: el.classList.contains('sent') ? currentUser.address : 'other', // Simplified check
1205
- text: bubbleEl ? bubbleEl.textContent : '',
1206
- display_name: senderEl ? senderEl.textContent : '',
1207
- // timestamp etc omitted for temp
1208
- };
1209
- }), tempMsg]); // Re-render with temp message
1210
-
1211
- await fetchMessages(activeChatroomId); // Fetch actual messages after sending
1212
-
1213
  } finally {
1214
- input.disabled = false;
1215
- sendBtn.disabled = false;
1216
- input.focus();
1217
  }
1218
  }
1219
  });
1220
 
 
1221
  document.getElementById('create-room-show-modal').addEventListener('click', () => {
1222
  createRoomModal.style.display = 'flex';
1223
  document.getElementById('create-room-form').reset();
@@ -1227,17 +1184,29 @@ def index():
1227
  });
1228
  document.getElementById('create-room-form').addEventListener('submit', async (e) => {
1229
  e.preventDefault();
1230
- const name = document.getElementById('room-name').value.trim();
1231
- const password = document.getElementById('room-password').value;
 
 
 
 
 
1232
  if (!name) {
1233
- showStatus('Название чата не может быть пустым.', 'error');
1234
- return;
1235
  }
1236
- if (name.length < 3 || name.length > 50) {
1237
- showStatus('Название чата должно быть от 3 до 50 символов.', 'error');
1238
  return;
1239
  }
 
 
 
 
1240
 
 
 
 
1241
 
1242
  try {
1243
  await apiCall('/api/create_chatroom', {
@@ -1247,8 +1216,14 @@ def index():
1247
  });
1248
  createRoomModal.style.display = 'none';
1249
  showStatus('Чат успешно создан!', 'success');
1250
- fetchChatrooms();
1251
- } catch (err) {}
 
 
 
 
 
 
1252
  });
1253
 
1254
  const showProfile = async (address) => {
@@ -1259,7 +1234,7 @@ def index():
1259
  body: JSON.stringify({ address: address })
1260
  });
1261
 
1262
- const username = userData.username || `Пользователь ${truncateAddress(address)}`;
1263
  const avatarContainer = document.getElementById('profile-avatar-container');
1264
  const usernameEl = document.getElementById('profile-username');
1265
  const addressEl = document.getElementById('profile-address');
@@ -1274,39 +1249,32 @@ def index():
1274
 
1275
  qrCodeEl.innerHTML = '';
1276
  if (profileQrCode) {
1277
- // Check if QR code instance exists and clear it
1278
- if (typeof profileQrCode.clear === 'function') {
1279
- profileQrCode.clear();
1280
- } else {
1281
- // Fallback if clear method is not available (older versions?)
1282
- qrCodeEl.innerHTML = '';
1283
- }
1284
  }
1285
- // Use try/catch for QRCode initialization as it can throw
1286
- try {
1287
- profileQrCode = new QRCode(qrCodeEl, {
1288
- text: address,
1289
- width: 150,
1290
- height: 150,
1291
- colorDark : "#000000",
1292
- colorLight : "#ffffff",
1293
- correctLevel : QRCode.CorrectLevel.H
1294
- });
1295
- } catch (e) {
1296
- console.error("Failed to generate QR code:", e);
1297
- qrCodeEl.innerHTML = '<p style="color: red;">Ошибка генерации QR</p>';
1298
- profileQrCode = null; // Ensure it's null if failed
1299
- }
1300
-
1301
-
1302
  sendTonBtn.onclick = async () => {
1303
  if (!tonConnectUI.connected) {
1304
  showStatus('Подключите кошелек для отправки TON.', 'error');
1305
  return;
1306
  }
 
 
 
 
 
1307
  const amountString = prompt("Введите сумму в TON для отправки:", "0.1");
1308
- if (amountString === null) return;
1309
-
1310
  const amount = parseFloat(amountString);
1311
  if (isNaN(amount) || amount <= 0) {
1312
  showStatus('Неверная сумма.', 'error');
@@ -1316,7 +1284,7 @@ def index():
1316
  const amountInNanoTon = Math.floor(amount * 1_000_000_000).toString();
1317
 
1318
  const transaction = {
1319
- validUntil: Math.floor(Date.now() / 1000) + 600,
1320
  messages: [ { address: address, amount: amountInNanoTon } ]
1321
  };
1322
 
@@ -1325,11 +1293,16 @@ def index():
1325
  showStatus(`Транзакция отправлена успешно!`, 'success');
1326
  profileModal.style.display = 'none';
1327
  } catch (error) {
1328
- showStatus('Транзакция отклонена.', 'error');
 
 
 
 
 
1329
  }
1330
  };
1331
 
1332
- sendTonBtn.style.display = (address === currentUser.address || !tonConnectUI.connected) ? 'none' : 'block';
1333
  profileModal.style.display = 'flex';
1334
  } catch (err) {
1335
  showStatus('Не удалось загрузить профиль.', 'error');
@@ -1338,148 +1311,88 @@ def index():
1338
 
1339
  const showScanner = () => {
1340
  scannerModal.style.display = 'flex';
1341
- html5QrCode = new Html5Qrcode("qr-reader");
 
 
 
 
1342
  const qrCodeSuccessCallback = (decodedText, decodedResult) => {
1343
- hideScanner();
1344
- // Basic check for TON address format
1345
- if (decodedText && typeof decodedText === 'string' && decodedText.length >= 48 && (decodedText.startsWith('EQ') || decodedText.startsWith('UQ'))) {
1346
- showProfile(decodedText);
 
1347
  } else {
1348
- showStatus('Отсканирован недействительный QR-код (ожидается адрес TON).', 'error');
1349
  }
1350
  };
1351
- const config = { fps: 10, qrbox: { width: 250, height: 250 } };
1352
- // Request camera permissions when starting scanner
1353
- html5QrCode.start({ facingMode: "environment" }, config, qrCodeSuccessCallback)
 
 
 
 
 
 
1354
  .catch(err => {
1355
- console.error("QR Scanner startup failed:", err);
1356
- showStatus('Не удалось запустить сканер. Проверьте разрешения камеры.', 'error');
1357
- hideScanner(); // Ensure modal closes on error
1358
  });
1359
  };
1360
 
1361
  const hideScanner = () => {
1362
  if (html5QrCode && html5QrCode.isScanning) {
1363
- html5QrCode.stop().catch(err => console.error("Failed to stop QR scanner.", err));
 
 
 
 
1364
  }
1365
  scannerModal.style.display = 'none';
1366
  };
1367
 
1368
- // Event Listeners
1369
  document.getElementById('my-profile-btn').addEventListener('click', () => {
1370
  if (currentUser.address) showProfile(currentUser.address);
1371
- else showStatus('Подключите кошелек', 'error');
1372
  });
1373
-
1374
- // Nav button listeners
1375
- navButtons.forEach(btn => {
1376
- btn.addEventListener('click', () => {
1377
- const viewId = btn.dataset.view;
1378
- switchView(viewId);
1379
- });
1380
- });
1381
-
1382
- // Central QR scan button
1383
- navScanBtn.addEventListener('click', showScanner);
1384
-
1385
  document.getElementById('profile-close-btn').addEventListener('click', () => profileModal.style.display = 'none');
1386
  document.getElementById('scanner-close-btn').addEventListener('click', hideScanner);
1387
- document.getElementById('back-to-list-btn').addEventListener('click', () => {
1388
- activeChat.style.display = 'none';
1389
- chatPlaceholder.style.display = 'flex';
1390
- activeChatroomId = null;
1391
- if (messagePollingInterval) clearInterval(messagePollingInterval);
1392
- showChatView(); // Show list view on mobile
1393
- });
1394
-
1395
- // Browser Navigation
1396
- browserForm.addEventListener('submit', (e) => {
1397
- e.preventDefault();
1398
- let url = browserUrlInput.value.trim();
1399
- if (url) {
1400
- // Add http/https if missing, basic check
1401
- if (!url.match(/^https?:\/\//i)) {
1402
- url = 'https://' + url; // Default to https
1403
- }
1404
- browserIframe.src = url;
1405
- }
1406
- });
1407
-
1408
- browserHomeBtn.addEventListener('click', () => {
1409
- const homeUrl = "https://www.google.com/";
1410
- browserIframe.src = homeUrl;
1411
- browserUrlInput.value = homeUrl;
1412
- });
1413
-
1414
- browserBackBtn.addEventListener('click', () => {
1415
- try {
1416
- browserIframe.contentWindow.history.back();
1417
- } catch (e) {
1418
- console.warn("Browser history back failed, likely due to cross-origin policy.", e);
1419
- showStatus("Браузер не может перейти назад из-за ограничений безопасности.", 'info');
1420
- }
1421
- });
1422
-
1423
- browserForwardBtn.addEventListener('click', () => {
1424
- try {
1425
- browserIframe.contentWindow.history.forward();
1426
- } catch (e) {
1427
- console.warn("Browser history forward failed, likely due to cross-origin policy.", e);
1428
- showStatus("Браузер не может перейти вперед из-за ограничений безопасности.", 'info');
1429
- }
1430
- });
1431
-
1432
- // Sync URL input with iframe location (best effort, limited by same-origin)
1433
- browserIframe.addEventListener('load', () => {
1434
- try {
1435
- // This might fail on cross-origin sites
1436
- browserUrlInput.value = browserIframe.contentWindow.location.href;
1437
- } catch (e) {
1438
- // Cannot access cross-origin frame location
1439
- browserUrlInput.value = browserIframe.src; // Fallback to src
1440
- }
1441
- });
1442
-
1443
 
1444
- const handleResize = () => {
 
1445
  const isMobile = window.innerWidth < 768;
1446
-
1447
- // Manage display within the 'chat' container based on mobile/desktop and active chat state
1448
- if (chatContainer.style.display !== 'none') { // Only apply resize logic if chat is the active view
1449
- if (isMobile) {
1450
- document.getElementById('back-to-list-btn').style.display = activeChatroomId ? 'block' : 'none';
1451
- if (activeChatroomId && activeChat.style.display === 'flex') {
1452
- // Mobile, chat window is open
1453
- chatroomListView.style.display = 'none';
1454
- chatWindowView.style.display = 'flex';
1455
- } else {
1456
- // Mobile, chat list is open or no chat selected
1457
- chatroomListView.style.display = 'flex';
1458
- chatWindowView.style.display = 'none';
1459
- }
1460
- } else {
1461
- // Desktop: always show both list and window
1462
- chatroomListView.style.display = 'flex';
1463
- chatWindowView.style.display = 'flex';
1464
- document.getElementById('back-to-list-btn').style.display = 'none';
 
1465
  }
 
 
 
 
1466
  }
1467
-
1468
- // Adjust app-view height for desktop
1469
- if (!isMobile) {
1470
- appView.style.height = '100%'; // Allow container to define height
1471
- bottomNav.style.display = 'none'; // Hide bottom nav on desktop
1472
- } else {
1473
- appView.style.height = `calc(100vh - var(--nav-height))`;
1474
- bottomNav.style.display = 'flex'; // Show bottom nav on mobile
1475
- }
1476
  };
1477
 
1478
  window.addEventListener('resize', handleResize);
1479
-
1480
- // Initial state setup
1481
- handleResize(); // Apply initial layout based on screen size
1482
- bottomNav.style.display = 'none'; // Hide nav until user is logged in
1483
 
1484
  tonConnectUI.onStatusChange(wallet => {
1485
  if (wallet) {
@@ -1489,16 +1402,37 @@ def index():
1489
  currentUser = { address: null, username: null };
1490
  appView.style.display = 'none';
1491
  loginView.style.display = 'flex';
1492
- bottomNav.style.display = 'none';
1493
  if (messagePollingInterval) clearInterval(messagePollingInterval);
 
1494
  activeChatroomId = null;
 
1495
  chatPlaceholder.style.display = 'flex';
1496
  activeChat.style.display = 'none';
 
 
 
 
1497
  renderChatrooms([]); // Clear chat list
1498
- renderMessages([]); // Clear messages
1499
- handleResize(); // Adjust layout back to login state
1500
  }
 
 
1501
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1502
  });
1503
  </script>
1504
  </body>
@@ -1514,8 +1448,8 @@ def get_user_data():
1514
  if not address:
1515
  return jsonify({'error': 'Address is required'}), 400
1516
  db = read_db()
1517
- user_info = db['users'].get(address)
1518
- username = user_info.get('username') if user_info else None
1519
  return jsonify({'username': username})
1520
 
1521
  @app.route('/api/set_username', methods=['POST'])
@@ -1523,15 +1457,21 @@ def set_username():
1523
  data = request.get_json()
1524
  address = data.get('address')
1525
  username = data.get('username')
1526
- if not address or not username:
1527
  return jsonify({'error': 'Address and username are required'}), 400
1528
- if len(username) < 3 or len(username) > 20:
1529
  return jsonify({'error': 'Username must be between 3 and 20 characters'}), 400
1530
 
1531
  db = read_db()
1532
  if address not in db['users']:
1533
  db['users'][address] = {}
1534
- db['users'][address]['username'] = username
 
 
 
 
 
 
1535
  write_db(db)
1536
  return jsonify({'success': True})
1537
 
@@ -1540,10 +1480,12 @@ def get_chatrooms():
1540
  db = read_db()
1541
  chatrooms_list = []
1542
  for room_id, room_data in db['chatrooms'].items():
 
 
1543
  chatrooms_list.append({
1544
  'id': room_id,
1545
  'name': room_data['name'],
1546
- 'is_private': room_data['password_hash'] is not None
1547
  })
1548
  return jsonify({'chatrooms': sorted(chatrooms_list, key=lambda x: x['name'])})
1549
 
@@ -1555,17 +1497,20 @@ def create_chatroom():
1555
  creator_address = data.get('creator_address')
1556
  if not name or not creator_address:
1557
  return jsonify({'error': 'Name and creator address are required'}), 400
 
 
 
 
1558
 
1559
  db = read_db()
1560
  room_id = str(uuid.uuid4())
1561
  db['chatrooms'][room_id] = {
1562
- 'id': room_id,
1563
  'name': name,
1564
  'creator': creator_address,
1565
  'is_private': bool(password),
1566
  'password_hash': generate_password_hash(password) if password else None
1567
  }
1568
- db['messages'][room_id] = []
1569
  write_db(db)
1570
  return jsonify({'success': True, 'chatroom_id': room_id})
1571
 
@@ -1574,30 +1519,48 @@ def join_chatroom():
1574
  data = request.get_json()
1575
  chatroom_id = data.get('chatroom_id')
1576
  password = data.get('password')
 
 
 
1577
  db = read_db()
1578
  chatroom = db['chatrooms'].get(chatroom_id)
1579
  if not chatroom:
1580
  return jsonify({'error': 'Chatroom not found'}), 404
1581
- if chatroom['is_private']:
1582
- if not password or not check_password_hash(chatroom['password_hash'], password):
 
1583
  return jsonify({'error': 'Invalid password'}), 403
 
 
 
1584
  return jsonify({'success': True})
1585
 
1586
  @app.route('/api/messages/<chatroom_id>', methods=['GET'])
1587
  def get_messages(chatroom_id):
1588
  db = read_db()
1589
  if chatroom_id not in db['messages']:
 
 
 
 
 
 
1590
  return jsonify({'error': 'Chatroom not found'}), 404
1591
 
1592
  messages_with_names = []
1593
- room_messages = db['messages'].get(chatroom_id, [])
 
 
 
 
1594
 
1595
  for msg in room_messages:
1596
  sender_address = msg['sender_address']
1597
- user_info = db['users'].get(sender_address)
1598
- display_name = (user_info.get('username') if user_info and user_info.get('username')
1599
- else f"{sender_address[:4]}...{sender_address[-4:]}")
1600
-
 
1601
  msg_copy = msg.copy()
1602
  msg_copy['display_name'] = display_name
1603
  messages_with_names.append(msg_copy)
@@ -1613,24 +1576,30 @@ def send_message():
1613
 
1614
  if not all([chatroom_id, sender_address, text]):
1615
  return jsonify({'error': 'Missing data'}), 400
 
 
 
1616
 
1617
  db = read_db()
1618
- if chatroom_id not in db['messages']:
 
1619
  return jsonify({'error': 'Chatroom not found'}), 404
1620
-
1621
- if sender_address not in db['users'] or not db['users'][sender_address].get('username'):
1622
- # Prevent sending if user has no username
1623
- return jsonify({'error': 'Username not set'}), 400
1624
 
1625
  message = {
1626
  'id': str(uuid.uuid4()),
1627
  'sender_address': sender_address,
1628
- 'text': text,
1629
- 'timestamp': datetime.utcnow().isoformat() + "Z"
1630
  }
1631
 
1632
- # Keep only the last 100 messages per chatroom
1633
- if len(db['messages'][chatroom_id]) >= 100:
 
 
1634
  db['messages'][chatroom_id].pop(0)
1635
 
1636
  db['messages'][chatroom_id].append(message)
 
47
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
48
  <style>
49
  :root {
50
+ --bg-primary: #0D1117; /* Deep Space */
51
+ --bg-secondary: #161B22; /* Darker Grey */
52
+ --bg-tertiary: #21262D; /* Slightly lighter grey */
53
+ --bg-hover: #30363D; /* Hover state */
54
+ --text-primary: #C9D1D9; /* Light Grey */
55
+ --text-secondary: #8B949E; /* Muted Grey */
56
+ --accent-blue: #0077CC; /* TON Blue */
57
+ --accent-blue-light: #33AADD; /* Lighter TON Blue */
58
+ --accent-purple: #6F42C1; /* Purple accent (optional) */
59
+ --border-color: #30363D; /* Border grey */
60
+ --success-color: #238636; /* Green */
61
+ --error-color: #DA3633; /* Red */
 
 
62
  --font-family: 'Inter', sans-serif;
63
+ --header-height: 56px;
64
+ --footer-height: 60px;
65
  }
66
 
67
  * {
 
85
  display: flex;
86
  flex-direction: column;
87
  }
88
+
89
  .main-container {
 
90
  width: 100%;
91
+ height: 100%;
92
  display: flex;
93
  flex-direction: column;
94
+ transition: opacity 0.3s ease;
95
  }
96
 
97
  #login-view {
 
101
  align-items: center;
102
  justify-content: center;
103
  text-align: center;
 
104
  padding: 20px;
105
+ background: radial-gradient(circle, #1a2a3a 0%, var(--bg-primary) 70%);
106
  }
107
  #login-view img {
108
+ width: 100px;
109
+ height: 100px;
110
+ margin-bottom: 24px;
111
  filter: drop-shadow(0 0 20px rgba(0, 136, 204, 0.6));
112
  }
113
  #login-view h1 {
114
+ font-size: 3rem;
115
  font-weight: 700;
116
+ background: linear-gradient(45deg, var(--accent-blue-light), var(--accent-blue));
117
  -webkit-background-clip: text;
118
  -webkit-text-fill-color: transparent;
119
+ margin-bottom: 12px;
120
  }
121
  #login-view p {
122
  font-size: 1.2rem;
123
  color: var(--text-secondary);
124
+ margin-bottom: 40px;
 
 
 
 
125
  }
126
 
127
  #app-view {
128
+ display: none; /* Managed by JS */
 
129
  flex-grow: 1;
130
+ flex-direction: column;
131
  }
132
+
133
+ .app-content {
134
+ flex-grow: 1;
135
+ position: relative; /* For absolute positioning of views */
136
+ overflow: hidden; /* Hide overflow when views are positioned */
137
  }
138
 
139
+ .view {
140
+ position: absolute;
141
+ top: 0;
142
+ left: 0;
143
+ width: 100%;
144
+ height: 100%;
145
+ display: none; /* Initially hidden */
146
  flex-direction: column;
147
+ background-color: var(--bg-primary); /* Default background */
148
+ overflow-y: hidden; /* Prevent double scroll bars */
149
  }
150
 
151
+ .view.active {
152
+ display: flex; /* Show active view */
153
+ }
154
+
155
  #chatroom-list-view {
 
 
 
156
  background-color: var(--bg-secondary);
 
157
  }
158
 
159
  .list-header {
 
198
  .username-input:focus { outline: none; border-color: var(--accent-blue); }
199
 
200
  .action-btn {
201
+ background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
202
  color: white;
203
  border: none;
204
  padding: 10px 16px;
 
210
  align-items: center;
211
  justify-content: center;
212
  gap: 8px;
213
+ text-decoration: none; /* For anchor tags acting as buttons */
 
 
214
  }
215
+ .action-btn:hover {
216
+ transform: translateY(-2px);
217
+ box-shadow: 0 4px 15px rgba(0, 136, 204, 0.3);
218
  }
219
  .action-btn.small {
220
  padding: 8px 12px;
221
  font-size: 0.9rem;
222
  }
223
+ .action-btn:disabled {
224
+ opacity: 0.5;
225
+ cursor: not-allowed;
226
+ transform: none;
227
+ box-shadow: none;
228
+ }
229
 
230
  #chatroom-list {
231
  flex-grow: 1;
 
243
  .chatroom-item:hover { background-color: var(--bg-hover); }
244
 
245
  .avatar {
246
+ width: 40px;
247
+ height: 40px;
248
  border-radius: 50%;
249
  display: flex;
250
  align-items: center;
 
252
  font-weight: 600;
253
  color: white;
254
  flex-shrink: 0;
255
+ text-transform: uppercase;
256
+ user-select: none;
257
  }
258
  .chatroom-info {
259
  flex-grow: 1;
 
264
  white-space: nowrap;
265
  overflow: hidden;
266
  text-overflow: ellipsis;
 
267
  }
268
  .lock-icon {
269
  width: 16px;
 
273
  }
274
 
275
  #chat-window-view {
276
+ background-color: var(--bg-primary);
 
 
 
 
277
  }
278
 
279
  .chat-header {
 
289
  background: none;
290
  border: none;
291
  cursor: pointer;
292
+ display: none; /* Managed by JS/CSS */
293
+ padding: 0;
294
+ width: 24px;
295
+ height: 24px;
296
  }
297
  .back-btn svg {
298
  width: 24px;
 
311
  display: flex;
312
  flex-direction: column;
313
  gap: 12px;
 
314
  }
315
 
316
  .message {
317
  display: flex;
318
  gap: 10px;
319
+ max-width: 85%; /* Increased max width */
320
  }
321
  .message .avatar {
322
+ width: 32px; /* Slightly smaller avatar in chat */
323
+ height: 32px;
324
  align-self: flex-end;
 
325
  }
326
  .message-content {
327
  display: flex;
 
329
  gap: 4px;
330
  }
331
  .message-sender {
332
+ font-size: 0.85rem; /* Slightly larger sender name */
333
  font-weight: 500;
334
  color: var(--text-secondary);
335
  word-break: break-all;
 
346
  .message.sent { align-self: flex-end; flex-direction: row-reverse; }
347
  .message.sent .message-sender { text-align: right; color: var(--accent-blue-light); }
348
  .message.sent .message-bubble {
349
+ background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
350
  color: white;
351
  border-bottom-right-radius: 4px;
352
  }
 
358
  }
359
 
360
  .chat-placeholder {
361
+ flex-grow: 1; /* Take full space */
362
  display: flex;
363
  flex-direction: column;
364
  align-items: center;
365
  justify-content: center;
 
366
  text-align: center;
367
  color: var(--text-secondary);
368
  padding: 20px;
 
371
 
372
  .message-form {
373
  display: flex;
374
+ padding: 12px 16px; /* Reduced padding */
375
  gap: 12px;
376
  background-color: var(--bg-secondary);
377
  border-top: 1px solid var(--border-color);
 
379
  }
380
  #message-input {
381
  flex-grow: 1;
382
+ padding: 10px 14px; /* Adjusted padding */
383
  border: 1px solid var(--border-color);
384
  background-color: var(--bg-tertiary);
385
  color: var(--text-primary);
 
391
  #message-input:focus { border-color: var(--accent-blue); }
392
 
393
  .send-btn {
394
+ width: 40px; /* Adjusted size */
395
+ height: 40px;
396
  border-radius: 50%;
397
  flex-shrink: 0;
398
  padding: 0;
 
399
  }
400
+ .send-btn svg { width: 18px; height: 18px; fill: white; } /* Adjusted icon size */
401
 
 
402
  #browser-view {
403
+ background-color: var(--bg-secondary);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
  }
405
+ #browser-view iframe {
406
+ width: 100%;
407
+ height: 100%;
408
+ border: none;
 
 
 
 
 
 
 
 
 
 
 
 
409
  }
410
 
 
 
411
  #bottom-nav {
 
 
 
 
 
 
412
  display: flex;
413
  justify-content: space-around;
414
  align-items: center;
415
+ height: var(--footer-height);
416
+ background-color: var(--bg-secondary);
417
  border-top: 1px solid var(--border-color);
418
+ flex-shrink: 0;
419
+ padding: 0 10px;
420
  }
421
+
422
+ .nav-item {
423
  display: flex;
424
  flex-direction: column;
425
  align-items: center;
426
  justify-content: center;
427
+ padding: 8px 10px;
 
428
  cursor: pointer;
429
+ color: var(--text-secondary);
430
+ font-size: 0.75rem;
431
+ font-weight: 500;
432
+ text-decoration: none;
433
  transition: color 0.2s ease;
 
 
 
434
  }
435
+ .nav-item svg {
436
+ width: 24px;
437
+ height: 24px;
438
+ fill: var(--text-secondary);
439
+ margin-bottom: 4px;
440
  transition: fill 0.2s ease;
441
  }
442
+ .nav-item.active {
443
+ color: var(--accent-blue-light);
444
+ }
445
+ .nav-item.active svg {
446
+ fill: var(--accent-blue-light);
447
+ }
448
+ .nav-item:hover {
449
+ color: var(--text-primary);
450
  }
451
+ .nav-item:hover svg {
452
+ fill: var(--text-primary);
453
  }
454
+
455
+ .nav-qr-btn {
456
+ width: 56px; /* Larger button in center */
 
 
457
  height: 56px;
458
+ border-radius: 50%;
459
+ background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
460
  display: flex;
461
  align-items: center;
462
  justify-content: center;
463
+ margin-top: -20px; /* Lift it slightly */
464
+ box-shadow: 0 4px 15px rgba(0, 136, 204, 0.4);
465
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
466
+ border: none;
467
+ cursor: pointer;
 
 
468
  }
469
+ .nav-qr-btn:hover {
470
+ transform: translateY(-22px) scale(1.05);
471
+ box-shadow: 0 6px 20px rgba(0, 136, 204, 0.6);
472
+ }
473
+ .nav-qr-btn svg {
474
+ fill: white;
475
+ width: 28px;
476
+ height: 28px;
477
+ margin-bottom: 0;
478
  }
479
 
 
 
480
  .modal-overlay {
481
  position: fixed;
482
  top: 0;
 
488
  align-items: center;
489
  justify-content: center;
490
  z-index: 1000;
491
+ -webkit-backdrop-filter: blur(8px); /* Increased blur */
492
+ backdrop-filter: blur(8px);
493
+ padding: 20px;
494
  }
495
  .modal-content {
496
  background-color: var(--bg-secondary);
497
  padding: 24px;
498
  border-radius: 12px;
499
+ width: 100%;
500
  max-width: 400px;
501
  border: 1px solid var(--border-color);
502
+ box-shadow: 0 10px 40px rgba(0,0,0,0.6);
503
  }
504
+ .modal-content h3 { margin-bottom: 20px; font-weight: 600; font-size: 1.4rem; text-align: center;}
505
  .modal-content label { display: block; margin-bottom: 8px; font-size: 0.9rem; color: var(--text-secondary); }
506
  .modal-content input {
507
  width: 100%;
 
513
  border-radius: 6px;
514
  font-size: 1rem;
515
  }
 
516
  .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
517
  .modal-btn {
518
  padding: 10px 20px;
 
520
  border: none;
521
  cursor: pointer;
522
  font-weight: 500;
523
+ transition: opacity 0.2s ease;
524
+ }
525
+ .modal-btn:hover {
526
+ opacity: 0.9;
527
  }
528
+ .secondary-btn { background-color: var(--bg-hover); color: var(--text-primary); }
 
 
529
 
 
 
 
 
 
 
 
 
 
 
 
530
  #status-bar {
531
  position: fixed;
532
+ bottom: var(--footer-height, 60px); /* Position above nav */
533
  left: 50%;
534
  transform: translateX(-50%);
535
  background-color: var(--bg-tertiary);
 
549
  #status-bar.error { background-color: var(--error-color); }
550
  #status-bar.visible { opacity: 1; visibility: visible; }
551
 
552
+ /* Desktop Layout */
553
  @media (min-width: 768px) {
554
+ body {
555
+ align-items: center;
556
+ justify-content: center;
557
+ }
558
  .main-container {
559
+ width: 100%;
560
+ height: 100%; /* Adjust if you want a fixed max size */
561
+ max-width: 1100px; /* Example max width */
562
+ max-height: 800px; /* Example max height */
563
  border-radius: 12px;
564
  overflow: hidden;
565
+ box-shadow: 0 15px 50px rgba(0,0,0,0.5);
566
  border: 1px solid var(--border-color);
 
 
567
  }
 
568
  #app-view {
569
+ flex-direction: row; /* Sidebar and main content */
570
  }
571
+ .app-content {
572
+ flex-direction: row; /* Main content area itself flexes */
573
+ position: static; /* Static positioning in desktop layout */
574
+ }
575
+ .view {
576
+ position: static; /* Static positioning */
577
+ flex-shrink: 0; /* Prevent shrinking */
578
+ width: auto; /* Flexible width */
579
+ height: auto; /* Flexible height */
580
+ display: flex !important; /* Always visible on desktop if active */
581
  }
 
582
  #chatroom-list-view {
583
+ width: 320px; /* Fixed sidebar width */
 
584
  border-right: 1px solid var(--border-color);
 
585
  }
586
+ #chat-window-view,
 
 
 
 
 
587
  #browser-view {
588
+ flex-grow: 1; /* Take remaining width */
589
+ width: auto;
590
+ /* Hide chat/browser on desktop initially */
591
+ display: none !important;
592
  }
593
 
594
+ #chat-window-view.active,
595
+ #browser-view.active {
596
+ display: flex !important; /* Show the active view */
597
+ }
598
+
599
+
600
+ #chat-window-view .chat-placeholder {
601
+ /* Keep placeholder centered in available space */
602
+ display: flex;
603
  }
604
 
605
+
606
+ .back-btn { display: none !important; } /* No back button on desktop */
607
+
608
  #bottom-nav {
609
+ /* Position the nav bar on desktop */
610
+ position: absolute; /* Or fixed, depending on desired scroll behavior */
611
+ bottom: 0;
612
+ left: 0;
613
+ right: 0;
614
+ width: 100%;
615
+ max-width: 1100px; /* Match main container width */
616
+ margin: 0 auto;
617
+ border-top: 1px solid var(--border-color);
618
+ border-bottom-left-radius: 12px; /* Match container border radius */
619
+ border-bottom-right-radius: 12px;
620
+ }
621
+ .nav-qr-btn {
622
+ margin-top: -20px; /* Keep the lift effect */
623
+ }
624
+
625
+ #status-bar {
626
+ bottom: var(--footer-height, 60px); /* Adjust position relative to nav */
627
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
628
  }
629
  </style>
630
  </head>
 
638
  </div>
639
 
640
  <div id="app-view" class="main-container">
641
+ <div class="app-content">
642
+ <div id="chatroom-list-view" class="view active">
643
+ <div class="list-header">
644
+ <div class="list-header-top">
645
+ <h2>Чаты</h2>
646
+ <div style="display: flex; align-items: center; gap: 8px;">
647
+ <button id="my-profile-btn" class="action-btn small" title="Мой профиль">
648
+ <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="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>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
649
  </button>
650
+ <button id="create-room-show-modal" class="action-btn small">
651
+ <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>
652
+ <span>Новый</span>
 
 
 
 
 
653
  </button>
654
+ </div>
655
+ </div>
656
+ <div class="user-profile">
657
+ <div id="user-wallet"></div>
658
+ <div id="user-nickname"></div>
659
+ <form class="username-form" id="username-form">
660
+ <input type="text" id="username-input" class="username-input" placeholder="Установить никнейм" autocomplete="off">
661
+ <button type="submit" class="action-btn small" disabled>✓</button>
662
  </form>
663
  </div>
664
  </div>
665
+ <div id="chatroom-list"></div>
666
  </div>
667
 
668
+ <div id="chat-window-view" class="view">
669
+ <div id="chat-placeholder" class="chat-placeholder">
670
+ <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
671
+ <h2>Выберите чат</h2>
672
+ <p>Начните общение в одном из существующих чатов или создайте свой собственный.</p>
673
+ </div>
674
+ <div id="active-chat" style="display: none; width: 100%; height: 100%; flex-direction: column;">
675
+ <div class="chat-header">
676
+ <button class="back-btn" id="back-to-list-btn">
677
+ <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>
678
+ </button>
679
+ <div id="chat-header-avatar" class="avatar"></div>
680
+ <span id="chat-header-title"></span>
681
+ </div>
682
+ <div id="messages-container"></div>
683
+ <form id="message-form" class="message-form">
684
+ <input type="text" id="message-input" placeholder="Сообщение..." autocomplete="off" disabled>
685
+ <button type="submit" class="action-btn send-btn" id="send-btn" disabled>
686
+ <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>
687
+ </button>
688
+ </form>
689
  </div>
 
690
  </div>
691
 
692
+ <div id="browser-view" class="view">
693
+ <iframe id="anonymous-browser" src="https://www.google.com/" title="Anonymous Browser"></iframe>
 
 
 
 
 
694
  </div>
695
+
696
+ </div>
697
+ <div id="bottom-nav">
698
+ <a href="#" class="nav-item active" data-view="chatroom-list-view">
699
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M20 17H4V5h8V3H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2v-3h-2v3zm-16-9h16V6H4v2zm8 7H8v-2h4v2zm1-7v.01L14 10l2-2 3 3-4 4 .01.01H13V15h1.99L19 10l-3-3-2 2z"/></svg>
700
+ <span>Чаты</span>
701
+ </a>
702
+ <button class="nav-qr-btn" id="scan-qr-btn">
703
  <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>
704
+ </button>
705
+ <a href="#" class="nav-item" data-view="browser-view">
706
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" 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-1 17.93c-3.95-.49-7-3.85-7-7.93s3.05-7.44 7-7.93V12h9.04c-.46 4.11-4.01 7.16-8.04 7.93zM20 12h-9V4.07c4.01.81 7.13 3.97 7.93 7.93z"/></svg>
707
+ <span>Браузер</span>
708
+ </a>
709
+ </div>
710
  </div>
711
 
 
712
  <div id="create-room-modal" class="modal-overlay">
713
  <div class="modal-content">
714
  <h3>Создать новый чат</h3>
 
740
  </div>
741
 
742
  <div id="profile-modal" class="modal-overlay">
743
+ <div class="modal-content" style="text-align: center;">
744
  <h3 id="profile-modal-title">Профиль пользователя</h3>
745
+ <div id="profile-avatar-container" style="margin: 20px auto; display: inline-block;"></div>
746
+ <p id="profile-username" style="font-size: 1.2rem; font-weight: 600;"></p>
747
+ <p id="profile-address" style="color: var(--text-secondary); font-size: 0.9rem; word-break: break-all; margin-top: 8px;"></p>
748
+ <div id="profile-qr-code" style="background: white; padding: 10px; margin: 20px auto; width: fit-content; border-radius: 8px;"></div>
749
+ <p style="text-align: center; color: var(--text-secondary); font-size: 0.8rem; margin-top: -10px; margin-bottom: 20px;">Отсканируйте для открытия профиля</p>
750
+ <div class="modal-actions" style="flex-direction: column; gap: 12px; align-items: stretch;">
751
  <button id="send-ton-btn" class="modal-btn action-btn">Отправить TON</button>
752
  <button id="profile-close-btn" class="modal-btn secondary-btn">Закрыть</button>
753
  </div>
 
757
  <div id="scanner-modal" class="modal-overlay">
758
  <div class="modal-content">
759
  <h3>Сканировать QR-код</h3>
760
+ <div id="qr-reader" style="width: 100%; border: 1px solid var(--border-color); margin-top: 16px; border-radius: 8px; overflow: hidden;"></div>
761
  <div class="modal-actions">
762
  <button id="scanner-close-btn" class="modal-btn secondary-btn">Отмена</button>
763
  </div>
 
779
  let chatroomsData = {};
780
  let html5QrCode = null;
781
  let profileQrCode = null;
782
+ let currentView = 'chatroom-list-view'; // Track active view
783
+
784
  const loginView = document.getElementById('login-view');
785
  const appView = document.getElementById('app-view');
786
+ const views = document.querySelectorAll('.view'); // All main views
787
  const chatroomListView = document.getElementById('chatroom-list-view');
788
  const chatWindowView = document.getElementById('chat-window-view');
789
  const browserView = document.getElementById('browser-view');
790
  const chatPlaceholder = document.getElementById('chat-placeholder');
791
  const activeChat = document.getElementById('active-chat');
 
 
 
 
 
 
792
  const profileModal = document.getElementById('profile-modal');
793
  const scannerModal = document.getElementById('scanner-modal');
794
+ const navItems = document.querySelectorAll('.nav-item');
795
+ const usernameInput = document.getElementById('username-input');
796
+ const usernameSubmitBtn = document.querySelector('#username-form button[type="submit"]');
797
+ const messageInput = document.getElementById('message-input');
798
+ const sendButton = document.getElementById('send-btn');
799
+ const backToListBtn = document.getElementById('back-to-list-btn');
800
 
801
+ const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292'];
 
 
 
 
 
 
 
 
802
 
803
  const getAvatar = (name) => {
804
+ const initial = (name ? name.trim()[0] : '?').toUpperCase();
805
  const charCode = initial.charCodeAt(0);
806
  const color = AVATAR_COLORS[charCode % AVATAR_COLORS.length];
807
  const avatar = document.createElement('div');
 
830
  throw new Error(errorData.error || 'Unknown error');
831
  }
832
  if (response.status === 204) return null;
833
+ const text = await response.text();
834
+ if (!text) return null; // Handle empty response body
835
+ return JSON.parse(text);
836
  } catch (error) {
837
  showStatus(`Ошибка: ${error.message}`, 'error');
838
  throw error;
 
844
  const updateUserInfo = () => {
845
  document.getElementById('user-wallet').textContent = `Кошелек: ${truncateAddress(currentUser.address)}`;
846
  const nicknameEl = document.getElementById('user-nickname');
 
847
  nicknameEl.textContent = currentUser.username ? `Ник: ${currentUser.username}` : `Никнейм не установлен`;
848
  usernameInput.value = currentUser.username || '';
849
+ usernameInput.disabled = false;
850
+ usernameSubmitBtn.disabled = false;
851
  };
852
 
853
  document.getElementById('username-form').addEventListener('submit', async (e) => {
854
  e.preventDefault();
855
+ const newUsername = usernameInput.value.trim();
856
+ if (!newUsername) {
857
+ showStatus('Введите никнейм.', 'error');
858
+ return;
859
+ }
860
+ if (newUsername.length < 3 || newUsername.length > 20) {
861
+ showStatus('Никнейм должен быть от 3 до 20 символов.', 'error');
862
  return;
863
  }
864
+ if (newUsername === currentUser.username) {
865
+ showStatus('Никнейм не изменился.', 'info');
866
+ return;
867
+ }
868
+
869
+ usernameInput.disabled = true;
870
+ usernameSubmitBtn.disabled = true;
871
+
872
  try {
873
  await apiCall('/api/set_username', {
874
  method: 'POST',
 
878
  currentUser.username = newUsername;
879
  updateUserInfo();
880
  showStatus('Никнейм успешно обновлен!', 'success');
881
+ fetchChatrooms(); // Update chat list names
882
+ if (activeChatroomId) fetchMessages(activeChatroomId); // Update sender names in active chat
883
+ } catch (err) {
884
+ usernameInput.disabled = false;
885
+ usernameSubmitBtn.disabled = false;
886
+ }
887
+ });
888
+
889
+ const showView = (viewId) => {
890
+ views.forEach(view => view.classList.remove('active'));
891
+ document.getElementById(viewId).classList.add('active');
892
+ currentView = viewId;
893
+
894
+ // Update nav active state
895
+ navItems.forEach(item => {
896
+ if (item.dataset.view === viewId) {
897
+ item.classList.add('active');
898
+ } else {
899
+ item.classList.remove('active');
900
+ }
901
+ });
902
+
903
+ // Handle chat window specific display
904
+ if (viewId === 'chat-window-view') {
905
+ const isMobile = window.innerWidth < 768;
906
+ backToListBtn.style.display = isMobile ? 'block' : 'none';
907
+ if (isMobile) {
908
+ chatroomListView.classList.remove('active'); // Hide list on mobile when chat is open
909
+ }
910
+ messageInput.disabled = false;
911
+ sendButton.disabled = false;
912
+
913
+ } else {
914
+ backToListBtn.style.display = 'none';
915
+ // Stop polling if leaving chat view
916
+ if (messagePollingInterval) {
917
+ clearInterval(messagePollingInterval);
918
+ messagePollingInterval = null;
919
+ }
920
+ messageInput.disabled = true;
921
+ sendButton.disabled = true;
922
+
923
+ // Ensure list view is active on desktop or when explicitly selected on mobile
924
+ if (window.innerWidth >= 768 && viewId !== 'chatroom-list-view') {
925
+ chatroomListView.classList.add('active');
926
+ } else if (viewId === 'chatroom-list-view') {
927
+ // On mobile, selecting list view should hide chat/browser
928
+ chatWindowView.classList.remove('active');
929
+ browserView.classList.remove('active');
930
+ }
931
+ }
932
+
933
+ // Handle desktop side-by-side layout
934
+ if (window.innerWidth >= 768) {
935
+ chatroomListView.classList.add('active'); // List is always active on desktop
936
+ }
937
+ };
938
+
939
+ navItems.forEach(item => {
940
+ item.addEventListener('click', (e) => {
941
+ e.preventDefault();
942
+ const viewId = item.dataset.view;
943
+ if (viewId) {
944
+ showView(viewId);
945
+ }
946
+ });
947
  });
948
 
949
  const initializeUser = async (address) => {
 
961
  updateUserInfo();
962
  loginView.style.display = 'none';
963
  appView.style.display = 'flex';
 
 
964
  fetchChatrooms();
965
+ showView('chatroom-list-view'); // Go to chat list after login
966
  };
967
 
968
  const renderChatrooms = (rooms) => {
969
  const list = document.getElementById('chatroom-list');
970
  list.innerHTML = '';
971
  chatroomsData = {};
972
+ if (rooms.length === 0) {
973
+ list.innerHTML = '<p style="text-align: center; color: var(--text-secondary); padding: 20px;">Чатрумов пока нет. Создайте первый!</p>';
974
+ }
975
  rooms.forEach(room => {
976
  chatroomsData[room.id] = room;
977
  const item = document.createElement('div');
 
1008
 
1009
  const renderMessages = (messages) => {
1010
  const container = document.getElementById('messages-container');
1011
+ const shouldScroll = container.scrollHeight - container.scrollTop - container.clientHeight <= 30; // Scroll if near bottom
1012
+
1013
  container.innerHTML = '';
1014
  messages.forEach(msg => {
1015
  const msgDiv = document.createElement('div');
 
1036
  contentDiv.appendChild(bubbleDiv);
1037
 
1038
  msgDiv.appendChild(contentDiv);
1039
+ // Insert avatar before content for received, after for sent
1040
+ if (msg.sender_address !== currentUser.address) {
1041
+ msgDiv.insertBefore(avatar, contentDiv);
1042
+ } else {
1043
+ msgDiv.appendChild(avatar);
1044
+ }
1045
+
1046
  container.appendChild(msgDiv);
1047
  });
1048
 
1049
+ if(shouldScroll) {
1050
  container.scrollTop = container.scrollHeight;
1051
  }
1052
  };
1053
 
1054
  const fetchMessages = async (roomId) => {
1055
+ if (currentView !== 'chat-window-view' || activeChatroomId !== roomId) {
1056
+ // Don't fetch if not in the chat view or if the room changed
1057
+ return;
1058
+ }
1059
  try {
1060
  const data = await apiCall(`/api/messages/${roomId}`);
1061
  renderMessages(data.messages);
1062
  } catch (err) {
1063
+ // Error fetching messages, potentially server issue or removed chat
1064
+ console.error("Failed to fetch messages:", err);
1065
+ // Stop polling on error
1066
+ if (messagePollingInterval) {
1067
+ clearInterval(messagePollingInterval);
1068
+ messagePollingInterval = null;
1069
+ }
1070
+ // Optionally show a message or navigate back
1071
  }
1072
  };
1073
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1074
  const selectChatroom = (roomId, isPrivate) => {
1075
  const roomData = chatroomsData[roomId];
1076
+ if (!roomData) {
1077
+ showStatus('Чат не найден.', 'error');
1078
+ fetchChatrooms(); // Refresh list
1079
+ return;
1080
+ }
1081
 
1082
  const proceedToRoom = () => {
1083
+ // Stop any previous polling
1084
  if (messagePollingInterval) clearInterval(messagePollingInterval);
1085
  activeChatroomId = roomId;
1086
 
 
1092
  chatPlaceholder.style.display = 'none';
1093
  activeChat.style.display = 'flex';
1094
 
1095
+ showView('chat-window-view'); // Switch to chat view
1096
+
1097
+ fetchMessages(roomId); // Initial fetch
1098
+ messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000); // Poll every 3 seconds
1099
  };
1100
 
1101
  if (isPrivate) {
1102
+ const passwordModal = document.getElementById('password-modal');
1103
  const passwordForm = document.getElementById('password-form');
1104
  const passwordInput = document.getElementById('password-input');
 
1105
  passwordModal.style.display = 'flex';
1106
  passwordInput.value = '';
1107
  passwordInput.focus();
1108
 
1109
+ const formSubmitHandler = async (e) => {
 
 
1110
  e.preventDefault();
 
1111
  const password = passwordInput.value;
1112
+ passwordForm.removeEventListener('submit', formSubmitHandler); // Remove to prevent multiple submissions
1113
+ document.getElementById('password-cancel').onclick = null; // Remove cancel handler too
1114
+ passwordModal.style.display = 'none'; // Hide immediately
1115
  try {
1116
  await apiCall('/api/join_chatroom', {
1117
  method: 'POST',
 
1120
  });
1121
  proceedToRoom();
1122
  } catch (err) {
1123
+ // Error message shown by apiCall
1124
+ // Re-show modal if needed or handle differently
 
1125
  }
1126
  };
1127
+ passwordForm.addEventListener('submit', formSubmitHandler);
1128
 
1129
  document.getElementById('password-cancel').onclick = () => {
1130
  passwordModal.style.display = 'none';
1131
+ passwordForm.removeEventListener('submit', formSubmitHandler);
1132
  };
 
1133
  } else {
 
1134
  proceedToRoom();
1135
  }
1136
  };
1137
 
1138
+ backToListBtn.addEventListener('click', () => {
1139
+ showView('chatroom-list-view'); // Go back to list view
1140
+ chatPlaceholder.style.display = 'flex'; // Show placeholder
1141
+ activeChat.style.display = 'none'; // Hide active chat
1142
+ activeChatroomId = null; // Clear active room
1143
+ if (messagePollingInterval) { // Stop polling
1144
+ clearInterval(messagePollingInterval);
1145
+ messagePollingInterval = null;
1146
+ }
1147
+ });
1148
+
1149
+
1150
  document.getElementById('message-form').addEventListener('submit', async (e) => {
1151
  e.preventDefault();
1152
+ const text = messageInput.value.trim();
 
 
1153
  if (text && activeChatroomId && currentUser.address) {
1154
+ messageInput.value = '';
1155
+ messageInput.disabled = true;
1156
+ sendButton.disabled = true;
1157
  try {
1158
  await apiCall('/api/send_message', {
1159
  method: 'POST',
 
1164
  text: text
1165
  })
1166
  });
1167
+ await fetchMessages(activeChatroomId); // Refresh messages after sending
1168
+ // Scroll to bottom is handled within renderMessages
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1169
  } finally {
1170
+ messageInput.disabled = false;
1171
+ sendButton.disabled = false;
1172
+ messageInput.focus();
1173
  }
1174
  }
1175
  });
1176
 
1177
+ const createRoomModal = document.getElementById('create-room-modal');
1178
  document.getElementById('create-room-show-modal').addEventListener('click', () => {
1179
  createRoomModal.style.display = 'flex';
1180
  document.getElementById('create-room-form').reset();
 
1184
  });
1185
  document.getElementById('create-room-form').addEventListener('submit', async (e) => {
1186
  e.preventDefault();
1187
+ const nameInput = document.getElementById('room-name');
1188
+ const passwordInput = document.getElementById('room-password');
1189
+ const createBtn = createRoomModal.querySelector('button[type="submit"]');
1190
+
1191
+ const name = nameInput.value.trim();
1192
+ const password = passwordInput.value;
1193
+
1194
  if (!name) {
1195
+ showStatus('Название чата обязательно.', 'error');
1196
+ return;
1197
  }
1198
+ if (name.length < 3 || name.length > 30) {
1199
+ showStatus('Название чата должно быть от 3 до 30 символов.', 'error');
1200
  return;
1201
  }
1202
+ if (password && password.length < 6) {
1203
+ showStatus('Пароль должен быть не менее 6 символов.', 'error');
1204
+ return;
1205
+ }
1206
 
1207
+ nameInput.disabled = true;
1208
+ passwordInput.disabled = true;
1209
+ createBtn.disabled = true;
1210
 
1211
  try {
1212
  await apiCall('/api/create_chatroom', {
 
1216
  });
1217
  createRoomModal.style.display = 'none';
1218
  showStatus('Чат успешно создан!', 'success');
1219
+ fetchChatrooms(); // Refresh list after creation
1220
+ } catch (err) {
1221
+ // Error message shown by apiCall
1222
+ } finally {
1223
+ nameInput.disabled = false;
1224
+ passwordInput.disabled = false;
1225
+ createBtn.disabled = false;
1226
+ }
1227
  });
1228
 
1229
  const showProfile = async (address) => {
 
1234
  body: JSON.stringify({ address: address })
1235
  });
1236
 
1237
+ const username = userData.username || `User ${truncateAddress(address)}`;
1238
  const avatarContainer = document.getElementById('profile-avatar-container');
1239
  const usernameEl = document.getElementById('profile-username');
1240
  const addressEl = document.getElementById('profile-address');
 
1249
 
1250
  qrCodeEl.innerHTML = '';
1251
  if (profileQrCode) {
1252
+ // Clear previous QR code if it exists and was rendered to this element
1253
+ // QRCode.js doesn't have a .clear() method on the instance itself,
1254
+ // but clearing the container div is sufficient.
 
 
 
 
1255
  }
1256
+ profileQrCode = new QRCode(qrCodeEl, {
1257
+ text: address,
1258
+ width: 150,
1259
+ height: 150,
1260
+ colorDark : "#000000",
1261
+ colorLight : "#ffffff",
1262
+ correctLevel : QRCode.CorrectLevel.H
1263
+ });
1264
+
 
 
 
 
 
 
 
 
1265
  sendTonBtn.onclick = async () => {
1266
  if (!tonConnectUI.connected) {
1267
  showStatus('Подключите кошелек для отправки TON.', 'error');
1268
  return;
1269
  }
1270
+ if (address === currentUser.address) {
1271
+ showStatus('Нельзя отправить TON себе.', 'error');
1272
+ return;
1273
+ }
1274
+
1275
  const amountString = prompt("Введите сумму в TON для отправки:", "0.1");
1276
+ if (amountString === null) return; // User cancelled
1277
+
1278
  const amount = parseFloat(amountString);
1279
  if (isNaN(amount) || amount <= 0) {
1280
  showStatus('Неверная сумма.', 'error');
 
1284
  const amountInNanoTon = Math.floor(amount * 1_000_000_000).toString();
1285
 
1286
  const transaction = {
1287
+ validUntil: Math.floor(Date.now() / 1000) + 600, // 10 minutes validity
1288
  messages: [ { address: address, amount: amountInNanoTon } ]
1289
  };
1290
 
 
1293
  showStatus(`Транзакция отправлена успешно!`, 'success');
1294
  profileModal.style.display = 'none';
1295
  } catch (error) {
1296
+ // Check for user rejection specifically
1297
+ if (error.code === 3) { // Example error code for user rejection (check actual library docs)
1298
+ showStatus('Транзакция отклонена пользователем.', 'info');
1299
+ } else {
1300
+ showStatus(`Ошибка транзакции: ${error.message || 'Неизвестная ошибка'}`, 'error');
1301
+ }
1302
  }
1303
  };
1304
 
1305
+ sendTonBtn.style.display = (address === currentUser.address) ? 'none' : 'block';
1306
  profileModal.style.display = 'flex';
1307
  } catch (err) {
1308
  showStatus('Не удалось загрузить профиль.', 'error');
 
1311
 
1312
  const showScanner = () => {
1313
  scannerModal.style.display = 'flex';
1314
+ if (!html5QrCode) {
1315
+ html5QrCode = new Html5Qrcode("qr-reader");
1316
+ }
1317
+
1318
+ // Define the success callback
1319
  const qrCodeSuccessCallback = (decodedText, decodedResult) => {
1320
+ hideScanner(); // Hide scanner immediately on success
1321
+ console.log(`QR code scanned: ${decodedText}`);
1322
+ // Basic validation for TON address pattern (starts with EQ/UQ, length > 40)
1323
+ if (decodedText && decodedText.length > 40 && (decodedText.startsWith('EQ') || decodedText.startsWith('UQ'))) {
1324
+ showProfile(decodedText);
1325
  } else {
1326
+ showStatus('Отсканирован недействительный QR-код (не похож на TON адрес).', 'error');
1327
  }
1328
  };
1329
+
1330
+ const config = { fps: 10, qrbox: { width: 250, height: 250 }, rememberLastUsedCamera: true };
1331
+
1332
+ // Start scanning
1333
+ html5QrCode.start({ facingMode: "environment" }, config, qrCodeSuccessCallback,
1334
+ (errorMessage) => {
1335
+ // Optional: handle scan error (e.g., no QR code found)
1336
+ // console.warn(`QR scan error: ${errorMessage}`);
1337
+ })
1338
  .catch(err => {
1339
+ console.error("Failed to start QR scanner:", err);
1340
+ showStatus('Не удалось запустить сканер. Убедитесь, что у вас есть камера и вы предоставили доступ.', 'error');
1341
+ hideScanner(); // Ensure modal is hidden on error
1342
  });
1343
  };
1344
 
1345
  const hideScanner = () => {
1346
  if (html5QrCode && html5QrCode.isScanning) {
1347
+ html5QrCode.stop().then(() => {
1348
+ console.log("QR scanning stopped.");
1349
+ }).catch(err => {
1350
+ console.error("Failed to stop QR scanner.", err);
1351
+ });
1352
  }
1353
  scannerModal.style.display = 'none';
1354
  };
1355
 
 
1356
  document.getElementById('my-profile-btn').addEventListener('click', () => {
1357
  if (currentUser.address) showProfile(currentUser.address);
1358
+ else showStatus('Подключите кошелек для просмотра профиля.', 'info');
1359
  });
1360
+ document.getElementById('scan-qr-btn').addEventListener('click', showScanner);
 
 
 
 
 
 
 
 
 
 
 
1361
  document.getElementById('profile-close-btn').addEventListener('click', () => profileModal.style.display = 'none');
1362
  document.getElementById('scanner-close-btn').addEventListener('click', hideScanner);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1363
 
1364
+ // Handle initial view and resize for responsiveness
1365
+ const handleResize = () => {
1366
  const isMobile = window.innerWidth < 768;
1367
+ // Adjust back button visibility based on mobile view AND if chat view is active
1368
+ backToListBtn.style.display = isMobile && currentView === 'chat-window-view' ? 'block' : 'none';
1369
+
1370
+ // On desktop, ensure list and the currently active view are visible
1371
+ if (!isMobile) {
1372
+ chatroomListView.classList.add('active'); // List always visible desktop
1373
+ if (currentView === 'chat-window-view') {
1374
+ chatWindowView.classList.add('active');
1375
+ browserView.classList.remove('active');
1376
+ } else if (currentView === 'browser-view') {
1377
+ browserView.classList.add('active');
1378
+ chatWindowView.classList.remove('active');
1379
+ } else { // Default to list being primary content if no chat/browser selected yet on desktop
1380
+ // This case might need refinement depending on desired desktop behavior
1381
+ // Currently, default is show list, and if a chat/browser is selected, show it next to list.
1382
+ // If no chat/browser selected, the right pane could be empty or show placeholder.
1383
+ if (!activeChatroomId && currentView !== 'browser-view') {
1384
+ // Maybe hide right panel or show a general placeholder if nothing is selected?
1385
+ // For now, just ensure the active non-list view is shown if one exists.
1386
+ }
1387
  }
1388
+ } else {
1389
+ // On mobile, only the currentView should be active
1390
+ views.forEach(view => view.classList.remove('active'));
1391
+ document.getElementById(currentView).classList.add('active');
1392
  }
 
 
 
 
 
 
 
 
 
1393
  };
1394
 
1395
  window.addEventListener('resize', handleResize);
 
 
 
 
1396
 
1397
  tonConnectUI.onStatusChange(wallet => {
1398
  if (wallet) {
 
1402
  currentUser = { address: null, username: null };
1403
  appView.style.display = 'none';
1404
  loginView.style.display = 'flex';
 
1405
  if (messagePollingInterval) clearInterval(messagePollingInterval);
1406
+ messagePollingInterval = null;
1407
  activeChatroomId = null;
1408
+ // Reset UI state
1409
  chatPlaceholder.style.display = 'flex';
1410
  activeChat.style.display = 'none';
1411
+ usernameInput.disabled = true;
1412
+ usernameSubmitBtn.disabled = true;
1413
+ messageInput.disabled = true;
1414
+ sendButton.disabled = true;
1415
  renderChatrooms([]); // Clear chat list
1416
+ // Hide all views and activate login view
1417
+ views.forEach(view => view.classList.remove('active'));
1418
  }
1419
+ // Trigger resize handler to adjust view display based on wallet status and screen size
1420
+ handleResize();
1421
  });
1422
+
1423
+ // Initial check on load
1424
+ if (tonConnectUI.connected) {
1425
+ const address = TON_CONNECT_UI.toUserFriendlyAddress(tonConnectUI.wallet.account.address, false);
1426
+ initializeUser(address);
1427
+ } else {
1428
+ loginView.style.display = 'flex';
1429
+ appView.style.display = 'none';
1430
+ usernameInput.disabled = true;
1431
+ usernameSubmitBtn.disabled = true;
1432
+ messageInput.disabled = true;
1433
+ sendButton.disabled = true;
1434
+ }
1435
+ handleResize(); // Apply initial layout based on screen size
1436
  });
1437
  </script>
1438
  </body>
 
1448
  if not address:
1449
  return jsonify({'error': 'Address is required'}), 400
1450
  db = read_db()
1451
+ user_info = db['users'].get(address, {})
1452
+ username = user_info.get('username', None)
1453
  return jsonify({'username': username})
1454
 
1455
  @app.route('/api/set_username', methods=['POST'])
 
1457
  data = request.get_json()
1458
  address = data.get('address')
1459
  username = data.get('username')
1460
+ if not address or username is None: # Allow setting to null/empty
1461
  return jsonify({'error': 'Address and username are required'}), 400
1462
+ if username and (len(username) < 3 or len(username) > 20):
1463
  return jsonify({'error': 'Username must be between 3 and 20 characters'}), 400
1464
 
1465
  db = read_db()
1466
  if address not in db['users']:
1467
  db['users'][address] = {}
1468
+
1469
+ # Ensure username is stored only if not null/empty string
1470
+ if username:
1471
+ db['users'][address]['username'] = username
1472
+ elif 'username' in db['users'][address]:
1473
+ del db['users'][address]['username'] # Remove username if set to empty
1474
+
1475
  write_db(db)
1476
  return jsonify({'success': True})
1477
 
 
1480
  db = read_db()
1481
  chatrooms_list = []
1482
  for room_id, room_data in db['chatrooms'].items():
1483
+ # Only include rooms that still have messages, or filter differently if needed
1484
+ # This basic example includes all rooms regardless of message count
1485
  chatrooms_list.append({
1486
  'id': room_id,
1487
  'name': room_data['name'],
1488
+ 'is_private': room_data['is_private']
1489
  })
1490
  return jsonify({'chatrooms': sorted(chatrooms_list, key=lambda x: x['name'])})
1491
 
 
1497
  creator_address = data.get('creator_address')
1498
  if not name or not creator_address:
1499
  return jsonify({'error': 'Name and creator address are required'}), 400
1500
+ if len(name) < 3 or len(name) > 30:
1501
+ return jsonify({'error': 'Chatroom name must be between 3 and 30 characters'}), 400
1502
+ if password and len(password) < 6:
1503
+ return jsonify({'error': 'Password must be at least 6 characters'}), 400
1504
 
1505
  db = read_db()
1506
  room_id = str(uuid.uuid4())
1507
  db['chatrooms'][room_id] = {
 
1508
  'name': name,
1509
  'creator': creator_address,
1510
  'is_private': bool(password),
1511
  'password_hash': generate_password_hash(password) if password else None
1512
  }
1513
+ db['messages'][room_id] = [] # Initialize message list for the new room
1514
  write_db(db)
1515
  return jsonify({'success': True, 'chatroom_id': room_id})
1516
 
 
1519
  data = request.get_json()
1520
  chatroom_id = data.get('chatroom_id')
1521
  password = data.get('password')
1522
+ if not chatroom_id:
1523
+ return jsonify({'error': 'Chatroom ID is required'}), 400
1524
+
1525
  db = read_db()
1526
  chatroom = db['chatrooms'].get(chatroom_id)
1527
  if not chatroom:
1528
  return jsonify({'error': 'Chatroom not found'}), 404
1529
+
1530
+ if chatroom.get('is_private'): # Use .get() for safety
1531
+ if not password or not chatroom.get('password_hash') or not check_password_hash(chatroom['password_hash'], password):
1532
  return jsonify({'error': 'Invalid password'}), 403
1533
+
1534
+ # In a real app, joining might involve adding user to a list or similar.
1535
+ # For this simple app, just verifying password for private rooms is sufficient for "joining".
1536
  return jsonify({'success': True})
1537
 
1538
  @app.route('/api/messages/<chatroom_id>', methods=['GET'])
1539
  def get_messages(chatroom_id):
1540
  db = read_db()
1541
  if chatroom_id not in db['messages']:
1542
+ # If the chatroom exists but has no messages entry, initialize it
1543
+ if chatroom_id in db['chatrooms'] and chatroom_id not in db['messages']:
1544
+ db['messages'][chatroom_id] = []
1545
+ write_db(db)
1546
+ return jsonify({'messages': []})
1547
+
1548
  return jsonify({'error': 'Chatroom not found'}), 404
1549
 
1550
  messages_with_names = []
1551
+ # Get a copy of the messages list to avoid modifying while iterating
1552
+ room_messages = list(db['messages'].get(chatroom_id, []))
1553
+
1554
+ # Fetch all necessary user data in one go if performance was critical
1555
+ # For this scale, iterating is fine.
1556
 
1557
  for msg in room_messages:
1558
  sender_address = msg['sender_address']
1559
+ user_info = db['users'].get(sender_address, {}) # Use .get with default empty dict
1560
+ # Display username if available, otherwise a truncated address
1561
+ display_name = (user_info.get('username') if user_info.get('username')
1562
+ else f"{sender_address[:6]}...{sender_address[-6:]}") # Slightly longer truncation
1563
+
1564
  msg_copy = msg.copy()
1565
  msg_copy['display_name'] = display_name
1566
  messages_with_names.append(msg_copy)
 
1576
 
1577
  if not all([chatroom_id, sender_address, text]):
1578
  return jsonify({'error': 'Missing data'}), 400
1579
+
1580
+ if len(text.strip()) == 0:
1581
+ return jsonify({'error': 'Message cannot be empty'}), 400
1582
 
1583
  db = read_db()
1584
+ # Check if chatroom exists before adding message
1585
+ if chatroom_id not in db['chatrooms']:
1586
  return jsonify({'error': 'Chatroom not found'}), 404
1587
+
1588
+ # Ensure messages list exists for the room
1589
+ if chatroom_id not in db['messages']:
1590
+ db['messages'][chatroom_id] = []
1591
 
1592
  message = {
1593
  'id': str(uuid.uuid4()),
1594
  'sender_address': sender_address,
1595
+ 'text': text.strip(), # Trim whitespace
1596
+ 'timestamp': datetime.utcnow().isoformat() + "Z" # ISO 8601 with Z for UTC
1597
  }
1598
 
1599
+ # Keep message list size reasonable (e.g., last 100 messages)
1600
+ MAX_MESSAGES = 100
1601
+ if len(db['messages'][chatroom_id]) >= MAX_MESSAGES:
1602
+ # Remove the oldest message (at index 0)
1603
  db['messages'][chatroom_id].pop(0)
1604
 
1605
  db['messages'][chatroom_id].append(message)