Aleksmorshen commited on
Commit
7a9f6f2
·
verified ·
1 Parent(s): b56109b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +572 -600
app.py CHANGED
@@ -19,12 +19,8 @@ def init_db():
19
  }, f, indent=4)
20
 
21
  def read_db():
22
- try:
23
- with open(DB_FILE, 'r') as f:
24
- return json.load(f)
25
- except (FileNotFoundError, json.JSONDecodeError):
26
- init_db()
27
- return read_db()
28
 
29
  def write_db(data):
30
  with open(DB_FILE, 'w') as f:
@@ -47,21 +43,19 @@ 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: #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
  * {
@@ -83,7 +77,9 @@ def index():
83
  height: 100vh;
84
  width: 100vw;
85
  display: flex;
86
- flex-direction: column;
 
 
87
  }
88
 
89
  .main-container {
@@ -94,24 +90,25 @@ def index():
94
  transition: opacity 0.3s ease;
95
  }
96
 
 
97
  #login-view {
98
- flex-grow: 1;
99
  display: flex;
100
  flex-direction: column;
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;
@@ -119,41 +116,35 @@ def index():
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 {
@@ -210,7 +201,7 @@ def index():
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);
@@ -220,12 +211,6 @@ def index():
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;
@@ -252,8 +237,7 @@ def index():
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;
@@ -272,8 +256,13 @@ def index():
272
  flex-shrink: 0;
273
  }
274
 
 
275
  #chat-window-view {
276
- background-color: var(--bg-primary);
 
 
 
 
277
  }
278
 
279
  .chat-header {
@@ -289,10 +278,9 @@ def index():
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;
@@ -316,11 +304,11 @@ def index():
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 {
@@ -329,7 +317,7 @@ def index():
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;
@@ -340,7 +328,6 @@ def index():
340
  border-radius: 18px;
341
  line-height: 1.4;
342
  word-wrap: break-word;
343
- font-size: 1rem;
344
  }
345
 
346
  .message.sent { align-self: flex-end; flex-direction: row-reverse; }
@@ -358,20 +345,20 @@ def index():
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;
369
  }
370
- .chat-placeholder img { width: 80px; margin-bottom: 20px; opacity: 0.5; filter: grayscale(100%); }
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,7 +366,7 @@ def index():
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,92 +378,56 @@ def index():
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,22 +439,21 @@ def index():
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%;
508
  padding: 12px;
509
  margin-bottom: 16px;
@@ -520,16 +470,113 @@ def index():
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);
@@ -539,92 +586,112 @@ def index():
539
  font-size: 0.9rem;
540
  opacity: 0;
541
  visibility: hidden;
542
- transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
543
  z-index: 2000;
544
  box-shadow: 0 5px 15px rgba(0,0,0,0.3);
545
  max-width: 90%;
546
- text-align: center;
547
  }
548
  #status-bar.success { background-color: var(--success-color); }
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,34 +705,47 @@ def index():
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>
@@ -681,32 +761,22 @@ def index():
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">
@@ -740,12 +810,12 @@ def index():
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>
@@ -757,7 +827,7 @@ def index():
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,29 +849,30 @@ def index():
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,9 +901,7 @@ def index():
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,31 +913,22 @@ def index():
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,72 +938,9 @@ def index():
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) => {
@@ -956,22 +953,19 @@ def index():
956
  });
957
  currentUser.username = data.username;
958
  } catch (err) {
959
- currentUser.username = null;
960
  }
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,8 +1002,8 @@ def index():
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,51 +1030,71 @@ def index():
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,10 +1106,9 @@ def index():
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) {
@@ -1108,10 +1121,9 @@ def index():
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,8 +1132,8 @@ def index():
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);
@@ -1135,25 +1147,16 @@ def index():
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,12 +1167,12 @@ def index():
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
  });
@@ -1178,35 +1181,19 @@ def index():
1178
  document.getElementById('create-room-show-modal').addEventListener('click', () => {
1179
  createRoomModal.style.display = 'flex';
1180
  document.getElementById('create-room-form').reset();
 
1181
  });
1182
  document.getElementById('create-room-cancel').addEventListener('click', () => {
1183
  createRoomModal.style.display = 'none';
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,17 +1203,12 @@ def index():
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) => {
 
1230
  try {
1231
  const userData = await apiCall('/api/user_data', {
1232
  method: 'POST',
@@ -1234,12 +1216,14 @@ def index():
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');
1241
  const qrCodeEl = document.getElementById('profile-qr-code');
1242
  const sendTonBtn = document.getElementById('send-ton-btn');
 
 
1243
 
1244
  avatarContainer.innerHTML = '';
1245
  avatarContainer.appendChild(getAvatar(username));
@@ -1249,9 +1233,7 @@ def index():
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,
@@ -1267,21 +1249,17 @@ def index():
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');
1281
  return;
1282
  }
1283
 
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
@@ -1289,150 +1267,149 @@ def index():
1289
  };
1290
 
1291
  try {
1292
- await tonConnectUI.sendTransaction(transaction);
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');
 
1309
  }
1310
  };
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) {
1399
- const address = TON_CONNECT_UI.toUserFriendlyAddress(wallet.account.address, false);
 
1400
  initializeUser(address);
1401
  } else {
 
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,8 +1425,8 @@ def get_user_data():
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,21 +1434,15 @@ def set_username():
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,14 +1451,12 @@ def get_chatrooms():
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
 
1492
  @app.route('/api/create_chatroom', methods=['POST'])
1493
  def create_chatroom():
@@ -1497,12 +1466,17 @@ def create_chatroom():
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,
@@ -1510,7 +1484,7 @@ def create_chatroom():
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,50 +1493,47 @@ def join_chatroom():
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)
1567
 
1568
  return jsonify({'messages': messages_with_names})
@@ -1577,29 +1548,30 @@ def send_message():
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)
@@ -1609,4 +1581,4 @@ def send_message():
1609
 
1610
  if __name__ == '__main__':
1611
  init_db()
1612
- app.run(host='0.0.0.0', port=7860)
 
19
  }, f, indent=4)
20
 
21
  def read_db():
22
+ with open(DB_FILE, 'r') as f:
23
+ return json.load(f)
 
 
 
 
24
 
25
  def write_db(data):
26
  with open(DB_FILE, 'w') as f:
 
43
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
44
  <style>
45
  :root {
46
+ --bg-primary: #0F0F11;
47
+ --bg-secondary: #18191C;
48
+ --bg-tertiary: #212328;
49
+ --bg-hover: #2A2C33;
50
+ --text-primary: #EAECEF;
51
+ --text-secondary: #8E9297;
52
+ --accent-blue: #0088CC;
53
+ --accent-blue-light: #33AADD;
54
+ --border-color: #2D2F34;
55
+ --success-color: #28a745;
56
+ --error-color: #dc3545;
 
57
  --font-family: 'Inter', sans-serif;
58
+ --nav-height: 60px;
 
59
  }
60
 
61
  * {
 
77
  height: 100vh;
78
  width: 100vw;
79
  display: flex;
80
+ align-items: center;
81
+ justify-content: center;
82
+ position: relative; /* Needed for fixed elements positioning context */
83
  }
84
 
85
  .main-container {
 
90
  transition: opacity 0.3s ease;
91
  }
92
 
93
+ /* --- Login View --- */
94
  #login-view {
 
95
  display: flex;
96
  flex-direction: column;
97
  align-items: center;
98
  justify-content: center;
99
  text-align: center;
100
+ width: 100%;
101
+ height: 100%;
102
  background: radial-gradient(circle, #1a2a3a 0%, var(--bg-primary) 70%);
103
  }
104
  #login-view img {
105
  width: 100px;
106
  height: 100px;
107
  margin-bottom: 24px;
108
+ filter: drop-shadow(0 0 15px rgba(0, 136, 204, 0.5));
109
  }
110
  #login-view h1 {
111
+ font-size: 2.8rem;
112
  font-weight: 700;
113
  background: linear-gradient(45deg, var(--accent-blue-light), var(--accent-blue));
114
  -webkit-background-clip: text;
 
116
  margin-bottom: 12px;
117
  }
118
  #login-view p {
119
+ font-size: 1.1rem;
120
  color: var(--text-secondary);
121
  margin-bottom: 40px;
122
  }
123
 
124
+ /* --- App View Container --- */
125
  #app-view {
126
+ display: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  width: 100%;
128
+ height: 100%; /* Full height initially */
 
129
  flex-direction: column;
130
+ padding-bottom: var(--nav-height); /* Space for nav bar */
 
 
 
 
 
131
  }
132
 
133
+ /* --- Content Area (Chats + Browser) --- */
134
+ #app-content {
135
+ flex-grow: 1;
136
+ display: flex;
137
+ overflow: hidden; /* Prevent content overflow */
138
+ }
139
+
140
+ /* --- Chat List View --- */
141
  #chatroom-list-view {
142
+ display: flex;
143
+ flex-direction: column;
144
+ height: 100%;
145
+ width: 100%; /* Full width on mobile */
146
  background-color: var(--bg-secondary);
147
+ flex-shrink: 0;
148
  }
149
 
150
  .list-header {
 
201
  align-items: center;
202
  justify-content: center;
203
  gap: 8px;
204
+ text-decoration: none; /* For potential links */
205
  }
206
  .action-btn:hover {
207
  transform: translateY(-2px);
 
211
  padding: 8px 12px;
212
  font-size: 0.9rem;
213
  }
 
 
 
 
 
 
214
 
215
  #chatroom-list {
216
  flex-grow: 1;
 
237
  font-weight: 600;
238
  color: white;
239
  flex-shrink: 0;
240
+ background-color: gray; /* Default */
 
241
  }
242
  .chatroom-info {
243
  flex-grow: 1;
 
256
  flex-shrink: 0;
257
  }
258
 
259
+ /* --- Chat Window View --- */
260
  #chat-window-view {
261
+ display: none; /* Hidden by default on mobile */
262
+ flex-direction: column;
263
+ height: 100%;
264
+ width: 100%; /* Full width on mobile */
265
+ background-color: var(--bg-primary);
266
  }
267
 
268
  .chat-header {
 
278
  background: none;
279
  border: none;
280
  cursor: pointer;
281
+ display: block; /* Visible on mobile */
282
  padding: 0;
283
+ margin-right: 8px;
 
284
  }
285
  .back-btn svg {
286
  width: 24px;
 
304
  .message {
305
  display: flex;
306
  gap: 10px;
307
+ max-width: 80%;
308
  }
309
  .message .avatar {
310
+ width: 36px;
311
+ height: 36px;
312
  align-self: flex-end;
313
  }
314
  .message-content {
 
317
  gap: 4px;
318
  }
319
  .message-sender {
320
+ font-size: 0.8rem;
321
  font-weight: 500;
322
  color: var(--text-secondary);
323
  word-break: break-all;
 
328
  border-radius: 18px;
329
  line-height: 1.4;
330
  word-wrap: break-word;
 
331
  }
332
 
333
  .message.sent { align-self: flex-end; flex-direction: row-reverse; }
 
345
  }
346
 
347
  .chat-placeholder {
 
348
  display: flex;
349
  flex-direction: column;
350
  align-items: center;
351
  justify-content: center;
352
+ height: 100%;
353
  text-align: center;
354
  color: var(--text-secondary);
355
  padding: 20px;
356
  }
357
+ .chat-placeholder img { width: 80px; margin-bottom: 20px; opacity: 0.5; }
358
 
359
  .message-form {
360
  display: flex;
361
+ padding: 16px;
362
  gap: 12px;
363
  background-color: var(--bg-secondary);
364
  border-top: 1px solid var(--border-color);
 
366
  }
367
  #message-input {
368
  flex-grow: 1;
369
+ padding: 12px 16px;
370
  border: 1px solid var(--border-color);
371
  background-color: var(--bg-tertiary);
372
  color: var(--text-primary);
 
378
  #message-input:focus { border-color: var(--accent-blue); }
379
 
380
  .send-btn {
381
+ width: 44px;
382
+ height: 44px;
383
  border-radius: 50%;
384
  flex-shrink: 0;
385
  padding: 0;
386
  }
387
+ .send-btn svg { width: 20px; height: 20px; fill: white; }
388
 
389
+ /* --- Browser View --- */
390
  #browser-view {
391
+ display: none; /* Hidden by default */
392
+ flex-direction: column;
393
+ height: 100%;
394
+ width: 100%; /* Full width on mobile */
395
+ background-color: var(--bg-primary);
 
396
  }
397
+ .browser-bar {
 
398
  display: flex;
399
+ padding: 8px;
 
 
400
  background-color: var(--bg-secondary);
401
+ gap: 8px;
402
  flex-shrink: 0;
 
403
  }
404
+ .browser-bar input {
405
+ flex-grow: 1;
406
+ padding: 8px 12px;
407
+ border: 1px solid var(--border-color);
408
+ background-color: var(--bg-tertiary);
409
+ color: var(--text-primary);
410
+ border-radius: 6px;
411
+ outline: none;
412
+ font-size: 0.9rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
  }
414
+ .browser-bar button {
415
+ padding: 8px 16px;
416
+ background: var(--bg-tertiary);
417
+ border: 1px solid var(--border-color);
418
+ color: var(--text-primary);
419
+ border-radius: 6px;
 
 
 
 
 
 
 
420
  cursor: pointer;
421
  }
422
+ #browser-iframe {
423
+ flex-grow: 1;
424
+ border: none;
425
+ width: 100%;
426
+ height: 100%; /* Will be managed by flex-grow */
 
 
 
 
427
  }
428
 
429
+
430
+ /* --- Modals --- */
431
  .modal-overlay {
432
  position: fixed;
433
  top: 0;
 
439
  align-items: center;
440
  justify-content: center;
441
  z-index: 1000;
442
+ -webkit-backdrop-filter: blur(5px);
443
+ backdrop-filter: blur(5px);
 
444
  }
445
  .modal-content {
446
  background-color: var(--bg-secondary);
447
  padding: 24px;
448
  border-radius: 12px;
449
+ width: 90%;
450
  max-width: 400px;
451
  border: 1px solid var(--border-color);
452
+ box-shadow: 0 10px 30px rgba(0,0,0,0.5);
453
  }
454
+ .modal-content h3 { margin-bottom: 20px; font-weight: 600; font-size: 1.3rem; }
455
  .modal-content label { display: block; margin-bottom: 8px; font-size: 0.9rem; color: var(--text-secondary); }
456
+ .modal-content input[type="text"], .modal-content input[type="password"] {
457
  width: 100%;
458
  padding: 12px;
459
  margin-bottom: 16px;
 
470
  border: none;
471
  cursor: pointer;
472
  font-weight: 500;
473
+ transition: background-color 0.2s ease;
 
 
 
474
  }
475
+ .secondary-btn { background-color: var(--bg-hover); color: white; }
476
+ .secondary-btn:hover { background-color: #3a3c44; }
477
 
478
+ #profile-modal .modal-content { text-align: center; }
479
+ #profile-avatar-container .avatar {
480
+ width: 80px;
481
+ height: 80px;
482
+ font-size: 2rem;
483
+ margin: 0 auto 20px;
484
+ }
485
+ #profile-qr-code {
486
+ background: white;
487
+ padding: 10px;
488
+ margin: 20px auto;
489
+ width: fit-content;
490
+ border-radius: 8px;
491
+ }
492
+ #profile-qr-code canvas, #profile-qr-code img { display: block; } /* Fix QRCode.js margin */
493
+
494
+ #scanner-modal .modal-content { text-align: center; }
495
+ #qr-reader {
496
+ width: 100%;
497
+ border: 1px solid var(--border-color);
498
+ margin-top: 16px;
499
+ border-radius: 8px;
500
+ overflow: hidden;
501
+ }
502
+
503
+ /* --- Navigation Bar --- */
504
+ #nav-bar {
505
+ position: fixed;
506
+ bottom: 0;
507
+ left: 0;
508
+ right: 0;
509
+ height: var(--nav-height);
510
+ background-color: var(--bg-secondary);
511
+ border-top: 1px solid var(--border-color);
512
+ display: flex;
513
+ justify-content: space-around;
514
+ align-items: center;
515
+ z-index: 500; /* Below modals */
516
+ box-shadow: 0 -5px 15px rgba(0,0,0,0.2);
517
+ }
518
+
519
+ .nav-item {
520
+ display: flex;
521
+ flex-direction: column;
522
+ align-items: center;
523
+ justify-content: center;
524
+ padding: 8px;
525
+ color: var(--text-secondary);
526
+ font-size: 0.7rem;
527
+ cursor: pointer;
528
+ transition: color 0.2s ease;
529
+ width: calc(100% / 3); /* Simple distribution */
530
+ user-select: none;
531
+ }
532
+
533
+ .nav-item svg {
534
+ width: 24px;
535
+ height: 24px;
536
+ fill: var(--text-secondary);
537
+ margin-bottom: 4px;
538
+ transition: fill 0.2s ease;
539
+ }
540
+
541
+ .nav-item.active {
542
+ color: var(--accent-blue-light);
543
+ }
544
+ .nav-item.active svg {
545
+ fill: var(--accent-blue-light);
546
+ }
547
+ .nav-item:hover {
548
+ color: var(--text-primary);
549
+ }
550
+ .nav-item:hover svg {
551
+ fill: var(--text-primary);
552
+ }
553
+
554
+ #nav-scanner-btn {
555
+ width: 56px;
556
+ height: 56px;
557
+ border-radius: 50%;
558
+ background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
559
+ display: flex;
560
+ align-items: center;
561
+ justify-content: center;
562
+ margin-bottom: 10px; /* Visually lift it slightly */
563
+ box-shadow: 0 4px 15px rgba(0, 136, 204, 0.5);
564
+ flex-shrink: 0;
565
+ }
566
+ #nav-scanner-btn svg {
567
+ width: 28px;
568
+ height: 28px;
569
+ fill: white;
570
+ margin-bottom: 0;
571
+ }
572
+ #nav-scanner-btn:active {
573
+ transform: scale(0.95);
574
+ }
575
+
576
+ /* --- Status Bar --- */
577
  #status-bar {
578
  position: fixed;
579
+ bottom: calc(var(--nav-height) + 20px); /* Above nav bar */
580
  left: 50%;
581
  transform: translateX(-50%);
582
  background-color: var(--bg-tertiary);
 
586
  font-size: 0.9rem;
587
  opacity: 0;
588
  visibility: hidden;
589
+ transition: opacity 0.3s, visibility 0.3s;
590
  z-index: 2000;
591
  box-shadow: 0 5px 15px rgba(0,0,0,0.3);
592
  max-width: 90%;
593
+ word-break: break-word;
594
  }
595
  #status-bar.success { background-color: var(--success-color); }
596
  #status-bar.error { background-color: var(--error-color); }
597
  #status-bar.visible { opacity: 1; visibility: visible; }
598
 
599
+ /* --- Desktop Media Query --- */
600
  @media (min-width: 768px) {
601
  body {
602
+ padding: 0;
 
603
  }
604
  .main-container {
605
+ max-width: 1000px; /* Slightly smaller overall width */
606
+ max-height: 650px; /* Reduced height */
 
 
607
  border-radius: 12px;
608
  overflow: hidden;
609
+ box-shadow: 0 10px 40px rgba(0,0,0,0.3);
610
  border: 1px solid var(--border-color);
611
+ padding-bottom: 0; /* No padding needed on main container */
612
  }
613
  #app-view {
614
+ flex-direction: row;
615
+ height: 100%;
616
+ padding-bottom: 0; /* No padding needed on app-view */
 
 
617
  }
618
+ #app-content {
619
+ flex-direction: row; /* Content lays out horizontally */
620
+ height: 100%;
621
+ overflow: hidden;
 
 
622
  }
623
+
624
  #chatroom-list-view {
625
+ width: 320px;
626
+ flex-shrink: 0;
627
  border-right: 1px solid var(--border-color);
628
+ display: flex !important; /* Always visible on desktop */
629
+ height: 100%; /* Takes height of app-content */
630
+ }
631
+ #chat-window-view {
632
+ width: auto;
633
+ flex-grow: 1;
634
+ height: 100%; /* Takes height of app-content */
635
+ display: flex; /* Initially visible on desktop */
636
+ }
637
+ #chat-window-view.hidden-on-desktop {
638
+ display: none !important;
639
  }
 
640
  #browser-view {
 
641
  width: auto;
642
+ flex-grow: 1;
643
+ height: 100%; /* Takes height of app-content */
644
+ display: none; /* Hidden initially */
645
  }
646
 
647
+ .back-btn { display: none !important; } /* Hide back button on desktop */
648
+
649
+ #nav-bar {
650
+ position: static; /* Integrate into the layout */
651
+ height: auto;
652
+ border-top: none;
653
+ border-right: 1px solid var(--border-color); /* Border on the right */
654
+ flex-direction: column; /* Items stack vertically */
655
+ width: 80px; /* Narrow sidebar */
656
+ justify-content: flex-start;
657
+ padding-top: 20px;
658
+ box-shadow: none;
659
+ }
660
+ .nav-item {
661
+ width: 100%;
662
+ padding: 12px 0;
663
+ margin-bottom: 10px;
664
+ }
665
+ .nav-item svg { margin-bottom: 2px; }
666
+ #nav-scanner-btn {
667
+ width: 60px;
668
+ height: 60px;
669
+ margin-bottom: 20px;
670
+ margin-top: 10px;
671
  }
672
+ #nav-scanner-btn svg { width: 30px; height: 30px; }
673
 
674
+ /* Adjust layout when browser view is active on desktop */
675
+ #app-content.browser-active #chatroom-list-view {
676
+ display: none !important; /* Hide chat list */
 
677
  }
678
+ #app-content.browser-active #browser-view {
679
+ display: flex !important; /* Show browser */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
680
  }
681
+ #app-content.browser-active #chat-window-view {
682
+ display: none !important; /* Hide chat window */
683
  }
684
 
685
+ /* Adjust layout when chat view is active on desktop */
686
+ #app-content:not(.browser-active) #chatroom-list-view {
687
+ display: flex !important; /* Show chat list */
688
+ }
689
+ #app-content:not(.browser-active) #chat-window-view {
690
+ display: flex !important; /* Show chat window */
691
+ }
692
+ #app-content:not(.browser-active) #browser-view {
693
+ display: none !important; /* Hide browser */
694
+ }
695
  }
696
  </style>
697
  </head>
 
705
  </div>
706
 
707
  <div id="app-view" class="main-container">
708
+ <div id="nav-bar">
709
+ <div class="nav-item active" id="nav-chats">
710
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM5.67 14l-.67.67V6h14v8H6.33L5.67 14z"/></svg>
711
+ <span>Чаты</span>
712
+ </div>
713
+ <div class="nav-item" id="nav-scanner-btn" title="Сканировать QR">
714
+ <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="M3 11h8V3H3v8zm2-6h4v4H5V5zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm8-12v8h8V3h-8zm6 6h-4V5h4v4zm-2 10a2 2 0 100-4 2 2 0 000 4z"/></svg>
715
+ </div>
716
+ <div class="nav-item" id="nav-browser">
717
+ <svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24" fill="currentColor"><g><rect fill="none" height="24" width="24"/></g><g><g><path d="M12,2C6.48,2,2,6.48,2,12c0,5.52,4.48,10,10,10s10-4.48,10-10C22,6.48,17.52,2,12,2z M12,20c-4.42,0-8-3.58-8-8 c0-1.85,0.63-3.55,1.69-4.9L12,12l6.31-4.9C15.55,4.63,13.85,4,12,4c-4.42,0-8,3.58-8,8c0,4.42,3.58,8,8,8s8-3.58,8-8 c0-1.85-0.63-3.55-1.69-4.9L12,12l5.69-4.42C18.61,8.37,19,10.12,19,12C19,16.42,15.42,20,12,20z"/></g></g></svg>
718
+ <span>Браузер</span>
719
+ </div>
720
+ <div class="nav-item" id="nav-profile">
721
+ <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" fill="currentColor"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
722
+ <span>Профиль</span>
723
+ </div>
724
+ </div>
725
+
726
+ <div id="app-content">
727
+ <div id="chatroom-list-view">
728
  <div class="list-header">
729
  <div class="list-header-top">
730
  <h2>Чаты</h2>
731
+ <button id="create-room-show-modal" class="action-btn small">
732
+ <svg xmlns="http://www.w3.org/2000/svg" height="16" viewBox="0 0 24 24" width="16" fill="white"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
733
+ <span>Новый</span>
734
+ </button>
 
 
 
 
 
735
  </div>
736
  <div class="user-profile">
737
  <div id="user-wallet"></div>
738
  <div id="user-nickname"></div>
739
  <form class="username-form" id="username-form">
740
+ <input type="text" id="username-input" class="username-input" placeholder="Установить никнейм" autocomplete="off" maxlength="20">
741
+ <button type="submit" class="action-btn small">✓</button>
742
  </form>
743
  </div>
744
  </div>
745
  <div id="chatroom-list"></div>
746
  </div>
747
 
748
+ <div id="chat-window-view" class="hidden-on-desktop">
749
  <div id="chat-placeholder" class="chat-placeholder">
750
  <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
751
  <h2>Выберите чат</h2>
 
761
  </div>
762
  <div id="messages-container"></div>
763
  <form id="message-form" class="message-form">
764
+ <input type="text" id="message-input" placeholder="Сообщение..." autocomplete="off">
765
+ <button type="submit" class="action-btn send-btn" id="send-btn">
766
  <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>
767
  </button>
768
  </form>
769
  </div>
770
  </div>
771
 
772
+ <div id="browser-view">
773
+ <form class="browser-bar" id="browser-form">
774
+ <input type="text" id="browser-url-input" value="https://www.google.com" placeholder="Введите URL или поисковый запрос">
775
+ <button type="submit">Перейти</button>
776
+ </form>
777
+ <iframe id="browser-iframe" src="https://www.google.com"></iframe>
778
  </div>
779
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
780
  </div>
781
 
782
  <div id="create-room-modal" class="modal-overlay">
 
810
  </div>
811
 
812
  <div id="profile-modal" class="modal-overlay">
813
+ <div class="modal-content">
814
  <h3 id="profile-modal-title">Профиль пользователя</h3>
815
+ <div id="profile-avatar-container"></div>
816
+ <p id="profile-username"></p>
817
+ <p id="profile-address"></p>
818
+ <div id="profile-qr-code"></div>
819
  <p style="text-align: center; color: var(--text-secondary); font-size: 0.8rem; margin-top: -10px; margin-bottom: 20px;">Отсканируйте для открытия профиля</p>
820
  <div class="modal-actions" style="flex-direction: column; gap: 12px; align-items: stretch;">
821
  <button id="send-ton-btn" class="modal-btn action-btn">Отправить TON</button>
 
827
  <div id="scanner-modal" class="modal-overlay">
828
  <div class="modal-content">
829
  <h3>Сканировать QR-код</h3>
830
+ <div id="qr-reader"></div>
831
  <div class="modal-actions">
832
  <button id="scanner-close-btn" class="modal-btn secondary-btn">Отмена</button>
833
  </div>
 
849
  let chatroomsData = {};
850
  let html5QrCode = null;
851
  let profileQrCode = null;
852
+ let currentMainView = 'chatList'; // 'chatList', 'chatWindow', 'browser'
853
 
854
  const loginView = document.getElementById('login-view');
855
  const appView = document.getElementById('app-view');
856
+ const appContent = document.getElementById('app-content');
857
  const chatroomListView = document.getElementById('chatroom-list-view');
858
  const chatWindowView = document.getElementById('chat-window-view');
 
859
  const chatPlaceholder = document.getElementById('chat-placeholder');
860
  const activeChat = document.getElementById('active-chat');
861
+ const browserView = document.getElementById('browser-view');
862
+
863
+ const navChatsBtn = document.getElementById('nav-chats');
864
+ const navScannerBtn = document.getElementById('nav-scanner-btn');
865
+ const navBrowserBtn = document.getElementById('nav-browser');
866
+ const navProfileBtn = document.getElementById('nav-profile');
867
+ const navItems = [navChatsBtn, navBrowserBtn, navProfileBtn];
868
+
869
  const profileModal = document.getElementById('profile-modal');
870
  const scannerModal = document.getElementById('scanner-modal');
 
 
 
 
 
 
871
 
872
  const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292'];
873
 
874
  const getAvatar = (name) => {
875
+ const initial = (name ? name[0] : '?').toUpperCase();
876
  const charCode = initial.charCodeAt(0);
877
  const color = AVATAR_COLORS[charCode % AVATAR_COLORS.length];
878
  const avatar = document.createElement('div');
 
901
  throw new Error(errorData.error || 'Unknown error');
902
  }
903
  if (response.status === 204) return null;
904
+ return await response.json();
 
 
905
  } catch (error) {
906
  showStatus(`Ошибка: ${error.message}`, 'error');
907
  throw error;
 
913
  const updateUserInfo = () => {
914
  document.getElementById('user-wallet').textContent = `Кошелек: ${truncateAddress(currentUser.address)}`;
915
  const nicknameEl = document.getElementById('user-nickname');
916
+ const usernameInput = document.getElementById('username-input');
917
  nicknameEl.textContent = currentUser.username ? `Ник: ${currentUser.username}` : `Никнейм не установлен`;
918
  usernameInput.value = currentUser.username || '';
 
 
919
  };
920
 
921
  document.getElementById('username-form').addEventListener('submit', async (e) => {
922
  e.preventDefault();
923
+ const newUsername = document.getElementById('username-input').value.trim();
924
+ if (!newUsername || newUsername.length < 3) {
925
+ showStatus('Никнейм должен быть не короче 3 символов.', 'error');
926
  return;
927
  }
928
+ if (newUsername.length > 20) {
929
+ showStatus('Никнейм должен быть не длиннее 20 символов.', 'error');
930
  return;
931
  }
 
 
 
 
 
 
 
 
932
  try {
933
  await apiCall('/api/set_username', {
934
  method: 'POST',
 
938
  currentUser.username = newUsername;
939
  updateUserInfo();
940
  showStatus('Никнейм успешно обновлен!', 'success');
941
+ fetchChatrooms();
942
+ if (activeChatroomId) fetchMessages(activeChatroomId);
943
+ } catch (err) {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
944
  });
945
 
946
  const initializeUser = async (address) => {
 
953
  });
954
  currentUser.username = data.username;
955
  } catch (err) {
956
+ currentUser.username = null; // User might not exist yet
957
  }
958
  updateUserInfo();
959
  loginView.style.display = 'none';
960
  appView.style.display = 'flex';
961
+ showView('chatList'); // Default view after login
962
  fetchChatrooms();
 
963
  };
964
 
965
  const renderChatrooms = (rooms) => {
966
  const list = document.getElementById('chatroom-list');
967
  list.innerHTML = '';
968
  chatroomsData = {};
 
 
 
969
  rooms.forEach(room => {
970
  chatroomsData[room.id] = room;
971
  const item = document.createElement('div');
 
1002
 
1003
  const renderMessages = (messages) => {
1004
  const container = document.getElementById('messages-container');
1005
+ // Check if user is near the bottom before rendering
1006
+ const isScrolledToBottom = container.scrollHeight - container.clientHeight <= container.scrollTop + 1;
1007
  container.innerHTML = '';
1008
  messages.forEach(msg => {
1009
  const msgDiv = document.createElement('div');
 
1030
  contentDiv.appendChild(bubbleDiv);
1031
 
1032
  msgDiv.appendChild(contentDiv);
1033
+ msgDiv.insertBefore(avatar, contentDiv);
 
 
 
 
 
 
1034
  container.appendChild(msgDiv);
1035
  });
1036
 
1037
+ // Scroll to bottom only if they were already there
1038
+ if (isScrolledToBottom || messages.length === 0) {
1039
  container.scrollTop = container.scrollHeight;
1040
  }
1041
  };
1042
 
1043
  const fetchMessages = async (roomId) => {
1044
+ if (!roomId) return;
 
 
 
1045
  try {
1046
  const data = await apiCall(`/api/messages/${roomId}`);
1047
  renderMessages(data.messages);
1048
  } catch (err) {
1049
+ // Error handling already in apiCall
 
 
 
 
 
 
 
1050
  }
1051
  };
1052
+
1053
+ const showView = (viewName) => {
1054
+ // Hide all main views
1055
+ chatroomListView.style.display = 'none';
1056
+ chatWindowView.style.display = 'none';
1057
+ browserView.style.display = 'none';
1058
+
1059
+ // Remove active class from all nav items
1060
+ navItems.forEach(item => item.classList.remove('active'));
1061
+ appContent.classList.remove('browser-active'); // Remove browser layout class
1062
+
1063
+ // Show the requested view and set active nav item
1064
+ if (viewName === 'chatList') {
1065
+ chatroomListView.style.display = 'flex';
1066
+ navChatsBtn.classList.add('active');
1067
+ if (window.innerWidth >= 768) {
1068
+ chatWindowView.style.display = 'flex'; // Show window alongside list on desktop
1069
+ }
1070
+ } else if (viewName === 'chatWindow') {
1071
+ chatWindowView.style.display = 'flex';
1072
+ navChatsBtn.classList.add('active');
1073
+ if (window.innerWidth >= 768) {
1074
+ chatroomListView.style.display = 'flex'; // Show list alongside window on desktop
1075
+ }
1076
+ } else if (viewName === 'browser') {
1077
+ browserView.style.display = 'flex';
1078
+ navBrowserBtn.classList.add('active');
1079
+ if (window.innerWidth >= 768) {
1080
+ appContent.classList.add('browser-active'); // Apply browser layout class
1081
+ }
1082
+ }
1083
+
1084
+ currentMainView = viewName;
1085
+
1086
+ // Hide modals
1087
+ profileModal.style.display = 'none';
1088
+ scannerModal.style.display = 'none';
1089
+ document.getElementById('create-room-modal').style.display = 'none';
1090
+ document.getElementById('password-modal').style.display = 'none';
1091
+ };
1092
+
1093
  const selectChatroom = (roomId, isPrivate) => {
1094
  const roomData = chatroomsData[roomId];
1095
+ if (!roomData) return;
 
 
 
 
1096
 
1097
  const proceedToRoom = () => {
 
1098
  if (messagePollingInterval) clearInterval(messagePollingInterval);
1099
  activeChatroomId = roomId;
1100
 
 
1106
  chatPlaceholder.style.display = 'none';
1107
  activeChat.style.display = 'flex';
1108
 
1109
+ showView('chatWindow'); // Switch to chat window view
1110
+ fetchMessages(roomId);
1111
+ messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000);
 
1112
  };
1113
 
1114
  if (isPrivate) {
 
1121
 
1122
  const formSubmitHandler = async (e) => {
1123
  e.preventDefault();
1124
+ passwordForm.removeEventListener('submit', formSubmitHandler);
1125
  const password = passwordInput.value;
1126
+ passwordModal.style.display = 'none';
 
 
1127
  try {
1128
  await apiCall('/api/join_chatroom', {
1129
  method: 'POST',
 
1132
  });
1133
  proceedToRoom();
1134
  } catch (err) {
1135
+ // Error is shown in apiCall
1136
+ // Re-open password modal or handle error? For now, just error status.
1137
  }
1138
  };
1139
  passwordForm.addEventListener('submit', formSubmitHandler);
 
1147
  }
1148
  };
1149
 
 
 
 
 
 
 
 
 
 
 
 
 
1150
  document.getElementById('message-form').addEventListener('submit', async (e) => {
1151
  e.preventDefault();
1152
+ const input = document.getElementById('message-input');
1153
+ const sendBtn = document.getElementById('send-btn');
1154
+ const text = input.value.trim();
1155
  if (text && activeChatroomId && currentUser.address) {
1156
+ if (messagePollingInterval) clearInterval(messagePollingInterval); // Pause polling while sending
1157
+ input.value = '';
1158
+ input.disabled = true;
1159
+ sendBtn.disabled = true;
1160
  try {
1161
  await apiCall('/api/send_message', {
1162
  method: 'POST',
 
1167
  text: text
1168
  })
1169
  });
1170
+ await fetchMessages(activeChatroomId); // Fetch updated messages immediately
 
1171
  } finally {
1172
+ input.disabled = false;
1173
+ sendBtn.disabled = false;
1174
+ input.focus();
1175
+ messagePollingInterval = setInterval(() => fetchMessages(activeChatroomId), 3000); // Resume polling
1176
  }
1177
  }
1178
  });
 
1181
  document.getElementById('create-room-show-modal').addEventListener('click', () => {
1182
  createRoomModal.style.display = 'flex';
1183
  document.getElementById('create-room-form').reset();
1184
+ document.getElementById('room-name').focus();
1185
  });
1186
  document.getElementById('create-room-cancel').addEventListener('click', () => {
1187
  createRoomModal.style.display = 'none';
1188
  });
1189
  document.getElementById('create-room-form').addEventListener('submit', async (e) => {
1190
  e.preventDefault();
1191
+ const name = document.getElementById('room-name').value.trim();
1192
+ const password = document.getElementById('room-password').value;
 
 
 
 
 
1193
  if (!name) {
1194
+ showStatus('Название чата обязательно.', 'error');
1195
+ return;
1196
  }
 
 
 
 
 
 
 
 
 
 
 
 
1197
 
1198
  try {
1199
  await apiCall('/api/create_chatroom', {
 
1203
  });
1204
  createRoomModal.style.display = 'none';
1205
  showStatus('Чат успешно создан!', 'success');
1206
+ fetchChatrooms();
1207
+ } catch (err) {}
 
 
 
 
 
 
1208
  });
1209
 
1210
  const showProfile = async (address) => {
1211
+ if (!address) return;
1212
  try {
1213
  const userData = await apiCall('/api/user_data', {
1214
  method: 'POST',
 
1216
  body: JSON.stringify({ address: address })
1217
  });
1218
 
1219
+ const username = userData.username || `Пользователь ${truncateAddress(address)}`;
1220
  const avatarContainer = document.getElementById('profile-avatar-container');
1221
  const usernameEl = document.getElementById('profile-username');
1222
  const addressEl = document.getElementById('profile-address');
1223
  const qrCodeEl = document.getElementById('profile-qr-code');
1224
  const sendTonBtn = document.getElementById('send-ton-btn');
1225
+
1226
+ document.getElementById('profile-modal-title').textContent = (address === currentUser.address) ? 'Мой профиль' : 'Профиль пользователя';
1227
 
1228
  avatarContainer.innerHTML = '';
1229
  avatarContainer.appendChild(getAvatar(username));
 
1233
 
1234
  qrCodeEl.innerHTML = '';
1235
  if (profileQrCode) {
1236
+ profileQrCode.clear();
 
 
1237
  }
1238
  profileQrCode = new QRCode(qrCodeEl, {
1239
  text: address,
 
1249
  showStatus('Подключите кошелек для отправки TON.', 'error');
1250
  return;
1251
  }
1252
+ const amountString = prompt(`Введите сумму в TON для отправки на адрес ${truncateAddress(address)}:`, "0.1");
1253
+ if (amountString === null) return;
1254
+
 
 
 
 
 
1255
  const amount = parseFloat(amountString);
1256
  if (isNaN(amount) || amount <= 0) {
1257
  showStatus('Неверная сумма.', 'error');
1258
  return;
1259
  }
1260
 
1261
+ // Convert TON to nanoTON
1262
+ const amountInNanoTon = (amount * 1_000_000_000).toFixed(0); // Use toFixed(0) to avoid scientific notation for large numbers
1263
 
1264
  const transaction = {
1265
  validUntil: Math.floor(Date.now() / 1000) + 600, // 10 minutes validity
 
1267
  };
1268
 
1269
  try {
1270
+ const result = await tonConnectUI.sendTransaction(transaction);
1271
+ showStatus(`Транзакция успешно отправлена: ${result.boc}`, 'success');
1272
+ // Optional: Close modal after success
1273
  profileModal.style.display = 'none';
1274
  } catch (error) {
1275
+ if (error.message.includes('User rejected the action')) {
1276
+ showStatus('Транзакция отменена пользователем.', 'info');
 
1277
  } else {
1278
+ showStatus('Ошибка отправки транзакции.', 'error');
1279
+ console.error("Send transaction failed:", error);
1280
  }
1281
  }
1282
  };
1283
 
1284
+ sendTonBtn.style.display = (address === currentUser.address || !tonConnectUI.connected) ? 'none' : 'flex'; // Hide send button for own profile or if not connected
1285
  profileModal.style.display = 'flex';
1286
  } catch (err) {
1287
+ // Error status already shown by apiCall
1288
+ showStatus('Не удалось загрузить профиль.', 'error');
1289
  }
1290
  };
1291
 
1292
  const showScanner = () => {
1293
  scannerModal.style.display = 'flex';
1294
+ // Ensure qr-reader div is empty before starting
1295
+ document.getElementById('qr-reader').innerHTML = '';
1296
+
1297
+ html5QrCode = new Html5Qrcode("qr-reader");
 
1298
  const qrCodeSuccessCallback = (decodedText, decodedResult) => {
1299
+ hideScanner();
1300
+ // Basic check for potential TON address format (EQ/UQ + length)
 
1301
  if (decodedText && decodedText.length > 40 && (decodedText.startsWith('EQ') || decodedText.startsWith('UQ'))) {
1302
+ showProfile(decodedText); // Show profile of the scanned address
1303
  } else {
1304
  showStatus('Отсканирован недействительный QR-код (не похож на TON адрес).', 'error');
1305
  }
1306
  };
1307
+ const config = { fps: 10, qrbox: { width: 250, height: 250 } };
1308
+ html5QrCode.start({ facingMode: "environment" }, config, qrCodeSuccessCallback)
 
 
 
 
 
 
 
1309
  .catch(err => {
1310
+ showStatus('Не удалось запустить сканер.', 'error');
1311
+ console.error("QR Scanner start failed:", err);
1312
+ hideScanner();
1313
  });
1314
  };
1315
 
1316
  const hideScanner = () => {
1317
  if (html5QrCode && html5QrCode.isScanning) {
1318
+ html5QrCode.stop().catch(err => console.error("Failed to stop QR scanner.", err));
 
 
 
 
1319
  }
1320
  scannerModal.style.display = 'none';
1321
  };
1322
 
1323
+ // --- Event Listeners ---
1324
+
1325
+ // Navigation Buttons
1326
+ navChatsBtn.addEventListener('click', () => showView('chatList'));
1327
+ navBrowserBtn.addEventListener('click', () => showView('browser'));
1328
+ navProfileBtn.addEventListener('click', () => {
1329
+ if (currentUser.address) showProfile(currentUser.address);
1330
+ // No showView call here, profile is a modal over the current view
1331
  });
1332
+ navScannerBtn.addEventListener('click', showScanner); // Scanner is also a modal
1333
+
1334
+ // Back button (mobile chat view)
1335
+ document.getElementById('back-to-list-btn').addEventListener('click', () => {
1336
+ if (messagePollingInterval) clearInterval(messagePollingInterval);
1337
+ activeChatroomId = null;
1338
+ showView('chatList');
1339
+ chatPlaceholder.style.display = 'flex'; // Show placeholder again
1340
+ activeChat.style.display = 'none';
1341
+ });
1342
+
1343
+ // Modal Close Buttons
1344
  document.getElementById('profile-close-btn').addEventListener('click', () => profileModal.style.display = 'none');
1345
  document.getElementById('scanner-close-btn').addEventListener('click', hideScanner);
1346
 
1347
+ // Browser URL form
1348
+ document.getElementById('browser-form').addEventListener('submit', (e) => {
1349
+ e.preventDefault();
1350
+ const urlInput = document.getElementById('browser-url-input');
1351
+ let url = urlInput.value.trim();
1352
+
1353
+ if (url) {
1354
+ // Simple check: if it doesn't look like a URL (doesn't contain .), assume it's a search term
1355
+ if (!url.includes('.') && !url.startsWith('http://') && !url.startsWith('https://')) {
1356
+ url = 'https://www.google.com/search?q=' + encodeURIComponent(url);
1357
+ } else if (!url.startsWith('http://') && !url.startsWith('https://')) {
1358
+ // If it looks like a domain but no protocol, add https
1359
+ url = 'https://' + url;
1360
+ }
1361
+ document.getElementById('browser-iframe').src = url;
1362
+ urlInput.value = url; // Update input with the corrected URL
1363
+ }
1364
+ });
1365
+
1366
+
1367
+ // Handle resize events for responsive layout
1368
+ const handleResize = () => {
1369
  const isMobile = window.innerWidth < 768;
1370
+
1371
+ // Update display of back button based on mobile/desktop
1372
+ document.getElementById('back-to-list-btn').style.display = isMobile ? 'block' : 'none';
1373
+
1374
+ // Re-apply the current main view based on new screen size
1375
+ // This ensures correct visibility of list/window side-by-side or stacked
1376
+ showView(currentMainView);
1377
+
1378
+ // Ensure modals are centered correctly on resize
1379
+ // This is often handled by flexbox centering, but could add specific logic if needed.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1380
  };
1381
 
1382
  window.addEventListener('resize', handleResize);
1383
 
1384
+ // Initial check for screen size layout
1385
+ handleResize();
1386
+
1387
+ // --- TON Connect Integration ---
1388
  tonConnectUI.onStatusChange(wallet => {
1389
  if (wallet) {
1390
+ // Wallet connected
1391
+ const address = TON_CONNECT_UI.toUserFriendlyAddress(wallet.account.address, false); // Get base64 address
1392
  initializeUser(address);
1393
  } else {
1394
+ // Wallet disconnected
1395
  currentUser = { address: null, username: null };
1396
  appView.style.display = 'none';
1397
  loginView.style.display = 'flex';
1398
  if (messagePollingInterval) clearInterval(messagePollingInterval);
1399
  messagePollingInterval = null;
1400
  activeChatroomId = null;
1401
+ // Reset views to default mobile state on logout
1402
+ chatroomListView.style.display = 'flex';
1403
+ chatWindowView.style.display = 'none';
1404
+ browserView.style.display = 'none';
1405
+ navItems.forEach(item => item.classList.remove('active'));
1406
+ navChatsBtn.classList.add('active');
1407
+ currentMainView = 'chatList';
1408
+ appContent.classList.remove('browser-active');
1409
+ document.getElementById('chat-placeholder').style.display = 'flex';
1410
+ document.getElementById('active-chat').style.display = 'none';
1411
  }
 
 
1412
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1413
  });
1414
  </script>
1415
  </body>
 
1425
  if not address:
1426
  return jsonify({'error': 'Address is required'}), 400
1427
  db = read_db()
1428
+ user_info = db['users'].get(address, {}) # Return empty dict if user not found
1429
+ username = user_info.get('username')
1430
  return jsonify({'username': username})
1431
 
1432
  @app.route('/api/set_username', methods=['POST'])
 
1434
  data = request.get_json()
1435
  address = data.get('address')
1436
  username = data.get('username')
1437
+ if not address or not username:
1438
  return jsonify({'error': 'Address and username are required'}), 400
1439
+ if len(username) < 3 or len(username) > 20:
1440
  return jsonify({'error': 'Username must be between 3 and 20 characters'}), 400
1441
 
1442
  db = read_db()
1443
  if address not in db['users']:
1444
  db['users'][address] = {}
1445
+ db['users'][address]['username'] = username
 
 
 
 
 
 
1446
  write_db(db)
1447
  return jsonify({'success': True})
1448
 
 
1451
  db = read_db()
1452
  chatrooms_list = []
1453
  for room_id, room_data in db['chatrooms'].items():
 
 
1454
  chatrooms_list.append({
1455
  'id': room_id,
1456
  'name': room_data['name'],
1457
  'is_private': room_data['is_private']
1458
  })
1459
+ return jsonify({'chatrooms': sorted(chatrooms_list, key=lambda x: x['name'].lower())}) # Sort by name case-insensitively
1460
 
1461
  @app.route('/api/create_chatroom', methods=['POST'])
1462
  def create_chatroom():
 
1466
  creator_address = data.get('creator_address')
1467
  if not name or not creator_address:
1468
  return jsonify({'error': 'Name and creator address are required'}), 400
1469
+ if len(name) < 1 or len(name) > 50:
1470
+ return jsonify({'error': 'Chatroom name must be between 1 and 50 characters'}), 400
1471
+ if password and len(password) < 4:
1472
+ return jsonify({'error': 'Password must be at least 4 characters long'}), 400
1473
 
1474
  db = read_db()
1475
+ # Optional: Check for duplicate names (though UUID makes ID unique)
1476
+ # for room_data in db['chatrooms'].values():
1477
+ # if room_data['name'] == name:
1478
+ # return jsonify({'error': 'Chatroom with this name already exists'}), 400
1479
+
1480
  room_id = str(uuid.uuid4())
1481
  db['chatrooms'][room_id] = {
1482
  'name': name,
 
1484
  'is_private': bool(password),
1485
  'password_hash': generate_password_hash(password) if password else None
1486
  }
1487
+ db['messages'][room_id] = []
1488
  write_db(db)
1489
  return jsonify({'success': True, 'chatroom_id': room_id})
1490
 
 
1493
  data = request.get_json()
1494
  chatroom_id = data.get('chatroom_id')
1495
  password = data.get('password')
 
 
 
1496
  db = read_db()
1497
  chatroom = db['chatrooms'].get(chatroom_id)
1498
  if not chatroom:
1499
  return jsonify({'error': 'Chatroom not found'}), 404
1500
+ if chatroom['is_private']:
1501
+ if not password or not check_password_hash(chatroom['password_hash'], password):
 
1502
  return jsonify({'error': 'Invalid password'}), 403
 
 
 
1503
  return jsonify({'success': True})
1504
 
1505
  @app.route('/api/messages/<chatroom_id>', methods=['GET'])
1506
  def get_messages(chatroom_id):
1507
  db = read_db()
1508
  if chatroom_id not in db['messages']:
1509
+ # Return empty list if chatroom exists but has no messages,
1510
+ # or 404 if chatroom ID is invalid. Let's assume 404 is safer
1511
+ # if the chatroom itself isn't in the chatrooms list.
1512
+ if chatroom_id not in db['chatrooms']:
1513
+ return jsonify({'error': 'Chatroom not found'}), 404
1514
+ # If chatroom exists but messages list is missing (shouldn't happen with init_db)
1515
+ # db['messages'][chatroom_id] = [] # Could auto-create
1516
+ # return jsonify({'messages': []})
1517
+ # Or simply return empty list if key exists but value is empty/null
1518
+ pass # Proceed to fetch messages
1519
+
1520
  messages_with_names = []
1521
+ room_messages = db['messages'].get(chatroom_id, [])
1522
+
1523
+ # Cache usernames for this request to avoid repeated lookups
1524
+ user_cache = {}
 
1525
 
1526
  for msg in room_messages:
1527
  sender_address = msg['sender_address']
1528
+
1529
+ if sender_address not in user_cache:
1530
+ user_info = db['users'].get(sender_address)
1531
+ display_name = (user_info.get('username') if user_info and user_info.get('username')
1532
+ else f"{sender_address[:4]}...{sender_address[-4:]}")
1533
+ user_cache[sender_address] = display_name
1534
 
1535
  msg_copy = msg.copy()
1536
+ msg_copy['display_name'] = user_cache[sender_address]
1537
  messages_with_names.append(msg_copy)
1538
 
1539
  return jsonify({'messages': messages_with_names})
 
1548
  if not all([chatroom_id, sender_address, text]):
1549
  return jsonify({'error': 'Missing data'}), 400
1550
 
1551
+ text = text.strip()
1552
+ if not text: # Prevent sending empty messages
1553
  return jsonify({'error': 'Message cannot be empty'}), 400
1554
+
1555
+ if len(text) > 1000: # Limit message length
1556
+ return jsonify({'error': 'Message is too long'}), 400
1557
 
 
 
 
 
1558
 
1559
+ db = read_db()
1560
  if chatroom_id not in db['messages']:
1561
+ if chatroom_id not in db['chatrooms']:
1562
+ return jsonify({'error': 'Chatroom not found'}), 404
1563
+ db['messages'][chatroom_id] = [] # Auto-create messages list if missing
1564
+
1565
 
1566
  message = {
1567
  'id': str(uuid.uuid4()),
1568
  'sender_address': sender_address,
1569
+ 'text': text,
1570
+ 'timestamp': datetime.utcnow().isoformat() + "Z"
1571
  }
1572
 
1573
+ # Keep only the last 100 messages per chatroom
1574
+ if len(db['messages'][chatroom_id]) >= 100:
 
 
1575
  db['messages'][chatroom_id].pop(0)
1576
 
1577
  db['messages'][chatroom_id].append(message)
 
1581
 
1582
  if __name__ == '__main__':
1583
  init_db()
1584
+ app.run(host='0.0.0.0', port=7860, debug=True) # Enabled debug for development