Aleksmorshen commited on
Commit
a7c18d2
·
verified ·
1 Parent(s): 6ff125b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +875 -331
app.py CHANGED
@@ -19,8 +19,16 @@ 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:
@@ -55,9 +63,7 @@ def index():
55
  --success-color: #28a745;
56
  --error-color: #dc3545;
57
  --font-family: 'Inter', sans-serif;
58
- --nav-bg: #18191C;
59
- --nav-icon-color: #8E9297;
60
- --nav-icon-active-color: #FFFFFF;
61
  }
62
 
63
  * {
@@ -79,7 +85,6 @@ def index():
79
  height: 100vh;
80
  width: 100vw;
81
  display: flex;
82
- flex-direction: column;
83
  align-items: center;
84
  justify-content: center;
85
  }
@@ -122,32 +127,155 @@ def index():
122
  margin-bottom: 40px;
123
  }
124
 
125
- #app-wrapper {
126
  display: none;
127
- flex-direction: column;
128
- height: 100%;
129
  width: 100%;
 
 
130
  }
131
-
132
- #app-content {
133
- display: flex;
134
  flex-grow: 1;
135
- overflow: hidden;
 
 
136
  }
137
 
138
- #chatroom-list-view {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  display: flex;
140
  flex-direction: column;
141
- height: 100%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  width: 100%;
 
 
 
 
 
 
 
 
143
  background-color: var(--bg-secondary);
144
- overflow: hidden;
145
  }
146
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  .list-header {
148
  padding: 16px;
149
  border-bottom: 1px solid var(--border-color);
150
  flex-shrink: 0;
 
151
  }
152
  .list-header-top {
153
  display: flex;
@@ -159,8 +287,8 @@ def index():
159
  font-size: 1.5rem;
160
  font-weight: 600;
161
  }
162
-
163
- .user-profile-section {
164
  padding: 12px;
165
  background-color: var(--bg-tertiary);
166
  border-radius: 8px;
@@ -172,7 +300,7 @@ def index():
172
  margin-bottom: 8px;
173
  word-break: break-all;
174
  }
175
-
176
  .username-form { display: flex; gap: 8px; }
177
  .username-input {
178
  flex-grow: 1;
@@ -207,13 +335,14 @@ def index():
207
  padding: 8px 12px;
208
  font-size: 0.9rem;
209
  }
210
-
211
- #chatroom-list {
212
  flex-grow: 1;
213
  overflow-y: auto;
214
- padding-bottom: 60px;
 
215
  }
216
- .chatroom-item {
217
  display: flex;
218
  align-items: center;
219
  gap: 12px;
@@ -222,7 +351,7 @@ def index():
222
  border-bottom: 1px solid var(--border-color);
223
  transition: background-color 0.2s ease;
224
  }
225
- .chatroom-item:hover { background-color: var(--bg-hover); }
226
 
227
  .avatar {
228
  width: 40px;
@@ -235,16 +364,23 @@ def index():
235
  color: white;
236
  flex-shrink: 0;
237
  }
238
- .chatroom-info {
239
  flex-grow: 1;
240
  overflow: hidden;
241
  }
242
- .chatroom-name {
243
  font-weight: 500;
244
  white-space: nowrap;
245
  overflow: hidden;
246
  text-overflow: ellipsis;
247
  }
 
 
 
 
 
 
 
248
  .lock-icon {
249
  width: 16px;
250
  height: 16px;
@@ -258,8 +394,12 @@ def index():
258
  height: 100%;
259
  width: 100%;
260
  background-color: var(--bg-primary);
 
 
 
 
261
  }
262
-
263
  .chat-header {
264
  display: flex;
265
  align-items: center;
@@ -273,7 +413,8 @@ def index():
273
  background: none;
274
  border: none;
275
  cursor: pointer;
276
- display: none;
 
277
  }
278
  .back-btn svg {
279
  width: 24px;
@@ -284,6 +425,11 @@ def index():
284
  font-size: 1.2rem;
285
  font-weight: 600;
286
  }
 
 
 
 
 
287
 
288
  #messages-container {
289
  flex-grow: 1;
@@ -336,7 +482,7 @@ def index():
336
  background-color: var(--bg-tertiary);
337
  border-bottom-left-radius: 4px;
338
  }
339
-
340
  .chat-placeholder {
341
  display: flex;
342
  flex-direction: column;
@@ -351,15 +497,15 @@ def index():
351
 
352
  .message-form {
353
  display: flex;
354
- padding: 16px;
355
- gap: 12px;
356
  background-color: var(--bg-secondary);
357
  border-top: 1px solid var(--border-color);
358
  flex-shrink: 0;
359
  }
360
  #message-input {
361
  flex-grow: 1;
362
- padding: 12px 16px;
363
  border: 1px solid var(--border-color);
364
  background-color: var(--bg-tertiary);
365
  color: var(--text-primary);
@@ -371,13 +517,13 @@ def index():
371
  #message-input:focus { border-color: var(--accent-blue); }
372
 
373
  .send-btn {
374
- width: 44px;
375
- height: 44px;
376
  border-radius: 50%;
377
  flex-shrink: 0;
378
  padding: 0;
379
  }
380
- .send-btn svg { width: 20px; height: 20px; fill: white; }
381
 
382
  .modal-overlay {
383
  position: fixed;
@@ -415,6 +561,7 @@ def index():
415
  font-size: 1rem;
416
  }
417
  .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
 
418
  .modal-btn {
419
  padding: 10px 20px;
420
  border-radius: 6px;
@@ -422,11 +569,12 @@ def index():
422
  cursor: pointer;
423
  font-weight: 500;
424
  }
 
425
  .secondary-btn { background-color: var(--bg-hover); color: white; }
426
-
427
  #status-bar {
428
  position: fixed;
429
- bottom: 20px;
430
  left: 50%;
431
  transform: translateX(-50%);
432
  background-color: var(--bg-tertiary);
@@ -439,58 +587,16 @@ def index():
439
  transition: opacity 0.3s, visibility 0.3s;
440
  z-index: 2000;
441
  box-shadow: 0 5px 15px rgba(0,0,0,0.3);
 
 
442
  }
 
 
 
443
  #status-bar.success { background-color: var(--success-color); }
444
  #status-bar.error { background-color: var(--error-color); }
445
  #status-bar.visible { opacity: 1; visibility: visible; }
446
 
447
- .bottom-nav {
448
- display: flex;
449
- justify-content: space-around;
450
- align-items: center;
451
- background-color: var(--nav-bg);
452
- padding: 12px 0;
453
- border-top: 1px solid var(--border-color);
454
- flex-shrink: 0;
455
- }
456
- .nav-item {
457
- display: flex;
458
- flex-direction: column;
459
- align-items: center;
460
- cursor: pointer;
461
- color: var(--nav-icon-color);
462
- transition: color 0.2s ease;
463
- font-size: 0.75rem;
464
- width: 25%;
465
- text-align: center;
466
- }
467
- .nav-item.center {
468
- transform: translateY(-10px);
469
- background-color: var(--accent-blue);
470
- width: 60px;
471
- height: 60px;
472
- border-radius: 50%;
473
- display: flex;
474
- align-items: center;
475
- justify-content: center;
476
- color: white;
477
- box-shadow: 0 5px 20px rgba(0, 136, 204, 0.4);
478
- }
479
- .nav-item svg {
480
- width: 24px;
481
- height: 24px;
482
- margin-bottom: 4px;
483
- fill: currentColor;
484
- }
485
- .nav-item.center svg {
486
- width: 30px;
487
- height: 30px;
488
- margin-bottom: 0;
489
- }
490
- .nav-item.active {
491
- color: var(--nav-icon-active-color);
492
- }
493
-
494
  @media (min-width: 768px) {
495
  .main-container {
496
  max-width: 1100px;
@@ -500,25 +606,70 @@ def index():
500
  box-shadow: 0 10px 40px rgba(0,0,0,0.3);
501
  border: 1px solid var(--border-color);
502
  }
503
- #app-wrapper {
504
- flex-direction: row;
505
  }
506
- #chatroom-list-view {
507
- width: 320px;
508
- flex-shrink: 0;
 
 
 
 
 
 
509
  border-right: 1px solid var(--border-color);
510
- display: flex !important;
 
 
 
 
 
511
  }
512
- #chat-window-view {
513
- width: auto;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
  flex-grow: 1;
515
- display: flex !important;
 
 
 
516
  }
517
- #chat-window-view.hidden-on-desktop {
518
- display: flex !important;
 
 
 
 
 
 
 
519
  }
520
- .back-btn { display: none !important; }
521
- .bottom-nav { display: none; }
522
  }
523
  </style>
524
  </head>
@@ -531,9 +682,27 @@ def index():
531
  <div id="ton-connect-button"></div>
532
  </div>
533
 
534
- <div id="app-wrapper" class="main-container">
535
- <div id="app-content">
536
- <div id="chatroom-list-view">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
  <div class="list-header">
538
  <div class="list-header-top">
539
  <h2>Чаты</h2>
@@ -544,7 +713,7 @@ def index():
544
  </button>
545
  </div>
546
  </div>
547
- <div class="user-profile-section">
548
  <div id="user-wallet"></div>
549
  <div id="user-nickname"></div>
550
  <form class="username-form" id="username-form">
@@ -556,7 +725,32 @@ def index():
556
  <div id="chatroom-list"></div>
557
  </div>
558
 
559
- <div id="chat-window-view" class="hidden-on-desktop">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
560
  <div id="chat-placeholder" class="chat-placeholder">
561
  <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
562
  <h2>Выберите чат</h2>
@@ -580,26 +774,8 @@ def index():
580
  </div>
581
  </div>
582
  </div>
583
-
584
- <div class="bottom-nav">
585
- <div class="nav-item" id="nav-chats">
586
- <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-2zm0 13l-2-2V4H4v11h14z"/><path fill="none" d="M0 0h24v24H0V0z"/><path d="M16 8a2 2 0 100-4 2 2 0 000 4zM8 12a2 2 0 100-4 2 2 0 000 4zM16 12a2 2 0 100-4 2 2 0 000 4z"/></svg>
587
- <span>Чаты</span>
588
- </div>
589
- <div class="nav-item" id="nav-users">
590
- <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>
591
- <span>Пользователи</span>
592
- </div>
593
- <div class="nav-item center" id="nav-scan">
594
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 4h-4v2h2v14h-14v-2h-2v4h20V4z"/><path d="M8 13a5 5 0 110-10 5 5 0 010 10zm0-6a3 3 0 100 6 3 3 0 000-6z"/></svg>
595
- </div>
596
- <div class="nav-item" id="nav-profile">
597
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
598
- <span>Профиль</span>
599
- </div>
600
- </div>
601
  </div>
602
-
603
  <div id="create-room-modal" class="modal-overlay">
604
  <div class="modal-content">
605
  <h3>Создать новый чат</h3>
@@ -630,17 +806,18 @@ def index():
630
  </div>
631
  </div>
632
 
633
- <div id="profile-modal" class="modal-overlay">
634
  <div class="modal-content" style="text-align: center;">
635
- <h3 id="profile-modal-title">Профиль пользователя</h3>
636
- <div id="profile-avatar-container" style="margin: 20px auto; display: inline-block;"></div>
637
- <p id="profile-username" style="font-size: 1.2rem; font-weight: 600;"></p>
638
- <p id="profile-address" style="color: var(--text-secondary); font-size: 0.9rem; word-break: break-all; margin-top: 8px;"></p>
639
- <div id="profile-qr-code" style="background: white; padding: 10px; margin: 20px auto; width: fit-content; border-radius: 8px;"></div>
640
- <p style="text-align: center; color: var(--text-secondary); font-size: 0.8rem; margin-top: -10px; margin-bottom: 20px;">Отсканируйте для открытия профиля</p>
641
- <div class="modal-actions" style="flex-direction: column; gap: 12px; align-items: stretch;">
 
642
  <button id="send-ton-btn" class="modal-btn action-btn">Отправить TON</button>
643
- <button id="profile-close-btn" class="modal-btn secondary-btn">Закрыть</button>
644
  </div>
645
  </div>
646
  </div>
@@ -669,18 +846,29 @@ def index():
669
  let messagePollingInterval = null;
670
  let chatroomsData = {};
671
  let html5QrCode = null;
672
- let profileQrCode = null;
673
- let currentNavView = 'chats';
674
-
 
 
675
  const loginView = document.getElementById('login-view');
676
- const appWrapper = document.getElementById('app-wrapper');
 
 
 
 
677
  const chatroomListView = document.getElementById('chatroom-list-view');
 
 
678
  const chatWindowView = document.getElementById('chat-window-view');
679
  const chatPlaceholder = document.getElementById('chat-placeholder');
680
  const activeChat = document.getElementById('active-chat');
681
- const profileModal = document.getElementById('profile-modal');
 
 
 
682
  const scannerModal = document.getElementById('scanner-modal');
683
- const bottomNav = document.querySelector('.bottom-nav');
684
 
685
  const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292'];
686
 
@@ -696,12 +884,16 @@ def index():
696
  };
697
 
698
  const showStatus = (message, type = 'info', duration = 3000) => {
699
- const statusBar = document.getElementById('status-bar');
700
  statusBar.textContent = message;
701
- statusBar.className = 'status-bar';
702
  if (type === 'success') statusBar.classList.add('success');
703
  else if (type === 'error') statusBar.classList.add('error');
704
-
 
 
 
 
 
705
  statusBar.classList.add('visible');
706
  setTimeout(() => statusBar.classList.remove('visible'), duration);
707
  };
@@ -720,24 +912,40 @@ def index():
720
  throw error;
721
  }
722
  };
723
-
724
  const truncateAddress = (address) => address ? `${address.substring(0, 4)}...${address.substring(address.length - 4)}` : '';
725
 
726
  const updateUserInfo = () => {
727
- document.getElementById('user-wallet').textContent = `Кошелек: ${truncateAddress(currentUser.address)}`;
728
  const nicknameEl = document.getElementById('user-nickname');
729
  const usernameInput = document.getElementById('username-input');
730
- nicknameEl.textContent = currentUser.username ? `Ник: ${currentUser.username}` : `Никнейм не установлен`;
731
- usernameInput.value = currentUser.username || '';
 
 
 
 
 
 
 
732
  };
733
-
734
  document.getElementById('username-form').addEventListener('submit', async (e) => {
735
  e.preventDefault();
736
  const newUsername = document.getElementById('username-input').value.trim();
737
- if (!newUsername || newUsername.length < 3) {
738
- showStatus('Никнейм должен быть не короче 3 символов.', 'error');
 
 
 
 
 
 
 
 
739
  return;
740
  }
 
741
  try {
742
  await apiCall('/api/set_username', {
743
  method: 'POST',
@@ -747,8 +955,9 @@ def index():
747
  currentUser.username = newUsername;
748
  updateUserInfo();
749
  showStatus('Никнейм успешно обновлен!', 'success');
750
- fetchChatrooms();
751
- if (activeChatroomId) fetchMessages(activeChatroomId);
 
752
  } catch (err) {}
753
  });
754
 
@@ -766,49 +975,170 @@ def index():
766
  }
767
  updateUserInfo();
768
  loginView.style.display = 'none';
769
- appWrapper.style.display = 'flex';
770
- showView('chats');
771
  fetchChatrooms();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
772
  };
773
 
774
  const renderChatrooms = (rooms) => {
775
  const list = document.getElementById('chatroom-list');
776
  list.innerHTML = '';
777
  chatroomsData = {};
778
- rooms.forEach(room => {
779
- chatroomsData[room.id] = room;
780
- const item = document.createElement('div');
781
- item.className = 'chatroom-item';
782
- item.dataset.id = room.id;
783
-
784
- item.appendChild(getAvatar(room.name));
785
-
786
- const infoDiv = document.createElement('div');
787
- infoDiv.className = 'chatroom-info';
788
- const nameSpan = document.createElement('div');
789
- nameSpan.className = 'chatroom-name';
790
- nameSpan.textContent = room.name;
791
- infoDiv.appendChild(nameSpan);
792
- item.appendChild(infoDiv);
793
-
794
- if (room.is_private) {
795
- const lockIcon = document.createElement('div');
796
- lockIcon.innerHTML = `<svg class="lock-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zM9 8V6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9z"/></svg>`;
797
- item.appendChild(lockIcon);
798
- }
 
 
 
799
 
800
- item.addEventListener('click', () => selectChatroom(room.id, room.is_private));
801
- list.appendChild(item);
802
- });
 
803
  };
804
-
805
  const fetchChatrooms = async () => {
806
  try {
807
  const data = await apiCall('/api/chatrooms');
808
- renderChatrooms(data.chatrooms);
809
  } catch (err) {}
810
  };
811
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
812
  const renderMessages = (messages) => {
813
  const container = document.getElementById('messages-container');
814
  const shouldScroll = container.scrollTop + container.clientHeight >= container.scrollHeight - 30;
@@ -820,16 +1150,28 @@ def index():
820
  const avatar = getAvatar(msg.display_name);
821
  avatar.classList.add('message-avatar');
822
  avatar.style.cursor = 'pointer';
823
- avatar.onclick = () => showProfile(msg.sender_address);
824
-
 
 
 
 
 
 
825
  const contentDiv = document.createElement('div');
826
  contentDiv.className = 'message-content';
827
 
828
  const senderDiv = document.createElement('div');
829
  senderDiv.className = 'message-sender';
830
  senderDiv.textContent = msg.display_name;
831
- senderDiv.onclick = () => showProfile(msg.sender_address);
832
-
 
 
 
 
 
 
833
  const bubbleDiv = document.createElement('div');
834
  bubbleDiv.className = 'message-bubble';
835
  bubbleDiv.textContent = msg.text;
@@ -842,33 +1184,20 @@ def index():
842
  container.appendChild(msgDiv);
843
  });
844
 
845
- if(shouldScroll) {
846
  container.scrollTop = container.scrollHeight;
847
  }
848
  };
849
-
850
  const fetchMessages = async (roomId) => {
 
851
  try {
852
  const data = await apiCall(`/api/messages/${roomId}`);
853
- renderMessages(data.messages);
854
  } catch (err) {
855
  if (messagePollingInterval) clearInterval(messagePollingInterval);
856
  }
857
  };
858
-
859
- const showChatView = () => {
860
- chatWindowView.style.display = 'flex';
861
- if (window.innerWidth < 768) {
862
- chatroomListView.style.display = 'none';
863
- }
864
- };
865
-
866
- const showListView = () => {
867
- chatroomListView.style.display = 'flex';
868
- if (window.innerWidth < 768) {
869
- chatWindowView.style.display = 'none';
870
- }
871
- };
872
 
873
  const selectChatroom = (roomId, isPrivate) => {
874
  const roomData = chatroomsData[roomId];
@@ -877,7 +1206,7 @@ def index():
877
  const proceedToRoom = () => {
878
  if (messagePollingInterval) clearInterval(messagePollingInterval);
879
  activeChatroomId = roomId;
880
-
881
  document.getElementById('chat-header-title').textContent = roomData.name;
882
  const headerAvatar = document.getElementById('chat-header-avatar');
883
  headerAvatar.innerHTML = '';
@@ -885,23 +1214,28 @@ def index():
885
 
886
  chatPlaceholder.style.display = 'none';
887
  activeChat.style.display = 'flex';
888
- showChatView();
889
-
890
  fetchMessages(roomId);
891
  messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000);
892
  };
893
-
894
  if (isPrivate) {
895
- const passwordModal = document.getElementById('password-modal');
896
  const passwordForm = document.getElementById('password-form');
897
  const passwordInput = document.getElementById('password-input');
898
  passwordModal.style.display = 'flex';
899
  passwordInput.value = '';
900
  passwordInput.focus();
901
 
 
 
 
 
 
 
902
  const formSubmitHandler = async (e) => {
903
  e.preventDefault();
904
- passwordForm.removeEventListener('submit', formSubmitHandler);
905
  const password = passwordInput.value;
906
  passwordModal.style.display = 'none';
907
  try {
@@ -913,11 +1247,11 @@ def index():
913
  proceedToRoom();
914
  } catch (err) {}
915
  };
916
- passwordForm.addEventListener('submit', formSubmitHandler);
917
 
918
  document.getElementById('password-cancel').onclick = () => {
919
  passwordModal.style.display = 'none';
920
- passwordForm.removeEventListener('submit', formSubmitHandler);
921
  };
922
  } else {
923
  proceedToRoom();
@@ -929,7 +1263,7 @@ def index():
929
  const input = document.getElementById('message-input');
930
  const sendBtn = document.getElementById('send-btn');
931
  const text = input.value.trim();
932
- if (text && activeChatroomId) {
933
  input.value = '';
934
  input.disabled = true;
935
  sendBtn.disabled = true;
@@ -943,8 +1277,8 @@ def index():
943
  text: text
944
  })
945
  });
946
- await fetchMessages(activeChatroomId);
947
- document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight;
948
  } finally {
949
  input.disabled = false;
950
  sendBtn.disabled = false;
@@ -953,10 +1287,10 @@ def index():
953
  }
954
  });
955
 
956
- const createRoomModal = document.getElementById('create-room-modal');
957
  document.getElementById('create-room-show-modal').addEventListener('click', () => {
958
  createRoomModal.style.display = 'flex';
959
  document.getElementById('create-room-form').reset();
 
960
  });
961
  document.getElementById('create-room-cancel').addEventListener('click', () => {
962
  createRoomModal.style.display = 'none';
@@ -965,7 +1299,22 @@ def index():
965
  e.preventDefault();
966
  const name = document.getElementById('room-name').value.trim();
967
  const password = document.getElementById('room-password').value;
968
- if (!name) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
969
 
970
  try {
971
  await apiCall('/api/create_chatroom', {
@@ -979,75 +1328,147 @@ def index():
979
  } catch (err) {}
980
  });
981
 
982
- const showProfile = async (address) => {
983
  try {
984
  const userData = await apiCall('/api/user_data', {
985
  method: 'POST',
986
  headers: { 'Content-Type': 'application/json' },
987
  body: JSON.stringify({ address: address })
988
  });
989
-
990
  const username = userData.username || `User ${truncateAddress(address)}`;
991
- const avatarContainer = document.getElementById('profile-avatar-container');
992
- const usernameEl = document.getElementById('profile-username');
993
- const addressEl = document.getElementById('profile-address');
994
- const qrCodeEl = document.getElementById('profile-qr-code');
995
- const sendTonBtn = document.getElementById('send-ton-btn');
996
-
997
- avatarContainer.innerHTML = '';
998
- avatarContainer.appendChild(getAvatar(username));
999
-
1000
- usernameEl.textContent = username;
1001
- addressEl.textContent = address;
1002
-
1003
- qrCodeEl.innerHTML = '';
1004
- if (profileQrCode) {
1005
- profileQrCode.clear();
1006
  }
1007
- profileQrCode = new QRCode(qrCodeEl, {
1008
- text: address,
1009
- width: 150,
1010
- height: 150,
1011
- colorDark : "#000000",
1012
- colorLight : "#ffffff",
1013
- correctLevel : QRCode.CorrectLevel.H
1014
- });
1015
-
1016
- sendTonBtn.onclick = async () => {
1017
- if (!tonConnectUI.connected) {
1018
- showStatus('Подключите кошелек для отправки TON.', 'error');
1019
- return;
 
 
 
 
1020
  }
1021
- const amountString = prompt("Введите сумму в TON для отправки:", "0.1");
1022
- if (amountString === null) return;
1023
-
1024
- const amount = parseFloat(amountString);
1025
- if (isNaN(amount) || amount <= 0) {
1026
- showStatus('Неверная сумма.', 'error');
1027
- return;
 
1028
  }
1029
-
1030
- const amountInNanoTon = Math.floor(amount * 1_000_000_000).toString();
1031
 
1032
- const transaction = {
1033
- validUntil: Math.floor(Date.now() / 1000) + 600,
1034
- messages: [ { address: address, amount: amountInNanoTon } ]
1035
- };
 
 
 
 
1036
 
1037
- try {
1038
- await tonConnectUI.sendTransaction(transaction);
1039
- showStatus(`Транзакция отправлена успешно!`, 'success');
1040
- profileModal.style.display = 'none';
1041
- } catch (error) {
1042
- showStatus('Транзакция отклонена.', 'error');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1043
  }
1044
- };
1045
-
1046
- sendTonBtn.style.display = (address === currentUser.address) ? 'none' : 'block';
1047
- profileModal.style.display = 'flex';
1048
  } catch (err) {
1049
  showStatus('Не удалось загрузить профиль.', 'error');
1050
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1051
  };
1052
 
1053
  const showScanner = () => {
@@ -1055,13 +1476,15 @@ def index():
1055
  html5QrCode = new Html5Qrcode("qr-reader");
1056
  const qrCodeSuccessCallback = (decodedText, decodedResult) => {
1057
  hideScanner();
 
1058
  if (decodedText && decodedText.length > 40 && (decodedText.startsWith('EQ') || decodedText.startsWith('UQ'))) {
1059
- showProfile(decodedText);
1060
  } else {
1061
- showStatus('Отсканирован недействительный QR-код.', 'error');
1062
  }
1063
  };
1064
- const config = { fps: 10, qrbox: { width: 250, height: 250 } };
 
1065
  html5QrCode.start({ facingMode: "environment" }, config, qrCodeSuccessCallback)
1066
  .catch(err => {
1067
  showStatus('Не удалось запустить сканер.', 'error');
@@ -1075,60 +1498,70 @@ def index():
1075
  }
1076
  scannerModal.style.display = 'none';
1077
  };
1078
-
1079
- const showView = (viewName) => {
1080
- currentNavView = viewName;
1081
- document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active'));
1082
- document.getElementById(`nav-${viewName}`).classList.add('active');
1083
-
1084
- if (viewName === 'chats') {
1085
- if (window.innerWidth < 768) {
1086
- chatroomListView.style.display = 'flex';
1087
- chatWindowView.style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1088
  }
1089
- } else if (viewName === 'users') {
1090
- showStatus('Функционал списка пользователей в разработке.', 'info');
1091
- } else if (viewName === 'scan') {
1092
- showScanner();
1093
- } else if (viewName === 'profile') {
1094
- if (currentUser.address) showProfile(currentUser.address);
1095
- }
1096
- };
1097
 
1098
- document.getElementById('nav-chats').addEventListener('click', () => showView('chats'));
1099
- document.getElementById('nav-users').addEventListener('click', () => showView('users'));
1100
- document.getElementById('nav-scan').addEventListener('click', () => showView('scan'));
1101
- document.getElementById('nav-profile').addEventListener('click', () => showView('profile'));
1102
-
1103
- document.getElementById('my-profile-btn').addEventListener('click', () => showView('profile'));
1104
- document.getElementById('scan-qr-btn').addEventListener('click', () => showView('scan'));
1105
- document.getElementById('profile-close-btn').addEventListener('click', () => profileModal.style.display = 'none');
1106
- document.getElementById('scanner-close-btn').addEventListener('click', hideScanner);
1107
- document.getElementById('back-to-list-btn').addEventListener('click', () => showView('chats'));
1108
-
1109
  const handleResize = () => {
1110
  const isMobile = window.innerWidth < 768;
1111
- bottomNav.style.display = isMobile ? 'flex' : 'none';
1112
-
1113
- if (!isMobile) {
1114
- chatroomListView.style.display = 'flex';
1115
- chatWindowView.style.display = 'flex';
1116
- } else {
1117
- if (currentNavView === 'chats') {
1118
- chatroomListView.style.display = 'flex';
1119
- chatWindowView.style.display = 'none';
1120
- } else if (activeChat.style.display === 'flex') {
1121
- chatroomListView.style.display = 'none';
1122
- chatWindowView.style.display = 'flex';
1123
  } else {
1124
- chatroomListView.style.display = 'flex';
1125
- chatWindowView.style.display = 'none';
 
 
1126
  }
 
 
1127
  }
1128
  };
1129
 
1130
  window.addEventListener('resize', handleResize);
1131
- handleResize();
1132
 
1133
  tonConnectUI.onStatusChange(wallet => {
1134
  if (wallet) {
@@ -1136,12 +1569,36 @@ def index():
1136
  initializeUser(address);
1137
  } else {
1138
  currentUser = { address: null, username: null };
1139
- appWrapper.style.display = 'none';
1140
  loginView.style.display = 'flex';
1141
  if (messagePollingInterval) clearInterval(messagePollingInterval);
1142
  activeChatroomId = null;
 
 
 
 
 
 
 
 
 
 
 
 
1143
  }
1144
  });
 
 
 
 
 
 
 
 
 
 
 
 
1145
  });
1146
  </script>
1147
  </body>
@@ -1157,9 +1614,53 @@ def get_user_data():
1157
  if not address:
1158
  return jsonify({'error': 'Address is required'}), 400
1159
  db = read_db()
1160
- user_info = db['users'].get(address)
1161
- username = user_info.get('username') if user_info else None
1162
- return jsonify({'username': username})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1163
 
1164
  @app.route('/api/set_username', methods=['POST'])
1165
  def set_username():
@@ -1170,8 +1671,16 @@ def set_username():
1170
  return jsonify({'error': 'Address and username are required'}), 400
1171
  if len(username) < 3 or len(username) > 20:
1172
  return jsonify({'error': 'Username must be between 3 and 20 characters'}), 400
 
 
 
1173
 
1174
  db = read_db()
 
 
 
 
 
1175
  if address not in db['users']:
1176
  db['users'][address] = {}
1177
  db['users'][address]['username'] = username
@@ -1186,7 +1695,7 @@ def get_chatrooms():
1186
  chatrooms_list.append({
1187
  'id': room_id,
1188
  'name': room_data['name'],
1189
- 'is_private': room_data['is_private']
1190
  })
1191
  return jsonify({'chatrooms': sorted(chatrooms_list, key=lambda x: x['name'])})
1192
 
@@ -1199,6 +1708,12 @@ def create_chatroom():
1199
  if not name or not creator_address:
1200
  return jsonify({'error': 'Name and creator address are required'}), 400
1201
 
 
 
 
 
 
 
1202
  db = read_db()
1203
  room_id = str(uuid.uuid4())
1204
  db['chatrooms'][room_id] = {
@@ -1220,8 +1735,10 @@ def join_chatroom():
1220
  chatroom = db['chatrooms'].get(chatroom_id)
1221
  if not chatroom:
1222
  return jsonify({'error': 'Chatroom not found'}), 404
1223
- if chatroom['is_private']:
1224
- if not password or not check_password_hash(chatroom['password_hash'], password):
 
 
1225
  return jsonify({'error': 'Invalid password'}), 403
1226
  return jsonify({'success': True})
1227
 
@@ -1229,17 +1746,30 @@ def join_chatroom():
1229
  def get_messages(chatroom_id):
1230
  db = read_db()
1231
  if chatroom_id not in db['messages']:
1232
- return jsonify({'error': 'Chatroom not found'}), 404
1233
-
 
 
 
 
 
1234
  messages_with_names = []
1235
  room_messages = db['messages'].get(chatroom_id, [])
1236
-
 
 
 
 
 
 
 
 
 
 
1237
  for msg in room_messages:
1238
  sender_address = msg['sender_address']
1239
- user_info = db['users'].get(sender_address)
1240
- display_name = (user_info.get('username') if user_info and user_info.get('username')
1241
- else f"{sender_address[:4]}...{sender_address[-4:]}")
1242
-
1243
  msg_copy = msg.copy()
1244
  msg_copy['display_name'] = display_name
1245
  messages_with_names.append(msg_copy)
@@ -1257,8 +1787,14 @@ def send_message():
1257
  return jsonify({'error': 'Missing data'}), 400
1258
 
1259
  db = read_db()
1260
- if chatroom_id not in db['messages']:
1261
- return jsonify({'error': 'Chatroom not found'}), 404
 
 
 
 
 
 
1262
 
1263
  message = {
1264
  'id': str(uuid.uuid4()),
@@ -1266,9 +1802,15 @@ def send_message():
1266
  'text': text,
1267
  'timestamp': datetime.utcnow().isoformat() + "Z"
1268
  }
1269
-
1270
- if len(db['messages'][chatroom_id]) >= 100:
1271
- db['messages'][chatroom_id].pop(0)
 
 
 
 
 
 
1272
 
1273
  db['messages'][chatroom_id].append(message)
1274
  write_db(db)
@@ -1277,4 +1819,6 @@ def send_message():
1277
 
1278
  if __name__ == '__main__':
1279
  init_db()
1280
- app.run(host='0.0.0.0', port=7860)
 
 
 
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 (json.JSONDecodeError, FileNotFoundError):
26
+ return {
27
+ "users": {},
28
+ "chatrooms": {},
29
+ "messages": {}
30
+ }
31
+
32
 
33
  def write_db(data):
34
  with open(DB_FILE, 'w') as f:
 
63
  --success-color: #28a745;
64
  --error-color: #dc3545;
65
  --font-family: 'Inter', sans-serif;
66
+ --nav-height: 60px;
 
 
67
  }
68
 
69
  * {
 
85
  height: 100vh;
86
  width: 100vw;
87
  display: flex;
 
88
  align-items: center;
89
  justify-content: center;
90
  }
 
127
  margin-bottom: 40px;
128
  }
129
 
130
+ #app-view {
131
  display: none;
 
 
132
  width: 100%;
133
+ height: 100%;
134
+ flex-direction: column;
135
  }
136
+
137
+ .app-content {
 
138
  flex-grow: 1;
139
+ display: flex;
140
+ overflow: hidden; /* Allows child views to scroll internally */
141
+ padding-bottom: var(--nav-height); /* Space for nav bar */
142
  }
143
 
144
+ #navigation-bar {
145
+ position: fixed;
146
+ bottom: 0;
147
+ left: 0;
148
+ width: 100%;
149
+ height: var(--nav-height);
150
+ background-color: var(--bg-secondary);
151
+ border-top: 1px solid var(--border-color);
152
+ display: flex;
153
+ justify-content: space-around;
154
+ align-items: center;
155
+ z-index: 500;
156
+ }
157
+ .nav-btn {
158
+ flex: 1;
159
  display: flex;
160
  flex-direction: column;
161
+ align-items: center;
162
+ justify-content: center;
163
+ background: none;
164
+ border: none;
165
+ color: var(--text-secondary);
166
+ font-size: 0.7rem;
167
+ cursor: pointer;
168
+ padding: 5px 0;
169
+ transition: color 0.2s ease;
170
+ }
171
+ .nav-btn svg {
172
+ width: 24px;
173
+ height: 24px;
174
+ fill: var(--text-secondary);
175
+ margin-bottom: 4px;
176
+ transition: fill 0.2s ease;
177
+ }
178
+ .nav-btn.scan-qr {
179
+ position: relative;
180
+ top: -10px;
181
+ background: var(--bg-primary);
182
+ border: 1px solid var(--border-color);
183
+ border-radius: 12px;
184
+ padding: 10px;
185
+ height: auto;
186
+ min-width: 60px;
187
+ max-width: 80px;
188
+ }
189
+ .nav-btn.scan-qr svg {
190
+ width: 30px;
191
+ height: 30px;
192
+ fill: var(--accent-blue);
193
+ margin-bottom: 0;
194
+ }
195
+
196
+ .nav-btn.active {
197
+ color: var(--text-primary);
198
+ }
199
+ .nav-btn.active svg {
200
+ fill: var(--accent-blue-light);
201
+ }
202
+ .nav-btn.scan-qr.active svg {
203
+ fill: var(--accent-blue-light);
204
+ }
205
+
206
+ .app-view-section {
207
  width: 100%;
208
+ height: 100%;
209
+ overflow-y: auto;
210
+ flex-shrink: 0;
211
+ display: none; /* Managed by JS */
212
+ flex-direction: column;
213
+ }
214
+
215
+ #chatroom-list-view {
216
  background-color: var(--bg-secondary);
 
217
  }
218
+
219
+ #users-list-view {
220
+ background-color: var(--bg-secondary);
221
+ }
222
+
223
+ #profile-view {
224
+ background-color: var(--bg-secondary);
225
+ padding: 20px;
226
+ text-align: center;
227
+ justify-content: flex-start;
228
+ }
229
+ #profile-view h3 {
230
+ margin-bottom: 20px;
231
+ font-weight: 600;
232
+ font-size: 1.3rem;
233
+ }
234
+ #profile-view #profile-avatar-container-view {
235
+ margin: 20px auto;
236
+ display: inline-block;
237
+ }
238
+ #profile-view #profile-username-view {
239
+ font-size: 1.2rem;
240
+ font-weight: 600;
241
+ }
242
+ #profile-view #profile-address-view {
243
+ color: var(--text-secondary);
244
+ font-size: 0.9rem;
245
+ word-break: break-all;
246
+ margin-top: 8px;
247
+ }
248
+ #profile-view #profile-balance-view {
249
+ color: var(--text-secondary);
250
+ font-size: 0.9rem;
251
+ margin-top: 4px;
252
+ }
253
+ #profile-view #profile-qr-code-view {
254
+ background: white;
255
+ padding: 10px;
256
+ margin: 20px auto;
257
+ width: fit-content;
258
+ border-radius: 8px;
259
+ }
260
+ #profile-view .qr-help-text {
261
+ text-align: center;
262
+ color: var(--text-secondary);
263
+ font-size: 0.8rem;
264
+ margin-top: -10px;
265
+ margin-bottom: 20px;
266
+ }
267
+ #profile-view .profile-actions {
268
+ display: flex;
269
+ flex-direction: column;
270
+ gap: 12px;
271
+ align-items: stretch;
272
+ }
273
+
274
  .list-header {
275
  padding: 16px;
276
  border-bottom: 1px solid var(--border-color);
277
  flex-shrink: 0;
278
+ background-color: var(--bg-secondary); /* Ensure header stays above scrolling content */
279
  }
280
  .list-header-top {
281
  display: flex;
 
287
  font-size: 1.5rem;
288
  font-weight: 600;
289
  }
290
+
291
+ .user-profile {
292
  padding: 12px;
293
  background-color: var(--bg-tertiary);
294
  border-radius: 8px;
 
300
  margin-bottom: 8px;
301
  word-break: break-all;
302
  }
303
+
304
  .username-form { display: flex; gap: 8px; }
305
  .username-input {
306
  flex-grow: 1;
 
335
  padding: 8px 12px;
336
  font-size: 0.9rem;
337
  }
338
+
339
+ #chatroom-list, #users-list {
340
  flex-grow: 1;
341
  overflow-y: auto;
342
+ /* Add padding to prevent last item being hidden by nav */
343
+ padding-bottom: var(--nav-height);
344
  }
345
+ .chatroom-item, .user-item {
346
  display: flex;
347
  align-items: center;
348
  gap: 12px;
 
351
  border-bottom: 1px solid var(--border-color);
352
  transition: background-color 0.2s ease;
353
  }
354
+ .chatroom-item:hover, .user-item:hover { background-color: var(--bg-hover); }
355
 
356
  .avatar {
357
  width: 40px;
 
364
  color: white;
365
  flex-shrink: 0;
366
  }
367
+ .item-info {
368
  flex-grow: 1;
369
  overflow: hidden;
370
  }
371
+ .item-name {
372
  font-weight: 500;
373
  white-space: nowrap;
374
  overflow: hidden;
375
  text-overflow: ellipsis;
376
  }
377
+ .item-secondary-text {
378
+ font-size: 0.9rem;
379
+ color: var(--text-secondary);
380
+ white-space: nowrap;
381
+ overflow: hidden;
382
+ text-overflow: ellipsis;
383
+ }
384
  .lock-icon {
385
  width: 16px;
386
  height: 16px;
 
394
  height: 100%;
395
  width: 100%;
396
  background-color: var(--bg-primary);
397
+ position: absolute; /* Position absolute for mobile fullscreen */
398
+ top: 0;
399
+ left: 0;
400
+ z-index: 400; /* Above list views but below modals/nav */
401
  }
402
+
403
  .chat-header {
404
  display: flex;
405
  align-items: center;
 
413
  background: none;
414
  border: none;
415
  cursor: pointer;
416
+ display: block; /* Always show on mobile */
417
+ padding: 0; /* Adjust padding */
418
  }
419
  .back-btn svg {
420
  width: 24px;
 
425
  font-size: 1.2rem;
426
  font-weight: 600;
427
  }
428
+ #chat-header-avatar {
429
+ width: 36px;
430
+ height: 36px;
431
+ }
432
+
433
 
434
  #messages-container {
435
  flex-grow: 1;
 
482
  background-color: var(--bg-tertiary);
483
  border-bottom-left-radius: 4px;
484
  }
485
+
486
  .chat-placeholder {
487
  display: flex;
488
  flex-direction: column;
 
497
 
498
  .message-form {
499
  display: flex;
500
+ padding: 10px 16px; /* Smaller padding for mobile */
501
+ gap: 8px; /* Smaller gap for mobile */
502
  background-color: var(--bg-secondary);
503
  border-top: 1px solid var(--border-color);
504
  flex-shrink: 0;
505
  }
506
  #message-input {
507
  flex-grow: 1;
508
+ padding: 10px 16px; /* Smaller padding */
509
  border: 1px solid var(--border-color);
510
  background-color: var(--bg-tertiary);
511
  color: var(--text-primary);
 
517
  #message-input:focus { border-color: var(--accent-blue); }
518
 
519
  .send-btn {
520
+ width: 40px; /* Smaller button */
521
+ height: 40px; /* Smaller button */
522
  border-radius: 50%;
523
  flex-shrink: 0;
524
  padding: 0;
525
  }
526
+ .send-btn svg { width: 18px; height: 18px; fill: white; } /* Smaller icon */
527
 
528
  .modal-overlay {
529
  position: fixed;
 
561
  font-size: 1rem;
562
  }
563
  .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
564
+ .modal-actions.column { flex-direction: column; align-items: stretch; }
565
  .modal-btn {
566
  padding: 10px 20px;
567
  border-radius: 6px;
 
569
  cursor: pointer;
570
  font-weight: 500;
571
  }
572
+ .modal-btn.full-width { width: 100%; text-align: center; }
573
  .secondary-btn { background-color: var(--bg-hover); color: white; }
574
+
575
  #status-bar {
576
  position: fixed;
577
+ bottom: calc(var(--nav-height) + 10px); /* Position above nav bar */
578
  left: 50%;
579
  transform: translateX(-50%);
580
  background-color: var(--bg-tertiary);
 
587
  transition: opacity 0.3s, visibility 0.3s;
588
  z-index: 2000;
589
  box-shadow: 0 5px 15px rgba(0,0,0,0.3);
590
+ max-width: 90%;
591
+ text-align: center;
592
  }
593
+ #status-bar.chat-open {
594
+ bottom: 20px; /* Position lower when chat is open (nav bar is hidden) */
595
+ }
596
  #status-bar.success { background-color: var(--success-color); }
597
  #status-bar.error { background-color: var(--error-color); }
598
  #status-bar.visible { opacity: 1; visibility: visible; }
599
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
600
  @media (min-width: 768px) {
601
  .main-container {
602
  max-width: 1100px;
 
606
  box-shadow: 0 10px 40px rgba(0,0,0,0.3);
607
  border: 1px solid var(--border-color);
608
  }
609
+ #app-view {
610
+ flex-direction: column; /* Keep column, content is flex-row */
611
  }
612
+ .app-content {
613
+ padding-bottom: 0; /* No space needed for nav bar */
614
+ flex-direction: row; /* Desktop layout */
615
+ }
616
+ #navigation-bar {
617
+ position: static; /* Nav bar is part of the layout */
618
+ width: auto;
619
+ height: auto; /* Or a fixed width column */
620
+ border-top: none;
621
  border-right: 1px solid var(--border-color);
622
+ flex-direction: column; /* Stack vertically */
623
+ justify-content: flex-start; /* Align to top */
624
+ padding: 20px 0;
625
+ gap: 15px;
626
+ flex-shrink: 0;
627
+ width: 80px; /* Fixed width for desktop nav */
628
  }
629
+ .nav-btn {
630
+ flex: none; /* Don't grow */
631
+ width: 100%; /* Full width of nav column */
632
+ padding: 10px 0;
633
+ font-size: 0.8rem;
634
+ }
635
+ .nav-btn svg {
636
+ width: 28px; /* Slightly larger icons */
637
+ height: 28px;
638
+ }
639
+ .nav-btn.scan-qr {
640
+ position: static; /* No floating effect */
641
+ background: none; /* No special background */
642
+ border: none; /* No special border */
643
+ padding: 0;
644
+ width: auto; /* Auto width */
645
+ max-width: none;
646
+ height: auto;
647
+ }
648
+ .nav-btn.scan-qr svg {
649
+ width: 32px; /* Scan icon slightly larger */
650
+ height: 32px;
651
+ }
652
+ .app-view-section {
653
+ width: 320px; /* Default width for list views */
654
+ }
655
+ #profile-view {
656
+ width: auto; /* Profile takes remaining space if selected */
657
  flex-grow: 1;
658
+ padding-top: 40px; /* More vertical padding */
659
+ }
660
+ #chatroom-list, #users-list {
661
+ padding-bottom: 0; /* No space needed for nav bar */
662
  }
663
+ #chat-window-view {
664
+ position: static; /* Static position */
665
+ width: auto; /* Takes remaining space */
666
+ flex-grow: 1;
667
+ z-index: 1; /* Lower z-index */
668
+ }
669
+ .back-btn { display: none; } /* Hide back button on desktop */
670
+ #status-bar {
671
+ bottom: 20px; /* Reset position */
672
  }
 
 
673
  }
674
  </style>
675
  </head>
 
682
  <div id="ton-connect-button"></div>
683
  </div>
684
 
685
+ <div id="app-view" class="main-container">
686
+ <div class="app-content">
687
+ <nav id="navigation-bar">
688
+ <button class="nav-btn" data-view="chatroom-list-view">
689
+ <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 2V4h16v12zM8 10h8v2H8zm0-3h8v2H8z"/></svg>
690
+ <span>Чаты</span>
691
+ </button>
692
+ <button class="nav-btn" data-view="users-list-view">
693
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M16 11c1.65 0 2.99-1.34 2.99-3S17.65 5 16 5s-3 1.34-3 3s1.35 3 3 3zm-8 0c1.65 0 2.99-1.34 2.99-3S9.65 5 8 5S5 6.34 5 8s1.35 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>
694
+ <span>Пользователи</span>
695
+ </button>
696
+ <button class="nav-btn scan-qr" data-view="scan-qr">
697
+ <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M3 11h8V3H3v8zm2-6h4v4H5V5zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm8-12v8h8V3h-8zm6 6h-4V5h4v4zm-2 10a2 2 0 100-4 2 2 0 000 4z"/></svg>
698
+ </button>
699
+ <button class="nav-btn" data-view="profile-view">
700
+ <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="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>
701
+ <span>Профиль</span>
702
+ </button>
703
+ </nav>
704
+
705
+ <div id="chatroom-list-view" class="app-view-section">
706
  <div class="list-header">
707
  <div class="list-header-top">
708
  <h2>Чаты</h2>
 
713
  </button>
714
  </div>
715
  </div>
716
+ <div class="user-profile">
717
  <div id="user-wallet"></div>
718
  <div id="user-nickname"></div>
719
  <form class="username-form" id="username-form">
 
725
  <div id="chatroom-list"></div>
726
  </div>
727
 
728
+ <div id="users-list-view" class="app-view-section">
729
+ <div class="list-header">
730
+ <div class="list-header-top">
731
+ <h2>Пользователи</h2>
732
+ </div>
733
+ </div>
734
+ <div id="users-list"></div>
735
+ </div>
736
+
737
+ <div id="profile-view" class="app-view-section">
738
+ <h3>Мой профиль</h3>
739
+ <div id="profile-avatar-container-view" style="margin: 20px auto; display: inline-block;"></div>
740
+ <p id="profile-username-view" style="font-size: 1.2rem; font-weight: 600;"></p>
741
+ <p id="profile-address-view" style="color: var(--text-secondary); font-size: 0.9rem; word-break: break-all; margin-top: 8px;"></p>
742
+ <p id="profile-balance-view" style="color: var(--text-secondary); font-size: 0.9rem; margin-top: 4px;">Баланс: Loading...</p>
743
+ <div id="profile-qr-code-view" style="background: white; padding: 10px; margin: 20px auto; width: fit-content; border-radius: 8px;"></div>
744
+ <p class="qr-help-text">Отсканируйте этот QR-код, чтобы другие могли открыть ваш профиль в Virton.</p>
745
+ <div class="profile-actions">
746
+ <!-- Add other profile actions here if needed -->
747
+ <!-- <button class="modal-btn action-btn full-width">Настройки</button> -->
748
+ <button id="disconnect-wallet-btn" class="modal-btn secondary-btn full-width">Отключить кошелек</button>
749
+ </div>
750
+ </div>
751
+
752
+
753
+ <div id="chat-window-view">
754
  <div id="chat-placeholder" class="chat-placeholder">
755
  <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
756
  <h2>Выберите чат</h2>
 
774
  </div>
775
  </div>
776
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
777
  </div>
778
+
779
  <div id="create-room-modal" class="modal-overlay">
780
  <div class="modal-content">
781
  <h3>Создать новый чат</h3>
 
806
  </div>
807
  </div>
808
 
809
+ <div id="user-profile-modal" class="modal-overlay">
810
  <div class="modal-content" style="text-align: center;">
811
+ <h3 id="user-profile-modal-title">Профиль пользователя</h3>
812
+ <div id="profile-avatar-container-modal" style="margin: 20px auto; display: inline-block;"></div>
813
+ <p id="profile-username-modal" style="font-size: 1.2rem; font-weight: 600;"></p>
814
+ <p id="profile-address-modal" style="color: var(--text-secondary); font-size: 0.9rem; word-break: break-all; margin-top: 8px;"></p>
815
+ <p id="profile-balance-modal" style="color: var(--text-secondary); font-size: 0.9rem; margin-top: 4px;">Баланс: Loading...</p>
816
+ <div id="profile-qr-code-modal" style="background: white; padding: 10px; margin: 20px auto; width: fit-content; border-radius: 8px;"></div>
817
+ <p class="qr-help-text">Отсканируйте этот QR-код, чтобы открыть профиль пользователя в Virton.</p>
818
+ <div class="modal-actions column">
819
  <button id="send-ton-btn" class="modal-btn action-btn">Отправить TON</button>
820
+ <button id="user-profile-close-btn" class="modal-btn secondary-btn">Закрыть</button>
821
  </div>
822
  </div>
823
  </div>
 
846
  let messagePollingInterval = null;
847
  let chatroomsData = {};
848
  let html5QrCode = null;
849
+ let profileQrCodeView = null; // QR code instance for profile-view
850
+ let profileQrCodeModal = null; // QR code instance for user-profile-modal
851
+ let currentView = 'chatroom-list-view'; // Keep track of the active main view
852
+ let lastListView = 'chatroom-list-view'; // Keep track of the last active list view (chats or users)
853
+
854
  const loginView = document.getElementById('login-view');
855
+ const appView = document.getElementById('app-view');
856
+ const appContent = document.querySelector('.app-content');
857
+ const navigationBar = document.getElementById('navigation-bar');
858
+ const navButtons = document.querySelectorAll('.nav-btn');
859
+
860
  const chatroomListView = document.getElementById('chatroom-list-view');
861
+ const usersListView = document.getElementById('users-list-view');
862
+ const profileView = document.getElementById('profile-view');
863
  const chatWindowView = document.getElementById('chat-window-view');
864
  const chatPlaceholder = document.getElementById('chat-placeholder');
865
  const activeChat = document.getElementById('active-chat');
866
+
867
+ const createRoomModal = document.getElementById('create-room-modal');
868
+ const passwordModal = document.getElementById('password-modal');
869
+ const userProfileModal = document.getElementById('user-profile-modal'); // Modal for other users
870
  const scannerModal = document.getElementById('scanner-modal');
871
+ const statusBar = document.getElementById('status-bar');
872
 
873
  const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292'];
874
 
 
884
  };
885
 
886
  const showStatus = (message, type = 'info', duration = 3000) => {
 
887
  statusBar.textContent = message;
888
+ statusBar.className = 'status-bar'; // Reset classes
889
  if (type === 'success') statusBar.classList.add('success');
890
  else if (type === 'error') statusBar.classList.add('error');
891
+ if (activeChatroomId && window.innerWidth < 768) {
892
+ statusBar.classList.add('chat-open'); // Position lower when chat is fullscreen on mobile
893
+ } else {
894
+ statusBar.classList.remove('chat-open');
895
+ }
896
+
897
  statusBar.classList.add('visible');
898
  setTimeout(() => statusBar.classList.remove('visible'), duration);
899
  };
 
912
  throw error;
913
  }
914
  };
915
+
916
  const truncateAddress = (address) => address ? `${address.substring(0, 4)}...${address.substring(address.length - 4)}` : '';
917
 
918
  const updateUserInfo = () => {
919
+ const walletEl = document.getElementById('user-wallet');
920
  const nicknameEl = document.getElementById('user-nickname');
921
  const usernameInput = document.getElementById('username-input');
922
+
923
+ if (walletEl) walletEl.textContent = `Кошелек: ${truncateAddress(currentUser.address)}`;
924
+ if (nicknameEl) nicknameEl.textContent = currentUser.username ? `Ник: ${currentUser.username}` : `Никнейм не установлен`;
925
+ if (usernameInput) usernameInput.value = currentUser.username || '';
926
+
927
+ // Update profile view if visible
928
+ if (currentView === 'profile-view') {
929
+ showMyProfile();
930
+ }
931
  };
932
+
933
  document.getElementById('username-form').addEventListener('submit', async (e) => {
934
  e.preventDefault();
935
  const newUsername = document.getElementById('username-input').value.trim();
936
+ if (!newUsername) {
937
+ showStatus('Никнейм не может быть пустым.', 'error');
938
+ return;
939
+ }
940
+ if (newUsername.length < 3 || newUsername.length > 20) {
941
+ showStatus('Никнейм должен быть от 3 до 20 символов.', 'error');
942
+ return;
943
+ }
944
+ if (!/^[a-zA-Z0-9_]+$/.test(newUsername)) {
945
+ showStatus('Никнейм может содержать только латинские буквы, цифры и _ .', 'error');
946
  return;
947
  }
948
+
949
  try {
950
  await apiCall('/api/set_username', {
951
  method: 'POST',
 
955
  currentUser.username = newUsername;
956
  updateUserInfo();
957
  showStatus('Никнейм успешно обновлен!', 'success');
958
+ fetchChatrooms(); // Chat list needs to update display names
959
+ if (activeChatroomId) fetchMessages(activeChatroomId); // Active chat needs to update display names
960
+ fetchUsers(); // User list needs to update
961
  } catch (err) {}
962
  });
963
 
 
975
  }
976
  updateUserInfo();
977
  loginView.style.display = 'none';
978
+ appView.style.display = 'flex';
979
+ showView('chatroom-list-view'); // Default view after login
980
  fetchChatrooms();
981
+ fetchUsers(); // Fetch users list on login
982
+ };
983
+
984
+ const showView = (viewId) => {
985
+ // Hide all main views first
986
+ document.querySelectorAll('.app-view-section').forEach(view => {
987
+ view.style.display = 'none';
988
+ });
989
+ chatWindowView.style.display = 'none';
990
+ activeChat.style.display = 'none'; // Ensure chat content is hidden
991
+ chatPlaceholder.style.display = 'flex'; // Show placeholder by default
992
+
993
+ // Hide modals just in case
994
+ document.querySelectorAll('.modal-overlay').forEach(modal => modal.style.display = 'none');
995
+
996
+ // Clear intervals/states related to potentially hidden views
997
+ if (messagePollingInterval) clearInterval(messagePollingInterval);
998
+ activeChatroomId = null;
999
+
1000
+ // Show the selected view
1001
+ const selectedView = document.getElementById(viewId);
1002
+ if (selectedView) {
1003
+ selectedView.style.display = 'flex';
1004
+ currentView = viewId;
1005
+ if (viewId === 'chatroom-list-view' || viewId === 'users-list-view') {
1006
+ lastListView = viewId; // Update last list view
1007
+ }
1008
+ }
1009
+
1010
+ // Update active nav button
1011
+ navButtons.forEach(btn => {
1012
+ btn.classList.remove('active');
1013
+ if (btn.dataset.view === viewId) {
1014
+ btn.classList.add('active');
1015
+ }
1016
+ // Special handling for scan button, it opens a modal, not a view section
1017
+ if (viewId === 'scan-qr') {
1018
+ document.querySelector('.nav-btn.scan-qr').classList.add('active');
1019
+ }
1020
+ });
1021
+
1022
+ // Handle desktop layout: list view always visible next to chat
1023
+ if (window.innerWidth >= 768) {
1024
+ if (viewId === 'chatroom-list-view' || viewId === 'users-list-view') {
1025
+ // Show the selected list view
1026
+ document.getElementById(viewId).style.display = 'flex';
1027
+ // Hide the other list view
1028
+ const otherListViewId = viewId === 'chatroom-list-view' ? 'users-list-view' : 'chatroom-list-view';
1029
+ document.getElementById(otherListViewId).style.display = 'none';
1030
+ // Show chat placeholder beside it
1031
+ chatWindowView.style.display = 'flex';
1032
+ chatPlaceholder.style.display = 'flex';
1033
+ activeChat.style.display = 'none';
1034
+ } else if (viewId === 'profile-view') {
1035
+ // Show profile view, hide both list views
1036
+ chatroomListView.style.display = 'none';
1037
+ usersListView.style.display = 'none';
1038
+ // Hide chat window placeholder/content beside it
1039
+ chatWindowView.style.display = 'none';
1040
+ } else if (viewId === 'chat-window-view') {
1041
+ // This case should ideally be handled by selectChatroom,
1042
+ // not directly by clicking a nav button.
1043
+ // On desktop, when chat is active, the list view should still be visible.
1044
+ // The selectChatroom function handles this.
1045
+ // If somehow showView('chat-window-view') is called, default to showing last list view beside it.
1046
+ document.getElementById(lastListView).style.display = 'flex';
1047
+ chatWindowView.style.display = 'flex';
1048
+ }
1049
+ } else { // Mobile layout
1050
+ // Ensure nav bar is visible unless in chat
1051
+ navigationBar.style.display = (viewId === 'chat-window-view') ? 'none' : 'flex';
1052
+ // Adjust status bar position based on nav visibility
1053
+ if (viewId === 'chat-window-view') statusBar.classList.add('chat-open');
1054
+ else statusBar.classList.remove('chat-open');
1055
+ }
1056
  };
1057
 
1058
  const renderChatrooms = (rooms) => {
1059
  const list = document.getElementById('chatroom-list');
1060
  list.innerHTML = '';
1061
  chatroomsData = {};
1062
+ if (rooms.length === 0) {
1063
+ list.innerHTML = '<div style="text-align: center; padding: 20px; color: var(--text-secondary);">Пока нет чатов. Создайте первый!</div>';
1064
+ } else {
1065
+ rooms.forEach(room => {
1066
+ chatroomsData[room.id] = room;
1067
+ const item = document.createElement('div');
1068
+ item.className = 'chatroom-item';
1069
+ item.dataset.id = room.id;
1070
+
1071
+ item.appendChild(getAvatar(room.name));
1072
+
1073
+ const infoDiv = document.createElement('div');
1074
+ infoDiv.className = 'item-info';
1075
+ const nameSpan = document.createElement('div');
1076
+ nameSpan.className = 'item-name';
1077
+ nameSpan.textContent = room.name;
1078
+ infoDiv.appendChild(nameSpan);
1079
+ item.appendChild(infoDiv);
1080
+
1081
+ if (room.is_private) {
1082
+ const lockIcon = document.createElement('div');
1083
+ lockIcon.innerHTML = `<svg class="lock-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zM9 8V6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9z"/></svg>`;
1084
+ item.appendChild(lockIcon);
1085
+ }
1086
 
1087
+ item.addEventListener('click', () => selectChatroom(room.id, room.is_private));
1088
+ list.appendChild(item);
1089
+ });
1090
+ }
1091
  };
1092
+
1093
  const fetchChatrooms = async () => {
1094
  try {
1095
  const data = await apiCall('/api/chatrooms');
1096
+ if (data) renderChatrooms(data.chatrooms);
1097
  } catch (err) {}
1098
  };
1099
 
1100
+ const renderUsers = (users) => {
1101
+ const list = document.getElementById('users-list');
1102
+ list.innerHTML = '';
1103
+ if (users.length === 0) {
1104
+ list.innerHTML = '<div style="text-align: center; padding: 20px; color: var(--text-secondary);">Пока нет других пользователей.</div>';
1105
+ } else {
1106
+ users.forEach(user => {
1107
+ if (user.address === currentUser.address) return; // Skip current user
1108
+
1109
+ const item = document.createElement('div');
1110
+ item.className = 'user-item';
1111
+ item.dataset.address = user.address;
1112
+
1113
+ item.appendChild(getAvatar(user.username));
1114
+
1115
+ const infoDiv = document.createElement('div');
1116
+ infoDiv.className = 'item-info';
1117
+ const nameSpan = document.createElement('div');
1118
+ nameSpan.className = 'item-name';
1119
+ nameSpan.textContent = user.username || `User ${truncateAddress(user.address)}`;
1120
+ infoDiv.appendChild(nameSpan);
1121
+
1122
+ const addressSpan = document.createElement('div');
1123
+ addressSpan.className = 'item-secondary-text';
1124
+ addressSpan.textContent = truncateAddress(user.address);
1125
+ infoDiv.appendChild(addressSpan);
1126
+
1127
+ item.appendChild(infoDiv);
1128
+
1129
+ item.addEventListener('click', () => showUserProfileModal(user.address));
1130
+ list.appendChild(item);
1131
+ });
1132
+ }
1133
+ };
1134
+
1135
+ const fetchUsers = async () => {
1136
+ try {
1137
+ const data = await apiCall('/api/users');
1138
+ if (data) renderUsers(data.users);
1139
+ } catch (err) {}
1140
+ };
1141
+
1142
  const renderMessages = (messages) => {
1143
  const container = document.getElementById('messages-container');
1144
  const shouldScroll = container.scrollTop + container.clientHeight >= container.scrollHeight - 30;
 
1150
  const avatar = getAvatar(msg.display_name);
1151
  avatar.classList.add('message-avatar');
1152
  avatar.style.cursor = 'pointer';
1153
+ avatar.onclick = () => {
1154
+ if (msg.sender_address !== currentUser.address) {
1155
+ showUserProfileModal(msg.sender_address);
1156
+ } else {
1157
+ showMyProfile(); // Click own avatar goes to own profile view
1158
+ }
1159
+ };
1160
+
1161
  const contentDiv = document.createElement('div');
1162
  contentDiv.className = 'message-content';
1163
 
1164
  const senderDiv = document.createElement('div');
1165
  senderDiv.className = 'message-sender';
1166
  senderDiv.textContent = msg.display_name;
1167
+ senderDiv.onclick = () => {
1168
+ if (msg.sender_address !== currentUser.address) {
1169
+ showUserProfileModal(msg.sender_address);
1170
+ } else {
1171
+ showMyProfile(); // Click own username goes to own profile view
1172
+ }
1173
+ };
1174
+
1175
  const bubbleDiv = document.createElement('div');
1176
  bubbleDiv.className = 'message-bubble';
1177
  bubbleDiv.textContent = msg.text;
 
1184
  container.appendChild(msgDiv);
1185
  });
1186
 
1187
+ if(shouldScroll || messages.length > 0) { // Scroll to bottom on initial load too
1188
  container.scrollTop = container.scrollHeight;
1189
  }
1190
  };
1191
+
1192
  const fetchMessages = async (roomId) => {
1193
+ if (!roomId) return;
1194
  try {
1195
  const data = await apiCall(`/api/messages/${roomId}`);
1196
+ if (data) renderMessages(data.messages);
1197
  } catch (err) {
1198
  if (messagePollingInterval) clearInterval(messagePollingInterval);
1199
  }
1200
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1201
 
1202
  const selectChatroom = (roomId, isPrivate) => {
1203
  const roomData = chatroomsData[roomId];
 
1206
  const proceedToRoom = () => {
1207
  if (messagePollingInterval) clearInterval(messagePollingInterval);
1208
  activeChatroomId = roomId;
1209
+
1210
  document.getElementById('chat-header-title').textContent = roomData.name;
1211
  const headerAvatar = document.getElementById('chat-header-avatar');
1212
  headerAvatar.innerHTML = '';
 
1214
 
1215
  chatPlaceholder.style.display = 'none';
1216
  activeChat.style.display = 'flex';
1217
+ showView('chat-window-view'); // Switch view to chat window
1218
+
1219
  fetchMessages(roomId);
1220
  messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000);
1221
  };
1222
+
1223
  if (isPrivate) {
 
1224
  const passwordForm = document.getElementById('password-form');
1225
  const passwordInput = document.getElementById('password-input');
1226
  passwordModal.style.display = 'flex';
1227
  passwordInput.value = '';
1228
  passwordInput.focus();
1229
 
1230
+ // Remove existing listener before adding a new one
1231
+ const oldForm = document.getElementById('password-form');
1232
+ const newForm = oldForm.cloneNode(true);
1233
+ oldForm.parentNode.replaceChild(newForm, oldForm);
1234
+ const passwordFormNew = document.getElementById('password-form');
1235
+
1236
  const formSubmitHandler = async (e) => {
1237
  e.preventDefault();
1238
+ passwordFormNew.removeEventListener('submit', formSubmitHandler);
1239
  const password = passwordInput.value;
1240
  passwordModal.style.display = 'none';
1241
  try {
 
1247
  proceedToRoom();
1248
  } catch (err) {}
1249
  };
1250
+ passwordFormNew.addEventListener('submit', formSubmitHandler);
1251
 
1252
  document.getElementById('password-cancel').onclick = () => {
1253
  passwordModal.style.display = 'none';
1254
+ passwordFormNew.removeEventListener('submit', formSubmitHandler);
1255
  };
1256
  } else {
1257
  proceedToRoom();
 
1263
  const input = document.getElementById('message-input');
1264
  const sendBtn = document.getElementById('send-btn');
1265
  const text = input.value.trim();
1266
+ if (text && activeChatroomId && currentUser.address) {
1267
  input.value = '';
1268
  input.disabled = true;
1269
  sendBtn.disabled = true;
 
1277
  text: text
1278
  })
1279
  });
1280
+ // Wait a moment before polling to ensure DB write completes
1281
+ setTimeout(() => fetchMessages(activeChatroomId), 200);
1282
  } finally {
1283
  input.disabled = false;
1284
  sendBtn.disabled = false;
 
1287
  }
1288
  });
1289
 
 
1290
  document.getElementById('create-room-show-modal').addEventListener('click', () => {
1291
  createRoomModal.style.display = 'flex';
1292
  document.getElementById('create-room-form').reset();
1293
+ document.getElementById('room-name').focus();
1294
  });
1295
  document.getElementById('create-room-cancel').addEventListener('click', () => {
1296
  createRoomModal.style.display = 'none';
 
1299
  e.preventDefault();
1300
  const name = document.getElementById('room-name').value.trim();
1301
  const password = document.getElementById('room-password').value;
1302
+ if (!name) {
1303
+ showStatus('Название чата не может быть пустым.', 'error');
1304
+ return;
1305
+ }
1306
+ if (name.length < 3 || name.length > 30) {
1307
+ showStatus('Название чата должно быть от 3 до 30 символов.', 'error');
1308
+ return;
1309
+ }
1310
+ if (password && password.length < 4) {
1311
+ showStatus('Пароль должен быть не короче 4 символов.', 'error');
1312
+ return;
1313
+ }
1314
+ if (!currentUser.address) {
1315
+ showStatus('Подключите кошелек для создания чата.', 'error');
1316
+ return;
1317
+ }
1318
 
1319
  try {
1320
  await apiCall('/api/create_chatroom', {
 
1328
  } catch (err) {}
1329
  });
1330
 
1331
+ const updateProfileDataInElements = async (address, isCurrentUser, containerId, avatarContainerId, usernameId, addressId, balanceId, qrCodeId, sendTonBtnId) => {
1332
  try {
1333
  const userData = await apiCall('/api/user_data', {
1334
  method: 'POST',
1335
  headers: { 'Content-Type': 'application/json' },
1336
  body: JSON.stringify({ address: address })
1337
  });
1338
+
1339
  const username = userData.username || `User ${truncateAddress(address)}`;
1340
+ const avatarContainer = document.getElementById(avatarContainerId);
1341
+ const usernameEl = document.getElementById(usernameId);
1342
+ const addressEl = document.getElementById(addressId);
1343
+ const balanceEl = document.getElementById(balanceId);
1344
+ const qrCodeEl = document.getElementById(qrCodeId);
1345
+ const sendTonBtn = document.getElementById(sendTonBtnId);
1346
+
1347
+ if (avatarContainer) {
1348
+ avatarContainer.innerHTML = '';
1349
+ avatarContainer.appendChild(getAvatar(username));
 
 
 
 
 
1350
  }
1351
+ if (usernameEl) usernameEl.textContent = username;
1352
+ if (addressEl) addressEl.textContent = address;
1353
+
1354
+ // Placeholder for balance - backend doesn't provide it
1355
+ if (balanceEl) {
1356
+ // You would fetch the actual balance here if the backend supported it
1357
+ balanceEl.textContent = `Баланс: ${isCurrentUser ? 'Loading...' : 'N/A'}`; // Show loading for self, N/A for others (as we don't have their balance)
1358
+ if (isCurrentUser && tonConnectUI.connected) {
1359
+ try {
1360
+ const balance = await tonConnectUI.getBalance();
1361
+ balanceEl.textContent = `Баланс: ${parseFloat(balance) / 1_000_000_000} TON`; // Display balance in TON
1362
+ } catch (balanceError) {
1363
+ balanceEl.textContent = 'Баланс: Ошибка загрузки';
1364
+ console.error("Failed to get balance:", balanceError);
1365
+ }
1366
+ } else if (!isCurrentUser) {
1367
+ balanceEl.textContent = 'Баланс: N/A';
1368
  }
1369
+ }
1370
+
1371
+
1372
+ if (qrCodeEl) {
1373
+ qrCodeEl.innerHTML = '';
1374
+ const qrCodeInstance = isCurrentUser ? profileQrCodeView : profileQrCodeModal;
1375
+ if (qrCodeInstance) {
1376
+ qrCodeInstance.clear();
1377
  }
 
 
1378
 
1379
+ const newQrCodeInstance = new QRCode(qrCodeEl, {
1380
+ text: address,
1381
+ width: 150,
1382
+ height: 150,
1383
+ colorDark : "#000000",
1384
+ colorLight : "#ffffff",
1385
+ correctLevel : QRCode.CorrectLevel.H
1386
+ });
1387
 
1388
+ if (isCurrentUser) profileQrCodeView = newQrCodeInstance;
1389
+ else profileQrCodeModal = newQrCodeInstance;
1390
+ }
1391
+
1392
+ if (sendTonBtn) {
1393
+ if (isCurrentUser) {
1394
+ sendTonBtn.style.display = 'none';
1395
+ } else {
1396
+ sendTonBtn.style.display = tonConnectUI.connected ? 'block' : 'none';
1397
+ sendTonBtn.onclick = async () => {
1398
+ if (!tonConnectUI.connected) {
1399
+ showStatus('Подключите кошелек для отправки TON.', 'error');
1400
+ return;
1401
+ }
1402
+ const amountString = prompt("Введите сумму в TON для отправки:", "0.1");
1403
+ if (amountString === null) return;
1404
+
1405
+ const amount = parseFloat(amountString);
1406
+ if (isNaN(amount) || amount <= 0) {
1407
+ showStatus('Неверная сумма.', 'error');
1408
+ return;
1409
+ }
1410
+
1411
+ const amountInNanoTon = Math.floor(amount * 1_000_000_000).toString();
1412
+
1413
+ const transaction = {
1414
+ validUntil: Math.floor(Date.now() / 1000) + 600,
1415
+ messages: [ { address: address, amount: amountInNanoTon } ]
1416
+ };
1417
+
1418
+ try {
1419
+ await tonConnectUI.sendTransaction(transaction);
1420
+ showStatus(`Транзакция отправлена успешно!`, 'success');
1421
+ userProfileModal.style.display = 'none'; // Close modal after sending
1422
+ } catch (error) {
1423
+ showStatus('Транзакция отменена или произошла ошибка.', 'error');
1424
+ }
1425
+ };
1426
  }
1427
+ }
1428
+
 
 
1429
  } catch (err) {
1430
  showStatus('Не удалось загрузить профиль.', 'error');
1431
  }
1432
+ };
1433
+
1434
+
1435
+ const showMyProfile = () => {
1436
+ if (!currentUser.address) {
1437
+ showStatus('Подключите кошелек, чтобы посмотреть профиль.', 'error');
1438
+ return;
1439
+ }
1440
+ showView('profile-view');
1441
+ updateProfileDataInElements(
1442
+ currentUser.address,
1443
+ true,
1444
+ 'profile-view',
1445
+ 'profile-avatar-container-view',
1446
+ 'profile-username-view',
1447
+ 'profile-address-view',
1448
+ 'profile-balance-view',
1449
+ 'profile-qr-code-view',
1450
+ null // No send TON button on own profile view
1451
+ );
1452
+ };
1453
+
1454
+ const showUserProfileModal = (address) => {
1455
+ if (!address) return;
1456
+ if (address === currentUser.address) {
1457
+ showMyProfile(); // If clicking self in list, go to profile view
1458
+ return;
1459
+ }
1460
+ userProfileModal.style.display = 'flex';
1461
+ updateProfileDataInElements(
1462
+ address,
1463
+ false,
1464
+ 'user-profile-modal',
1465
+ 'profile-avatar-container-modal',
1466
+ 'profile-username-modal',
1467
+ 'profile-address-modal',
1468
+ 'profile-balance-modal',
1469
+ 'profile-qr-code-modal',
1470
+ 'send-ton-btn' // Send TON button on other users' modal
1471
+ );
1472
  };
1473
 
1474
  const showScanner = () => {
 
1476
  html5QrCode = new Html5Qrcode("qr-reader");
1477
  const qrCodeSuccessCallback = (decodedText, decodedResult) => {
1478
  hideScanner();
1479
+ // Basic check if it looks like a TON address (EQ/UQ followed by many chars)
1480
  if (decodedText && decodedText.length > 40 && (decodedText.startsWith('EQ') || decodedText.startsWith('UQ'))) {
1481
+ showUserProfileModal(decodedText); // Show profile modal for scanned address
1482
  } else {
1483
+ showStatus('Отсканирован недействительный QR-код (ожидается TON адрес).', 'error');
1484
  }
1485
  };
1486
+ const config = { fps: 10, qrbox: { width: 250, height: 250 } }; // Adjusted qrbox size
1487
+ // Request camera permissions and start scanning
1488
  html5QrCode.start({ facingMode: "environment" }, config, qrCodeSuccessCallback)
1489
  .catch(err => {
1490
  showStatus('Не удалось запустить сканер.', 'error');
 
1498
  }
1499
  scannerModal.style.display = 'none';
1500
  };
1501
+
1502
+ document.getElementById('disconnect-wallet-btn').addEventListener('click', () => {
1503
+ tonConnectUI.disconnect();
1504
+ });
1505
+ document.getElementById('user-profile-close-btn').addEventListener('click', () => userProfileModal.style.display = 'none');
1506
+ document.getElementById('scanner-close-btn').addEventListener('click', hideScanner);
1507
+ document.getElementById('back-to-list-btn').addEventListener('click', () => {
1508
+ if (window.innerWidth < 768) { // Only on mobile
1509
+ showView(lastListView); // Go back to the last visited list view
1510
+ }
1511
+ // On desktop, back button is hidden, list view is always visible
1512
+ });
1513
+
1514
+ // Navigation button listeners
1515
+ navButtons.forEach(button => {
1516
+ button.addEventListener('click', () => {
1517
+ const viewId = button.dataset.view;
1518
+ if (viewId === 'scan-qr') {
1519
+ showScanner();
1520
+ } else {
1521
+ // If coming from chat window, stop polling
1522
+ if (activeChatroomId) {
1523
+ clearInterval(messagePollingInterval);
1524
+ activeChatroomId = null;
1525
+ activeChat.style.display = 'none';
1526
+ chatPlaceholder.style.display = 'flex';
1527
+ }
1528
+ showView(viewId);
1529
+ // Fetch data for the view if needed
1530
+ if (viewId === 'chatroom-list-view') fetchChatrooms();
1531
+ if (viewId === 'users-list-view') fetchUsers();
1532
+ if (viewId === 'profile-view') showMyProfile();
1533
  }
1534
+ });
1535
+ });
 
 
 
 
 
 
1536
 
1537
+ // Handle initial view and resize
 
 
 
 
 
 
 
 
 
 
1538
  const handleResize = () => {
1539
  const isMobile = window.innerWidth < 768;
1540
+ const backBtn = document.getElementById('back-to-list-btn');
1541
+ if (backBtn) backBtn.style.display = isMobile ? 'block' : 'none';
1542
+
1543
+ // Re-apply view display logic based on current view and screen size
1544
+ const currentViewId = currentView; // Capture current view before showView might change it
1545
+ if (activeChatroomId) { // If currently in a chat
1546
+ // Ensure desktop layout shows list + chat, mobile shows only chat
1547
+ if (window.innerWidth >= 768) {
1548
+ document.getElementById(lastListView).style.display = 'flex'; // Show last list
1549
+ chatWindowView.style.display = 'flex'; // Show chat
1550
+ navigationBar.style.display = 'flex'; // Nav bar is column
1551
+ statusBar.classList.remove('chat-open'); // Status bar standard position
1552
  } else {
1553
+ document.querySelectorAll('.app-view-section').forEach(view => view.style.display = 'none'); // Hide lists/profile
1554
+ chatWindowView.style.display = 'flex'; // Show chat only
1555
+ navigationBar.style.display = 'none'; // Hide nav bar
1556
+ statusBar.classList.add('chat-open'); // Status bar lower position
1557
  }
1558
+ } else { // Not in a chat
1559
+ showView(currentViewId); // Re-apply display for the current view
1560
  }
1561
  };
1562
 
1563
  window.addEventListener('resize', handleResize);
1564
+
1565
 
1566
  tonConnectUI.onStatusChange(wallet => {
1567
  if (wallet) {
 
1569
  initializeUser(address);
1570
  } else {
1571
  currentUser = { address: null, username: null };
1572
+ appView.style.display = 'none';
1573
  loginView.style.display = 'flex';
1574
  if (messagePollingInterval) clearInterval(messagePollingInterval);
1575
  activeChatroomId = null;
1576
+ currentView = 'chatroom-list-view'; // Reset view state
1577
+ lastListView = 'chatroom-list-view';
1578
+ // Clear displayed user data on logout
1579
+ updateUserInfo();
1580
+ if (profileQrCodeView) profileQrCodeView.clear();
1581
+ if (profileQrCodeModal) profileQrCodeModal.clear();
1582
+ document.getElementById('profile-qr-code-view').innerHTML = '';
1583
+ document.getElementById('profile-qr-code-modal').innerHTML = '';
1584
+ }
1585
+ // Update profile view balance status if wallet connection changes
1586
+ if (currentView === 'profile-view') {
1587
+ showMyProfile();
1588
  }
1589
  });
1590
+
1591
+ // Check initial connection status on load
1592
+ if (tonConnectUI.connected) {
1593
+ const wallet = tonConnectUI.wallet;
1594
+ const address = TON_CONNECT_UI.toUserFriendlyAddress(wallet.account.address, false);
1595
+ initializeUser(address);
1596
+ } else {
1597
+ loginView.style.display = 'flex';
1598
+ }
1599
+
1600
+ // Initial resize check to set up correct layout
1601
+ handleResize();
1602
  });
1603
  </script>
1604
  </body>
 
1614
  if not address:
1615
  return jsonify({'error': 'Address is required'}), 400
1616
  db = read_db()
1617
+ user_info = db['users'].get(address, {}) # Return empty dict if user not found
1618
+ username = user_info.get('username')
1619
+ return jsonify({'address': address, 'username': username})
1620
+
1621
+ @app.route('/api/users', methods=['GET'])
1622
+ def get_users():
1623
+ db = read_db()
1624
+ users_list = []
1625
+ for address, user_info in db['users'].items():
1626
+ # Only include users who have set a username for potentially cleaner list,
1627
+ # or include all with default name? Include all for simplicity.
1628
+ users_list.append({
1629
+ 'address': address,
1630
+ 'username': user_info.get('username')
1631
+ })
1632
+ # Include users who might exist in messages but not in 'users' table?
1633
+ # This requires scanning all messages, which is inefficient.
1634
+ # Stick to users explicitly stored in 'users' for now.
1635
+
1636
+ # Simple way to add users who exist in messages but not necessarily set a username yet
1637
+ # This is still inefficient for large DBs but better than scanning all messages every time.
1638
+ all_addresses_in_messages = set()
1639
+ for chatroom_messages in db['messages'].values():
1640
+ for msg in chatroom_messages:
1641
+ all_addresses_in_messages.add(msg['sender_address'])
1642
+
1643
+ existing_user_addresses = {user['address'] for user in users_list}
1644
+
1645
+ for address in all_addresses_in_messages:
1646
+ if address not in existing_user_addresses:
1647
+ # Add user with null username if they sent a message but haven't set a username
1648
+ users_list.append({
1649
+ 'address': address,
1650
+ 'username': None
1651
+ })
1652
+
1653
+ # Ensure addresses are unique in the final list
1654
+ unique_users = {}
1655
+ for user in users_list:
1656
+ if user['address'] not in unique_users:
1657
+ unique_users[user['address']] = user
1658
+ elif user.get('username') and not unique_users[user['address']].get('username'):
1659
+ # If we found a username for an address already added with null, update it
1660
+ unique_users[user['address']]['username'] = user['username']
1661
+
1662
+ return jsonify({'users': list(unique_users.values())})
1663
+
1664
 
1665
  @app.route('/api/set_username', methods=['POST'])
1666
  def set_username():
 
1671
  return jsonify({'error': 'Address and username are required'}), 400
1672
  if len(username) < 3 or len(username) > 20:
1673
  return jsonify({'error': 'Username must be between 3 and 20 characters'}), 400
1674
+ if not username.replace('_', '').isalnum(): # Allow alphanumeric and underscore
1675
+ return jsonify({'error': 'Username can only contain letters, numbers, and underscores.'}), 400
1676
+
1677
 
1678
  db = read_db()
1679
+ # Check if username is already taken by another address
1680
+ for addr, user_info in db['users'].items():
1681
+ if addr != address and user_info.get('username', '').lower() == username.lower():
1682
+ return jsonify({'error': 'Username is already taken'}), 409 # Conflict
1683
+
1684
  if address not in db['users']:
1685
  db['users'][address] = {}
1686
  db['users'][address]['username'] = username
 
1695
  chatrooms_list.append({
1696
  'id': room_id,
1697
  'name': room_data['name'],
1698
+ 'is_private': room_data.get('is_private', False) # Default to False if key is missing
1699
  })
1700
  return jsonify({'chatrooms': sorted(chatrooms_list, key=lambda x: x['name'])})
1701
 
 
1708
  if not name or not creator_address:
1709
  return jsonify({'error': 'Name and creator address are required'}), 400
1710
 
1711
+ if len(name) < 3 or len(name) > 30:
1712
+ return jsonify({'error': 'Chatroom name must be between 3 and 30 characters'}), 400
1713
+ if password and len(password) < 4:
1714
+ return jsonify({'error': 'Password must be at least 4 characters long'}), 400
1715
+
1716
+
1717
  db = read_db()
1718
  room_id = str(uuid.uuid4())
1719
  db['chatrooms'][room_id] = {
 
1735
  chatroom = db['chatrooms'].get(chatroom_id)
1736
  if not chatroom:
1737
  return jsonify({'error': 'Chatroom not found'}), 404
1738
+ if chatroom.get('is_private', False): # Default to False if key is missing
1739
+ # Use get with default None in case password_hash was somehow not set for a private room
1740
+ stored_hash = chatroom.get('password_hash', None)
1741
+ if not stored_hash or not check_password_hash(stored_hash, password or ''): # Compare against empty string if password is None
1742
  return jsonify({'error': 'Invalid password'}), 403
1743
  return jsonify({'success': True})
1744
 
 
1746
  def get_messages(chatroom_id):
1747
  db = read_db()
1748
  if chatroom_id not in db['messages']:
1749
+ # If chatroom exists but has no messages entry yet, create one
1750
+ if chatroom_id in db['chatrooms']:
1751
+ db['messages'][chatroom_id] = []
1752
+ write_db(db) # Write the new empty message list
1753
+ else:
1754
+ return jsonify({'error': 'Chatroom not found'}), 404
1755
+
1756
  messages_with_names = []
1757
  room_messages = db['messages'].get(chatroom_id, [])
1758
+
1759
+ # Collect all unique sender addresses in this chat
1760
+ sender_addresses = {msg['sender_address'] for msg in room_messages}
1761
+
1762
+ # Fetch usernames for all senders in one go
1763
+ user_names_map = {}
1764
+ for address in sender_addresses:
1765
+ user_info = db['users'].get(address)
1766
+ user_names_map[address] = (user_info.get('username') if user_info and user_info.get('username')
1767
+ else f"{address[:4]}...{address[-4:]}")
1768
+
1769
  for msg in room_messages:
1770
  sender_address = msg['sender_address']
1771
+ display_name = user_names_map.get(sender_address, f"{sender_address[:4]}...{sender_address[-4:]}") # Fallback just in case
1772
+
 
 
1773
  msg_copy = msg.copy()
1774
  msg_copy['display_name'] = display_name
1775
  messages_with_names.append(msg_copy)
 
1787
  return jsonify({'error': 'Missing data'}), 400
1788
 
1789
  db = read_db()
1790
+ # Check if chatroom exists before adding message
1791
+ if chatroom_id not in db['chatrooms']:
1792
+ return jsonify({'error': 'Chatroom not found'}), 404
1793
+
1794
+ # Ensure user entry exists, even if they haven't set a username yet
1795
+ if sender_address not in db['users']:
1796
+ db['users'][sender_address] = {}
1797
+ # Don't write to DB immediately, will write after adding message
1798
 
1799
  message = {
1800
  'id': str(uuid.uuid4()),
 
1802
  'text': text,
1803
  'timestamp': datetime.utcnow().isoformat() + "Z"
1804
  }
1805
+
1806
+ # Ensure message list exists for the chatroom
1807
+ if chatroom_id not in db['messages']:
1808
+ db['messages'][chatroom_id] = []
1809
+
1810
+ # Keep message list length manageable (e.g., last 200 messages)
1811
+ MAX_MESSAGES = 200
1812
+ if len(db['messages'][chatroom_id]) >= MAX_MESSAGES:
1813
+ db['messages'][chatroom_id].pop(0) # Remove oldest message
1814
 
1815
  db['messages'][chatroom_id].append(message)
1816
  write_db(db)
 
1819
 
1820
  if __name__ == '__main__':
1821
  init_db()
1822
+ # Consider using a more robust web server like Gunicorn or uWSGI
1823
+ # for production, but for a simple Flask app this is fine.
1824
+ app.run(host='0.0.0.0', port=7860, debug=True) # Turn debug=False for production