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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +627 -158
app.py CHANGED
@@ -19,8 +19,12 @@ def init_db():
19
  }, f, indent=4)
20
 
21
  def read_db():
22
- with open(DB_FILE, 'r') as f:
23
- return json.load(f)
 
 
 
 
24
 
25
  def write_db(data):
26
  with open(DB_FILE, 'w') as f:
@@ -43,18 +47,22 @@ 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;
 
58
  }
59
 
60
  * {
@@ -76,60 +84,77 @@ def index():
76
  height: 100vh;
77
  width: 100vw;
78
  display: flex;
79
- align-items: center;
80
- justify-content: center;
81
  }
82
-
83
  .main-container {
 
84
  width: 100%;
85
- height: 100%;
86
  display: flex;
87
  flex-direction: column;
88
- transition: opacity 0.3s ease;
89
  }
90
 
91
  #login-view {
 
92
  display: flex;
93
  flex-direction: column;
94
  align-items: center;
95
  justify-content: center;
96
  text-align: center;
97
- width: 100%;
98
- height: 100%;
99
- background: radial-gradient(circle, #1a2a3a 0%, var(--bg-primary) 70%);
100
  }
101
  #login-view img {
102
- width: 100px;
103
- height: 100px;
104
- margin-bottom: 24px;
105
- filter: drop-shadow(0 0 15px rgba(0, 136, 204, 0.5));
106
  }
107
  #login-view h1 {
108
- font-size: 2.8rem;
109
  font-weight: 700;
110
- background: linear-gradient(45deg, var(--accent-blue-light), var(--accent-blue));
111
  -webkit-background-clip: text;
112
  -webkit-text-fill-color: transparent;
113
- margin-bottom: 12px;
114
  }
115
  #login-view p {
116
- font-size: 1.1rem;
117
  color: var(--text-secondary);
118
- margin-bottom: 40px;
 
 
 
 
119
  }
120
 
121
  #app-view {
122
  display: none;
123
- width: 100%;
124
- height: 100%;
 
125
  }
126
 
127
- #chatroom-list-view {
 
 
 
 
 
 
128
  display: flex;
129
  flex-direction: column;
 
 
 
 
 
 
 
130
  height: 100%;
131
- width: 100%;
132
  background-color: var(--bg-secondary);
 
133
  }
134
 
135
  .list-header {
@@ -174,7 +199,7 @@ def index():
174
  .username-input:focus { outline: none; border-color: var(--accent-blue); }
175
 
176
  .action-btn {
177
- background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
178
  color: white;
179
  border: none;
180
  padding: 10px 16px;
@@ -186,10 +211,12 @@ def index():
186
  align-items: center;
187
  justify-content: center;
188
  gap: 8px;
 
 
 
189
  }
190
- .action-btn:hover {
191
- transform: translateY(-2px);
192
- box-shadow: 0 4px 15px rgba(0, 136, 204, 0.3);
193
  }
194
  .action-btn.small {
195
  padding: 8px 12px;
@@ -212,8 +239,8 @@ def index():
212
  .chatroom-item:hover { background-color: var(--bg-hover); }
213
 
214
  .avatar {
215
- width: 40px;
216
- height: 40px;
217
  border-radius: 50%;
218
  display: flex;
219
  align-items: center;
@@ -221,6 +248,7 @@ def index():
221
  font-weight: 600;
222
  color: white;
223
  flex-shrink: 0;
 
224
  }
225
  .chatroom-info {
226
  flex-grow: 1;
@@ -231,6 +259,7 @@ def index():
231
  white-space: nowrap;
232
  overflow: hidden;
233
  text-overflow: ellipsis;
 
234
  }
235
  .lock-icon {
236
  width: 16px;
@@ -240,9 +269,9 @@ def index():
240
  }
241
 
242
  #chat-window-view {
243
- display: none;
244
  flex-direction: column;
245
- height: 100%;
246
  width: 100%;
247
  background-color: var(--bg-primary);
248
  }
@@ -260,7 +289,7 @@ def index():
260
  background: none;
261
  border: none;
262
  cursor: pointer;
263
- display: none;
264
  }
265
  .back-btn svg {
266
  width: 24px;
@@ -279,17 +308,19 @@ def index():
279
  display: flex;
280
  flex-direction: column;
281
  gap: 12px;
 
282
  }
283
 
284
  .message {
285
  display: flex;
286
  gap: 10px;
287
- max-width: 80%;
288
  }
289
  .message .avatar {
290
  width: 36px;
291
  height: 36px;
292
  align-self: flex-end;
 
293
  }
294
  .message-content {
295
  display: flex;
@@ -297,7 +328,7 @@ def index():
297
  gap: 4px;
298
  }
299
  .message-sender {
300
- font-size: 0.8rem;
301
  font-weight: 500;
302
  color: var(--text-secondary);
303
  word-break: break-all;
@@ -308,12 +339,13 @@ def index():
308
  border-radius: 18px;
309
  line-height: 1.4;
310
  word-wrap: break-word;
 
311
  }
312
 
313
  .message.sent { align-self: flex-end; flex-direction: row-reverse; }
314
  .message.sent .message-sender { text-align: right; color: var(--accent-blue-light); }
315
  .message.sent .message-bubble {
316
- background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
317
  color: white;
318
  border-bottom-right-radius: 4px;
319
  }
@@ -334,11 +366,11 @@ def index():
334
  color: var(--text-secondary);
335
  padding: 20px;
336
  }
337
- .chat-placeholder img { width: 80px; margin-bottom: 20px; opacity: 0.5; }
338
 
339
  .message-form {
340
  display: flex;
341
- padding: 16px;
342
  gap: 12px;
343
  background-color: var(--bg-secondary);
344
  border-top: 1px solid var(--border-color);
@@ -346,7 +378,7 @@ def index():
346
  }
347
  #message-input {
348
  flex-grow: 1;
349
- padding: 12px 16px;
350
  border: 1px solid var(--border-color);
351
  background-color: var(--bg-tertiary);
352
  color: var(--text-primary);
@@ -363,9 +395,126 @@ def index():
363
  border-radius: 50%;
364
  flex-shrink: 0;
365
  padding: 0;
 
366
  }
367
  .send-btn svg { width: 20px; height: 20px; fill: white; }
368
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  .modal-overlay {
370
  position: fixed;
371
  top: 0;
@@ -401,6 +550,7 @@ def index():
401
  border-radius: 6px;
402
  font-size: 1rem;
403
  }
 
404
  .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
405
  .modal-btn {
406
  padding: 10px 20px;
@@ -408,12 +558,28 @@ def index():
408
  border: none;
409
  cursor: pointer;
410
  font-weight: 500;
 
 
 
411
  }
 
412
  .secondary-btn { background-color: var(--bg-hover); color: white; }
 
413
 
 
 
 
 
 
 
 
 
 
 
 
414
  #status-bar {
415
  position: fixed;
416
- bottom: 20px;
417
  left: 50%;
418
  transform: translateX(-50%);
419
  background-color: var(--bg-tertiary);
@@ -423,15 +589,21 @@ def index():
423
  font-size: 0.9rem;
424
  opacity: 0;
425
  visibility: hidden;
426
- transition: opacity 0.3s, visibility 0.3s;
427
  z-index: 2000;
428
  box-shadow: 0 5px 15px rgba(0,0,0,0.3);
 
 
429
  }
430
  #status-bar.success { background-color: var(--success-color); }
431
  #status-bar.error { background-color: var(--error-color); }
432
  #status-bar.visible { opacity: 1; visibility: visible; }
433
 
434
  @media (min-width: 768px) {
 
 
 
 
435
  .main-container {
436
  max-width: 1100px;
437
  max-height: 800px;
@@ -439,25 +611,71 @@ def index():
439
  overflow: hidden;
440
  box-shadow: 0 10px 40px rgba(0,0,0,0.3);
441
  border: 1px solid var(--border-color);
 
 
442
  }
 
443
  #app-view {
444
- flex-direction: row;
 
 
 
 
445
  }
 
446
  #chatroom-list-view {
447
  width: 320px;
448
  flex-shrink: 0;
449
  border-right: 1px solid var(--border-color);
450
- display: flex !important;
451
  }
452
  #chat-window-view {
453
  width: auto;
454
  flex-grow: 1;
455
- display: flex !important;
 
 
 
 
 
 
 
 
456
  }
457
- #chat-window-view.hidden-on-desktop {
458
- display: flex !important;
 
 
 
 
 
 
 
 
459
  }
460
- .back-btn { display: none !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
  }
462
  </style>
463
  </head>
@@ -471,60 +689,95 @@ def index():
471
  </div>
472
 
473
  <div id="app-view" class="main-container">
474
- <div id="chatroom-list-view">
475
- <div class="list-header">
476
- <div class="list-header-top">
477
- <h2>Чаты</h2>
478
- <div style="display: flex; align-items: center; gap: 8px;">
479
- <button id="my-profile-btn" class="action-btn small" title="Мой профиль">
480
- <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>
481
- </button>
482
- <button id="scan-qr-btn" class="action-btn small" title="Сканировать QR">
483
- <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>
484
- </button>
485
- <button id="create-room-show-modal" class="action-btn small">
486
- <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>
487
- <span>Новый</span>
488
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
489
  </div>
 
490
  </div>
491
- <div class="user-profile">
492
- <div id="user-wallet"></div>
493
- <div id="user-nickname"></div>
494
- <form class="username-form" id="username-form">
495
- <input type="text" id="username-input" class="username-input" placeholder="Установить никнейм" autocomplete="off">
496
- <button type="submit" class="action-btn small">✓</button>
497
- </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
  </div>
499
  </div>
500
- <div id="chatroom-list"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
  </div>
502
 
503
- <div id="chat-window-view" class="hidden-on-desktop">
504
- <div id="chat-placeholder" class="chat-placeholder">
505
- <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
506
- <h2>Выберите чат</h2>
507
- <p>Начните общение в одном из существующих чатов или создайте свой собственный.</p>
508
  </div>
509
- <div id="active-chat" style="display: none; width: 100%; height: 100%; flex-direction: column;">
510
- <div class="chat-header">
511
- <button class="back-btn" id="back-to-list-btn">
512
- <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>
513
- </button>
514
- <div id="chat-header-avatar" class="avatar"></div>
515
- <span id="chat-header-title"></span>
516
- </div>
517
- <div id="messages-container"></div>
518
- <form id="message-form" class="message-form">
519
- <input type="text" id="message-input" placeholder="Сообщение..." autocomplete="off">
520
- <button type="submit" class="action-btn send-btn" id="send-btn">
521
- <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>
522
- </button>
523
- </form>
524
  </div>
525
  </div>
526
  </div>
527
 
 
528
  <div id="create-room-modal" class="modal-overlay">
529
  <div class="modal-content">
530
  <h3>Создать новый чат</h3>
@@ -556,14 +809,14 @@ def index():
556
  </div>
557
 
558
  <div id="profile-modal" class="modal-overlay">
559
- <div class="modal-content" style="text-align: center;">
560
  <h3 id="profile-modal-title">Профиль пользователя</h3>
561
- <div id="profile-avatar-container" style="margin: 20px auto; display: inline-block;"></div>
562
- <p id="profile-username" style="font-size: 1.2rem; font-weight: 600;"></p>
563
- <p id="profile-address" style="color: var(--text-secondary); font-size: 0.9rem; word-break: break-all; margin-top: 8px;"></p>
564
- <div id="profile-qr-code" style="background: white; padding: 10px; margin: 20px auto; width: fit-content; border-radius: 8px;"></div>
565
- <p style="text-align: center; color: var(--text-secondary); font-size: 0.8rem; margin-top: -10px; margin-bottom: 20px;">Отсканируйте для открытия профиля</p>
566
- <div class="modal-actions" style="flex-direction: column; gap: 12px; align-items: stretch;">
567
  <button id="send-ton-btn" class="modal-btn action-btn">Отправить TON</button>
568
  <button id="profile-close-btn" class="modal-btn secondary-btn">Закрыть</button>
569
  </div>
@@ -573,7 +826,7 @@ def index():
573
  <div id="scanner-modal" class="modal-overlay">
574
  <div class="modal-content">
575
  <h3>Сканировать QR-код</h3>
576
- <div id="qr-reader" style="width: 100%; border: 1px solid var(--border-color); margin-top: 16px; border-radius: 8px; overflow: hidden;"></div>
577
  <div class="modal-actions">
578
  <button id="scanner-close-btn" class="modal-btn secondary-btn">Отмена</button>
579
  </div>
@@ -598,14 +851,30 @@ def index():
598
 
599
  const loginView = document.getElementById('login-view');
600
  const appView = document.getElementById('app-view');
 
601
  const chatroomListView = document.getElementById('chatroom-list-view');
602
  const chatWindowView = document.getElementById('chat-window-view');
 
603
  const chatPlaceholder = document.getElementById('chat-placeholder');
604
  const activeChat = document.getElementById('active-chat');
 
 
 
 
 
 
605
  const profileModal = document.getElementById('profile-modal');
606
  const scannerModal = document.getElementById('scanner-modal');
607
 
608
- const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292'];
 
 
 
 
 
 
 
 
609
 
610
  const getAvatar = (name) => {
611
  const initial = (name ? name[0] : '?').toUpperCase();
@@ -690,6 +959,8 @@ def index():
690
  updateUserInfo();
691
  loginView.style.display = 'none';
692
  appView.style.display = 'flex';
 
 
693
  fetchChatrooms();
694
  };
695
 
@@ -764,34 +1035,69 @@ def index():
764
  container.appendChild(msgDiv);
765
  });
766
 
767
- if(shouldScroll) {
768
  container.scrollTop = container.scrollHeight;
769
  }
770
  };
771
 
772
  const fetchMessages = async (roomId) => {
 
773
  try {
774
  const data = await apiCall(`/api/messages/${roomId}`);
775
  renderMessages(data.messages);
776
  } catch (err) {
777
- if (messagePollingInterval) clearInterval(messagePollingInterval);
778
  }
779
  };
780
 
781
  const showChatView = () => {
782
- chatWindowView.style.display = 'flex';
783
- if (window.innerWidth < 768) {
784
- chatroomListView.style.display = 'none';
785
- }
786
- };
787
 
788
- const showListView = () => {
789
- chatroomListView.style.display = 'flex';
790
  if (window.innerWidth < 768) {
791
- chatWindowView.style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
792
  }
793
  };
794
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
795
  const selectChatroom = (roomId, isPrivate) => {
796
  const roomData = chatroomsData[roomId];
797
  if (!roomData) return;
@@ -807,23 +1113,26 @@ def index():
807
 
808
  chatPlaceholder.style.display = 'none';
809
  activeChat.style.display = 'flex';
810
- showChatView();
811
 
 
 
812
  fetchMessages(roomId);
813
  messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000);
814
  };
815
 
816
  if (isPrivate) {
817
- const passwordModal = document.getElementById('password-modal');
818
  const passwordForm = document.getElementById('password-form');
819
  const passwordInput = document.getElementById('password-input');
 
820
  passwordModal.style.display = 'flex';
821
  passwordInput.value = '';
822
  passwordInput.focus();
823
 
824
- const formSubmitHandler = async (e) => {
 
 
825
  e.preventDefault();
826
- passwordForm.removeEventListener('submit', formSubmitHandler);
827
  const password = passwordInput.value;
828
  passwordModal.style.display = 'none';
829
  try {
@@ -833,15 +1142,21 @@ def index():
833
  body: JSON.stringify({ chatroom_id: roomId, password })
834
  });
835
  proceedToRoom();
836
- } catch (err) {}
 
 
 
 
837
  };
838
- passwordForm.addEventListener('submit', formSubmitHandler);
839
 
840
  document.getElementById('password-cancel').onclick = () => {
841
  passwordModal.style.display = 'none';
842
- passwordForm.removeEventListener('submit', formSubmitHandler);
843
  };
 
844
  } else {
 
845
  proceedToRoom();
846
  }
847
  };
@@ -851,7 +1166,7 @@ def index():
851
  const input = document.getElementById('message-input');
852
  const sendBtn = document.getElementById('send-btn');
853
  const text = input.value.trim();
854
- if (text && activeChatroomId) {
855
  input.value = '';
856
  input.disabled = true;
857
  sendBtn.disabled = true;
@@ -865,8 +1180,36 @@ def index():
865
  text: text
866
  })
867
  });
868
- await fetchMessages(activeChatroomId);
869
- document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
870
  } finally {
871
  input.disabled = false;
872
  sendBtn.disabled = false;
@@ -875,7 +1218,6 @@ def index():
875
  }
876
  });
877
 
878
- const createRoomModal = document.getElementById('create-room-modal');
879
  document.getElementById('create-room-show-modal').addEventListener('click', () => {
880
  createRoomModal.style.display = 'flex';
881
  document.getElementById('create-room-form').reset();
@@ -887,7 +1229,15 @@ def index():
887
  e.preventDefault();
888
  const name = document.getElementById('room-name').value.trim();
889
  const password = document.getElementById('room-password').value;
890
- if (!name) return;
 
 
 
 
 
 
 
 
891
 
892
  try {
893
  await apiCall('/api/create_chatroom', {
@@ -909,7 +1259,7 @@ def index():
909
  body: JSON.stringify({ address: address })
910
  });
911
 
912
- const username = userData.username || `User ${truncateAddress(address)}`;
913
  const avatarContainer = document.getElementById('profile-avatar-container');
914
  const usernameEl = document.getElementById('profile-username');
915
  const addressEl = document.getElementById('profile-address');
@@ -924,17 +1274,31 @@ def index():
924
 
925
  qrCodeEl.innerHTML = '';
926
  if (profileQrCode) {
927
- profileQrCode.clear();
 
 
 
 
 
 
928
  }
929
- profileQrCode = new QRCode(qrCodeEl, {
930
- text: address,
931
- width: 150,
932
- height: 150,
933
- colorDark : "#000000",
934
- colorLight : "#ffffff",
935
- correctLevel : QRCode.CorrectLevel.H
936
- });
937
-
 
 
 
 
 
 
 
 
938
  sendTonBtn.onclick = async () => {
939
  if (!tonConnectUI.connected) {
940
  showStatus('Подключите кошелек для отправки TON.', 'error');
@@ -965,7 +1329,7 @@ def index():
965
  }
966
  };
967
 
968
- sendTonBtn.style.display = (address === currentUser.address) ? 'none' : 'block';
969
  profileModal.style.display = 'flex';
970
  } catch (err) {
971
  showStatus('Не удалось загрузить профиль.', 'error');
@@ -977,17 +1341,20 @@ def index():
977
  html5QrCode = new Html5Qrcode("qr-reader");
978
  const qrCodeSuccessCallback = (decodedText, decodedResult) => {
979
  hideScanner();
980
- if (decodedText && decodedText.length > 40 && (decodedText.startsWith('EQ') || decodedText.startsWith('UQ'))) {
981
- showProfile(decodedText);
 
982
  } else {
983
- showStatus('Отсканирован недействительный QR-код.', 'error');
984
  }
985
  };
986
  const config = { fps: 10, qrbox: { width: 250, height: 250 } };
 
987
  html5QrCode.start({ facingMode: "environment" }, config, qrCodeSuccessCallback)
988
  .catch(err => {
989
- showStatus('Не удалось запустить сканер.', 'error');
990
- hideScanner();
 
991
  });
992
  };
993
 
@@ -998,31 +1365,121 @@ def index():
998
  scannerModal.style.display = 'none';
999
  };
1000
 
 
1001
  document.getElementById('my-profile-btn').addEventListener('click', () => {
1002
  if (currentUser.address) showProfile(currentUser.address);
 
1003
  });
1004
- document.getElementById('scan-qr-btn').addEventListener('click', showScanner);
 
 
 
 
 
 
 
 
 
 
 
1005
  document.getElementById('profile-close-btn').addEventListener('click', () => profileModal.style.display = 'none');
1006
  document.getElementById('scanner-close-btn').addEventListener('click', hideScanner);
1007
- document.getElementById('back-to-list-btn').addEventListener('click', showListView);
 
 
 
 
 
 
1008
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1009
  const handleResize = () => {
1010
  const isMobile = window.innerWidth < 768;
1011
- document.getElementById('back-to-list-btn').style.display = isMobile ? 'block' : 'none';
1012
- if (!isMobile) {
1013
- chatroomListView.style.display = 'flex';
1014
- chatWindowView.style.display = 'flex';
1015
- } else {
1016
- if (activeChat.style.display === 'flex') {
1017
- chatroomListView.style.display = 'none';
 
 
 
 
 
 
 
1018
  } else {
1019
- chatWindowView.style.display = 'none';
 
 
 
1020
  }
1021
  }
 
 
 
 
 
 
 
 
 
1022
  };
1023
 
1024
  window.addEventListener('resize', handleResize);
1025
- handleResize();
 
 
 
1026
 
1027
  tonConnectUI.onStatusChange(wallet => {
1028
  if (wallet) {
@@ -1032,8 +1489,14 @@ def index():
1032
  currentUser = { address: null, username: null };
1033
  appView.style.display = 'none';
1034
  loginView.style.display = 'flex';
 
1035
  if (messagePollingInterval) clearInterval(messagePollingInterval);
1036
  activeChatroomId = null;
 
 
 
 
 
1037
  }
1038
  });
1039
  });
@@ -1080,7 +1543,7 @@ def get_chatrooms():
1080
  chatrooms_list.append({
1081
  'id': room_id,
1082
  'name': room_data['name'],
1083
- 'is_private': room_data['is_private']
1084
  })
1085
  return jsonify({'chatrooms': sorted(chatrooms_list, key=lambda x: x['name'])})
1086
 
@@ -1096,6 +1559,7 @@ def create_chatroom():
1096
  db = read_db()
1097
  room_id = str(uuid.uuid4())
1098
  db['chatrooms'][room_id] = {
 
1099
  'name': name,
1100
  'creator': creator_address,
1101
  'is_private': bool(password),
@@ -1153,6 +1617,10 @@ def send_message():
1153
  db = read_db()
1154
  if chatroom_id not in db['messages']:
1155
  return jsonify({'error': 'Chatroom not found'}), 404
 
 
 
 
1156
 
1157
  message = {
1158
  'id': str(uuid.uuid4()),
@@ -1161,6 +1629,7 @@ def send_message():
1161
  'timestamp': datetime.utcnow().isoformat() + "Z"
1162
  }
1163
 
 
1164
  if len(db['messages'][chatroom_id]) >= 100:
1165
  db['messages'][chatroom_id].pop(0)
1166
 
 
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
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
48
  <style>
49
  :root {
50
+ --bg-primary: #0E0E11;
51
+ --bg-secondary: #17171A;
52
+ --bg-tertiary: #222227;
53
+ --bg-hover: #2B2B33;
54
  --text-primary: #EAECEF;
55
  --text-secondary: #8E9297;
56
+ --accent-blue: #007AFF; /* iOS Blue */
57
+ --accent-blue-dark: #005BBB; /* Deeper Blue */
58
+ --accent-blue-light: #34C759; /* Greenish Blue */
59
+ --ton-blue: #0088CC;
60
+ --ton-teal: #00BFFF;
61
+ --border-color: #323239;
62
+ --success-color: #34C759;
63
+ --error-color: #FF3B30;
64
  --font-family: 'Inter', sans-serif;
65
+ --nav-height: 60px;
66
  }
67
 
68
  * {
 
84
  height: 100vh;
85
  width: 100vw;
86
  display: flex;
87
+ flex-direction: column;
 
88
  }
89
+
90
  .main-container {
91
+ flex-grow: 1;
92
  width: 100%;
93
+ overflow: hidden;
94
  display: flex;
95
  flex-direction: column;
 
96
  }
97
 
98
  #login-view {
99
+ flex-grow: 1;
100
  display: flex;
101
  flex-direction: column;
102
  align-items: center;
103
  justify-content: center;
104
  text-align: center;
105
+ background: radial-gradient(circle, rgba(0, 136, 204, 0.1) 0%, var(--bg-primary) 70%);
106
+ padding: 20px;
 
107
  }
108
  #login-view img {
109
+ width: 120px;
110
+ height: 120px;
111
+ margin-bottom: 32px;
112
+ filter: drop-shadow(0 0 20px rgba(0, 136, 204, 0.6));
113
  }
114
  #login-view h1 {
115
+ font-size: 3.5rem;
116
  font-weight: 700;
117
+ background: linear-gradient(45deg, var(--ton-blue), var(--ton-teal));
118
  -webkit-background-clip: text;
119
  -webkit-text-fill-color: transparent;
120
+ margin-bottom: 16px;
121
  }
122
  #login-view p {
123
+ font-size: 1.2rem;
124
  color: var(--text-secondary);
125
+ margin-bottom: 50px;
126
+ }
127
+ #ton-connect-button {
128
+ --tc-button-height: 50px;
129
+ --tc-button-border-radius: 12px;
130
  }
131
 
132
  #app-view {
133
  display: none;
134
+ flex-direction: column;
135
+ flex-grow: 1;
136
+ height: calc(100vh - var(--nav-height)); /* Adjust for nav bar */
137
  }
138
 
139
+ #content-container {
140
+ flex-grow: 1;
141
+ display: flex;
142
+ overflow: hidden;
143
+ }
144
+
145
+ #chat-container {
146
  display: flex;
147
  flex-direction: column;
148
+ flex-grow: 1;
149
+ width: 100%; /* Start with full width, media query changes */
150
+ }
151
+
152
+ #chatroom-list-view {
153
+ display: none; /* Hidden by default on mobile */
154
+ flex-direction: column;
155
  height: 100%;
 
156
  background-color: var(--bg-secondary);
157
+ width: 100%;
158
  }
159
 
160
  .list-header {
 
199
  .username-input:focus { outline: none; border-color: var(--accent-blue); }
200
 
201
  .action-btn {
202
+ background: linear-gradient(45deg, var(--ton-blue), var(--ton-teal));
203
  color: white;
204
  border: none;
205
  padding: 10px 16px;
 
211
  align-items: center;
212
  justify-content: center;
213
  gap: 8px;
214
+ -webkit-user-select: none;
215
+ -ms-user-select: none;
216
+ user-select: none;
217
  }
218
+ .action-btn:active {
219
+ transform: scale(0.98);
 
220
  }
221
  .action-btn.small {
222
  padding: 8px 12px;
 
239
  .chatroom-item:hover { background-color: var(--bg-hover); }
240
 
241
  .avatar {
242
+ width: 44px;
243
+ height: 44px;
244
  border-radius: 50%;
245
  display: flex;
246
  align-items: center;
 
248
  font-weight: 600;
249
  color: white;
250
  flex-shrink: 0;
251
+ font-size: 1.2rem;
252
  }
253
  .chatroom-info {
254
  flex-grow: 1;
 
259
  white-space: nowrap;
260
  overflow: hidden;
261
  text-overflow: ellipsis;
262
+ font-size: 1.1rem;
263
  }
264
  .lock-icon {
265
  width: 16px;
 
269
  }
270
 
271
  #chat-window-view {
272
+ display: none; /* Hidden by default on mobile */
273
  flex-direction: column;
274
+ flex-grow: 1;
275
  width: 100%;
276
  background-color: var(--bg-primary);
277
  }
 
289
  background: none;
290
  border: none;
291
  cursor: pointer;
292
+ display: none; /* Hidden by default, shown on mobile chat */
293
  }
294
  .back-btn svg {
295
  width: 24px;
 
308
  display: flex;
309
  flex-direction: column;
310
  gap: 12px;
311
+ -webkit-overflow-scrolling: touch;
312
  }
313
 
314
  .message {
315
  display: flex;
316
  gap: 10px;
317
+ max-width: 85%;
318
  }
319
  .message .avatar {
320
  width: 36px;
321
  height: 36px;
322
  align-self: flex-end;
323
+ font-size: 1rem;
324
  }
325
  .message-content {
326
  display: flex;
 
328
  gap: 4px;
329
  }
330
  .message-sender {
331
+ font-size: 0.85rem;
332
  font-weight: 500;
333
  color: var(--text-secondary);
334
  word-break: break-all;
 
339
  border-radius: 18px;
340
  line-height: 1.4;
341
  word-wrap: break-word;
342
+ font-size: 1rem;
343
  }
344
 
345
  .message.sent { align-self: flex-end; flex-direction: row-reverse; }
346
  .message.sent .message-sender { text-align: right; color: var(--accent-blue-light); }
347
  .message.sent .message-bubble {
348
+ background: linear-gradient(to right bottom, var(--accent-blue), var(--accent-blue-dark));
349
  color: white;
350
  border-bottom-right-radius: 4px;
351
  }
 
366
  color: var(--text-secondary);
367
  padding: 20px;
368
  }
369
+ .chat-placeholder img { width: 80px; margin-bottom: 20px; opacity: 0.5; filter: grayscale(100%); }
370
 
371
  .message-form {
372
  display: flex;
373
+ padding: 8px 16px;
374
  gap: 12px;
375
  background-color: var(--bg-secondary);
376
  border-top: 1px solid var(--border-color);
 
378
  }
379
  #message-input {
380
  flex-grow: 1;
381
+ padding: 10px 16px;
382
  border: 1px solid var(--border-color);
383
  background-color: var(--bg-tertiary);
384
  color: var(--text-primary);
 
395
  border-radius: 50%;
396
  flex-shrink: 0;
397
  padding: 0;
398
+ background: linear-gradient(45deg, var(--accent-blue-dark), var(--accent-blue-light));
399
  }
400
  .send-btn svg { width: 20px; height: 20px; fill: white; }
401
 
402
+ /* Browser View */
403
+ #browser-view {
404
+ display: none; /* Hidden by default */
405
+ flex-direction: column;
406
+ flex-grow: 1;
407
+ width: 100%;
408
+ }
409
+ .browser-bar {
410
+ display: flex;
411
+ padding: 8px 16px;
412
+ gap: 8px;
413
+ background-color: var(--bg-secondary);
414
+ border-bottom: 1px solid var(--border-color);
415
+ align-items: center;
416
+ flex-shrink: 0;
417
+ }
418
+ .browser-bar input[type="text"] {
419
+ flex-grow: 1;
420
+ padding: 8px 12px;
421
+ border: 1px solid var(--border-color);
422
+ background-color: var(--bg-tertiary);
423
+ color: var(--text-primary);
424
+ border-radius: 6px;
425
+ font-size: 0.9rem;
426
+ }
427
+ .browser-bar input[type="text"]:focus { outline: none; border-color: var(--accent-blue); }
428
+ .browser-bar button {
429
+ background: var(--bg-hover);
430
+ border: none;
431
+ color: var(--text-secondary);
432
+ padding: 8px;
433
+ border-radius: 6px;
434
+ cursor: pointer;
435
+ transition: background-color 0.2s ease;
436
+ }
437
+ .browser-bar button:active { transform: scale(0.95); }
438
+ .browser-bar button svg { width: 20px; height: 20px; fill: currentColor; }
439
+ #browser-iframe {
440
+ flex-grow: 1;
441
+ width: 100%;
442
+ border: none;
443
+ }
444
+
445
+
446
+ /* Bottom Navigation */
447
+ #bottom-nav {
448
+ position: fixed;
449
+ bottom: 0;
450
+ left: 0;
451
+ width: 100%;
452
+ height: var(--nav-height);
453
+ background-color: var(--bg-secondary);
454
+ display: flex;
455
+ justify-content: space-around;
456
+ align-items: center;
457
+ border-top: 1px solid var(--border-color);
458
+ z-index: 500; /* Below modals, above main content */
459
+ padding-bottom: env(safe-area-inset-bottom); /* Handle iPhone notches */
460
+ }
461
+ .nav-btn {
462
+ flex-grow: 1;
463
+ display: flex;
464
+ flex-direction: column;
465
+ align-items: center;
466
+ justify-content: center;
467
+ color: var(--text-secondary);
468
+ font-size: 0.7rem;
469
+ cursor: pointer;
470
+ padding: 4px 0;
471
+ transition: color 0.2s ease;
472
+ -webkit-user-select: none;
473
+ -ms-user-select: none;
474
+ user-select: none;
475
+ }
476
+ .nav-btn svg {
477
+ width: 24px;
478
+ height: 24px;
479
+ margin-bottom: 2px;
480
+ fill: currentColor;
481
+ transition: fill 0.2s ease;
482
+ }
483
+ .nav-btn:active { transform: scale(0.95); }
484
+
485
+ .nav-btn.active {
486
+ color: var(--ton-blue);
487
+ }
488
+ .nav-btn.active svg {
489
+ fill: var(--ton-blue);
490
+ }
491
+
492
+ #nav-scan-btn {
493
+ background: linear-gradient(45deg, var(--ton-blue), var(--ton-teal));
494
+ border-radius: 50%;
495
+ width: 56px;
496
+ height: 56px;
497
+ display: flex;
498
+ align-items: center;
499
+ justify-content: center;
500
+ position: relative;
501
+ bottom: 8px;
502
+ box-shadow: 0 4px 10px rgba(0, 136, 204, 0.5);
503
+ flex-shrink: 0;
504
+ -webkit-user-select: none;
505
+ -ms-user-select: none;
506
+ user-select: none;
507
+ }
508
+ #nav-scan-btn:active { transform: scale(0.9); }
509
+ #nav-scan-btn svg {
510
+ width: 28px;
511
+ height: 28px;
512
+ fill: white;
513
+ margin-bottom: 0;
514
+ }
515
+
516
+
517
+ /* Modals */
518
  .modal-overlay {
519
  position: fixed;
520
  top: 0;
 
550
  border-radius: 6px;
551
  font-size: 1rem;
552
  }
553
+ .modal-content input:focus { outline: none; border-color: var(--accent-blue); }
554
  .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
555
  .modal-btn {
556
  padding: 10px 20px;
 
558
  border: none;
559
  cursor: pointer;
560
  font-weight: 500;
561
+ -webkit-user-select: none;
562
+ -ms-user-select: none;
563
+ user-select: none;
564
  }
565
+ .modal-btn:active { transform: scale(0.95); }
566
  .secondary-btn { background-color: var(--bg-hover); color: white; }
567
+ .modal-btn.action-btn { background: linear-gradient(45deg, var(--ton-blue), var(--ton-teal)); color: white; }
568
 
569
+ #profile-modal .modal-content { text-align: center; }
570
+ #profile-modal .avatar { margin: 20px auto; width: 60px; height: 60px; font-size: 1.8rem; }
571
+ #profile-modal #profile-username { font-size: 1.2rem; font-weight: 600; margin-bottom: 8px; }
572
+ #profile-modal #profile-address { color: var(--text-secondary); font-size: 0.9rem; word-break: break-all; margin-top: 0; }
573
+ #profile-modal #profile-qr-code { background: white; padding: 10px; margin: 20px auto; width: fit-content; border-radius: 8px; }
574
+ #profile-modal .qr-label { text-align: center; color: var(--text-secondary); font-size: 0.8rem; margin-top: -10px; margin-bottom: 20px; }
575
+ #profile-modal .modal-actions { flex-direction: column; gap: 12px; align-items: stretch; }
576
+
577
+
578
+ #scanner-modal #qr-reader { width: 100%; border: 1px solid var(--border-color); margin-top: 16px; border-radius: 8px; overflow: hidden; }
579
+
580
  #status-bar {
581
  position: fixed;
582
+ bottom: calc(var(--nav-height) + 10px); /* Position above nav bar */
583
  left: 50%;
584
  transform: translateX(-50%);
585
  background-color: var(--bg-tertiary);
 
589
  font-size: 0.9rem;
590
  opacity: 0;
591
  visibility: hidden;
592
+ transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
593
  z-index: 2000;
594
  box-shadow: 0 5px 15px rgba(0,0,0,0.3);
595
+ max-width: 90%;
596
+ text-align: center;
597
  }
598
  #status-bar.success { background-color: var(--success-color); }
599
  #status-bar.error { background-color: var(--error-color); }
600
  #status-bar.visible { opacity: 1; visibility: visible; }
601
 
602
  @media (min-width: 768px) {
603
+ body {
604
+ justify-content: center;
605
+ align-items: center;
606
+ }
607
  .main-container {
608
  max-width: 1100px;
609
  max-height: 800px;
 
611
  overflow: hidden;
612
  box-shadow: 0 10px 40px rgba(0,0,0,0.3);
613
  border: 1px solid var(--border-color);
614
+ height: 100%;
615
+ flex-grow: 0;
616
  }
617
+
618
  #app-view {
619
+ height: 100%;
620
+ }
621
+
622
+ #content-container {
623
+ flex-direction: row;
624
  }
625
+
626
  #chatroom-list-view {
627
  width: 320px;
628
  flex-shrink: 0;
629
  border-right: 1px solid var(--border-color);
630
+ display: flex !important; /* Always show on desktop */
631
  }
632
  #chat-window-view {
633
  width: auto;
634
  flex-grow: 1;
635
+ display: flex !important; /* Always show on desktop */
636
+ }
637
+
638
+ #browser-view {
639
+ display: flex; /* Always show on desktop when active */
640
+ }
641
+
642
+ #chat-container {
643
+ flex-direction: row; /* Side-by-side on desktop */
644
  }
645
+
646
+ #bottom-nav {
647
+ position: static; /* Not fixed */
648
+ width: auto;
649
+ height: auto;
650
+ background-color: transparent;
651
+ border-top: none;
652
+ z-index: auto;
653
+ padding-bottom: 0;
654
+ margin-top: 20px; /* Example spacing if needed */
655
  }
656
+ .nav-btn {
657
+ display: none; /* Hide text nav buttons */
658
+ }
659
+ #nav-scan-btn {
660
+ display: none !important; /* Hide the large central scan button */
661
+ }
662
+ .list-header-top {
663
+ margin-bottom: 12px; /* Less space needed */
664
+ }
665
+ .list-header-top h2 { font-size: 1.8rem; }
666
+ .user-profile { margin-bottom: 20px; }
667
+ .chat-header { padding: 16px; }
668
+ #chat-header-title { font-size: 1.4rem; }
669
+ .message-form { padding: 12px 16px; }
670
+ #message-input { padding: 14px 18px; }
671
+ .send-btn { width: 48px; height: 48px; }
672
+ .send-btn svg { width: 24px; height: 24px; }
673
+
674
+ .back-btn { display: none !important; } /* Hide back button on desktop */
675
+
676
+ #status-bar {
677
+ bottom: 20px; /* Back to standard bottom position */
678
+ }
679
  }
680
  </style>
681
  </head>
 
689
  </div>
690
 
691
  <div id="app-view" class="main-container">
692
+ <div id="content-container">
693
+
694
+ <!-- Chat Views -->
695
+ <div id="chat-container">
696
+ <div id="chatroom-list-view">
697
+ <div class="list-header">
698
+ <div class="list-header-top">
699
+ <h2>Чаты</h2>
700
+ <div style="display: flex; align-items: center; gap: 8px;">
701
+ <button id="my-profile-btn" class="action-btn small" title="Мой профиль">
702
+ <svg xmlns="http://www.w3.org/2000/svg" height="16" viewBox="0 0 24 24" width="16" fill="white"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
703
+ </button>
704
+ <!-- QR Scan button moved to bottom nav -->
705
+ <button id="create-room-show-modal" class="action-btn small">
706
+ <svg xmlns="http://www.w3.org/2000/svg" height="16" viewBox="0 0 24 24" width="16" fill="white"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
707
+ <span>Новый</span>
708
+ </button>
709
+ </div>
710
+ </div>
711
+ <div class="user-profile">
712
+ <div id="user-wallet"></div>
713
+ <div id="user-nickname"></div>
714
+ <form class="username-form" id="username-form">
715
+ <input type="text" id="username-input" class="username-input" placeholder="Установить никнейм" autocomplete="off">
716
+ <button type="submit" class="action-btn small">✓</button>
717
+ </form>
718
+ </div>
719
  </div>
720
+ <div id="chatroom-list"></div>
721
  </div>
722
+
723
+ <div id="chat-window-view">
724
+ <div id="chat-placeholder" class="chat-placeholder">
725
+ <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
726
+ <h2>Выберите чат</h2>
727
+ <p>Начните общение в одном из существующих чатов или создайте свой собственный.</p>
728
+ </div>
729
+ <div id="active-chat" style="display: none; width: 100%; height: 100%; flex-direction: column;">
730
+ <div class="chat-header">
731
+ <button class="back-btn" id="back-to-list-btn">
732
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
733
+ </button>
734
+ <div id="chat-header-avatar" class="avatar"></div>
735
+ <span id="chat-header-title"></span>
736
+ </div>
737
+ <div id="messages-container"></div>
738
+ <form id="message-form" class="message-form">
739
+ <input type="text" id="message-input" placeholder="Сообщение..." autocomplete="off">
740
+ <button type="submit" class="action-btn send-btn" id="send-btn">
741
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24px" height="24px"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
742
+ </button>
743
+ </form>
744
+ </div>
745
  </div>
746
  </div>
747
+
748
+ <!-- Browser View -->
749
+ <div id="browser-view">
750
+ <div class="browser-bar">
751
+ <button id="browser-back"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20z"/></svg></button>
752
+ <button id="browser-forward"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 11h12.17l-5.58-5.59L12 4l8 8-8 8-1.41-1.41L16.17 13H4z"/></svg></button>
753
+ <button id="browser-home"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg></button>
754
+ <form id="browser-form" style="flex-grow: 1; display: flex; gap: 8px;">
755
+ <input type="text" id="browser-url" value="https://www.google.com/" placeholder="Введите адрес или запрос">
756
+ <button type="submit"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg></button>
757
+ </form>
758
+ </div>
759
+ <iframe id="browser-iframe" src="https://www.google.com/"></iframe>
760
+ </div>
761
+
762
  </div>
763
 
764
+ <!-- Bottom Navigation Bar -->
765
+ <div id="bottom-nav">
766
+ <div class="nav-btn active" data-view="chat">
767
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM6 9h12v2H6V9zm8 6H6v-2h8v2zm4-4H6V7h12v4z"/></svg>
768
+ <span>Чаты</span>
769
  </div>
770
+ <div id="nav-scan-btn">
771
+ <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" fill="white"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M3 11h8V3H3v8zm2-6h4v4H5V5zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm8-12v8h8V3h-8zm6 6h-4V5h4v4zm-2 10a2 2 0 100-4 2 2 0 000 4z"/></svg>
772
+ </div>
773
+ <div class="nav-btn" data-view="browser">
774
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93s3.05-7.44 7-7.93v15.86zm2 0V4.07c3.95.49 7 3.85 7 7.93s-3.05 7.44-7 7.93z"/></svg>
775
+ <span>Браузер</span>
 
 
 
 
 
 
 
 
 
776
  </div>
777
  </div>
778
  </div>
779
 
780
+ <!-- Modals (outside main-container flow) -->
781
  <div id="create-room-modal" class="modal-overlay">
782
  <div class="modal-content">
783
  <h3>Создать новый чат</h3>
 
809
  </div>
810
 
811
  <div id="profile-modal" class="modal-overlay">
812
+ <div class="modal-content">
813
  <h3 id="profile-modal-title">Профиль пользователя</h3>
814
+ <div id="profile-avatar-container"></div>
815
+ <p id="profile-username"></p>
816
+ <p id="profile-address"></p>
817
+ <div id="profile-qr-code"></div>
818
+ <p class="qr-label">Отсканируйте для открытия профиля</p>
819
+ <div class="modal-actions">
820
  <button id="send-ton-btn" class="modal-btn action-btn">Отправить TON</button>
821
  <button id="profile-close-btn" class="modal-btn secondary-btn">Закрыть</button>
822
  </div>
 
826
  <div id="scanner-modal" class="modal-overlay">
827
  <div class="modal-content">
828
  <h3>Сканировать QR-код</h3>
829
+ <div id="qr-reader"></div>
830
  <div class="modal-actions">
831
  <button id="scanner-close-btn" class="modal-btn secondary-btn">Отмена</button>
832
  </div>
 
851
 
852
  const loginView = document.getElementById('login-view');
853
  const appView = document.getElementById('app-view');
854
+ const chatContainer = document.getElementById('chat-container');
855
  const chatroomListView = document.getElementById('chatroom-list-view');
856
  const chatWindowView = document.getElementById('chat-window-view');
857
+ const browserView = document.getElementById('browser-view');
858
  const chatPlaceholder = document.getElementById('chat-placeholder');
859
  const activeChat = document.getElementById('active-chat');
860
+ const bottomNav = document.getElementById('bottom-nav');
861
+ const navButtons = bottomNav.querySelectorAll('.nav-btn');
862
+ const navScanBtn = document.getElementById('nav-scan-btn');
863
+
864
+ const createRoomModal = document.getElementById('create-room-modal');
865
+ const passwordModal = document.getElementById('password-modal');
866
  const profileModal = document.getElementById('profile-modal');
867
  const scannerModal = document.getElementById('scanner-modal');
868
 
869
+ const browserIframe = document.getElementById('browser-iframe');
870
+ const browserUrlInput = document.getElementById('browser-url');
871
+ const browserForm = document.getElementById('browser-form');
872
+ const browserBackBtn = document.getElementById('browser-back');
873
+ const browserForwardBtn = document.getElementById('browser-forward');
874
+ const browserHomeBtn = document.getElementById('browser-home');
875
+
876
+
877
+ const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292', '#a1887f', '#ff8a65'];
878
 
879
  const getAvatar = (name) => {
880
  const initial = (name ? name[0] : '?').toUpperCase();
 
959
  updateUserInfo();
960
  loginView.style.display = 'none';
961
  appView.style.display = 'flex';
962
+ bottomNav.style.display = 'flex';
963
+ switchView('chat'); // Default view is chat
964
  fetchChatrooms();
965
  };
966
 
 
1035
  container.appendChild(msgDiv);
1036
  });
1037
 
1038
+ if(shouldScroll || messages.length === 0) {
1039
  container.scrollTop = container.scrollHeight;
1040
  }
1041
  };
1042
 
1043
  const fetchMessages = async (roomId) => {
1044
+ if (!currentUser.address) return;
1045
  try {
1046
  const data = await apiCall(`/api/messages/${roomId}`);
1047
  renderMessages(data.messages);
1048
  } catch (err) {
1049
+ console.error("Failed to fetch messages:", err);
1050
  }
1051
  };
1052
 
1053
  const showChatView = () => {
1054
+ chatContainer.style.display = 'flex';
1055
+ browserView.style.display = 'none';
 
 
 
1056
 
 
 
1057
  if (window.innerWidth < 768) {
1058
+ // On mobile, determine whether to show list or window
1059
+ if (activeChatroomId && activeChat.style.display === 'flex') {
1060
+ chatroomListView.style.display = 'none';
1061
+ chatWindowView.style.display = 'flex';
1062
+ document.getElementById('back-to-list-btn').style.display = 'block';
1063
+ } else {
1064
+ chatroomListView.style.display = 'flex';
1065
+ chatWindowView.style.display = 'none';
1066
+ document.getElementById('back-to-list-btn').style.display = 'none';
1067
+ }
1068
+ } else {
1069
+ // On desktop, always show both
1070
+ chatroomListView.style.display = 'flex';
1071
+ chatWindowView.style.display = 'flex';
1072
+ document.getElementById('back-to-list-btn').style.display = 'none';
1073
  }
1074
  };
1075
 
1076
+ const showBrowserView = () => {
1077
+ chatContainer.style.display = 'none';
1078
+ browserView.style.display = 'flex';
1079
+ if (messagePollingInterval) clearInterval(messagePollingInterval);
1080
+ activeChatroomId = null;
1081
+ };
1082
+
1083
+ const switchView = (viewId) => {
1084
+ navButtons.forEach(btn => btn.classList.remove('active'));
1085
+ let targetView = null;
1086
+
1087
+ if (viewId === 'chat') {
1088
+ showChatView();
1089
+ targetView = document.querySelector('.nav-btn[data-view="chat"]');
1090
+ } else if (viewId === 'browser') {
1091
+ showBrowserView();
1092
+ targetView = document.querySelector('.nav-btn[data-view="browser"]');
1093
+ }
1094
+
1095
+ if (targetView) {
1096
+ targetView.classList.add('active');
1097
+ }
1098
+ };
1099
+
1100
+
1101
  const selectChatroom = (roomId, isPrivate) => {
1102
  const roomData = chatroomsData[roomId];
1103
  if (!roomData) return;
 
1113
 
1114
  chatPlaceholder.style.display = 'none';
1115
  activeChat.style.display = 'flex';
 
1116
 
1117
+ showChatView(); // Ensure chat view is active and handle mobile/desktop split
1118
+
1119
  fetchMessages(roomId);
1120
  messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000);
1121
  };
1122
 
1123
  if (isPrivate) {
 
1124
  const passwordForm = document.getElementById('password-form');
1125
  const passwordInput = document.getElementById('password-input');
1126
+
1127
  passwordModal.style.display = 'flex';
1128
  passwordInput.value = '';
1129
  passwordInput.focus();
1130
 
1131
+ // Use a flag to prevent multiple listeners if modal is shown multiple times
1132
+ let passwordFormListener = null;
1133
+ passwordFormListener = async (e) => {
1134
  e.preventDefault();
1135
+ passwordForm.removeEventListener('submit', passwordFormListener); // Remove listener after submit
1136
  const password = passwordInput.value;
1137
  passwordModal.style.display = 'none';
1138
  try {
 
1142
  body: JSON.stringify({ chatroom_id: roomId, password })
1143
  });
1144
  proceedToRoom();
1145
+ } catch (err) {
1146
+ showStatus('Неверный пароль', 'error');
1147
+ // Re-attach listener if join failed and modal might be shown again
1148
+ // This part is tricky; a better approach might be a dedicated join attempt handler
1149
+ }
1150
  };
1151
+ passwordForm.addEventListener('submit', passwordFormListener);
1152
 
1153
  document.getElementById('password-cancel').onclick = () => {
1154
  passwordModal.style.display = 'none';
1155
+ passwordForm.removeEventListener('submit', passwordFormListener); // Clean up listener on cancel
1156
  };
1157
+
1158
  } else {
1159
+ // Auto-join public rooms
1160
  proceedToRoom();
1161
  }
1162
  };
 
1166
  const input = document.getElementById('message-input');
1167
  const sendBtn = document.getElementById('send-btn');
1168
  const text = input.value.trim();
1169
+ if (text && activeChatroomId && currentUser.address) {
1170
  input.value = '';
1171
  input.disabled = true;
1172
  sendBtn.disabled = true;
 
1180
  text: text
1181
  })
1182
  });
1183
+ // Don't wait for poll, add message locally for instant feedback
1184
+ const tempMsg = {
1185
+ id: 'temp-' + Date.now(), // Temporary ID
1186
+ sender_address: currentUser.address,
1187
+ text: text,
1188
+ timestamp: new Date().toISOString(),
1189
+ display_name: currentUser.username || `${currentUser.address[:4]}...${currentUser.address[-4:]}`
1190
+ };
1191
+ const messagesContainer = document.getElementById('messages-container');
1192
+ // Check if the last message is from the current user to group bubbles
1193
+ const lastMsgElement = messagesContainer.lastElementChild;
1194
+ if (lastMsgElement && lastMsgElement.classList.contains('sent')) {
1195
+ // Append to existing sent bubble structure (simplified)
1196
+ // For real grouping, you'd restructure the message HTML
1197
+ // For now, just adding as a new message item
1198
+ }
1199
+ renderMessages([...Array.from(messagesContainer.children).map(el => {
1200
+ // Basic extraction for temp rendering
1201
+ const senderEl = el.querySelector('.message-sender');
1202
+ const bubbleEl = el.querySelector('.message-bubble');
1203
+ return {
1204
+ sender_address: el.classList.contains('sent') ? currentUser.address : 'other', // Simplified check
1205
+ text: bubbleEl ? bubbleEl.textContent : '',
1206
+ display_name: senderEl ? senderEl.textContent : '',
1207
+ // timestamp etc omitted for temp
1208
+ };
1209
+ }), tempMsg]); // Re-render with temp message
1210
+
1211
+ await fetchMessages(activeChatroomId); // Fetch actual messages after sending
1212
+
1213
  } finally {
1214
  input.disabled = false;
1215
  sendBtn.disabled = false;
 
1218
  }
1219
  });
1220
 
 
1221
  document.getElementById('create-room-show-modal').addEventListener('click', () => {
1222
  createRoomModal.style.display = 'flex';
1223
  document.getElementById('create-room-form').reset();
 
1229
  e.preventDefault();
1230
  const name = document.getElementById('room-name').value.trim();
1231
  const password = document.getElementById('room-password').value;
1232
+ if (!name) {
1233
+ showStatus('Название чата не может быть пустым.', 'error');
1234
+ return;
1235
+ }
1236
+ if (name.length < 3 || name.length > 50) {
1237
+ showStatus('Название чата должно быть от 3 до 50 символов.', 'error');
1238
+ return;
1239
+ }
1240
+
1241
 
1242
  try {
1243
  await apiCall('/api/create_chatroom', {
 
1259
  body: JSON.stringify({ address: address })
1260
  });
1261
 
1262
+ const username = userData.username || `Пользователь ${truncateAddress(address)}`;
1263
  const avatarContainer = document.getElementById('profile-avatar-container');
1264
  const usernameEl = document.getElementById('profile-username');
1265
  const addressEl = document.getElementById('profile-address');
 
1274
 
1275
  qrCodeEl.innerHTML = '';
1276
  if (profileQrCode) {
1277
+ // Check if QR code instance exists and clear it
1278
+ if (typeof profileQrCode.clear === 'function') {
1279
+ profileQrCode.clear();
1280
+ } else {
1281
+ // Fallback if clear method is not available (older versions?)
1282
+ qrCodeEl.innerHTML = '';
1283
+ }
1284
  }
1285
+ // Use try/catch for QRCode initialization as it can throw
1286
+ try {
1287
+ profileQrCode = new QRCode(qrCodeEl, {
1288
+ text: address,
1289
+ width: 150,
1290
+ height: 150,
1291
+ colorDark : "#000000",
1292
+ colorLight : "#ffffff",
1293
+ correctLevel : QRCode.CorrectLevel.H
1294
+ });
1295
+ } catch (e) {
1296
+ console.error("Failed to generate QR code:", e);
1297
+ qrCodeEl.innerHTML = '<p style="color: red;">Ошибка генерации QR</p>';
1298
+ profileQrCode = null; // Ensure it's null if failed
1299
+ }
1300
+
1301
+
1302
  sendTonBtn.onclick = async () => {
1303
  if (!tonConnectUI.connected) {
1304
  showStatus('Подключите кошелек для отправки TON.', 'error');
 
1329
  }
1330
  };
1331
 
1332
+ sendTonBtn.style.display = (address === currentUser.address || !tonConnectUI.connected) ? 'none' : 'block';
1333
  profileModal.style.display = 'flex';
1334
  } catch (err) {
1335
  showStatus('Не удалось загрузить профиль.', 'error');
 
1341
  html5QrCode = new Html5Qrcode("qr-reader");
1342
  const qrCodeSuccessCallback = (decodedText, decodedResult) => {
1343
  hideScanner();
1344
+ // Basic check for TON address format
1345
+ if (decodedText && typeof decodedText === 'string' && decodedText.length >= 48 && (decodedText.startsWith('EQ') || decodedText.startsWith('UQ'))) {
1346
+ showProfile(decodedText);
1347
  } else {
1348
+ showStatus('Отсканирован недействительный QR-код (ожидается адрес TON).', 'error');
1349
  }
1350
  };
1351
  const config = { fps: 10, qrbox: { width: 250, height: 250 } };
1352
+ // Request camera permissions when starting scanner
1353
  html5QrCode.start({ facingMode: "environment" }, config, qrCodeSuccessCallback)
1354
  .catch(err => {
1355
+ console.error("QR Scanner startup failed:", err);
1356
+ showStatus('Не удалось запустить сканер. Проверьте разрешения камеры.', 'error');
1357
+ hideScanner(); // Ensure modal closes on error
1358
  });
1359
  };
1360
 
 
1365
  scannerModal.style.display = 'none';
1366
  };
1367
 
1368
+ // Event Listeners
1369
  document.getElementById('my-profile-btn').addEventListener('click', () => {
1370
  if (currentUser.address) showProfile(currentUser.address);
1371
+ else showStatus('Подключите кошелек', 'error');
1372
  });
1373
+
1374
+ // Nav button listeners
1375
+ navButtons.forEach(btn => {
1376
+ btn.addEventListener('click', () => {
1377
+ const viewId = btn.dataset.view;
1378
+ switchView(viewId);
1379
+ });
1380
+ });
1381
+
1382
+ // Central QR scan button
1383
+ navScanBtn.addEventListener('click', showScanner);
1384
+
1385
  document.getElementById('profile-close-btn').addEventListener('click', () => profileModal.style.display = 'none');
1386
  document.getElementById('scanner-close-btn').addEventListener('click', hideScanner);
1387
+ document.getElementById('back-to-list-btn').addEventListener('click', () => {
1388
+ activeChat.style.display = 'none';
1389
+ chatPlaceholder.style.display = 'flex';
1390
+ activeChatroomId = null;
1391
+ if (messagePollingInterval) clearInterval(messagePollingInterval);
1392
+ showChatView(); // Show list view on mobile
1393
+ });
1394
 
1395
+ // Browser Navigation
1396
+ browserForm.addEventListener('submit', (e) => {
1397
+ e.preventDefault();
1398
+ let url = browserUrlInput.value.trim();
1399
+ if (url) {
1400
+ // Add http/https if missing, basic check
1401
+ if (!url.match(/^https?:\/\//i)) {
1402
+ url = 'https://' + url; // Default to https
1403
+ }
1404
+ browserIframe.src = url;
1405
+ }
1406
+ });
1407
+
1408
+ browserHomeBtn.addEventListener('click', () => {
1409
+ const homeUrl = "https://www.google.com/";
1410
+ browserIframe.src = homeUrl;
1411
+ browserUrlInput.value = homeUrl;
1412
+ });
1413
+
1414
+ browserBackBtn.addEventListener('click', () => {
1415
+ try {
1416
+ browserIframe.contentWindow.history.back();
1417
+ } catch (e) {
1418
+ console.warn("Browser history back failed, likely due to cross-origin policy.", e);
1419
+ showStatus("Браузер не может перейти назад из-за ограничений безопасности.", 'info');
1420
+ }
1421
+ });
1422
+
1423
+ browserForwardBtn.addEventListener('click', () => {
1424
+ try {
1425
+ browserIframe.contentWindow.history.forward();
1426
+ } catch (e) {
1427
+ console.warn("Browser history forward failed, likely due to cross-origin policy.", e);
1428
+ showStatus("Браузер не может перейти вперед из-за ограничений безопасности.", 'info');
1429
+ }
1430
+ });
1431
+
1432
+ // Sync URL input with iframe location (best effort, limited by same-origin)
1433
+ browserIframe.addEventListener('load', () => {
1434
+ try {
1435
+ // This might fail on cross-origin sites
1436
+ browserUrlInput.value = browserIframe.contentWindow.location.href;
1437
+ } catch (e) {
1438
+ // Cannot access cross-origin frame location
1439
+ browserUrlInput.value = browserIframe.src; // Fallback to src
1440
+ }
1441
+ });
1442
+
1443
+
1444
  const handleResize = () => {
1445
  const isMobile = window.innerWidth < 768;
1446
+
1447
+ // Manage display within the 'chat' container based on mobile/desktop and active chat state
1448
+ if (chatContainer.style.display !== 'none') { // Only apply resize logic if chat is the active view
1449
+ if (isMobile) {
1450
+ document.getElementById('back-to-list-btn').style.display = activeChatroomId ? 'block' : 'none';
1451
+ if (activeChatroomId && activeChat.style.display === 'flex') {
1452
+ // Mobile, chat window is open
1453
+ chatroomListView.style.display = 'none';
1454
+ chatWindowView.style.display = 'flex';
1455
+ } else {
1456
+ // Mobile, chat list is open or no chat selected
1457
+ chatroomListView.style.display = 'flex';
1458
+ chatWindowView.style.display = 'none';
1459
+ }
1460
  } else {
1461
+ // Desktop: always show both list and window
1462
+ chatroomListView.style.display = 'flex';
1463
+ chatWindowView.style.display = 'flex';
1464
+ document.getElementById('back-to-list-btn').style.display = 'none';
1465
  }
1466
  }
1467
+
1468
+ // Adjust app-view height for desktop
1469
+ if (!isMobile) {
1470
+ appView.style.height = '100%'; // Allow container to define height
1471
+ bottomNav.style.display = 'none'; // Hide bottom nav on desktop
1472
+ } else {
1473
+ appView.style.height = `calc(100vh - var(--nav-height))`;
1474
+ bottomNav.style.display = 'flex'; // Show bottom nav on mobile
1475
+ }
1476
  };
1477
 
1478
  window.addEventListener('resize', handleResize);
1479
+
1480
+ // Initial state setup
1481
+ handleResize(); // Apply initial layout based on screen size
1482
+ bottomNav.style.display = 'none'; // Hide nav until user is logged in
1483
 
1484
  tonConnectUI.onStatusChange(wallet => {
1485
  if (wallet) {
 
1489
  currentUser = { address: null, username: null };
1490
  appView.style.display = 'none';
1491
  loginView.style.display = 'flex';
1492
+ bottomNav.style.display = 'none';
1493
  if (messagePollingInterval) clearInterval(messagePollingInterval);
1494
  activeChatroomId = null;
1495
+ chatPlaceholder.style.display = 'flex';
1496
+ activeChat.style.display = 'none';
1497
+ renderChatrooms([]); // Clear chat list
1498
+ renderMessages([]); // Clear messages
1499
+ handleResize(); // Adjust layout back to login state
1500
  }
1501
  });
1502
  });
 
1543
  chatrooms_list.append({
1544
  'id': room_id,
1545
  'name': room_data['name'],
1546
+ 'is_private': room_data['password_hash'] is not None
1547
  })
1548
  return jsonify({'chatrooms': sorted(chatrooms_list, key=lambda x: x['name'])})
1549
 
 
1559
  db = read_db()
1560
  room_id = str(uuid.uuid4())
1561
  db['chatrooms'][room_id] = {
1562
+ 'id': room_id,
1563
  'name': name,
1564
  'creator': creator_address,
1565
  'is_private': bool(password),
 
1617
  db = read_db()
1618
  if chatroom_id not in db['messages']:
1619
  return jsonify({'error': 'Chatroom not found'}), 404
1620
+
1621
+ if sender_address not in db['users'] or not db['users'][sender_address].get('username'):
1622
+ # Prevent sending if user has no username
1623
+ return jsonify({'error': 'Username not set'}), 400
1624
 
1625
  message = {
1626
  'id': str(uuid.uuid4()),
 
1629
  'timestamp': datetime.utcnow().isoformat() + "Z"
1630
  }
1631
 
1632
+ # Keep only the last 100 messages per chatroom
1633
  if len(db['messages'][chatroom_id]) >= 100:
1634
  db['messages'][chatroom_id].pop(0)
1635