Aleksmorshen commited on
Commit
76fae43
·
verified ·
1 Parent(s): 3ec3bd6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +764 -303
app.py CHANGED
@@ -19,6 +19,8 @@ def init_db():
19
  }, f, indent=4)
20
 
21
  def read_db():
 
 
22
  with open(DB_FILE, 'r') as f:
23
  return json.load(f)
24
 
@@ -43,15 +45,15 @@ def index():
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;
@@ -64,7 +66,9 @@ def index():
64
  -webkit-tap-highlight-color: transparent;
65
  }
66
 
67
- html { font-size: 16px; }
 
 
68
 
69
  body {
70
  font-family: var(--font-family);
@@ -84,6 +88,7 @@ def index():
84
  display: flex;
85
  flex-direction: column;
86
  transition: opacity 0.3s ease;
 
87
  }
88
 
89
  #login-view {
@@ -97,13 +102,13 @@ def index():
97
  background: radial-gradient(circle, #1a2a3a 0%, var(--bg-primary) 70%);
98
  }
99
  #login-view img {
100
- width: 100px;
101
- height: 100px;
102
  margin-bottom: 24px;
103
- filter: drop-shadow(0 0 15px rgba(0, 136, 204, 0.5));
104
  }
105
  #login-view h1 {
106
- font-size: 2.8rem;
107
  font-weight: 700;
108
  background: linear-gradient(45deg, var(--accent-blue-light), var(--accent-blue));
109
  -webkit-background-clip: text;
@@ -111,7 +116,7 @@ def index():
111
  margin-bottom: 12px;
112
  }
113
  #login-view p {
114
- font-size: 1.1rem;
115
  color: var(--text-secondary);
116
  margin-bottom: 40px;
117
  }
@@ -120,39 +125,50 @@ def index():
120
  display: none;
121
  width: 100%;
122
  height: 100%;
123
- flex-direction: column;
124
  }
125
 
126
- .view-content {
127
- height: calc(100% - 60px);
128
- display: flex;
129
- flex-direction: row;
130
  }
131
- .view-panel {
132
- display: none;
133
- height: 100%;
134
  width: 100%;
 
 
 
 
 
 
 
135
  }
136
- .view-panel.active {
137
  display: flex;
138
  }
139
-
140
- #chat-view {
141
- flex-direction: row;
142
- }
143
 
144
- #chatroom-list-view {
145
- display: flex;
146
  flex-direction: column;
147
- height: 100%;
 
148
  width: 100%;
149
- background-color: var(--bg-secondary);
150
  }
151
-
 
 
 
 
 
 
 
152
  .list-header {
153
  padding: 16px;
154
  border-bottom: 1px solid var(--border-color);
155
  flex-shrink: 0;
 
156
  }
157
  .list-header-top {
158
  display: flex;
@@ -160,7 +176,10 @@ def index():
160
  align-items: center;
161
  margin-bottom: 16px;
162
  }
163
- .list-header-top h2 { font-size: 1.5rem; font-weight: 600; }
 
 
 
164
 
165
  .user-profile {
166
  padding: 12px;
@@ -205,13 +224,24 @@ def index():
205
  transform: translateY(-2px);
206
  box-shadow: 0 4px 15px rgba(0, 136, 204, 0.3);
207
  }
 
 
 
 
 
 
 
 
208
  .action-btn.small {
209
  padding: 8px 12px;
210
  font-size: 0.9rem;
211
  }
212
 
213
- #chatroom-list { flex-grow: 1; overflow-y: auto; }
214
- .chatroom-item {
 
 
 
215
  display: flex;
216
  align-items: center;
217
  gap: 12px;
@@ -220,7 +250,7 @@ def index():
220
  border-bottom: 1px solid var(--border-color);
221
  transition: background-color 0.2s ease;
222
  }
223
- .chatroom-item:hover { background-color: var(--bg-hover); }
224
 
225
  .avatar {
226
  width: 40px;
@@ -232,9 +262,14 @@ def index():
232
  font-weight: 600;
233
  color: white;
234
  flex-shrink: 0;
 
 
235
  }
236
- .chatroom-info { flex-grow: 1; overflow: hidden; }
237
- .chatroom-name {
 
 
 
238
  font-weight: 500;
239
  white-space: nowrap;
240
  overflow: hidden;
@@ -247,14 +282,11 @@ def index():
247
  flex-shrink: 0;
248
  }
249
 
 
250
  #chat-window-view {
251
- display: none;
252
- flex-direction: column;
253
- height: 100%;
254
- width: 100%;
255
- background-color: var(--bg-primary);
256
  }
257
-
258
  .chat-header {
259
  display: flex;
260
  align-items: center;
@@ -264,9 +296,25 @@ def index():
264
  border-bottom: 1px solid var(--border-color);
265
  flex-shrink: 0;
266
  }
267
- .back-btn { background: none; border: none; cursor: pointer; display: none; }
268
- .back-btn svg { width: 24px; height: 24px; fill: var(--text-primary); }
269
- #chat-header-title { font-size: 1.2rem; font-weight: 600; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
 
271
  #messages-container {
272
  flex-grow: 1;
@@ -275,11 +323,25 @@ def index():
275
  display: flex;
276
  flex-direction: column;
277
  gap: 12px;
 
278
  }
279
 
280
- .message { display: flex; gap: 10px; max-width: 80%; }
281
- .message .avatar { width: 36px; height: 36px; align-self: flex-end; }
282
- .message-content { display: flex; flex-direction: column; gap: 4px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  .message-sender {
284
  font-size: 0.8rem;
285
  font-weight: 500;
@@ -322,7 +384,7 @@ def index():
322
 
323
  .message-form {
324
  display: flex;
325
- padding: 16px;
326
  gap: 12px;
327
  background-color: var(--bg-secondary);
328
  border-top: 1px solid var(--border-color);
@@ -341,93 +403,131 @@ def index():
341
  }
342
  #message-input:focus { border-color: var(--accent-blue); }
343
 
344
- .send-btn { width: 44px; height: 44px; border-radius: 50%; flex-shrink: 0; padding: 0; }
345
- .send-btn svg { width: 20px; height: 20px; fill: white; }
346
-
347
- #browser-view { flex-direction: column; background-color: var(--bg-primary); }
348
- .browser-header {
349
- display: flex;
350
- gap: 8px;
351
- padding: 8px 12px;
352
- background-color: var(--bg-secondary);
353
- border-bottom: 1px solid var(--border-color);
354
- align-items: center;
355
- flex-shrink: 0;
356
- }
357
- .browser-header .nav-icon-btn {
358
- background: var(--bg-tertiary);
359
  border-radius: 50%;
360
- width: 36px;
361
- height: 36px;
362
- display: flex;
363
- align-items: center;
364
- justify-content: center;
365
- border: none;
366
- cursor: pointer;
367
  }
368
- .browser-header .nav-icon-btn svg { fill: var(--text-primary); width: 20px; height: 20px; }
369
- #url-input {
370
- flex-grow: 1;
371
- padding: 8px 12px;
372
- border: 1px solid var(--border-color);
373
- background-color: var(--bg-primary);
374
- color: var(--text-primary);
375
- border-radius: 18px;
376
- outline: none;
377
- font-size: 0.9rem;
378
  }
379
- #browser-iframe {
380
- flex-grow: 1;
381
- border: none;
382
- background-color: #fff;
383
  }
384
 
385
- #bottom-nav {
386
- position: absolute;
387
- bottom: 0;
388
- left: 0;
389
- right: 0;
390
- height: 60px;
391
- background-color: var(--bg-secondary);
392
- border-top: 1px solid var(--border-color);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  display: flex;
394
  justify-content: space-around;
395
  align-items: center;
 
 
 
 
 
 
396
  }
397
- .nav-btn {
 
398
  display: flex;
399
  flex-direction: column;
400
  align-items: center;
401
  justify-content: center;
402
- gap: 4px;
403
  background: none;
404
  border: none;
405
  color: var(--text-secondary);
406
- cursor: pointer;
407
- transition: color 0.2s;
408
  font-size: 0.7rem;
409
- flex-grow: 1;
410
- height: 100%;
 
 
 
 
 
411
  }
412
- .nav-btn svg {
413
  width: 24px;
414
  height: 24px;
415
- fill: currentColor;
 
 
416
  }
417
- .nav-btn.active { color: var(--accent-blue); }
418
- #nav-scan-btn {
419
- transform: translateY(-15px);
420
- background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
421
- border-radius: 50%;
422
- width: 56px;
423
- height: 56px;
424
- border: 4px solid var(--bg-secondary);
425
- color: white;
426
- box-shadow: 0 -4px 15px rgba(0, 136, 204, 0.2);
427
- flex-grow: 0;
428
  }
429
- #nav-scan-btn svg { width: 28px; height: 28px; }
430
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  .modal-overlay {
432
  position: fixed;
433
  top: 0;
@@ -464,12 +564,43 @@ def index():
464
  font-size: 1rem;
465
  }
466
  .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
467
- .modal-btn { padding: 10px 20px; border-radius: 6px; border: none; cursor: pointer; font-weight: 500; }
 
 
 
 
 
 
 
468
  .secondary-btn { background-color: var(--bg-hover); color: white; }
 
469
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
  #status-bar {
471
  position: fixed;
472
- bottom: 75px;
473
  left: 50%;
474
  transform: translateX(-50%);
475
  background-color: var(--bg-tertiary);
@@ -479,7 +610,7 @@ def index():
479
  font-size: 0.9rem;
480
  opacity: 0;
481
  visibility: hidden;
482
- transition: opacity 0.3s, visibility 0.3s, bottom 0.3s;
483
  z-index: 2000;
484
  box-shadow: 0 5px 15px rgba(0,0,0,0.3);
485
  }
@@ -487,31 +618,76 @@ def index():
487
  #status-bar.error { background-color: var(--error-color); }
488
  #status-bar.visible { opacity: 1; visibility: visible; }
489
 
 
490
  @media (min-width: 768px) {
 
 
 
491
  .main-container {
492
- max-width: 1100px;
493
- max-height: 760px;
494
  border-radius: 12px;
495
  overflow: hidden;
496
  box-shadow: 0 10px 40px rgba(0,0,0,0.3);
497
  border: 1px solid var(--border-color);
498
  }
499
- .view-content {
500
- height: 100%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
  }
502
  #chatroom-list-view {
503
- width: 320px;
504
- flex-shrink: 0;
505
- border-right: 1px solid var(--border-color);
506
- display: flex !important;
507
  }
508
- #chat-window-view { width: auto; flex-grow: 1; display: flex !important; }
509
- #chat-window-view.hidden-on-desktop { display: flex !important; }
510
- .back-btn { display: none !important; }
511
- #bottom-nav { display: none; }
512
- #status-bar { bottom: 20px; }
513
- #chat-view.active { display: flex; }
514
- #browser-view.active { display: flex; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
515
  }
516
  </style>
517
  </head>
@@ -525,21 +701,17 @@ def index():
525
  </div>
526
 
527
  <div id="app-view" class="main-container">
528
- <div class="view-content">
529
- <div id="chat-view" class="view-panel active">
 
530
  <div id="chatroom-list-view">
531
  <div class="list-header">
532
  <div class="list-header-top">
533
  <h2>Чаты</h2>
534
- <div style="display: flex; align-items: center; gap: 8px;">
535
- <button id="my-profile-btn" class="action-btn small" title="Мой профиль">
536
- <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>
537
- </button>
538
- <button id="create-room-show-modal" class="action-btn small">
539
- <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>
540
- <span>Новый</span>
541
- </button>
542
- </div>
543
  </div>
544
  <div class="user-profile">
545
  <div id="user-wallet"></div>
@@ -553,8 +725,8 @@ def index():
553
  <div id="chatroom-list"></div>
554
  </div>
555
 
556
- <div id="chat-window-view" class="hidden-on-desktop">
557
- <div id="chat-placeholder" class="chat-placeholder">
558
  <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
559
  <h2>Выберите чат</h2>
560
  <p>Начните общение в одном из существующих чатов или создайте свой собственный.</p>
@@ -577,40 +749,63 @@ def index():
577
  </div>
578
  </div>
579
  </div>
580
- <div id="browser-view" class="view-panel">
581
- <div class="browser-header">
582
- <button id="browser-back" class="nav-icon-btn"><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></button>
583
- <button id="browser-forward" class="nav-icon-btn"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8z"/></svg></button>
584
- <button id="browser-reload" class="nav-icon-btn"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg></button>
585
- <form id="url-form" style="flex-grow: 1; display: flex;"><input type="text" id="url-input" placeholder="Поиск или адрес" value="https://www.google.com"></form>
 
 
 
 
 
 
 
 
 
 
 
 
 
586
  </div>
587
- <iframe id="browser-iframe" src="https://www.google.com/webhp?igu=1"></iframe>
588
  </div>
589
  </div>
590
 
591
- <nav id="bottom-nav">
592
- <button id="nav-chat-btn" class="nav-btn active">
593
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg>
594
- <span>Чаты</span>
 
 
 
 
 
595
  </button>
596
- <button id="nav-scan-btn" class="nav-btn">
597
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M3 11h8V3H3v8zm2-6h4v4H5V5zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm8-12v8h8V3h-8zm6 6h-4V5h4v4zm-2 10a2 2 0 100-4 2 2 0 000 4z"/></svg>
 
598
  </button>
599
- <button id="nav-browser-btn" class="nav-btn">
600
- <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.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v-.07zM12 10c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm7.93-1L15 15v-1c0-1.1-.9-2-2-2V9c0-1.1.9-2 2-2h3.03c.52 1.25.8 2.58.8 4 0 3.95-3.05 7.23-7 7.93v-2.02c1.9-.42 3.48-1.73 4.19-3.45l-2.19-.9v.01z"/></svg>
601
- <span>Браузер</span>
602
  </button>
603
- </nav>
 
 
 
 
604
  </div>
605
 
 
606
  <div id="create-room-modal" class="modal-overlay">
607
  <div class="modal-content">
608
  <h3>Создать новый чат</h3>
609
  <form id="create-room-form">
610
  <label for="room-name">Название чата</label>
611
- <input type="text" id="room-name" required>
612
  <label for="room-password">Пароль (оставьте пустым для открытого)</label>
613
- <input type="password" id="room-password">
614
  <div class="modal-actions">
615
  <button type="button" id="create-room-cancel" class="modal-btn secondary-btn">Отмена</button>
616
  <button type="submit" class="modal-btn action-btn">Создать</button>
@@ -624,7 +819,7 @@ def index():
624
  <h3>Вход в приватный чат</h3>
625
  <form id="password-form">
626
  <label for="password-input">Введите пароль</label>
627
- <input type="password" id="password-input" required>
628
  <div class="modal-actions">
629
  <button type="button" id="password-cancel" class="modal-btn secondary-btn">Отмена</button>
630
  <button type="submit" class="modal-btn action-btn">Войти</button>
@@ -634,14 +829,19 @@ def index():
634
  </div>
635
 
636
  <div id="profile-modal" class="modal-overlay">
637
- <div class="modal-content" style="text-align: center;">
638
  <h3 id="profile-modal-title">Профиль пользователя</h3>
639
- <div id="profile-avatar-container" style="margin: 20px auto; display: inline-block;"></div>
640
- <p id="profile-username" style="font-size: 1.2rem; font-weight: 600;"></p>
641
- <p id="profile-address" style="color: var(--text-secondary); font-size: 0.9rem; word-break: break-all; margin-top: 8px;"></p>
642
- <div id="profile-qr-code" style="background: white; padding: 10px; margin: 20px auto; width: fit-content; border-radius: 8px;"></div>
643
- <p style="text-align: center; color: var(--text-secondary); font-size: 0.8rem; margin-top: -10px; margin-bottom: 20px;">Отсканируйт�� для открытия профиля</p>
644
- <div class="modal-actions" style="flex-direction: column; gap: 12px; align-items: stretch;">
 
 
 
 
 
645
  <button id="send-ton-btn" class="modal-btn action-btn">Отправить TON</button>
646
  <button id="profile-close-btn" class="modal-btn secondary-btn">Закрыть</button>
647
  </div>
@@ -651,7 +851,7 @@ def index():
651
  <div id="scanner-modal" class="modal-overlay">
652
  <div class="modal-content">
653
  <h3>Сканировать QR-код</h3>
654
- <div id="qr-reader" style="width: 100%; border: 1px solid var(--border-color); margin-top: 16px; border-radius: 8px; overflow: hidden;"></div>
655
  <div class="modal-actions">
656
  <button id="scanner-close-btn" class="modal-btn secondary-btn">Отмена</button>
657
  </div>
@@ -667,13 +867,14 @@ def index():
667
  buttonRootId: 'ton-connect-button'
668
  });
669
 
670
- let currentUser = { address: null, username: null };
671
  let activeChatroomId = null;
672
  let messagePollingInterval = null;
673
  let chatroomsData = {};
 
674
  let html5QrCode = null;
675
- let profileQrCode = null;
676
-
677
  const loginView = document.getElementById('login-view');
678
  const appView = document.getElementById('app-view');
679
  const chatroomListView = document.getElementById('chatroom-list-view');
@@ -682,6 +883,9 @@ def index():
682
  const activeChat = document.getElementById('active-chat');
683
  const profileModal = document.getElementById('profile-modal');
684
  const scannerModal = document.getElementById('scanner-modal');
 
 
 
685
 
686
  const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292'];
687
 
@@ -723,6 +927,7 @@ def index():
723
  };
724
 
725
  const truncateAddress = (address) => address ? `${address.substring(0, 4)}...${address.substring(address.length - 4)}` : '';
 
726
 
727
  const updateUserInfo = () => {
728
  document.getElementById('user-wallet').textContent = `Кошелек: ${truncateAddress(currentUser.address)}`;
@@ -731,13 +936,66 @@ def index():
731
  nicknameEl.textContent = currentUser.username ? `Ник: ${currentUser.username}` : `Никнейм не установлен`;
732
  usernameInput.value = currentUser.username || '';
733
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
734
 
 
 
 
 
 
 
 
 
 
 
 
735
  document.getElementById('username-form').addEventListener('submit', async (e) => {
736
  e.preventDefault();
737
  const newUsername = document.getElementById('username-input').value.trim();
738
  if (!newUsername || newUsername.length < 3) {
739
  showStatus('Никнейм должен быть не короче 3 символов.', 'error');
740
  return;
 
 
 
 
741
  }
742
  try {
743
  await apiCall('/api/set_username', {
@@ -748,30 +1006,65 @@ def index():
748
  currentUser.username = newUsername;
749
  updateUserInfo();
750
  showStatus('Никнейм успешно обновлен!', 'success');
751
- fetchChatrooms();
752
- if (activeChatroomId) fetchMessages(activeChatroomId);
 
753
  } catch (err) {}
754
  });
755
 
756
  const initializeUser = async (address) => {
757
  currentUser.address = address;
758
  try {
759
- const data = await apiCall('/api/user_data', {
760
  method: 'POST',
761
  headers: { 'Content-Type': 'application/json' },
762
  body: JSON.stringify({ address: currentUser.address })
763
  });
764
- currentUser.username = data.username;
 
765
  } catch (err) {
766
  currentUser.username = null;
 
767
  }
768
  updateUserInfo();
769
  loginView.style.display = 'none';
770
  appView.style.display = 'flex';
 
771
  fetchChatrooms();
772
- switchView('chat-view');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
773
  };
774
 
 
 
 
 
 
 
 
 
775
  const renderChatrooms = (rooms) => {
776
  const list = document.getElementById('chatroom-list');
777
  list.innerHTML = '';
@@ -779,15 +1072,15 @@ def index():
779
  rooms.forEach(room => {
780
  chatroomsData[room.id] = room;
781
  const item = document.createElement('div');
782
- item.className = 'chatroom-item';
783
  item.dataset.id = room.id;
784
 
785
  item.appendChild(getAvatar(room.name));
786
 
787
  const infoDiv = document.createElement('div');
788
- infoDiv.className = 'chatroom-info';
789
  const nameSpan = document.createElement('div');
790
- nameSpan.className = 'chatroom-name';
791
  nameSpan.textContent = room.name;
792
  infoDiv.appendChild(nameSpan);
793
  item.appendChild(infoDiv);
@@ -807,9 +1100,52 @@ def index():
807
  try {
808
  const data = await apiCall('/api/chatrooms');
809
  renderChatrooms(data.chatrooms);
810
- } catch (err) {}
 
 
811
  };
812
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
813
  const renderMessages = (messages) => {
814
  const container = document.getElementById('messages-container');
815
  const shouldScroll = container.scrollTop + container.clientHeight >= container.scrollHeight - 30;
@@ -853,24 +1189,35 @@ def index():
853
  const data = await apiCall(`/api/messages/${roomId}`);
854
  renderMessages(data.messages);
855
  } catch (err) {
856
- if (messagePollingInterval) clearInterval(messagePollingInterval);
 
 
857
  }
858
  };
859
 
860
- const showChatView = () => {
861
- chatWindowView.style.display = 'flex';
862
- if (window.innerWidth < 768) {
 
863
  chatroomListView.style.display = 'none';
 
 
864
  }
865
  };
866
 
867
- const showListView = () => {
868
- chatroomListView.style.display = 'flex';
869
- if (window.innerWidth < 768) {
870
- chatWindowView.style.display = 'none';
871
- }
 
 
 
 
 
872
  };
873
 
 
874
  const selectChatroom = (roomId, isPrivate) => {
875
  const roomData = chatroomsData[roomId];
876
  if (!roomData) return;
@@ -884,9 +1231,7 @@ def index():
884
  headerAvatar.innerHTML = '';
885
  headerAvatar.appendChild(getAvatar(roomData.name));
886
 
887
- chatPlaceholder.style.display = 'none';
888
- activeChat.style.display = 'flex';
889
- showChatView();
890
 
891
  fetchMessages(roomId);
892
  messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000);
@@ -900,9 +1245,19 @@ def index():
900
  passwordInput.value = '';
901
  passwordInput.focus();
902
 
 
 
 
 
 
 
 
 
 
 
 
903
  const formSubmitHandler = async (e) => {
904
  e.preventDefault();
905
- passwordForm.removeEventListener('submit', formSubmitHandler);
906
  const password = passwordInput.value;
907
  passwordModal.style.display = 'none';
908
  try {
@@ -913,13 +1268,27 @@ def index():
913
  });
914
  proceedToRoom();
915
  } catch (err) {}
 
 
 
916
  };
917
- passwordForm.addEventListener('submit', formSubmitHandler);
918
 
919
- document.getElementById('password-cancel').onclick = () => {
920
  passwordModal.style.display = 'none';
921
  passwordForm.removeEventListener('submit', formSubmitHandler);
922
- };
 
 
 
 
 
 
 
 
 
 
 
 
923
  } else {
924
  proceedToRoom();
925
  }
@@ -930,7 +1299,7 @@ def index():
930
  const input = document.getElementById('message-input');
931
  const sendBtn = document.getElementById('send-btn');
932
  const text = input.value.trim();
933
- if (text && activeChatroomId) {
934
  input.value = '';
935
  input.disabled = true;
936
  sendBtn.disabled = true;
@@ -944,6 +1313,7 @@ def index():
944
  text: text
945
  })
946
  });
 
947
  await fetchMessages(activeChatroomId);
948
  document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight;
949
  } finally {
@@ -958,6 +1328,7 @@ def index():
958
  document.getElementById('create-room-show-modal').addEventListener('click', () => {
959
  createRoomModal.style.display = 'flex';
960
  document.getElementById('create-room-form').reset();
 
961
  });
962
  document.getElementById('create-room-cancel').addEventListener('click', () => {
963
  createRoomModal.style.display = 'none';
@@ -966,7 +1337,10 @@ def index():
966
  e.preventDefault();
967
  const name = document.getElementById('room-name').value.trim();
968
  const password = document.getElementById('room-password').value;
969
- if (!name) return;
 
 
 
970
 
971
  try {
972
  await apiCall('/api/create_chatroom', {
@@ -981,6 +1355,7 @@ def index():
981
  });
982
 
983
  const showProfile = async (address) => {
 
984
  try {
985
  const userData = await apiCall('/api/user_data', {
986
  method: 'POST',
@@ -989,21 +1364,40 @@ def index():
989
  });
990
 
991
  const username = userData.username || `User ${truncateAddress(address)}`;
992
- const avatarContainer = document.getElementById('profile-avatar-container');
993
  const usernameEl = document.getElementById('profile-username');
994
  const addressEl = document.getElementById('profile-address');
 
995
  const qrCodeEl = document.getElementById('profile-qr-code');
996
  const sendTonBtn = document.getElementById('send-ton-btn');
997
-
998
- avatarContainer.innerHTML = '';
999
- avatarContainer.appendChild(getAvatar(username));
 
 
 
 
 
 
 
1000
 
1001
  usernameEl.textContent = username;
1002
  addressEl.textContent = address;
1003
-
 
 
 
 
 
 
 
 
 
1004
  qrCodeEl.innerHTML = '';
1005
- if (profileQrCode) profileQrCode.clear();
1006
- profileQrCode = new QRCode(qrCodeEl, {
 
 
1007
  text: address,
1008
  width: 150,
1009
  height: 150,
@@ -1012,13 +1406,14 @@ def index():
1012
  correctLevel : QRCode.CorrectLevel.H
1013
  });
1014
 
 
1015
  sendTonBtn.onclick = async () => {
1016
  if (!tonConnectUI.connected) {
1017
  showStatus('Подключите кошелек для отправки TON.', 'error');
1018
  return;
1019
  }
1020
  const amountString = prompt("Введите сумму в TON для отправки:", "0.1");
1021
- if (amountString === null) return;
1022
 
1023
  const amount = parseFloat(amountString);
1024
  if (isNaN(amount) || amount <= 0) {
@@ -1028,44 +1423,69 @@ def index():
1028
 
1029
  const amountInNanoTon = Math.floor(amount * 1_000_000_000).toString();
1030
 
1031
- const transaction = {
1032
- validUntil: Math.floor(Date.now() / 1000) + 600,
1033
- messages: [ { address: address, amount: amountInNanoTon } ]
1034
- };
 
 
 
 
 
 
 
 
1035
 
1036
  try {
 
1037
  await tonConnectUI.sendTransaction(transaction);
1038
  showStatus(`Транзакция отправлена успешно!`, 'success');
1039
  profileModal.style.display = 'none';
 
1040
  } catch (error) {
1041
- showStatus('Транзакция отклонена.', 'error');
 
 
 
 
 
1042
  }
1043
  };
1044
 
1045
- sendTonBtn.style.display = (address === currentUser.address) ? 'none' : 'block';
1046
  profileModal.style.display = 'flex';
1047
  } catch (err) {
1048
  showStatus('Не удалось загрузить профиль.', 'error');
 
1049
  }
1050
  };
1051
 
1052
  const showScanner = () => {
1053
  scannerModal.style.display = 'flex';
1054
- html5QrCode = new Html5Qrcode("qr-reader");
1055
- const qrCodeSuccessCallback = (decodedText, decodedResult) => {
1056
- hideScanner();
1057
- if (decodedText && decodedText.length > 40 && (decodedText.startsWith('EQ') || decodedText.startsWith('UQ'))) {
1058
- showProfile(decodedText);
1059
- } else {
1060
- showStatus('Отсканирован недействительный QR-код.', 'error');
1061
- }
1062
- };
1063
- const config = { fps: 10, qrbox: { width: 250, height: 250 } };
1064
- html5QrCode.start({ facingMode: "environment" }, config, qrCodeSuccessCallback)
1065
- .catch(err => {
1066
- showStatus('Не удалось запустить сканер.', 'error');
1067
- hideScanner();
1068
- });
 
 
 
 
 
 
 
 
 
1069
  };
1070
 
1071
  const hideScanner = () => {
@@ -1075,91 +1495,105 @@ def index():
1075
  scannerModal.style.display = 'none';
1076
  };
1077
 
1078
- document.getElementById('my-profile-btn').addEventListener('click', () => {
1079
- if (currentUser.address) showProfile(currentUser.address);
1080
- });
1081
  document.getElementById('profile-close-btn').addEventListener('click', () => profileModal.style.display = 'none');
1082
  document.getElementById('scanner-close-btn').addEventListener('click', hideScanner);
1083
- document.getElementById('back-to-list-btn').addEventListener('click', showListView);
1084
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1085
  const handleResize = () => {
1086
  const isMobile = window.innerWidth < 768;
1087
- document.getElementById('back-to-list-btn').style.display = isMobile ? 'block' : 'none';
1088
- if (!isMobile) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1089
  chatroomListView.style.display = 'flex';
1090
  chatWindowView.style.display = 'flex';
1091
- } else {
1092
- if (document.querySelector('.view-panel.active').id === 'chat-view') {
1093
- if (activeChat.style.display === 'flex') {
1094
- chatroomListView.style.display = 'none';
1095
- } else {
1096
- chatWindowView.style.display = 'none';
1097
- chatroomListView.style.display = 'flex';
1098
- }
1099
- }
1100
  }
1101
  };
1102
 
1103
  window.addEventListener('resize', handleResize);
1104
- handleResize();
1105
 
1106
- tonConnectUI.onStatusChange(wallet => {
1107
  if (wallet) {
1108
  const address = TON_CONNECT_UI.toUserFriendlyAddress(wallet.account.address, false);
1109
- initializeUser(address);
1110
  } else {
1111
- currentUser = { address: null, username: null };
1112
  appView.style.display = 'none';
1113
  loginView.style.display = 'flex';
1114
  if (messagePollingInterval) clearInterval(messagePollingInterval);
1115
  activeChatroomId = null;
 
 
 
1116
  }
1117
  });
1118
 
1119
- const viewPanels = document.querySelectorAll('.view-panel');
1120
- const navButtons = document.querySelectorAll('.nav-btn');
1121
-
1122
- function switchView(targetViewId) {
1123
- viewPanels.forEach(panel => {
1124
- panel.classList.remove('active');
1125
- });
1126
- document.getElementById(targetViewId).classList.add('active');
1127
-
1128
- navButtons.forEach(btn => {
1129
- btn.classList.remove('active');
1130
- if (btn.id === `nav-${targetViewId.split('-')[0]}-btn`) {
1131
- btn.classList.add('active');
1132
- }
1133
- });
1134
- handleResize();
1135
- }
1136
-
1137
- document.getElementById('nav-chat-btn').addEventListener('click', () => switchView('chat-view'));
1138
- document.getElementById('nav-browser-btn').addEventListener('click', () => switchView('browser-view'));
1139
- document.getElementById('nav-scan-btn').addEventListener('click', showScanner);
1140
-
1141
- const browserIframe = document.getElementById('browser-iframe');
1142
- const urlForm = document.getElementById('url-form');
1143
- const urlInput = document.getElementById('url-input');
1144
-
1145
- urlForm.addEventListener('submit', (e) => {
1146
- e.preventDefault();
1147
- let url = urlInput.value.trim();
1148
- if (!url) return;
1149
- if (!url.startsWith('http://') && !url.startsWith('https://')) {
1150
- if (url.includes('.') && !url.includes(' ')) {
1151
- url = 'https://' + url;
1152
- } else {
1153
- url = `https://www.google.com/search?q=${encodeURIComponent(url)}`;
1154
- }
1155
- }
1156
- browserIframe.src = url;
1157
- });
1158
-
1159
- document.getElementById('browser-back').onclick = () => { try { browserIframe.contentWindow.history.back(); } catch(e){} };
1160
- document.getElementById('browser-forward').onclick = () => { try { browserIframe.contentWindow.history.forward(); } catch(e){} };
1161
- document.getElementById('browser-reload').onclick = () => { try { browserIframe.contentWindow.location.reload(); } catch(e){} };
1162
 
 
 
1163
  });
1164
  </script>
1165
  </body>
@@ -1175,9 +1609,12 @@ def get_user_data():
1175
  if not address:
1176
  return jsonify({'error': 'Address is required'}), 400
1177
  db = read_db()
1178
- user_info = db['users'].get(address)
1179
- username = user_info.get('username') if user_info else None
1180
- return jsonify({'username': username})
 
 
 
1181
 
1182
  @app.route('/api/set_username', methods=['POST'])
1183
  def set_username():
@@ -1206,7 +1643,7 @@ def get_chatrooms():
1206
  'name': room_data['name'],
1207
  'is_private': room_data['is_private']
1208
  })
1209
- return jsonify({'chatrooms': sorted(chatrooms_list, key=lambda x: x['name'])})
1210
 
1211
  @app.route('/api/create_chatroom', methods=['POST'])
1212
  def create_chatroom():
@@ -1216,6 +1653,8 @@ def create_chatroom():
1216
  creator_address = data.get('creator_address')
1217
  if not name or not creator_address:
1218
  return jsonify({'error': 'Name and creator address are required'}), 400
 
 
1219
 
1220
  db = read_db()
1221
  room_id = str(uuid.uuid4())
@@ -1271,8 +1710,12 @@ def send_message():
1271
  sender_address = data.get('sender_address')
1272
  text = data.get('text')
1273
 
1274
- if not all([chatroom_id, sender_address, text]):
1275
- return jsonify({'error': 'Missing data'}), 400
 
 
 
 
1276
 
1277
  db = read_db()
1278
  if chatroom_id not in db['messages']:
@@ -1281,10 +1724,11 @@ def send_message():
1281
  message = {
1282
  'id': str(uuid.uuid4()),
1283
  'sender_address': sender_address,
1284
- 'text': text,
1285
  'timestamp': datetime.utcnow().isoformat() + "Z"
1286
  }
1287
 
 
1288
  if len(db['messages'][chatroom_id]) >= 100:
1289
  db['messages'][chatroom_id].pop(0)
1290
 
@@ -1292,6 +1736,23 @@ def send_message():
1292
  write_db(db)
1293
  return jsonify({'success': True})
1294
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1295
 
1296
  if __name__ == '__main__':
1297
  init_db()
 
19
  }, f, indent=4)
20
 
21
  def read_db():
22
+ if not os.path.exists(DB_FILE):
23
+ return {"users": {}, "chatrooms": {}, "messages": {}}
24
  with open(DB_FILE, 'r') as f:
25
  return json.load(f)
26
 
 
45
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
46
  <style>
47
  :root {
48
+ --bg-primary: #0F0F11; /* Dark */
49
+ --bg-secondary: #18191C; /* Slightly lighter dark */
50
+ --bg-tertiary: #212328; /* Even lighter dark */
51
+ --bg-hover: #2A2C33; /* Hover background */
52
+ --text-primary: #EAECEF; /* Light text */
53
+ --text-secondary: #8E9297; /* Muted text */
54
+ --accent-blue: #0088CC; /* TON Blue */
55
+ --accent-blue-light: #33AADD; /* Lighter TON Blue */
56
+ --border-color: #2D2F34; /* Border color */
57
  --success-color: #28a745;
58
  --error-color: #dc3545;
59
  --font-family: 'Inter', sans-serif;
 
66
  -webkit-tap-highlight-color: transparent;
67
  }
68
 
69
+ html {
70
+ font-size: 16px;
71
+ }
72
 
73
  body {
74
  font-family: var(--font-family);
 
88
  display: flex;
89
  flex-direction: column;
90
  transition: opacity 0.3s ease;
91
+ position: relative; /* Needed for fixed elements inside */
92
  }
93
 
94
  #login-view {
 
102
  background: radial-gradient(circle, #1a2a3a 0%, var(--bg-primary) 70%);
103
  }
104
  #login-view img {
105
+ width: 120px;
106
+ height: 120px;
107
  margin-bottom: 24px;
108
+ filter: drop-shadow(0 0 20px rgba(0, 136, 204, 0.6));
109
  }
110
  #login-view h1 {
111
+ font-size: 3.5rem;
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.2rem;
120
  color: var(--text-secondary);
121
  margin-bottom: 40px;
122
  }
 
125
  display: none;
126
  width: 100%;
127
  height: 100%;
128
+ flex-direction: column; /* Default to column for mobile */
129
  }
130
 
131
+ .content-area {
132
+ flex-grow: 1;
133
+ overflow: hidden; /* Hide overflow when panels switch */
134
+ position: relative; /* For absolute positioned panels */
135
  }
136
+
137
+ .panel {
 
138
  width: 100%;
139
+ height: 100%;
140
+ position: absolute;
141
+ top: 0;
142
+ left: 0;
143
+ display: none;
144
+ flex-direction: column;
145
+ background-color: var(--bg-primary);
146
  }
147
+ .panel.active {
148
  display: flex;
149
  }
 
 
 
 
150
 
151
+ /* Chat Panel */
152
+ #chats-panel {
153
  flex-direction: column;
154
+ }
155
+ #chatroom-list-view, #chat-window-view {
156
  width: 100%;
157
+ height: 100%;
158
  }
159
+ #chatroom-list-view {
160
+ background-color: var(--bg-secondary);
161
+ border-right: 1px solid var(--border-color); /* For desktop */
162
+ }
163
+ #chat-window-view {
164
+ background-color: var(--bg-primary);
165
+ }
166
+
167
  .list-header {
168
  padding: 16px;
169
  border-bottom: 1px solid var(--border-color);
170
  flex-shrink: 0;
171
+ background-color: var(--bg-secondary);
172
  }
173
  .list-header-top {
174
  display: flex;
 
176
  align-items: center;
177
  margin-bottom: 16px;
178
  }
179
+ .list-header-top h2 {
180
+ font-size: 1.5rem;
181
+ font-weight: 600;
182
+ }
183
 
184
  .user-profile {
185
  padding: 12px;
 
224
  transform: translateY(-2px);
225
  box-shadow: 0 4px 15px rgba(0, 136, 204, 0.3);
226
  }
227
+ .action-btn:active {
228
+ transform: translateY(0);
229
+ box-shadow: none;
230
+ }
231
+ .action-btn:disabled {
232
+ opacity: 0.5;
233
+ cursor: not-allowed;
234
+ }
235
  .action-btn.small {
236
  padding: 8px 12px;
237
  font-size: 0.9rem;
238
  }
239
 
240
+ #chatroom-list, #users-list {
241
+ flex-grow: 1;
242
+ overflow-y: auto;
243
+ }
244
+ .list-item {
245
  display: flex;
246
  align-items: center;
247
  gap: 12px;
 
250
  border-bottom: 1px solid var(--border-color);
251
  transition: background-color 0.2s ease;
252
  }
253
+ .list-item:hover { background-color: var(--bg-hover); }
254
 
255
  .avatar {
256
  width: 40px;
 
262
  font-weight: 600;
263
  color: white;
264
  flex-shrink: 0;
265
+ font-size: 1.2rem;
266
+ background-color: #ccc; /* Default */
267
  }
268
+ .list-item-info {
269
+ flex-grow: 1;
270
+ overflow: hidden;
271
+ }
272
+ .list-item-name {
273
  font-weight: 500;
274
  white-space: nowrap;
275
  overflow: hidden;
 
282
  flex-shrink: 0;
283
  }
284
 
285
+ /* Chat Window */
286
  #chat-window-view {
287
+ display: none; /* Start hidden on mobile */
 
 
 
 
288
  }
289
+
290
  .chat-header {
291
  display: flex;
292
  align-items: center;
 
296
  border-bottom: 1px solid var(--border-color);
297
  flex-shrink: 0;
298
  }
299
+ .back-btn {
300
+ background: none;
301
+ border: none;
302
+ cursor: pointer;
303
+ display: none; /* Show on mobile only */
304
+ padding: 0;
305
+ }
306
+ .back-btn svg {
307
+ width: 24px;
308
+ height: 24px;
309
+ fill: var(--text-primary);
310
+ }
311
+ #chat-header-title {
312
+ font-size: 1.2rem;
313
+ font-weight: 600;
314
+ white-space: nowrap;
315
+ overflow: hidden;
316
+ text-overflow: ellipsis;
317
+ }
318
 
319
  #messages-container {
320
  flex-grow: 1;
 
323
  display: flex;
324
  flex-direction: column;
325
  gap: 12px;
326
+ -webkit-overflow-scrolling: touch; /* Improve scrolling on mobile */
327
  }
328
 
329
+ .message {
330
+ display: flex;
331
+ gap: 10px;
332
+ max-width: 85%; /* Slightly larger max-width */
333
+ }
334
+ .message .avatar {
335
+ width: 32px; /* Smaller avatar in chat */
336
+ height: 32px;
337
+ align-self: flex-end;
338
+ font-size: 1rem;
339
+ }
340
+ .message-content {
341
+ display: flex;
342
+ flex-direction: column;
343
+ gap: 4px;
344
+ }
345
  .message-sender {
346
  font-size: 0.8rem;
347
  font-weight: 500;
 
384
 
385
  .message-form {
386
  display: flex;
387
+ padding: 10px 16px; /* Slightly less padding */
388
  gap: 12px;
389
  background-color: var(--bg-secondary);
390
  border-top: 1px solid var(--border-color);
 
403
  }
404
  #message-input:focus { border-color: var(--accent-blue); }
405
 
406
+ .send-btn {
407
+ width: 44px;
408
+ height: 44px;
 
 
 
 
 
 
 
 
 
 
 
 
409
  border-radius: 50%;
410
+ flex-shrink: 0;
411
+ padding: 0;
 
 
 
 
 
412
  }
413
+ .send-btn svg { width: 20px; height: 20px; fill: white; }
414
+
415
+ /* Users Panel */
416
+ #users-panel .list-header {
417
+ background-color: var(--bg-secondary); /* Consistent header bg */
 
 
 
 
 
418
  }
419
+ .user-item-address {
420
+ font-size: 0.8rem;
421
+ color: var(--text-secondary);
422
+ word-break: break-all;
423
  }
424
 
425
+ /* Browser Panel */
426
+ #browser-panel {
427
+ flex-direction: column;
428
+ }
429
+ .browser-bar {
430
+ display: flex;
431
+ padding: 12px 16px;
432
+ gap: 8px;
433
+ background-color: var(--bg-secondary);
434
+ border-bottom: 1px solid var(--border-color);
435
+ flex-shrink: 0;
436
+ }
437
+ .browser-bar input {
438
+ flex-grow: 1;
439
+ padding: 8px 12px;
440
+ background-color: var(--bg-tertiary);
441
+ border: 1px solid var(--border-color);
442
+ color: var(--text-primary);
443
+ border-radius: 6px;
444
+ font-size: 0.9rem;
445
+ outline: none;
446
+ }
447
+ .browser-bar input:focus { border-color: var(--accent-blue); }
448
+ .browser-bar button {
449
+ padding: 8px 12px;
450
+ font-size: 0.9rem;
451
+ }
452
+ #browser-iframe-container {
453
+ flex-grow: 1;
454
+ overflow: hidden;
455
+ }
456
+ #browser-iframe {
457
+ width: 100%;
458
+ height: 100%;
459
+ border: none;
460
+ }
461
+
462
+
463
+ /* Navigation Bar */
464
+ .nav-bar {
465
  display: flex;
466
  justify-content: space-around;
467
  align-items: center;
468
+ height: 60px; /* Standard nav bar height */
469
+ background-color: var(--bg-secondary);
470
+ border-top: 1px solid var(--border-color);
471
+ flex-shrink: 0;
472
+ z-index: 10; /* Above content */
473
+ padding: 0 8px;
474
  }
475
+ .nav-button {
476
+ flex: 1;
477
  display: flex;
478
  flex-direction: column;
479
  align-items: center;
480
  justify-content: center;
 
481
  background: none;
482
  border: none;
483
  color: var(--text-secondary);
 
 
484
  font-size: 0.7rem;
485
+ cursor: pointer;
486
+ padding: 4px 0;
487
+ transition: color 0.2s ease;
488
+ position: relative; /* For scanner circle */
489
+ }
490
+ .nav-button.active {
491
+ color: var(--accent-blue-light);
492
  }
493
+ .nav-button svg {
494
  width: 24px;
495
  height: 24px;
496
+ fill: var(--text-secondary);
497
+ margin-bottom: 2px;
498
+ transition: fill 0.2s ease;
499
  }
500
+ .nav-button.active svg {
501
+ fill: var(--accent-blue-light);
502
+ }
503
+ .nav-button span {
504
+ display: none; /* Hide text by default on mobile */
 
 
 
 
 
 
505
  }
 
506
 
507
+ .nav-button#scanner-nav-btn {
508
+ width: 56px; /* Larger button area */
509
+ height: 56px;
510
+ margin-top: -20px; /* Lift it up */
511
+ border-radius: 50%;
512
+ background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
513
+ box-shadow: 0 4px 15px rgba(0, 136, 204, 0.4);
514
+ color: white; /* Text color for scanner */
515
+ }
516
+ .nav-button#scanner-nav-btn svg {
517
+ fill: white; /* Icon color for scanner */
518
+ margin-bottom: 0;
519
+ }
520
+ .nav-button#scanner-nav-btn span {
521
+ display: block; /* Show text for scanner */
522
+ font-size: 0.6rem;
523
+ margin-top: 2px;
524
+ }
525
+ .nav-button#scanner-nav-btn:hover {
526
+ transform: translateY(-2px);
527
+ }
528
+
529
+
530
+ /* Modals (Keep existing styles mostly) */
531
  .modal-overlay {
532
  position: fixed;
533
  top: 0;
 
564
  font-size: 1rem;
565
  }
566
  .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
567
+ .modal-actions.vertical { flex-direction: column; align-items: stretch; }
568
+ .modal-btn {
569
+ padding: 10px 20px;
570
+ border-radius: 6px;
571
+ border: none;
572
+ cursor: pointer;
573
+ font-weight: 500;
574
+ }
575
  .secondary-btn { background-color: var(--bg-hover); color: white; }
576
+ .modal-btn.action-btn { /* Inherits action-btn gradient */ }
577
 
578
+ #profile-modal .modal-content {
579
+ text-align: center;
580
+ }
581
+ #profile-modal .profile-avatar-container .avatar {
582
+ width: 80px; height: 80px; font-size: 2rem; margin: 20px auto;
583
+ }
584
+ #profile-modal .profile-info {
585
+ margin-bottom: 20px;
586
+ }
587
+ #profile-modal .profile-info p {
588
+ margin-bottom: 8px;
589
+ }
590
+ #profile-modal .profile-info h3 {
591
+ margin-bottom: 8px;
592
+ }
593
+ #profile-modal #profile-qr-code {
594
+ background: white; padding: 10px; margin: 20px auto 10px auto; width: fit-content; border-radius: 8px;
595
+ }
596
+ #profile-modal .qr-label {
597
+ text-align: center; color: var(--text-secondary); font-size: 0.8rem; margin-bottom: 20px;
598
+ }
599
+
600
+
601
  #status-bar {
602
  position: fixed;
603
+ bottom: calc(60px + 20px); /* Above nav bar + 20px */
604
  left: 50%;
605
  transform: translateX(-50%);
606
  background-color: var(--bg-tertiary);
 
610
  font-size: 0.9rem;
611
  opacity: 0;
612
  visibility: hidden;
613
+ transition: opacity 0.3s, visibility 0.3s;
614
  z-index: 2000;
615
  box-shadow: 0 5px 15px rgba(0,0,0,0.3);
616
  }
 
618
  #status-bar.error { background-color: var(--error-color); }
619
  #status-bar.visible { opacity: 1; visibility: visible; }
620
 
621
+ /* Desktop Layout */
622
  @media (min-width: 768px) {
623
+ body {
624
+ padding: 20px; /* Add some padding around the app on desktop */
625
+ }
626
  .main-container {
627
+ max-width: 1000px; /* Max width on desktop */
628
+ max-height: 700px; /* Max height on desktop */
629
  border-radius: 12px;
630
  overflow: hidden;
631
  box-shadow: 0 10px 40px rgba(0,0,0,0.3);
632
  border: 1px solid var(--border-color);
633
  }
634
+ #app-view {
635
+ flex-direction: row; /* Side-by-side layout on desktop */
636
+ }
637
+
638
+ .content-area {
639
+ position: static; /* No absolute positioning needed for panels */
640
+ flex-direction: row; /* Arrange list and chat side-by-side */
641
+ }
642
+
643
+ .panel {
644
+ position: static; /* Panels are side-by-side */
645
+ display: flex !important; /* Always flex on desktop */
646
+ flex-direction: column; /* Content within panel is column */
647
+ width: auto; /* Width managed by flex-grow */
648
+ flex-grow: 1; /* Fill available space */
649
+ }
650
+
651
+ #chats-panel {
652
+ flex-direction: row; /* List and window side-by-side within chats panel */
653
+ flex-grow: 1;
654
  }
655
  #chatroom-list-view {
656
+ width: 320px; /* Fixed width for chat list */
657
+ flex-shrink: 0;
658
+ display: flex !important; /* Always show list */
659
+ flex-direction: column;
660
  }
661
+ #chat-window-view {
662
+ width: auto; /* Takes remaining space */
663
+ flex-grow: 1;
664
+ display: flex !important; /* Always show chat window placeholder or active */
665
+ flex-direction: column;
666
+ border-left: 1px solid var(--border-color);
667
+ }
668
+ #chat-window-view.hidden-on-desktop {
669
+ /* This class is now obsolete with the new layout */
670
+ }
671
+
672
+ .back-btn { display: none !important; } /* Hide back button on desktop */
673
+
674
+ #users-panel { flex-direction: column; }
675
+ #browser-panel { flex-direction: column; }
676
+
677
+ .nav-bar {
678
+ display: none; /* Hide bottom nav bar on desktop */
679
+ }
680
+
681
+ .list-header {
682
+ background-color: var(--bg-secondary); /* Consistent header bg */
683
+ }
684
+ #users-panel .list-header {
685
+ background-color: var(--bg-secondary);
686
+ }
687
+
688
+ .nav-button span {
689
+ display: none; /* Hide text even more consistently */
690
+ }
691
  }
692
  </style>
693
  </head>
 
701
  </div>
702
 
703
  <div id="app-view" class="main-container">
704
+ <div class="content-area">
705
+ <!-- Chats Panel -->
706
+ <div id="chats-panel" class="panel active">
707
  <div id="chatroom-list-view">
708
  <div class="list-header">
709
  <div class="list-header-top">
710
  <h2>Чаты</h2>
711
+ <button id="create-room-show-modal" class="action-btn small">
712
+ <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>
713
+ <span>Новый</span>
714
+ </button>
 
 
 
 
 
715
  </div>
716
  <div class="user-profile">
717
  <div id="user-wallet"></div>
 
725
  <div id="chatroom-list"></div>
726
  </div>
727
 
728
+ <div id="chat-window-view">
729
+ <div id="chat-placeholder" class="chat-placeholder">
730
  <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
731
  <h2>Выберите чат</h2>
732
  <p>Начните общение в одном из существующих чатов или создайте свой собственный.</p>
 
749
  </div>
750
  </div>
751
  </div>
752
+
753
+ <!-- Users Panel -->
754
+ <div id="users-panel" class="panel">
755
+ <div class="list-header">
756
+ <h2>Пользователи</h2>
757
+ </div>
758
+ <div id="users-list">
759
+ <!-- User list items will be loaded here -->
760
+ </div>
761
+ </div>
762
+
763
+ <!-- Browser Panel -->
764
+ <div id="browser-panel" class="panel">
765
+ <div class="browser-bar">
766
+ <input type="text" id="browser-url-input" placeholder="Введите URL">
767
+ <button id="browser-go-btn" class="action-btn small">Перейти</button>
768
+ </div>
769
+ <div id="browser-iframe-container">
770
+ <iframe id="browser-iframe" src="https://www.google.com/"></iframe>
771
  </div>
 
772
  </div>
773
  </div>
774
 
775
+ <!-- Bottom Navigation Bar -->
776
+ <div class="nav-bar">
777
+ <button class="nav-button active" data-panel="chats-panel" id="nav-chats-btn">
778
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/></svg>
779
+ <span>Чаты</span>
780
+ </button>
781
+ <button class="nav-button" data-panel="users-panel" id="nav-users-btn">
782
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M16 17.01V10h-2v7.01h-3L15 21l4-3.99h-3zM9 3L5 7h3v7H6V7H3l4-4zm7.5 1.35l-1.41 1.41A5.98 5.98 0 0112 11c-.34 0-.68.03-1 .08V9.02c.3-.01.6-.02.99-.02 2.21 0 4-1.79 4-4 0-.33-.05-.65-.15-.97zM12 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>
783
+ <span>Юзеры</span>
784
  </button>
785
+ <button class="nav-button" id="scanner-nav-btn">
786
+ <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>
787
+ <span>Сканнер</span>
788
  </button>
789
+ <button class="nav-button" data-panel="browser-panel" id="nav-browser-btn">
790
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 14H4V8h16v10zm-2-7H6v-2h12v2z"/></svg>
791
+ <span>Браузер</span>
792
  </button>
793
+ <button class="nav-button" id="nav-profile-btn">
794
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
795
+ <span>Профиль</span>
796
+ </button>
797
+ </div>
798
  </div>
799
 
800
+ <!-- Modals (Keep as overlays) -->
801
  <div id="create-room-modal" class="modal-overlay">
802
  <div class="modal-content">
803
  <h3>Создать новый чат</h3>
804
  <form id="create-room-form">
805
  <label for="room-name">Название чата</label>
806
+ <input type="text" id="room-name" required autocomplete="off">
807
  <label for="room-password">Пароль (оставьте пустым для открытого)</label>
808
+ <input type="password" id="room-password" autocomplete="new-password">
809
  <div class="modal-actions">
810
  <button type="button" id="create-room-cancel" class="modal-btn secondary-btn">Отмена</button>
811
  <button type="submit" class="modal-btn action-btn">Создать</button>
 
819
  <h3>Вход в приватный чат</h3>
820
  <form id="password-form">
821
  <label for="password-input">Введите пароль</label>
822
+ <input type="password" id="password-input" required autocomplete="current-password">
823
  <div class="modal-actions">
824
  <button type="button" id="password-cancel" class="modal-btn secondary-btn">Отмена</button>
825
  <button type="submit" class="modal-btn action-btn">Войти</button>
 
829
  </div>
830
 
831
  <div id="profile-modal" class="modal-overlay">
832
+ <div class="modal-content">
833
  <h3 id="profile-modal-title">Профиль пользователя</h3>
834
+ <div class="profile-avatar-container">
835
+ <div id="profile-avatar" class="avatar"></div>
836
+ </div>
837
+ <div class="profile-info">
838
+ <p id="profile-username" style="font-size: 1.2rem; font-weight: 600;"></p>
839
+ <p id="profile-address" style="color: var(--text-secondary); font-size: 0.9rem; word-break: break-all;"></p>
840
+ <p id="profile-balance" style="color: var(--text-secondary); font-size: 0.9rem; margin-top: 4px;"></p>
841
+ </div>
842
+ <div id="profile-qr-code" style="background: white; padding: 10px; margin: 20px auto 10px auto; width: fit-content; border-radius: 8px;"></div>
843
+ <p class="qr-label">Отсканируйте для открытия профиля</p>
844
+ <div class="modal-actions vertical">
845
  <button id="send-ton-btn" class="modal-btn action-btn">Отправить TON</button>
846
  <button id="profile-close-btn" class="modal-btn secondary-btn">Закрыть</button>
847
  </div>
 
851
  <div id="scanner-modal" class="modal-overlay">
852
  <div class="modal-content">
853
  <h3>Сканировать QR-код</h3>
854
+ <div id="qr-reader" style="width: 100%; min-height: 200px; border: 1px solid var(--border-color); margin-top: 16px; border-radius: 8px; overflow: hidden;"></div>
855
  <div class="modal-actions">
856
  <button id="scanner-close-btn" class="modal-btn secondary-btn">Отмена</button>
857
  </div>
 
867
  buttonRootId: 'ton-connect-button'
868
  });
869
 
870
+ let currentUser = { address: null, username: null, balance: 'Loading...' };
871
  let activeChatroomId = null;
872
  let messagePollingInterval = null;
873
  let chatroomsData = {};
874
+ let usersData = {};
875
  let html5QrCode = null;
876
+ let profileQrCodeInstance = null;
877
+
878
  const loginView = document.getElementById('login-view');
879
  const appView = document.getElementById('app-view');
880
  const chatroomListView = document.getElementById('chatroom-list-view');
 
883
  const activeChat = document.getElementById('active-chat');
884
  const profileModal = document.getElementById('profile-modal');
885
  const scannerModal = document.getElementById('scanner-modal');
886
+ const contentArea = document.querySelector('.content-area');
887
+ const navButtons = document.querySelectorAll('.nav-button');
888
+ const panels = document.querySelectorAll('.panel');
889
 
890
  const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292'];
891
 
 
927
  };
928
 
929
  const truncateAddress = (address) => address ? `${address.substring(0, 4)}...${address.substring(address.length - 4)}` : '';
930
+ const formatBalance = (balance) => balance === 'Loading...' ? balance : `${(balance / 1_000_000_000).toFixed(2)} TON`;
931
 
932
  const updateUserInfo = () => {
933
  document.getElementById('user-wallet').textContent = `Кошелек: ${truncateAddress(currentUser.address)}`;
 
936
  nicknameEl.textContent = currentUser.username ? `Ник: ${currentUser.username}` : `Никнейм не установлен`;
937
  usernameInput.value = currentUser.username || '';
938
  };
939
+
940
+ const showPanel = (panelId) => {
941
+ panels.forEach(panel => {
942
+ panel.classList.remove('active');
943
+ });
944
+ document.getElementById(panelId).classList.add('active');
945
+
946
+ navButtons.forEach(btn => {
947
+ btn.classList.remove('active');
948
+ if (btn.dataset.panel === panelId) {
949
+ btn.classList.add('active');
950
+ }
951
+ });
952
+
953
+ // Special handling for chat panel internal views on mobile
954
+ if (panelId === 'chats-panel') {
955
+ handleResize(); // Re-evaluate chat list vs window visibility
956
+ if (activeChatroomId) {
957
+ // If a chat is active, ensure chat window is visible if on mobile
958
+ if (window.innerWidth < 768) {
959
+ chatroomListView.style.display = 'none';
960
+ chatWindowView.style.display = 'flex';
961
+ }
962
+ } else {
963
+ // If no chat active, ensure list is visible
964
+ if (window.innerWidth < 768) {
965
+ chatroomListView.style.display = 'flex';
966
+ chatWindowView.style.display = 'none';
967
+ }
968
+ }
969
+ } else {
970
+ // Hide chat window on mobile if switching away from chats panel
971
+ if (window.innerWidth < 768) {
972
+ chatWindowView.style.display = 'none';
973
+ chatroomListView.style.display = 'flex'; // Ensure list view state is correct if returning
974
+ }
975
+ }
976
+ };
977
 
978
+ navButtons.forEach(button => {
979
+ if (button.dataset.panel) {
980
+ button.addEventListener('click', () => showPanel(button.dataset.panel));
981
+ }
982
+ });
983
+ document.getElementById('nav-profile-btn').addEventListener('click', () => {
984
+ if (currentUser.address) showProfile(currentUser.address);
985
+ });
986
+ document.getElementById('scanner-nav-btn').addEventListener('click', showScanner);
987
+
988
+
989
  document.getElementById('username-form').addEventListener('submit', async (e) => {
990
  e.preventDefault();
991
  const newUsername = document.getElementById('username-input').value.trim();
992
  if (!newUsername || newUsername.length < 3) {
993
  showStatus('Никнейм должен быть не короче 3 символов.', 'error');
994
  return;
995
+ }
996
+ if (newUsername.length > 20) {
997
+ showStatus('Никнейм должен быть не длиннее 20 символов.', 'error');
998
+ return;
999
  }
1000
  try {
1001
  await apiCall('/api/set_username', {
 
1006
  currentUser.username = newUsername;
1007
  updateUserInfo();
1008
  showStatus('Никнейм успешно обновлен!', 'success');
1009
+ fetchChatrooms(); // Update chatroom list with potential new usernames
1010
+ fetchUsers(); // Update users list
1011
+ if (activeChatroomId) fetchMessages(activeChatroomId); // Update messages with potential new name
1012
  } catch (err) {}
1013
  });
1014
 
1015
  const initializeUser = async (address) => {
1016
  currentUser.address = address;
1017
  try {
1018
+ const userData = await apiCall('/api/user_data', {
1019
  method: 'POST',
1020
  headers: { 'Content-Type': 'application/json' },
1021
  body: JSON.stringify({ address: currentUser.address })
1022
  });
1023
+ currentUser.username = userData.username;
1024
+ currentUser.balance = userData.balance || 'N/A'; // Assume backend returns balance
1025
  } catch (err) {
1026
  currentUser.username = null;
1027
+ currentUser.balance = 'Error';
1028
  }
1029
  updateUserInfo();
1030
  loginView.style.display = 'none';
1031
  appView.style.display = 'flex';
1032
+ showPanel('chats-panel'); // Default view is chats
1033
  fetchChatrooms();
1034
+ fetchUsers();
1035
+ fetchBalance(); // Ensure balance is fetched
1036
+ };
1037
+
1038
+ const fetchBalance = async () => {
1039
+ if (!tonConnectUI.connected || !currentUser.address) {
1040
+ currentUser.balance = 'N/A';
1041
+ updateProfileModalBalance(); // Update profile modal if open
1042
+ return;
1043
+ }
1044
+ try {
1045
+ // Using TON Connect's method if available and reliable, or a simple API endpoint
1046
+ // A real app would need a secure backend way to get balance or rely solely on TON Connect
1047
+ // For this example, let's simulate or use a placeholder
1048
+ const balance = await tonConnectUI.getBalance(); // This is simplified; TON Connect doesn't directly expose this easily. A real app needs a backend or helper library.
1049
+ currentUser.balance = balance || 'N/A'; // Placeholder if not available
1050
+ // Fallback to simulate or use a placeholder API if TON Connect UI doesn't provide balance easily
1051
+ // const data = await apiCall('/api/balance', { ... }); currentUser.balance = data.balance;
1052
+
1053
+ } catch (e) {
1054
+ console.error("Failed to fetch balance:", e);
1055
+ currentUser.balance = 'Error';
1056
+ }
1057
+ updateProfileModalBalance(); // Update profile modal if open
1058
  };
1059
 
1060
+ const updateProfileModalBalance = () => {
1061
+ const balanceEl = document.getElementById('profile-balance');
1062
+ if (balanceEl) {
1063
+ balanceEl.textContent = `Баланс: ${formatBalance(currentUser.balance)}`;
1064
+ }
1065
+ };
1066
+
1067
+
1068
  const renderChatrooms = (rooms) => {
1069
  const list = document.getElementById('chatroom-list');
1070
  list.innerHTML = '';
 
1072
  rooms.forEach(room => {
1073
  chatroomsData[room.id] = room;
1074
  const item = document.createElement('div');
1075
+ item.className = 'list-item chatroom-item';
1076
  item.dataset.id = room.id;
1077
 
1078
  item.appendChild(getAvatar(room.name));
1079
 
1080
  const infoDiv = document.createElement('div');
1081
+ infoDiv.className = 'list-item-info chatroom-info';
1082
  const nameSpan = document.createElement('div');
1083
+ nameSpan.className = 'list-item-name chatroom-name';
1084
  nameSpan.textContent = room.name;
1085
  infoDiv.appendChild(nameSpan);
1086
  item.appendChild(infoDiv);
 
1100
  try {
1101
  const data = await apiCall('/api/chatrooms');
1102
  renderChatrooms(data.chatrooms);
1103
+ } catch (err) {
1104
+ // Handle error visually if needed
1105
+ }
1106
  };
1107
 
1108
+ const renderUsers = (users) => {
1109
+ const list = document.getElementById('users-list');
1110
+ list.innerHTML = '';
1111
+ usersData = {};
1112
+ users.forEach(user => {
1113
+ usersData[user.address] = user;
1114
+ const item = document.createElement('div');
1115
+ item.className = 'list-item user-item';
1116
+ item.dataset.address = user.address;
1117
+
1118
+ item.appendChild(getAvatar(user.username || truncateAddress(user.address)));
1119
+
1120
+ const infoDiv = document.createElement('div');
1121
+ infoDiv.className = 'list-item-info user-info';
1122
+ const nameSpan = document.createElement('div');
1123
+ nameSpan.className = 'list-item-name user-name';
1124
+ nameSpan.textContent = user.username || `User ${truncateAddress(user.address)}`;
1125
+ infoDiv.appendChild(nameSpan);
1126
+
1127
+ const addressSpan = document.createElement('div');
1128
+ addressSpan.className = 'user-item-address';
1129
+ addressSpan.textContent = truncateAddress(user.address);
1130
+ infoDiv.appendChild(addressSpan);
1131
+
1132
+ item.appendChild(infoDiv);
1133
+
1134
+ item.addEventListener('click', () => showProfile(user.address));
1135
+ list.appendChild(item);
1136
+ });
1137
+ };
1138
+
1139
+ const fetchUsers = async () => {
1140
+ try {
1141
+ const data = await apiCall('/api/users');
1142
+ renderUsers(data.users);
1143
+ } catch (err) {
1144
+ // Handle error visually if needed
1145
+ }
1146
+ };
1147
+
1148
+
1149
  const renderMessages = (messages) => {
1150
  const container = document.getElementById('messages-container');
1151
  const shouldScroll = container.scrollTop + container.clientHeight >= container.scrollHeight - 30;
 
1189
  const data = await apiCall(`/api/messages/${roomId}`);
1190
  renderMessages(data.messages);
1191
  } catch (err) {
1192
+ // Polling errors are less critical to show loudly
1193
+ console.error("Failed to fetch messages:", err);
1194
+ // if (messagePollingInterval) clearInterval(messagePollingInterval);
1195
  }
1196
  };
1197
 
1198
+ const showChatWindow = () => {
1199
+ chatPlaceholder.style.display = 'none';
1200
+ activeChat.style.display = 'flex';
1201
+ if (window.innerWidth < 768) {
1202
  chatroomListView.style.display = 'none';
1203
+ chatWindowView.style.display = 'flex';
1204
+ document.getElementById('back-to-list-btn').style.display = 'block';
1205
  }
1206
  };
1207
 
1208
+ const hideChatWindow = () => {
1209
+ activeChat.style.display = 'none';
1210
+ chatPlaceholder.style.display = 'flex';
1211
+ if (window.innerWidth < 768) {
1212
+ chatroomListView.style.display = 'flex';
1213
+ chatWindowView.style.display = 'none';
1214
+ document.getElementById('back-to-list-btn').style.display = 'none';
1215
+ }
1216
+ activeChatroomId = null;
1217
+ if (messagePollingInterval) clearInterval(messagePollingInterval);
1218
  };
1219
 
1220
+
1221
  const selectChatroom = (roomId, isPrivate) => {
1222
  const roomData = chatroomsData[roomId];
1223
  if (!roomData) return;
 
1231
  headerAvatar.innerHTML = '';
1232
  headerAvatar.appendChild(getAvatar(roomData.name));
1233
 
1234
+ showChatWindow();
 
 
1235
 
1236
  fetchMessages(roomId);
1237
  messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000);
 
1245
  passwordInput.value = '';
1246
  passwordInput.focus();
1247
 
1248
+ // Ensure previous listeners are removed to prevent duplicates
1249
+ const oldFormHandler = passwordForm.dataset.handler;
1250
+ if (oldFormHandler) {
1251
+ passwordForm.removeEventListener('submit', window[oldFormHandler]);
1252
+ }
1253
+ const oldCancelHandler = document.getElementById('password-cancel').dataset.handler;
1254
+ if (oldCancelHandler) {
1255
+ document.getElementById('password-cancel').removeEventListener('click', window[oldCancelHandler]);
1256
+ }
1257
+
1258
+
1259
  const formSubmitHandler = async (e) => {
1260
  e.preventDefault();
 
1261
  const password = passwordInput.value;
1262
  passwordModal.style.display = 'none';
1263
  try {
 
1268
  });
1269
  proceedToRoom();
1270
  } catch (err) {}
1271
+ // Clean up handler after submission
1272
+ passwordForm.removeEventListener('submit', formSubmitHandler);
1273
+ document.getElementById('password-cancel').removeEventListener('click', cancelHandler);
1274
  };
 
1275
 
1276
+ const cancelHandler = () => {
1277
  passwordModal.style.display = 'none';
1278
  passwordForm.removeEventListener('submit', formSubmitHandler);
1279
+ document.getElementById('password-cancel').removeEventListener('click', cancelHandler);
1280
+ };
1281
+
1282
+ passwordForm.addEventListener('submit', formSubmitHandler);
1283
+ document.getElementById('password-cancel').addEventListener('click', cancelHandler);
1284
+
1285
+ // Store handlers to remove them later
1286
+ passwordForm.dataset.handler = formSubmitHandler.name; // Note: Function name might not be reliable, better to store the function object
1287
+ document.getElementById('password-cancel').dataset.handler = cancelHandler.name;
1288
+ passwordForm._handler = formSubmitHandler; // Store function object
1289
+ document.getElementById('password-cancel')._handler = cancelHandler; // Store function object
1290
+
1291
+
1292
  } else {
1293
  proceedToRoom();
1294
  }
 
1299
  const input = document.getElementById('message-input');
1300
  const sendBtn = document.getElementById('send-btn');
1301
  const text = input.value.trim();
1302
+ if (text && activeChatroomId && currentUser.address) {
1303
  input.value = '';
1304
  input.disabled = true;
1305
  sendBtn.disabled = true;
 
1313
  text: text
1314
  })
1315
  });
1316
+ // Fetch messages immediately after sending
1317
  await fetchMessages(activeChatroomId);
1318
  document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight;
1319
  } finally {
 
1328
  document.getElementById('create-room-show-modal').addEventListener('click', () => {
1329
  createRoomModal.style.display = 'flex';
1330
  document.getElementById('create-room-form').reset();
1331
+ document.getElementById('room-name').focus();
1332
  });
1333
  document.getElementById('create-room-cancel').addEventListener('click', () => {
1334
  createRoomModal.style.display = 'none';
 
1337
  e.preventDefault();
1338
  const name = document.getElementById('room-name').value.trim();
1339
  const password = document.getElementById('room-password').value;
1340
+ if (!name || !currentUser.address) {
1341
+ showStatus('Название чата и адрес пользователя обязательны.', 'error');
1342
+ return;
1343
+ }
1344
 
1345
  try {
1346
  await apiCall('/api/create_chatroom', {
 
1355
  });
1356
 
1357
  const showProfile = async (address) => {
1358
+ if (!address) return;
1359
  try {
1360
  const userData = await apiCall('/api/user_data', {
1361
  method: 'POST',
 
1364
  });
1365
 
1366
  const username = userData.username || `User ${truncateAddress(address)}`;
1367
+ const avatarEl = document.getElementById('profile-avatar');
1368
  const usernameEl = document.getElementById('profile-username');
1369
  const addressEl = document.getElementById('profile-address');
1370
+ const balanceEl = document.getElementById('profile-balance');
1371
  const qrCodeEl = document.getElementById('profile-qr-code');
1372
  const sendTonBtn = document.getElementById('send-ton-btn');
1373
+
1374
+ // Clear and update avatar
1375
+ avatarEl.innerHTML = '';
1376
+ const newAvatar = getAvatar(username);
1377
+ newAvatar.style.width = '80px'; // Override modal specific size
1378
+ newAvatar.style.height = '80px';
1379
+ newAvatar.style.fontSize = '2rem';
1380
+ newAvatar.style.margin = '20px auto';
1381
+ avatarEl.parentNode.replaceChild(newAvatar, avatarEl);
1382
+ newAvatar.id = 'profile-avatar'; // Restore ID
1383
 
1384
  usernameEl.textContent = username;
1385
  addressEl.textContent = address;
1386
+
1387
+ // Update balance only for current user's profile
1388
+ if (address === currentUser.address) {
1389
+ balanceEl.style.display = 'block';
1390
+ balanceEl.textContent = `Баланс: ${formatBalance(currentUser.balance)}`;
1391
+ } else {
1392
+ balanceEl.style.display = 'none';
1393
+ }
1394
+
1395
+ // Generate QR code
1396
  qrCodeEl.innerHTML = '';
1397
+ if (profileQrCodeInstance) {
1398
+ profileQrCodeInstance.clear();
1399
+ }
1400
+ profileQrCodeInstance = new QRCode(qrCodeEl, {
1401
  text: address,
1402
  width: 150,
1403
  height: 150,
 
1406
  correctLevel : QRCode.CorrectLevel.H
1407
  });
1408
 
1409
+ // TON Send button logic
1410
  sendTonBtn.onclick = async () => {
1411
  if (!tonConnectUI.connected) {
1412
  showStatus('Подключите кошелек для отправки TON.', 'error');
1413
  return;
1414
  }
1415
  const amountString = prompt("Введите сумму в TON для отправки:", "0.1");
1416
+ if (amountString === null) return; // User cancelled
1417
 
1418
  const amount = parseFloat(amountString);
1419
  if (isNaN(amount) || amount <= 0) {
 
1423
 
1424
  const amountInNanoTon = Math.floor(amount * 1_000_000_000).toString();
1425
 
1426
+ // Prepare transaction object (simplified)
1427
+ const transaction = {
1428
+ validUntil: Math.floor(Date.now() / 1000) + 600, // 10 minutes expiry
1429
+ messages: [
1430
+ {
1431
+ address: address, // Destination address
1432
+ amount: amountInNanoTon // Amount in nanoTON
1433
+ }
1434
+ // Add payload if needed
1435
+ ]
1436
+ };
1437
+
1438
 
1439
  try {
1440
+ showStatus('Ожидание подтверждения транзакции в кошельке...', 'info', 5000);
1441
  await tonConnectUI.sendTransaction(transaction);
1442
  showStatus(`Транзакция отправлена успешно!`, 'success');
1443
  profileModal.style.display = 'none';
1444
+ fetchBalance(); // Refresh balance after sending
1445
  } catch (error) {
1446
+ if (error.message.includes('cancelled')) {
1447
+ showStatus('Транзакция отменена.', 'error');
1448
+ } else {
1449
+ showStatus('Ошибка транзакции.', 'error');
1450
+ }
1451
+ console.error("Transaction failed:", error);
1452
  }
1453
  };
1454
 
1455
+ sendTonBtn.style.display = (address === currentUser.address) ? 'none' : 'flex'; // Hide send button on own profile
1456
  profileModal.style.display = 'flex';
1457
  } catch (err) {
1458
  showStatus('Не удалось загрузить профиль.', 'error');
1459
+ console.error("Failed to show profile:", err);
1460
  }
1461
  };
1462
 
1463
  const showScanner = () => {
1464
  scannerModal.style.display = 'flex';
1465
+ // Delay starting QR reader slightly to ensure modal is visible
1466
+ setTimeout(() => {
1467
+ html5QrCode = new Html5Qrcode("qr-reader");
1468
+ const qrCodeSuccessCallback = (decodedText, decodedResult) => {
1469
+ hideScanner();
1470
+ // Attempt to validate if it looks like a TON address
1471
+ // Simple check: starts with EQ or UQ and is long enough
1472
+ if (decodedText && decodedText.length > 40 && (decodedText.startsWith('EQ') || decodedText.startsWith('UQ'))) {
1473
+ showProfile(decodedText);
1474
+ } else {
1475
+ showStatus('Отсканирован недействительный QR-код TON.', 'error');
1476
+ }
1477
+ };
1478
+ const config = { fps: 10, qrbox: { width: 250, height: 250 }, aspectRatio: 1.0 };
1479
+ html5QrCode.start({ facingMode: "environment" }, config, qrCodeSuccessCallback, (errorMessage) => {
1480
+ // Optional: Handle scan errors dynamically if needed
1481
+ console.warn(`QR scan error: ${errorMessage}`);
1482
+ })
1483
+ .catch(err => {
1484
+ showStatus('Не удалось запустить сканер. Проверьте разрешения камеры.', 'error');
1485
+ console.error("Failed to start QR scanner:", err);
1486
+ hideScanner(); // Ensure modal is closed on error
1487
+ });
1488
+ }, 100); // Short delay
1489
  };
1490
 
1491
  const hideScanner = () => {
 
1495
  scannerModal.style.display = 'none';
1496
  };
1497
 
 
 
 
1498
  document.getElementById('profile-close-btn').addEventListener('click', () => profileModal.style.display = 'none');
1499
  document.getElementById('scanner-close-btn').addEventListener('click', hideScanner);
1500
+ document.getElementById('back-to-list-btn').addEventListener('click', hideChatWindow);
1501
 
1502
+ // Browser panel logic
1503
+ document.getElementById('browser-go-btn').addEventListener('click', () => {
1504
+ const urlInput = document.getElementById('browser-url-input');
1505
+ let url = urlInput.value.trim();
1506
+ if (!url) return;
1507
+
1508
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
1509
+ url = 'https://' + url; // Default to https
1510
+ }
1511
+ try {
1512
+ const browserIframe = document.getElementById('browser-iframe');
1513
+ browserIframe.src = url;
1514
+ } catch (e) {
1515
+ showStatus('Неверный формат URL.', 'error');
1516
+ console.error("Failed to load URL:", e);
1517
+ }
1518
+ });
1519
+ document.getElementById('browser-url-input').addEventListener('keypress', (e) => {
1520
+ if (e.key === 'Enter') {
1521
+ e.preventDefault();
1522
+ document.getElementById('browser-go-btn').click();
1523
+ }
1524
+ });
1525
+
1526
+
1527
  const handleResize = () => {
1528
  const isMobile = window.innerWidth < 768;
1529
+
1530
+ if (isMobile) {
1531
+ // Mobile: Hide chat window if not active, show back button if active
1532
+ document.getElementById('back-to-list-btn').style.display = activeChatroomId ? 'block' : 'none';
1533
+ if (activeChatroomId) {
1534
+ chatroomListView.style.display = 'none';
1535
+ chatWindowView.style.display = 'flex';
1536
+ } else {
1537
+ chatroomListView.style.display = 'flex';
1538
+ chatWindowView.style.display = 'none';
1539
+ }
1540
+ // Only show chats panel on mobile if it's the active panel
1541
+ if (document.getElementById('chats-panel').classList.contains('active')) {
1542
+ document.getElementById('chats-panel').style.display = 'flex';
1543
+ document.getElementById('users-panel').style.display = 'none';
1544
+ document.getElementById('browser-panel').style.display = 'none';
1545
+ } else if (document.getElementById('users-panel').classList.contains('active')) {
1546
+ document.getElementById('chats-panel').style.display = 'none';
1547
+ document.getElementById('users-panel').style.display = 'flex';
1548
+ document.getElementById('browser-panel').style.display = 'none';
1549
+ } else if (document.getElementById('browser-panel').classList.contains('active')) {
1550
+ document.getElementById('chats-panel').style.display = 'none';
1551
+ document.getElementById('users-panel').style.display = 'none';
1552
+ document.getElementById('browser-panel').style.display = 'flex';
1553
+ } else {
1554
+ // Default to chats if none active
1555
+ showPanel('chats-panel');
1556
+ }
1557
+
1558
+ } else {
1559
+ // Desktop: Always show chat list and chat window side-by-side within chats panel
1560
  chatroomListView.style.display = 'flex';
1561
  chatWindowView.style.display = 'flex';
1562
+ document.getElementById('back-to-list-btn').style.display = 'none'; // Hide back button
1563
+
1564
+ // Desktop: Ensure all panels are potentially visible for flex layout,
1565
+ // but only the active one is within the content area flow (managed by .panel.active css)
1566
+ panels.forEach(panel => panel.style.display = 'flex'); // Allow flex to manage layout
1567
+
 
 
 
1568
  }
1569
  };
1570
 
1571
  window.addEventListener('resize', handleResize);
 
1572
 
1573
+ tonConnectUI.onStatusChange(async wallet => {
1574
  if (wallet) {
1575
  const address = TON_CONNECT_UI.toUserFriendlyAddress(wallet.account.address, false);
1576
+ await initializeUser(address); // Use await here
1577
  } else {
1578
+ currentUser = { address: null, username: null, balance: 'N/A' };
1579
  appView.style.display = 'none';
1580
  loginView.style.display = 'flex';
1581
  if (messagePollingInterval) clearInterval(messagePollingInterval);
1582
  activeChatroomId = null;
1583
+ hideChatWindow(); // Hide chat window on logout
1584
+ // Reset panels to default state (chats)
1585
+ showPanel('chats-panel');
1586
  }
1587
  });
1588
 
1589
+ // Initial check for connected wallet on load
1590
+ if (tonConnectUI.connected) {
1591
+ const address = TON_CONNECT_UI.toUserFriendlyAddress(tonConnectUI.wallet.account.address, false);
1592
+ initializeUser(address);
1593
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1594
 
1595
+ // Initial layout adjustment
1596
+ handleResize();
1597
  });
1598
  </script>
1599
  </body>
 
1609
  if not address:
1610
  return jsonify({'error': 'Address is required'}), 400
1611
  db = read_db()
1612
+ user_info = db['users'].get(address, {})
1613
+ username = user_info.get('username')
1614
+ # In a real app, you'd fetch balance via TON API/SDK
1615
+ # For this example, we'll simulate or provide N/A
1616
+ balance = "Simulated Balance" # Placeholder or N/A
1617
+ return jsonify({'username': username, 'address': address, 'balance': balance})
1618
 
1619
  @app.route('/api/set_username', methods=['POST'])
1620
  def set_username():
 
1643
  'name': room_data['name'],
1644
  'is_private': room_data['is_private']
1645
  })
1646
+ return jsonify({'chatrooms': sorted(chatrooms_list, key=lambda x: x['name'].lower())})
1647
 
1648
  @app.route('/api/create_chatroom', methods=['POST'])
1649
  def create_chatroom():
 
1653
  creator_address = data.get('creator_address')
1654
  if not name or not creator_address:
1655
  return jsonify({'error': 'Name and creator address are required'}), 400
1656
+ if len(name) < 3 or len(name) > 50:
1657
+ return jsonify({'error': 'Chatroom name must be between 3 and 50 characters'}), 400
1658
 
1659
  db = read_db()
1660
  room_id = str(uuid.uuid4())
 
1710
  sender_address = data.get('sender_address')
1711
  text = data.get('text')
1712
 
1713
+ if not all([chatroom_id, sender_address, text is not None]): # Allow empty text? Maybe not for chat
1714
+ if text is None or text.strip() == "": # Explicitly check for empty/whitespace
1715
+ return jsonify({'error': 'Message text is empty'}), 400
1716
+ if not chatroom_id or not sender_address:
1717
+ return jsonify({'error': 'Missing chatroom ID or sender address'}), 400
1718
+
1719
 
1720
  db = read_db()
1721
  if chatroom_id not in db['messages']:
 
1724
  message = {
1725
  'id': str(uuid.uuid4()),
1726
  'sender_address': sender_address,
1727
+ 'text': text.strip(), # Store stripped text
1728
  'timestamp': datetime.utcnow().isoformat() + "Z"
1729
  }
1730
 
1731
+ # Keep only the last 100 messages per chatroom
1732
  if len(db['messages'][chatroom_id]) >= 100:
1733
  db['messages'][chatroom_id].pop(0)
1734
 
 
1736
  write_db(db)
1737
  return jsonify({'success': True})
1738
 
1739
+ @app.route('/api/users', methods=['GET'])
1740
+ def get_users():
1741
+ db = read_db()
1742
+ users_list = []
1743
+ # Iterate through users explicitly stored in the 'users' key
1744
+ # This is better than inferring from messages, which might miss users who haven't sent messages
1745
+ # Add users from chatroom creators too if they exist only there (less likely with current flow)
1746
+ # For simplicity, just list users from the 'users' key.
1747
+ for address, user_data in db['users'].items():
1748
+ users_list.append({
1749
+ 'address': address,
1750
+ 'username': user_data.get('username')
1751
+ })
1752
+
1753
+ # You might want to sort this list
1754
+ return jsonify({'users': sorted(users_list, key=lambda x: (x.get('username') or x['address']).lower())})
1755
+
1756
 
1757
  if __name__ == '__main__':
1758
  init_db()