Aleksmorshen commited on
Commit
2594174
·
verified ·
1 Parent(s): 1da0b5c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +954 -430
app.py CHANGED
@@ -19,8 +19,11 @@ 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 +46,20 @@ 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: #000000;
47
- --bg-secondary: #101010;
48
- --bg-tertiary: #1C1C1D;
49
- --bg-hover: #2c2c2e;
50
- --text-primary: #FFFFFF;
51
- --text-secondary: #8E8E93;
52
- --accent-blue: #007AFF;
53
- --accent-blue-light: #3498DB;
54
- --border-color: #2A2A2A;
55
- --success-color: #34C759;
56
- --error-color: #FF3B30;
57
  --font-family: 'Inter', sans-serif;
 
 
58
  }
59
 
60
  * {
@@ -74,11 +79,9 @@ def index():
74
  color: var(--text-primary);
75
  overflow: hidden;
76
  height: 100vh;
77
- height: -webkit-fill-available;
78
  width: 100vw;
79
  display: flex;
80
- align-items: center;
81
- justify-content: center;
82
  }
83
 
84
  .main-container {
@@ -87,25 +90,22 @@ def index():
87
  display: flex;
88
  flex-direction: column;
89
  transition: opacity 0.3s ease;
90
- background-color: var(--bg-primary);
91
  }
92
 
93
  #login-view {
94
- display: flex;
95
- flex-direction: column;
96
  align-items: center;
97
  justify-content: center;
98
  text-align: center;
99
  background: radial-gradient(circle, #1a2a3a 0%, var(--bg-primary) 70%);
100
  }
101
  #login-view img {
102
- width: 120px;
103
- height: 120px;
104
  margin-bottom: 24px;
105
- filter: drop-shadow(0 0 25px rgba(0, 122, 255, 0.6));
106
  }
107
  #login-view h1 {
108
- font-size: 3.5rem;
109
  font-weight: 700;
110
  background: linear-gradient(45deg, var(--accent-blue-light), var(--accent-blue));
111
  -webkit-background-clip: text;
@@ -113,78 +113,130 @@ def index():
113
  margin-bottom: 12px;
114
  }
115
  #login-view p {
116
- font-size: 1.2rem;
117
  color: var(--text-secondary);
118
  margin-bottom: 40px;
119
  }
120
 
121
  #app-view {
122
  display: none;
 
 
 
123
  }
124
-
125
- #main-content {
126
  flex-grow: 1;
127
  overflow: hidden;
128
  position: relative;
129
- display: flex;
130
  }
131
 
132
- .page {
 
 
133
  position: absolute;
134
  top: 0;
135
  left: 0;
136
- width: 100%;
137
- height: 100%;
138
- display: flex;
139
  flex-direction: column;
140
- opacity: 0;
141
- visibility: hidden;
142
- transition: opacity 0.2s ease-in-out, visibility 0.2s;
143
  background-color: var(--bg-primary);
144
- }
145
- .page.active {
146
- opacity: 1;
147
- visibility: visible;
148
- z-index: 10;
149
  }
150
 
151
- #chats-page { flex-direction: row; }
 
 
 
 
 
 
 
 
152
 
153
- .list-view {
154
  display: flex;
155
  flex-direction: column;
156
  height: 100%;
157
  width: 100%;
158
  background-color: var(--bg-secondary);
159
- border-right: 1px solid var(--border-color);
160
  }
161
- .page-header {
 
162
  padding: 16px;
163
  border-bottom: 1px solid var(--border-color);
164
  flex-shrink: 0;
165
- background: rgba(28, 28, 29, 0.8);
166
- backdrop-filter: blur(10px);
167
- -webkit-backdrop-filter: blur(10px);
168
- z-index: 5;
169
  }
170
- .page-header-top {
171
  display: flex;
172
  justify-content: space-between;
173
  align-items: center;
 
174
  }
175
- .page-header-top h2 {
176
- font-size: 1.8rem;
177
- font-weight: 700;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  }
179
- .header-actions {
180
- display: flex; align-items: center; gap: 8px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  }
182
 
183
- .scrollable-content {
184
  flex-grow: 1;
185
  overflow-y: auto;
186
  }
187
- .list-item {
188
  display: flex;
189
  align-items: center;
190
  gap: 12px;
@@ -193,49 +245,51 @@ def index():
193
  border-bottom: 1px solid var(--border-color);
194
  transition: background-color 0.2s ease;
195
  }
196
- .list-item:hover { background-color: var(--bg-hover); }
197
 
198
  .avatar {
199
- width: 48px;
200
- height: 48px;
201
  border-radius: 50%;
202
  display: flex;
203
  align-items: center;
204
  justify-content: center;
205
  font-weight: 600;
206
- font-size: 1.2rem;
207
  color: white;
208
  flex-shrink: 0;
209
- }
210
- .item-info {
211
- flex-grow: 1;
212
  overflow: hidden;
213
  }
214
- .item-name {
215
- font-weight: 600;
216
- font-size: 1.1rem;
217
- white-space: nowrap;
 
 
 
 
 
 
 
218
  overflow: hidden;
219
- text-overflow: ellipsis;
220
  }
221
- .item-subtext {
222
- font-size: 0.9rem;
223
- color: var(--text-secondary);
224
  white-space: nowrap;
225
  overflow: hidden;
226
  text-overflow: ellipsis;
227
  }
228
  .lock-icon {
229
- width: 18px;
230
- height: 18px;
231
  fill: var(--text-secondary);
232
  flex-shrink: 0;
233
  }
234
-
235
  #chat-window-view {
236
- display: none;
237
  flex-direction: column;
238
- flex-grow: 1;
 
239
  background-color: var(--bg-primary);
240
  }
241
 
@@ -244,7 +298,7 @@ def index():
244
  align-items: center;
245
  gap: 12px;
246
  padding: 12px 16px;
247
- background-color: var(--bg-tertiary);
248
  border-bottom: 1px solid var(--border-color);
249
  flex-shrink: 0;
250
  }
@@ -252,7 +306,7 @@ def index():
252
  background: none;
253
  border: none;
254
  cursor: pointer;
255
- display: none;
256
  }
257
  .back-btn svg {
258
  width: 24px;
@@ -273,16 +327,46 @@ def index():
273
  gap: 12px;
274
  }
275
 
276
- .message { display: flex; gap: 10px; max-width: 80%; }
277
- .message .avatar { width: 36px; height: 36px; font-size: 1rem; align-self: flex-end; }
278
- .message-content { display: flex; flex-direction: column; gap: 4px; }
279
- .message-sender { font-size: 0.9rem; font-weight: 600; color: var(--text-secondary); word-break: break-all; cursor: pointer; }
280
- .message-bubble { padding: 10px 14px; border-radius: 18px; line-height: 1.4; word-wrap: break-word; font-size: 1rem;}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  .message.sent { align-self: flex-end; flex-direction: row-reverse; }
282
  .message.sent .message-sender { text-align: right; color: var(--accent-blue-light); }
283
- .message.sent .message-bubble { background: var(--accent-blue); color: white; border-bottom-right-radius: 4px; }
 
 
 
 
 
284
  .message.received { align-self: flex-start; }
285
- .message.received .message-bubble { background-color: var(--bg-tertiary); border-bottom-left-radius: 4px; }
 
 
 
286
 
287
  .chat-placeholder {
288
  display: flex;
@@ -293,14 +377,12 @@ def index():
293
  text-align: center;
294
  color: var(--text-secondary);
295
  padding: 20px;
296
- background-color: var(--bg-primary);
297
- flex-grow: 1;
298
  }
299
  .chat-placeholder img { width: 80px; margin-bottom: 20px; opacity: 0.5; }
300
 
301
  .message-form {
302
  display: flex;
303
- padding: 12px 16px;
304
  gap: 12px;
305
  background-color: var(--bg-secondary);
306
  border-top: 1px solid var(--border-color);
@@ -308,123 +390,224 @@ def index():
308
  }
309
  #message-input {
310
  flex-grow: 1;
311
- padding: 12px 18px;
312
- border: none;
313
  background-color: var(--bg-tertiary);
314
  color: var(--text-primary);
315
  border-radius: 22px;
316
  outline: none;
317
  font-size: 1rem;
 
318
  }
 
 
319
  .send-btn {
320
- background: var(--accent-blue);
321
- color: white;
322
- border: none;
323
  width: 44px;
324
  height: 44px;
325
  border-radius: 50%;
326
- cursor: pointer;
327
  flex-shrink: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  display: flex;
329
  align-items: center;
330
- justify-content: center;
331
- transition: transform 0.1s ease;
 
 
 
332
  }
333
- .send-btn:active { transform: scale(0.9); }
334
- .send-btn svg { width: 22px; height: 22px; fill: white; margin-left: 2px; }
335
-
336
- #browser-page { flex-direction: column; background-color: var(--bg-secondary); }
337
- .browser-header {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  display: flex;
339
  gap: 8px;
340
- padding: 12px;
341
- background-color: var(--bg-tertiary);
342
  border-bottom: 1px solid var(--border-color);
 
343
  flex-shrink: 0;
344
- }
345
- #browser-url-input {
346
  flex-grow: 1;
347
- background-color: var(--bg-primary);
348
  border: 1px solid var(--border-color);
 
349
  color: var(--text-primary);
350
- border-radius: 8px;
351
- padding: 10px;
352
- font-size: 1rem;
353
- }
354
- #browser-frame {
 
355
  flex-grow: 1;
356
  border: none;
357
- background-color: white;
358
- }
359
 
360
- #profile-page {
361
- align-items: center;
362
- justify-content: center;
363
  padding: 24px;
 
 
364
  overflow-y: auto;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
  text-align: center;
 
 
 
 
366
  }
367
- #my-profile-avatar { margin-bottom: 16px; }
368
- #my-profile-username { font-size: 1.5rem; font-weight: 600; margin-bottom: 8px; }
369
- #my-profile-address { font-size: 0.9rem; color: var(--text-secondary); word-break: break-all; margin-bottom: 24px; }
370
- #my-profile-qr-code { background: white; padding: 10px; margin: 0 auto 24px; width: fit-content; border-radius: 8px; }
371
- .username-form { display: flex; gap: 8px; margin-top: 24px; width: 100%; max-width: 400px; }
372
- .username-input {
373
- flex-grow: 1;
374
- background-color: var(--bg-tertiary);
375
- border: 1px solid var(--border-color);
376
- color: var(--text-primary);
377
- border-radius: 8px;
378
- padding: 12px;
379
- font-size: 1rem;
380
  }
381
- .username-input:focus { outline: none; border-color: var(--accent-blue); }
382
 
 
 
383
  #bottom-nav {
 
 
 
 
 
 
 
384
  display: flex;
385
  justify-content: space-around;
386
- background-color: var(--bg-tertiary);
387
- border-top: 1px solid var(--border-color);
388
- flex-shrink: 0;
389
- padding: 5px 0;
390
- z-index: 100;
391
  }
392
- .nav-btn {
393
- background: none;
394
- border: none;
395
- color: var(--text-secondary);
396
- cursor: pointer;
397
- padding: 8px 12px;
398
  display: flex;
399
  flex-direction: column;
400
  align-items: center;
401
- gap: 4px;
402
- font-size: 0.75rem;
403
- transition: color 0.2s;
 
 
 
 
 
404
  }
405
- .nav-btn svg {
 
406
  width: 24px;
407
  height: 24px;
408
  fill: var(--text-secondary);
409
- transition: fill 0.2s;
410
  }
411
- .nav-btn.active { color: var(--accent-blue); }
412
- .nav-btn.active svg { fill: var(--accent-blue); }
413
- #nav-scan-btn {
414
- transform: translateY(-20px);
415
- background: linear-gradient(45deg, var(--accent-blue-light), var(--accent-blue));
416
- width: 60px;
417
- height: 60px;
418
- border-radius: 50%;
419
- border: 4px solid var(--bg-primary);
420
- box-shadow: 0 0 20px rgba(0, 122, 255, 0.5);
421
  }
422
- #nav-scan-btn svg {
423
- fill: white;
424
- width: 30px;
425
- height: 30px;
426
  }
427
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  .modal-overlay {
429
  position: fixed;
430
  top: 0;
@@ -436,6 +619,7 @@ def index():
436
  align-items: center;
437
  justify-content: center;
438
  z-index: 1000;
 
439
  backdrop-filter: blur(5px);
440
  }
441
  .modal-content {
@@ -456,43 +640,24 @@ def index():
456
  background-color: var(--bg-tertiary);
457
  border: 1px solid var(--border-color);
458
  color: white;
459
- border-radius: 8px;
460
  font-size: 1rem;
461
  }
462
  .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
463
  .modal-btn {
464
  padding: 10px 20px;
465
- border-radius: 8px;
466
  border: none;
467
  cursor: pointer;
468
  font-weight: 500;
 
469
  }
470
  .secondary-btn { background-color: var(--bg-hover); color: white; }
471
-
472
- .action-btn {
473
- background: var(--accent-blue);
474
- color: white;
475
- border: none;
476
- padding: 10px 16px;
477
- border-radius: 8px;
478
- cursor: pointer;
479
- font-weight: 500;
480
- transition: background-color 0.2s ease;
481
- display: flex;
482
- align-items: center;
483
- justify-content: center;
484
- gap: 8px;
485
- }
486
- .action-btn:hover { background-color: var(--accent-blue-light); }
487
- .action-btn.small {
488
- padding: 8px 12px;
489
- font-size: 0.9rem;
490
- border-radius: 8px;
491
- }
492
 
493
  #status-bar {
494
  position: fixed;
495
- bottom: 80px;
496
  left: 50%;
497
  transform: translateX(-50%);
498
  background-color: var(--bg-tertiary);
@@ -502,7 +667,7 @@ def index():
502
  font-size: 0.9rem;
503
  opacity: 0;
504
  visibility: hidden;
505
- transition: all 0.3s;
506
  z-index: 2000;
507
  box-shadow: 0 5px 15px rgba(0,0,0,0.3);
508
  }
@@ -510,38 +675,94 @@ def index():
510
  #status-bar.error { background-color: var(--error-color); }
511
  #status-bar.visible { opacity: 1; visibility: visible; }
512
 
 
513
  @media (min-width: 768px) {
 
 
 
 
514
  .main-container {
515
- max-width: 1200px;
516
- max-height: 850px;
517
  border-radius: 12px;
518
  overflow: hidden;
519
  box-shadow: 0 10px 40px rgba(0,0,0,0.3);
520
  border: 1px solid var(--border-color);
 
 
 
 
 
 
521
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
  #chatroom-list-view {
523
- width: 350px;
524
  flex-shrink: 0;
 
 
 
 
525
  }
526
  #chat-window-view {
527
- display: flex;
528
- }
529
- #chats-page.chat-active #chatroom-list-view {
530
- display: flex;
 
531
  }
532
- .back-btn { display: none !important; }
533
- }
534
 
535
- @media (max-width: 767px) {
536
- #chats-page.chat-active #chatroom-list-view {
537
- display: none;
538
- }
539
- #chat-window-view {
540
  width: 100%;
 
 
 
 
 
541
  }
542
- .back-btn {
543
- display: block;
 
 
 
 
 
 
 
544
  }
 
 
 
 
 
 
 
 
 
 
 
545
  }
546
  </style>
547
  </head>
@@ -555,28 +776,38 @@ def index():
555
  </div>
556
 
557
  <div id="app-view" class="main-container">
558
- <div id="main-content">
559
-
560
- <div id="chats-page" class="page active">
561
- <div id="chatroom-list-view" class="list-view">
562
- <div class="page-header">
563
- <div class="page-header-top">
 
564
  <h2>Чаты</h2>
565
- <div class="header-actions">
566
  <button id="create-room-show-modal" class="action-btn small">
567
  <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>
 
568
  </button>
569
  </div>
570
  </div>
 
 
 
 
 
 
 
 
571
  </div>
572
- <div id="chatroom-list" class="scrollable-content"></div>
573
  </div>
574
 
575
  <div id="chat-window-view">
576
  <div id="chat-placeholder" class="chat-placeholder">
577
  <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
578
  <h2>Выберите чат</h2>
579
- <p>Начните общение в одном из существующих чатов или создайте свой.</p>
580
  </div>
581
  <div id="active-chat" style="display: none; width: 100%; height: 100%; flex-direction: column;">
582
  <div class="chat-header">
@@ -589,77 +820,88 @@ def index():
589
  <div id="messages-container"></div>
590
  <form id="message-form" class="message-form">
591
  <input type="text" id="message-input" placeholder="Сообщение..." autocomplete="off">
592
- <button type="submit" class="send-btn" id="send-btn">
593
- <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>
594
  </button>
595
  </form>
596
  </div>
597
  </div>
598
  </div>
599
 
600
- <div id="users-page" class="page">
601
- <div class="list-view">
602
- <div class="page-header">
603
- <div class="page-header-top">
604
- <h2>Пользователи</h2>
605
- </div>
606
- </div>
607
- <div id="user-list" class="scrollable-content"></div>
608
  </div>
 
609
  </div>
610
 
611
- <div id="browser-page" class="page">
612
- <div class="browser-header">
613
- <input type="text" id="browser-url-input" placeholder="https://">
614
- <button id="browser-go-btn" class="action-btn small">Go</button>
615
- </div>
616
- <iframe id="browser-frame" src="https://www.google.com/search?igu=1"></iframe>
 
 
 
 
617
  </div>
618
 
619
- <div id="profile-page" class="page">
620
- <div id="my-profile-avatar" class="avatar" style="width: 100px; height: 100px; font-size: 3rem;"></div>
621
- <h3 id="my-profile-username"></h3>
622
- <p id="my-profile-address"></p>
623
- <div id="my-profile-qr-code"></div>
624
- <form class="username-form" id="username-form">
625
- <input type="text" id="username-input" class="username-input" placeholder="Ваш никнейм" autocomplete="off">
626
- <button type="submit" class="action-btn small">✓</button>
627
- </form>
 
 
 
 
 
 
 
628
  </div>
629
 
630
  </div>
631
 
632
- <nav id="bottom-nav">
633
- <button class="nav-btn active" data-page="chats-page">
634
- <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/></svg>
 
635
  <span>Чаты</span>
636
- </button>
637
- <button class="nav-btn" data-page="users-page">
638
- <svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px"><g><rect fill="none" height="24" width="24"/></g><g><g><path d="M16,11c1.66,0,2.99-1.34,2.99-3S17.66,5,16,5C14.34,5,13,6.34,13,8S14.34,11,16,11z M8,11C9.66,11,11,9.66,11,8S9.66,5,8,5 C6.34,5,5,6.34,5,8S6.34,11,8,11z M8,13c-2.33,0-7,1.17-7,3.5V19h14v-2.5C15,14.17,10.33,13,8,13z M16,13c-0.29,0-0.62,0.02-0.97,0.05 c1.16,0.84,1.97,1.97,1.97,3.45V19h6v-2.5C23,14.17,18.33,13,16,13z"/></g></g></svg>
639
- <span>Люди</span>
640
- </button>
641
- <button class="nav-btn" id="nav-scan-btn">
642
- <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"><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>
643
- </button>
644
- <button class="nav-btn" data-page="browser-page">
645
- <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>
 
646
  <span>Браузер</span>
647
- </button>
648
- <button class="nav-btn" data-page="profile-page">
649
- <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"><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>
650
  <span>Профиль</span>
651
- </button>
652
- </nav>
653
  </div>
654
 
 
655
  <div id="create-room-modal" class="modal-overlay">
656
  <div class="modal-content">
657
  <h3>Создать новый чат</h3>
658
  <form id="create-room-form">
659
  <label for="room-name">Название чата</label>
660
- <input type="text" id="room-name" required>
661
  <label for="room-password">Пароль (оставьте пустым для открытого)</label>
662
- <input type="password" id="room-password">
663
  <div class="modal-actions">
664
  <button type="button" id="create-room-cancel" class="modal-btn secondary-btn">Отмена</button>
665
  <button type="submit" class="modal-btn action-btn">Создать</button>
@@ -673,7 +915,7 @@ def index():
673
  <h3>Вход в приватный чат</h3>
674
  <form id="password-form">
675
  <label for="password-input">Введите пароль</label>
676
- <input type="password" id="password-input" required>
677
  <div class="modal-actions">
678
  <button type="button" id="password-cancel" class="modal-btn secondary-btn">Отмена</button>
679
  <button type="submit" class="modal-btn action-btn">Войти</button>
@@ -682,6 +924,7 @@ def index():
682
  </div>
683
  </div>
684
 
 
685
  <div id="profile-modal" class="modal-overlay">
686
  <div class="modal-content" style="text-align: center;">
687
  <h3 id="profile-modal-title">Профиль пользователя</h3>
@@ -712,39 +955,56 @@ def index():
712
  <script>
713
  document.addEventListener('DOMContentLoaded', () => {
714
  const tonConnectUI = new TON_CONNECT_UI.TonConnectUI({
715
- manifestUrl: 'https://huggingface.co/spaces/Aleksmorshen/MorshenGroup/resolve/main/tonconnect-manifest.json',
716
  buttonRootId: 'ton-connect-button'
717
  });
718
 
719
- let currentUser = { address: null, username: null };
720
  let activeChatroomId = null;
721
  let messagePollingInterval = null;
722
  let chatroomsData = {};
723
- let usersData = {};
724
  let html5QrCode = null;
725
  let profileQrCode = null;
726
  let myProfileQrCode = null;
727
 
728
  const loginView = document.getElementById('login-view');
729
  const appView = document.getElementById('app-view');
730
- const chatsPage = document.getElementById('chats-page');
 
 
 
 
731
  const chatroomListView = document.getElementById('chatroom-list-view');
732
  const chatWindowView = document.getElementById('chat-window-view');
733
  const chatPlaceholder = document.getElementById('chat-placeholder');
734
  const activeChat = document.getElementById('active-chat');
 
735
  const profileModal = document.getElementById('profile-modal');
736
  const scannerModal = document.getElementById('scanner-modal');
737
 
738
- const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292', '#ff8a65', '#a1887f', '#90a4ae'];
 
 
 
 
 
739
 
740
- const getAvatar = (name, className = 'avatar') => {
 
 
741
  const initial = (name ? name[0] : '?').toUpperCase();
742
  const charCode = initial.charCodeAt(0);
743
  const color = AVATAR_COLORS[charCode % AVATAR_COLORS.length];
744
  const avatar = document.createElement('div');
745
- avatar.className = className;
746
  avatar.style.backgroundColor = color;
747
  avatar.textContent = initial;
 
 
 
 
 
748
  return avatar;
749
  };
750
 
@@ -769,56 +1029,67 @@ def index():
769
  if (response.status === 204) return null;
770
  return await response.json();
771
  } catch (error) {
772
- showStatus(`Ошибка: ${error.message}`, 'error');
773
  throw error;
774
  }
775
  };
776
 
777
  const truncateAddress = (address) => address ? `${address.substring(0, 4)}...${address.substring(address.length - 4)}` : '';
778
 
779
- const showPage = (pageId) => {
780
- document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
781
- document.getElementById(pageId)?.classList.add('active');
782
-
783
- document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
784
- const navButton = document.querySelector(`.nav-btn[data-page="${pageId}"]`);
785
- if(navButton) navButton.classList.add('active');
786
-
787
- if(pageId === 'profile-page') renderMyProfile();
788
- if(pageId === 'users-page') fetchUsers();
789
  };
790
 
791
- document.querySelectorAll('.nav-btn[data-page]').forEach(button => {
792
- button.addEventListener('click', () => showPage(button.dataset.page));
793
- });
794
-
795
- const renderMyProfile = () => {
796
- if(!currentUser.address) return;
797
-
798
- const avatarContainer = document.getElementById('my-profile-avatar');
799
  avatarContainer.innerHTML = '';
800
- avatarContainer.appendChild(getAvatar(currentUser.username || `?`, 'avatar'));
801
- document.getElementById('my-profile-username').textContent = currentUser.username || 'Никнейм не установлен';
802
- document.getElementById('my-profile-address').textContent = currentUser.address;
803
- document.getElementById('username-input').value = currentUser.username || '';
 
804
 
805
- const qrCodeEl = document.getElementById('my-profile-qr-code');
806
  qrCodeEl.innerHTML = '';
807
- if (myProfileQrCode) myProfileQrCode.clear();
808
- myProfileQrCode = new QRCode(qrCodeEl, {
809
- text: currentUser.address,
810
- width: 200, height: 200, colorDark : "#000000", colorLight : "#ffffff",
811
- correctLevel : QRCode.CorrectLevel.H
812
- });
813
- };
 
 
 
 
 
 
 
 
814
 
815
  document.getElementById('username-form').addEventListener('submit', async (e) => {
816
  e.preventDefault();
817
  const newUsername = document.getElementById('username-input').value.trim();
818
- if (!newUsername || newUsername.length < 3) {
819
- showStatus('Никнейм должен быть не короче 3 символов.', 'error');
 
 
 
 
820
  return;
821
  }
 
 
 
 
822
  try {
823
  await apiCall('/api/set_username', {
824
  method: 'POST',
@@ -826,15 +1097,34 @@ def index():
826
  body: JSON.stringify({ address: currentUser.address, username: newUsername })
827
  });
828
  currentUser.username = newUsername;
 
 
829
  showStatus('Никнейм успешно обновлен!', 'success');
830
- renderMyProfile();
831
- fetchChatrooms();
832
- if (activeChatroomId) fetchMessages(activeChatroomId);
833
- } catch (err) {}
 
 
 
 
834
  });
835
 
836
  const initializeUser = async (address) => {
837
  currentUser.address = address;
 
 
 
 
 
 
 
 
 
 
 
 
 
838
  try {
839
  const data = await apiCall('/api/user_data', {
840
  method: 'POST',
@@ -843,13 +1133,21 @@ def index():
843
  });
844
  currentUser.username = data.username;
845
  } catch (err) {
846
- currentUser.username = null;
847
  }
 
 
 
 
848
  loginView.style.display = 'none';
849
  appView.style.display = 'flex';
 
 
850
  fetchChatrooms();
851
  fetchUsers();
852
- showPage('chats-page');
 
 
853
  };
854
 
855
  const renderChatrooms = (rooms) => {
@@ -859,15 +1157,15 @@ def index():
859
  rooms.forEach(room => {
860
  chatroomsData[room.id] = room;
861
  const item = document.createElement('div');
862
- item.className = 'list-item';
863
  item.dataset.id = room.id;
864
 
865
  item.appendChild(getAvatar(room.name));
866
 
867
  const infoDiv = document.createElement('div');
868
- infoDiv.className = 'item-info';
869
  const nameSpan = document.createElement('div');
870
- nameSpan.className = 'item-name';
871
  nameSpan.textContent = room.name;
872
  infoDiv.appendChild(nameSpan);
873
  item.appendChild(infoDiv);
@@ -877,67 +1175,77 @@ def index():
877
  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>`;
878
  item.appendChild(lockIcon);
879
  }
 
880
  item.addEventListener('click', () => selectChatroom(room.id, room.is_private));
881
  list.appendChild(item);
882
  });
883
  };
884
 
885
  const fetchChatrooms = async () => {
 
886
  try {
887
  const data = await apiCall('/api/chatrooms');
888
  renderChatrooms(data.chatrooms);
889
- } catch (err) {}
 
 
890
  };
891
 
892
- const renderUsers = (users) => {
893
- const list = document.getElementById('user-list');
894
- list.innerHTML = '';
895
- usersData = {};
896
- users.forEach(user => {
897
- usersData[user.address] = user;
898
- const displayName = user.username || `User ${truncateAddress(user.address)}`;
899
- const item = document.createElement('div');
900
- item.className = 'list-item';
901
- item.dataset.address = user.address;
902
-
903
- item.appendChild(getAvatar(displayName));
904
-
905
- const infoDiv = document.createElement('div');
906
- infoDiv.className = 'item-info';
907
- const nameSpan = document.createElement('div');
908
- nameSpan.className = 'item-name';
909
- nameSpan.textContent = displayName;
910
- infoDiv.appendChild(nameSpan);
911
-
912
- const subtext = document.createElement('div');
913
- subtext.className = 'item-subtext';
914
- subtext.textContent = truncateAddress(user.address);
915
- infoDiv.appendChild(subtext);
916
- item.appendChild(infoDiv);
917
-
918
- item.addEventListener('click', () => showProfile(user.address));
919
- list.appendChild(item);
920
- });
921
- }
922
-
923
- const fetchUsers = async () => {
924
- try {
925
- const data = await apiCall('/api/users');
926
- renderUsers(data.users);
927
- } catch (err) {}
928
- }
 
 
 
929
 
930
  const renderMessages = (messages) => {
931
  const container = document.getElementById('messages-container');
932
- const shouldScroll = container.scrollTop + container.clientHeight >= container.scrollHeight - 30;
 
 
933
  container.innerHTML = '';
934
  messages.forEach(msg => {
935
  const msgDiv = document.createElement('div');
936
  msgDiv.className = 'message ' + (msg.sender_address === currentUser.address ? 'sent' : 'received');
937
 
938
- const avatar = getAvatar(msg.display_name, 'avatar message-avatar');
 
939
  avatar.style.cursor = 'pointer';
940
- avatar.onclick = () => showProfile(msg.sender_address);
941
 
942
  const contentDiv = document.createElement('div');
943
  contentDiv.className = 'message-content';
@@ -945,7 +1253,7 @@ def index():
945
  const senderDiv = document.createElement('div');
946
  senderDiv.className = 'message-sender';
947
  senderDiv.textContent = msg.display_name;
948
- senderDiv.onclick = () => showProfile(msg.sender_address);
949
 
950
  const bubbleDiv = document.createElement('div');
951
  bubbleDiv.className = 'message-bubble';
@@ -955,33 +1263,44 @@ def index():
955
  contentDiv.appendChild(bubbleDiv);
956
 
957
  msgDiv.appendChild(contentDiv);
958
- msgDiv.insertBefore(avatar, contentDiv);
959
  container.appendChild(msgDiv);
960
  });
961
 
962
- if(shouldScroll) {
963
- container.scrollTop = container.scrollHeight;
964
  }
965
  };
966
 
967
  const fetchMessages = async (roomId) => {
 
968
  try {
969
  const data = await apiCall(`/api/messages/${roomId}`);
970
  renderMessages(data.messages);
971
  } catch (err) {
 
972
  if (messagePollingInterval) clearInterval(messagePollingInterval);
 
 
973
  }
974
  };
975
-
976
- const showChatView = () => {
 
977
  chatWindowView.style.display = 'flex';
978
- chatsPage.classList.add('chat-active');
979
  };
980
 
981
- const showListView = () => {
982
- chatsPage.classList.remove('chat-active');
983
- if (messagePollingInterval) clearInterval(messagePollingInterval);
984
- activeChatroomId = null;
 
 
 
 
 
 
 
985
  };
986
 
987
  const selectChatroom = (roomId, isPrivate) => {
@@ -999,8 +1318,9 @@ def index():
999
 
1000
  chatPlaceholder.style.display = 'none';
1001
  activeChat.style.display = 'flex';
1002
- showChatView();
1003
 
 
 
1004
  fetchMessages(roomId);
1005
  messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000);
1006
  };
@@ -1016,8 +1336,14 @@ def index():
1016
  const formSubmitHandler = async (e) => {
1017
  e.preventDefault();
1018
  passwordForm.removeEventListener('submit', formSubmitHandler);
 
 
1019
  const password = passwordInput.value;
1020
  passwordModal.style.display = 'none';
 
 
 
 
1021
  try {
1022
  await apiCall('/api/join_chatroom', {
1023
  method: 'POST',
@@ -1025,14 +1351,21 @@ def index():
1025
  body: JSON.stringify({ chatroom_id: roomId, password })
1026
  });
1027
  proceedToRoom();
1028
- } catch (err) {}
 
 
 
 
1029
  };
1030
  passwordForm.addEventListener('submit', formSubmitHandler);
1031
 
1032
- document.getElementById('password-cancel').onclick = () => {
1033
  passwordModal.style.display = 'none';
1034
  passwordForm.removeEventListener('submit', formSubmitHandler);
 
1035
  };
 
 
1036
  } else {
1037
  proceedToRoom();
1038
  }
@@ -1043,7 +1376,7 @@ def index():
1043
  const input = document.getElementById('message-input');
1044
  const sendBtn = document.getElementById('send-btn');
1045
  const text = input.value.trim();
1046
- if (text && activeChatroomId) {
1047
  input.value = '';
1048
  input.disabled = true;
1049
  sendBtn.disabled = true;
@@ -1057,8 +1390,12 @@ def index():
1057
  text: text
1058
  })
1059
  });
 
1060
  await fetchMessages(activeChatroomId);
1061
  document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight;
 
 
 
1062
  } finally {
1063
  input.disabled = false;
1064
  sendBtn.disabled = false;
@@ -1079,7 +1416,17 @@ def index():
1079
  e.preventDefault();
1080
  const name = document.getElementById('room-name').value.trim();
1081
  const password = document.getElementById('room-password').value;
1082
- if (!name) return;
 
 
 
 
 
 
 
 
 
 
1083
 
1084
  try {
1085
  await apiCall('/api/create_chatroom', {
@@ -1090,14 +1437,15 @@ def index():
1090
  createRoomModal.style.display = 'none';
1091
  showStatus('Чат успешно создан!', 'success');
1092
  fetchChatrooms();
1093
- } catch (err) {}
 
 
 
 
1094
  });
1095
 
1096
- const showProfile = async (address) => {
1097
- if (address === currentUser.address) {
1098
- showPage('profile-page');
1099
- return;
1100
- }
1101
  try {
1102
  const userData = await apiCall('/api/user_data', {
1103
  method: 'POST',
@@ -1105,66 +1453,104 @@ def index():
1105
  body: JSON.stringify({ address: address })
1106
  });
1107
 
1108
- const username = userData.username || `User ${truncateAddress(address)}`;
1109
- const avatarContainer = document.getElementById('profile-avatar-container');
1110
-
1111
- document.getElementById('profile-modal-title').textContent = username;
1112
- document.getElementById('profile-username').textContent = username;
1113
- document.getElementById('profile-address').textContent = address;
1114
 
 
 
 
 
 
 
 
 
 
1115
  avatarContainer.innerHTML = '';
1116
- avatarContainer.appendChild(getAvatar(username, 'avatar'));
 
 
 
1117
 
1118
- const qrCodeEl = document.getElementById('profile-qr-code');
1119
  qrCodeEl.innerHTML = '';
1120
- if (profileQrCode) profileQrCode.clear();
1121
- profileQrCode = new QRCode(qrCodeEl, { text: address, width: 150, height: 150 });
 
 
 
 
 
 
 
 
 
 
 
 
1122
 
1123
- document.getElementById('send-ton-btn').onclick = async () => {
1124
- if (!tonConnectUI.connected) {
1125
- showStatus('Подключите кошелек для отправки TON.', 'error'); return;
1126
- }
1127
- const amountString = prompt("Введите сумму в TON для отправки:", "0.1");
1128
- if (amountString === null) return;
1129
- const amount = parseFloat(amountString);
1130
- if (isNaN(amount) || amount <= 0) {
1131
- showStatus('Неверная сумма.', 'error'); return;
1132
- }
1133
- const transaction = {
1134
- validUntil: Math.floor(Date.now() / 1000) + 600,
1135
- messages: [ { address: address, amount: Math.floor(amount * 1e9).toString() } ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1136
  };
1137
- try {
1138
- await tonConnectUI.sendTransaction(transaction);
1139
- showStatus(`Транзакция отправлена успешно!`, 'success');
1140
- profileModal.style.display = 'none';
1141
- } catch (error) {
1142
- showStatus('Транзакция отклонена.', 'error');
1143
- }
1144
- };
1145
-
1146
  profileModal.style.display = 'flex';
1147
  } catch (err) {
1148
- showStatus('Не удалось загрузить профиль.', 'error');
1149
  }
1150
  };
1151
 
1152
  const showScanner = () => {
1153
  scannerModal.style.display = 'flex';
1154
- html5QrCode = new Html5Qrcode("qr-reader");
 
 
1155
  const qrCodeSuccessCallback = (decodedText, decodedResult) => {
1156
  hideScanner();
 
1157
  if (decodedText && decodedText.length > 40 && (decodedText.startsWith('EQ') || decodedText.startsWith('UQ'))) {
1158
- showProfile(decodedText);
1159
  } else {
1160
- showStatus('Отсканирован недействительный QR-код.', 'error');
1161
  }
1162
  };
1163
- const config = { fps: 10, qrbox: { width: 250, height: 250 } };
1164
- html5QrCode.start({ facingMode: "environment" }, config, qrCodeSuccessCallback)
 
 
 
 
1165
  .catch(err => {
1166
- showStatus('Не удалось запустить сканер.', 'error');
1167
- hideScanner();
 
1168
  });
1169
  };
1170
 
@@ -1175,41 +1561,165 @@ def index():
1175
  scannerModal.style.display = 'none';
1176
  };
1177
 
1178
- document.getElementById('nav-scan-btn').addEventListener('click', showScanner);
1179
  document.getElementById('profile-close-btn').addEventListener('click', () => profileModal.style.display = 'none');
1180
  document.getElementById('scanner-close-btn').addEventListener('click', hideScanner);
1181
- document.getElementById('back-to-list-btn').addEventListener('click', showListView);
1182
 
1183
- document.getElementById('browser-go-btn').addEventListener('click', () => {
1184
- const input = document.getElementById('browser-url-input');
1185
- let url = input.value.trim();
1186
- if(url && !url.startsWith('http')) {
1187
- url = 'https://' + url;
1188
- }
1189
- if(url) {
1190
- document.getElementById('browser-frame').src = url;
1191
- }
1192
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1193
 
1194
- window.addEventListener('resize', () => {
1195
- const isDesktop = window.innerWidth >= 768;
1196
- if(isDesktop) {
1197
- chatsPage.classList.remove('chat-active');
1198
  }
1199
- });
1200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1201
  tonConnectUI.onStatusChange(wallet => {
1202
  if (wallet) {
1203
  const address = TON_CONNECT_UI.toUserFriendlyAddress(wallet.account.address, false);
1204
  initializeUser(address);
1205
  } else {
1206
- currentUser = { address: null, username: null };
1207
  appView.style.display = 'none';
1208
  loginView.style.display = 'flex';
 
 
 
 
 
1209
  if (messagePollingInterval) clearInterval(messagePollingInterval);
 
1210
  activeChatroomId = null;
 
 
 
 
 
 
 
 
 
 
 
1211
  }
1212
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1213
  });
1214
  </script>
1215
  </body>
@@ -1217,7 +1727,6 @@ def index():
1217
  '''
1218
  return Response(html_content, mimetype='text/html')
1219
 
1220
-
1221
  @app.route('/api/user_data', methods=['POST'])
1222
  def get_user_data():
1223
  data = request.get_json()
@@ -1238,8 +1747,11 @@ def get_users():
1238
  'address': address,
1239
  'username': user_data.get('username')
1240
  })
 
 
1241
  return jsonify({'users': users_list})
1242
 
 
1243
  @app.route('/api/set_username', methods=['POST'])
1244
  def set_username():
1245
  data = request.get_json()
@@ -1249,8 +1761,13 @@ def set_username():
1249
  return jsonify({'error': 'Address and username are required'}), 400
1250
  if len(username) < 3 or len(username) > 20:
1251
  return jsonify({'error': 'Username must be between 3 and 20 characters'}), 400
1252
-
 
1253
  db = read_db()
 
 
 
 
1254
  if address not in db['users']:
1255
  db['users'][address] = {}
1256
  db['users'][address]['username'] = username
@@ -1277,6 +1794,8 @@ def create_chatroom():
1277
  creator_address = data.get('creator_address')
1278
  if not name or not creator_address:
1279
  return jsonify({'error': 'Name and creator address are required'}), 400
 
 
1280
 
1281
  db = read_db()
1282
  room_id = str(uuid.uuid4())
@@ -1300,7 +1819,7 @@ def join_chatroom():
1300
  if not chatroom:
1301
  return jsonify({'error': 'Chatroom not found'}), 404
1302
  if chatroom['is_private']:
1303
- if not password or not check_password_hash(chatroom['password_hash'], password):
1304
  return jsonify({'error': 'Invalid password'}), 403
1305
  return jsonify({'success': True})
1306
 
@@ -1334,6 +1853,10 @@ def send_message():
1334
 
1335
  if not all([chatroom_id, sender_address, text]):
1336
  return jsonify({'error': 'Missing data'}), 400
 
 
 
 
1337
 
1338
  db = read_db()
1339
  if chatroom_id not in db['messages']:
@@ -1342,10 +1865,11 @@ def send_message():
1342
  message = {
1343
  'id': str(uuid.uuid4()),
1344
  'sender_address': sender_address,
1345
- 'text': text,
1346
  'timestamp': datetime.utcnow().isoformat() + "Z"
1347
  }
1348
 
 
1349
  if len(db['messages'][chatroom_id]) >= 100:
1350
  db['messages'][chatroom_id].pop(0)
1351
 
 
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
+ return {"users": {}, "chatrooms": {}, "messages": {}}
27
 
28
  def write_db(data):
29
  with open(DB_FILE, 'w') as f:
 
46
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
47
  <style>
48
  :root {
49
+ --bg-primary: #0F0F11;
50
+ --bg-secondary: #18191C;
51
+ --bg-tertiary: #212328;
52
+ --bg-hover: #2A2C33;
53
+ --text-primary: #EAECEF;
54
+ --text-secondary: #8E9297;
55
+ --accent-blue: #0088CC;
56
+ --accent-blue-light: #33AADD;
57
+ --border-color: #2D2F34;
58
+ --success-color: #28a745;
59
+ --error-color: #dc3545;
60
  --font-family: 'Inter', sans-serif;
61
+ --ton-purple: #6A5EE2;
62
+ --ton-purple-light: #948AE4;
63
  }
64
 
65
  * {
 
79
  color: var(--text-primary);
80
  overflow: hidden;
81
  height: 100vh;
 
82
  width: 100vw;
83
  display: flex;
84
+ flex-direction: column;
 
85
  }
86
 
87
  .main-container {
 
90
  display: flex;
91
  flex-direction: column;
92
  transition: opacity 0.3s ease;
 
93
  }
94
 
95
  #login-view {
 
 
96
  align-items: center;
97
  justify-content: center;
98
  text-align: center;
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;
 
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
+ flex-direction: column;
124
+ height: 100%;
125
+ overflow: hidden;
126
  }
127
+
128
+ .content-area {
129
  flex-grow: 1;
130
  overflow: hidden;
131
  position: relative;
132
+ padding-bottom: 60px; /* Height of nav bar */
133
  }
134
 
135
+ .view {
136
+ width: 100%;
137
+ height: 100%;
138
  position: absolute;
139
  top: 0;
140
  left: 0;
141
+ display: none;
 
 
142
  flex-direction: column;
 
 
 
143
  background-color: var(--bg-primary);
144
+ overflow-y: auto;
145
+ -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
 
 
 
146
  }
147
 
148
+ .view.active {
149
+ display: flex;
150
+ }
151
+
152
+ /* Chats View Specific Styles */
153
+ #chats-view {
154
+ display: flex; /* Default display */
155
+ flex-direction: column;
156
+ }
157
 
158
+ #chatroom-list-view {
159
  display: flex;
160
  flex-direction: column;
161
  height: 100%;
162
  width: 100%;
163
  background-color: var(--bg-secondary);
 
164
  }
165
+
166
+ .list-header {
167
  padding: 16px;
168
  border-bottom: 1px solid var(--border-color);
169
  flex-shrink: 0;
 
 
 
 
170
  }
171
+ .list-header-top {
172
  display: flex;
173
  justify-content: space-between;
174
  align-items: center;
175
+ margin-bottom: 16px;
176
  }
177
+ .list-header-top h2 {
178
+ font-size: 1.5rem;
179
+ font-weight: 600;
180
+ }
181
+
182
+ .user-profile-summary {
183
+ padding: 12px;
184
+ background-color: var(--bg-tertiary);
185
+ border-radius: 8px;
186
+ margin-bottom: 16px;
187
+ }
188
+ #user-wallet-summary, #user-nickname-summary {
189
+ font-size: 0.9rem;
190
+ color: var(--text-secondary);
191
+ margin-bottom: 8px;
192
+ word-break: break-all;
193
+ }
194
+ #user-nickname-summary { margin-bottom: 0; }
195
+
196
+ .username-form { display: flex; gap: 8px; margin-top: 8px;}
197
+ .username-input {
198
+ flex-grow: 1;
199
+ background-color: var(--bg-primary);
200
+ border: 1px solid var(--border-color);
201
+ color: var(--text-primary);
202
+ border-radius: 6px;
203
+ padding: 8px 12px;
204
+ font-size: 0.9rem;
205
  }
206
+ .username-input:focus { outline: none; border-color: var(--accent-blue); }
207
+
208
+ .action-btn {
209
+ background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
210
+ color: white;
211
+ border: none;
212
+ padding: 10px 16px;
213
+ border-radius: 6px;
214
+ cursor: pointer;
215
+ font-weight: 500;
216
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
217
+ display: flex;
218
+ align-items: center;
219
+ justify-content: center;
220
+ gap: 8px;
221
+ }
222
+ .action-btn:disabled {
223
+ opacity: 0.5;
224
+ cursor: not-allowed;
225
+ }
226
+ .action-btn:hover:not(:disabled) {
227
+ transform: translateY(-2px);
228
+ box-shadow: 0 4px 15px rgba(0, 136, 204, 0.3);
229
+ }
230
+ .action-btn.small {
231
+ padding: 8px 12px;
232
+ font-size: 0.9rem;
233
  }
234
 
235
+ #chatroom-list {
236
  flex-grow: 1;
237
  overflow-y: auto;
238
  }
239
+ .chatroom-item {
240
  display: flex;
241
  align-items: center;
242
  gap: 12px;
 
245
  border-bottom: 1px solid var(--border-color);
246
  transition: background-color 0.2s ease;
247
  }
248
+ .chatroom-item:hover { background-color: var(--bg-hover); }
249
 
250
  .avatar {
251
+ width: 40px;
252
+ height: 40px;
253
  border-radius: 50%;
254
  display: flex;
255
  align-items: center;
256
  justify-content: center;
257
  font-weight: 600;
 
258
  color: white;
259
  flex-shrink: 0;
 
 
 
260
  overflow: hidden;
261
  }
262
+ .avatar.message-avatar {
263
+ width: 36px;
264
+ height: 36px;
265
+ }
266
+ .avatar img {
267
+ width: 100%;
268
+ height: 100%;
269
+ object-fit: cover;
270
+ }
271
+ .chatroom-info {
272
+ flex-grow: 1;
273
  overflow: hidden;
 
274
  }
275
+ .chatroom-name {
276
+ font-weight: 500;
 
277
  white-space: nowrap;
278
  overflow: hidden;
279
  text-overflow: ellipsis;
280
  }
281
  .lock-icon {
282
+ width: 16px;
283
+ height: 16px;
284
  fill: var(--text-secondary);
285
  flex-shrink: 0;
286
  }
287
+
288
  #chat-window-view {
289
+ display: none; /* Hidden by default on mobile */
290
  flex-direction: column;
291
+ height: 100%;
292
+ width: 100%;
293
  background-color: var(--bg-primary);
294
  }
295
 
 
298
  align-items: center;
299
  gap: 12px;
300
  padding: 12px 16px;
301
+ background-color: var(--bg-secondary);
302
  border-bottom: 1px solid var(--border-color);
303
  flex-shrink: 0;
304
  }
 
306
  background: none;
307
  border: none;
308
  cursor: pointer;
309
+ display: block; /* Shown by default on mobile */
310
  }
311
  .back-btn svg {
312
  width: 24px;
 
327
  gap: 12px;
328
  }
329
 
330
+ .message {
331
+ display: flex;
332
+ gap: 10px;
333
+ max-width: 80%;
334
+ }
335
+ .message .avatar {
336
+ align-self: flex-end;
337
+ }
338
+ .message-content {
339
+ display: flex;
340
+ flex-direction: column;
341
+ gap: 4px;
342
+ }
343
+ .message-sender {
344
+ font-size: 0.8rem;
345
+ font-weight: 500;
346
+ color: var(--text-secondary);
347
+ word-break: break-all;
348
+ cursor: pointer;
349
+ }
350
+ .message-bubble {
351
+ padding: 10px 14px;
352
+ border-radius: 18px;
353
+ line-height: 1.4;
354
+ word-wrap: break-word;
355
+ }
356
+
357
  .message.sent { align-self: flex-end; flex-direction: row-reverse; }
358
  .message.sent .message-sender { text-align: right; color: var(--accent-blue-light); }
359
+ .message.sent .message-bubble {
360
+ background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
361
+ color: white;
362
+ border-bottom-right-radius: 4px;
363
+ }
364
+
365
  .message.received { align-self: flex-start; }
366
+ .message.received .message-bubble {
367
+ background-color: var(--bg-tertiary);
368
+ border-bottom-left-radius: 4px;
369
+ }
370
 
371
  .chat-placeholder {
372
  display: flex;
 
377
  text-align: center;
378
  color: var(--text-secondary);
379
  padding: 20px;
 
 
380
  }
381
  .chat-placeholder img { width: 80px; margin-bottom: 20px; opacity: 0.5; }
382
 
383
  .message-form {
384
  display: flex;
385
+ padding: 16px;
386
  gap: 12px;
387
  background-color: var(--bg-secondary);
388
  border-top: 1px solid var(--border-color);
 
390
  }
391
  #message-input {
392
  flex-grow: 1;
393
+ padding: 12px 16px;
394
+ border: 1px solid var(--border-color);
395
  background-color: var(--bg-tertiary);
396
  color: var(--text-primary);
397
  border-radius: 22px;
398
  outline: none;
399
  font-size: 1rem;
400
+ transition: border-color 0.2s;
401
  }
402
+ #message-input:focus { border-color: var(--accent-blue); }
403
+
404
  .send-btn {
 
 
 
405
  width: 44px;
406
  height: 44px;
407
  border-radius: 50%;
 
408
  flex-shrink: 0;
409
+ padding: 0;
410
+ }
411
+ .send-btn svg { width: 20px; height: 20px; fill: white; }
412
+
413
+ /* Users View Specific Styles */
414
+ #users-view .view-header {
415
+ padding: 16px;
416
+ border-bottom: 1px solid var(--border-color);
417
+ flex-shrink: 0;
418
+ }
419
+ #users-view .view-header h2 {
420
+ font-size: 1.5rem;
421
+ font-weight: 600;
422
+ }
423
+ #user-list {
424
+ flex-grow: 1;
425
+ overflow-y: auto;
426
+ padding: 0 16px;
427
+ }
428
+ .user-item {
429
  display: flex;
430
  align-items: center;
431
+ gap: 12px;
432
+ padding: 12px 0;
433
+ cursor: pointer;
434
+ border-bottom: 1px solid var(--border-color);
435
+ transition: background-color 0.2s ease;
436
  }
437
+ .user-item:last-child { border-bottom: none; }
438
+ .user-item:hover { background-color: var(--bg-hover); }
439
+ .user-item-info {
440
+ flex-grow: 1;
441
+ overflow: hidden;
442
+ }
443
+ .user-item-name {
444
+ font-weight: 500;
445
+ white-space: nowrap;
446
+ overflow: hidden;
447
+ text-overflow: ellipsis;
448
+ }
449
+ .user-item-address {
450
+ font-size: 0.9rem;
451
+ color: var(--text-secondary);
452
+ white-space: nowrap;
453
+ overflow: hidden;
454
+ text-overflow: ellipsis;
455
+ }
456
+
457
+ /* Browser View Specific Styles */
458
+ #browser-view .view-header {
459
+ padding: 16px;
460
+ border-bottom: 1px solid var(--border-color);
461
+ flex-shrink: 0;
462
+ }
463
+ #browser-view .view-header h2 {
464
+ font-size: 1.5rem;
465
+ font-weight: 600;
466
+ }
467
+ .browser-bar {
468
  display: flex;
469
  gap: 8px;
470
+ padding: 12px 16px;
 
471
  border-bottom: 1px solid var(--border-color);
472
+ background-color: var(--bg-tertiary);
473
  flex-shrink: 0;
474
+ }
475
+ .browser-url-input {
476
  flex-grow: 1;
477
+ padding: 8px 12px;
478
  border: 1px solid var(--border-color);
479
+ background-color: var(--bg-primary);
480
  color: var(--text-primary);
481
+ border-radius: 6px;
482
+ font-size: 0.9rem;
483
+ }
484
+ .browser-url-input:focus { outline: none; border-color: var(--accent-blue); }
485
+ .browser-iframe {
486
+ width: 100%;
487
  flex-grow: 1;
488
  border: none;
489
+ }
 
490
 
491
+ /* Profile View Specific Styles */
492
+ #profile-view {
 
493
  padding: 24px;
494
+ align-items: center;
495
+ text-align: center;
496
  overflow-y: auto;
497
+ }
498
+ #profile-view .avatar {
499
+ width: 80px;
500
+ height: 80px;
501
+ margin-bottom: 20px;
502
+ }
503
+ #profile-view h2 {
504
+ font-size: 1.8rem;
505
+ font-weight: 600;
506
+ margin-bottom: 8px;
507
+ }
508
+ #profile-view p {
509
+ font-size: 1rem;
510
+ color: var(--text-secondary);
511
+ margin-bottom: 8px;
512
+ }
513
+ #profile-view .action-btn {
514
+ margin-top: 20px;
515
+ width: auto;
516
+ padding: 12px 24px;
517
+ }
518
+ #profile-view #profile-qr-code {
519
+ background: white;
520
+ padding: 10px;
521
+ margin: 20px auto;
522
+ width: fit-content;
523
+ border-radius: 8px;
524
+ }
525
+ #profile-view .qr-label {
526
  text-align: center;
527
+ color: var(--text-secondary);
528
+ font-size: 0.8rem;
529
+ margin-top: -10px;
530
+ margin-bottom: 20px;
531
  }
532
+ #profile-view .profile-balance {
533
+ font-size: 1.1rem;
534
+ font-weight: 500;
535
+ margin-top: 16px;
536
+ color: var(--accent-blue-light);
 
 
 
 
 
 
 
 
537
  }
 
538
 
539
+
540
+ /* Bottom Navigation Bar */
541
  #bottom-nav {
542
+ position: fixed;
543
+ bottom: 0;
544
+ left: 0;
545
+ width: 100%;
546
+ height: 60px;
547
+ background-color: var(--bg-secondary);
548
+ border-top: 1px solid var(--border-color);
549
  display: flex;
550
  justify-content: space-around;
551
+ align-items: center;
552
+ z-index: 500;
553
+ box-shadow: 0 -5px 15px rgba(0,0,0,0.2);
 
 
554
  }
555
+
556
+ .nav-item {
557
+ flex: 1;
 
 
 
558
  display: flex;
559
  flex-direction: column;
560
  align-items: center;
561
+ justify-content: center;
562
+ padding: 5px;
563
+ cursor: pointer;
564
+ color: var(--text-secondary);
565
+ transition: color 0.2s ease;
566
+ font-size: 0.7rem;
567
+ text-align: center;
568
+ gap: 2px;
569
  }
570
+
571
+ .nav-item svg {
572
  width: 24px;
573
  height: 24px;
574
  fill: var(--text-secondary);
575
+ transition: fill 0.2s ease;
576
  }
577
+
578
+ .nav-item.active {
579
+ color: var(--accent-blue-light);
 
 
 
 
 
 
 
580
  }
581
+ .nav-item.active svg {
582
+ fill: var(--accent-blue-light);
 
 
583
  }
584
+
585
+ .nav-item#nav-scan {
586
+ flex-grow: 0;
587
+ width: 56px;
588
+ height: 56px;
589
+ border-radius: 50%;
590
+ background: linear-gradient(45deg, var(--ton-purple), var(--ton-purple-light));
591
+ color: white;
592
+ transform: translateY(-15px);
593
+ box-shadow: 0 5px 15px rgba(0,0,0,0.3);
594
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
595
+ }
596
+ .nav-item#nav-scan:hover {
597
+ transform: translateY(-17px);
598
+ box-shadow: 0 7px 20px rgba(0,0,0,0.4);
599
+ }
600
+ .nav-item#nav-scan svg {
601
+ fill: white;
602
+ width: 28px;
603
+ height: 28px;
604
+ }
605
+ .nav-item#nav-scan span {
606
+ display: none; /* Hide text below central icon */
607
+ }
608
+
609
+
610
+ /* Modals */
611
  .modal-overlay {
612
  position: fixed;
613
  top: 0;
 
619
  align-items: center;
620
  justify-content: center;
621
  z-index: 1000;
622
+ -webkit-backdrop-filter: blur(5px);
623
  backdrop-filter: blur(5px);
624
  }
625
  .modal-content {
 
640
  background-color: var(--bg-tertiary);
641
  border: 1px solid var(--border-color);
642
  color: white;
643
+ border-radius: 6px;
644
  font-size: 1rem;
645
  }
646
  .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
647
  .modal-btn {
648
  padding: 10px 20px;
649
+ border-radius: 6px;
650
  border: none;
651
  cursor: pointer;
652
  font-weight: 500;
653
+ transition: background-color 0.2s ease;
654
  }
655
  .secondary-btn { background-color: var(--bg-hover); color: white; }
656
+ .secondary-btn:hover { background-color: #3a3d45; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
657
 
658
  #status-bar {
659
  position: fixed;
660
+ bottom: calc(60px + 20px); /* Above nav bar */
661
  left: 50%;
662
  transform: translateX(-50%);
663
  background-color: var(--bg-tertiary);
 
667
  font-size: 0.9rem;
668
  opacity: 0;
669
  visibility: hidden;
670
+ transition: opacity 0.3s, visibility 0.3s;
671
  z-index: 2000;
672
  box-shadow: 0 5px 15px rgba(0,0,0,0.3);
673
  }
 
675
  #status-bar.error { background-color: var(--error-color); }
676
  #status-bar.visible { opacity: 1; visibility: visible; }
677
 
678
+ /* Desktop Layout (min-width 768px) */
679
  @media (min-width: 768px) {
680
+ body {
681
+ align-items: center;
682
+ justify-content: center;
683
+ }
684
  .main-container {
685
+ max-width: 1100px;
686
+ max-height: 800px;
687
  border-radius: 12px;
688
  overflow: hidden;
689
  box-shadow: 0 10px 40px rgba(0,0,0,0.3);
690
  border: 1px solid var(--border-color);
691
+ /* Remove fixed positioning for login view */
692
+ position: relative;
693
+ bottom: auto;
694
+ left: auto;
695
+ transform: none;
696
+ padding-bottom: 0; /* No nav bar padding on desktop container */
697
  }
698
+ #app-view {
699
+ flex-direction: row; /* Side-by-side layout */
700
+ }
701
+
702
+ .content-area {
703
+ flex-grow: 1;
704
+ padding-bottom: 0; /* No padding for desktop layout */
705
+ position: static; /* Static positioning */
706
+ overflow: hidden; /* Prevent content area scroll bar */
707
+ display: flex; /* Use flex for side-by-side views if needed, or handle within views */
708
+ }
709
+
710
+ .view {
711
+ position: static; /* Static positioning in desktop flex layout */
712
+ overflow-y: hidden; /* Prevent individual view scroll bars unless needed */
713
+ }
714
+ #chats-view {
715
+ flex-direction: row; /* Side-by-side list and window */
716
+ }
717
+
718
  #chatroom-list-view {
719
+ width: 320px;
720
  flex-shrink: 0;
721
+ border-right: 1px solid var(--border-color);
722
+ display: flex !important; /* Always show list on desktop */
723
+ overflow-y: auto; /* Add scroll to list view */
724
+ height: 100%;
725
  }
726
  #chat-window-view {
727
+ width: auto;
728
+ flex-grow: 1;
729
+ display: flex !important; /* Always show window on desktop */
730
+ overflow: hidden; /* Handle message scroll within messages-container */
731
+ height: 100%;
732
  }
 
 
733
 
734
+ .back-btn { display: none !important; } /* Hide back button on desktop */
735
+
736
+ /* Ensure other views take full space when active */
737
+ #users-view.active, #browser-view.active, #profile-view.active {
 
738
  width: 100%;
739
+ height: 100%;
740
+ overflow-y: auto; /* Add scroll back for these views */
741
+ }
742
+ #users-view, #browser-view, #profile-view {
743
+ /* When inactive, they are hidden by JS, no need for specific desktop hidden style */
744
  }
745
+
746
+
747
+ /* Hide bottom nav bar on desktop */
748
+ #bottom-nav {
749
+ display: none;
750
+ }
751
+
752
+ #status-bar {
753
+ bottom: 20px; /* Revert status bar position */
754
  }
755
+
756
+ #profile-view {
757
+ padding-top: 40px; /* Adjust padding */
758
+ }
759
+ #profile-view .avatar {
760
+ width: 100px;
761
+ height: 100px;
762
+ margin-bottom: 24px;
763
+ }
764
+ #profile-view h2 { font-size: 2rem; }
765
+ #profile-view p { font-size: 1.1rem; }
766
  }
767
  </style>
768
  </head>
 
776
  </div>
777
 
778
  <div id="app-view" class="main-container">
779
+ <div class="content-area">
780
+
781
+ <!-- Chats View -->
782
+ <div id="chats-view" class="view active">
783
+ <div id="chatroom-list-view">
784
+ <div class="list-header">
785
+ <div class="list-header-top">
786
  <h2>Чаты</h2>
787
+ <div style="display: flex; align-items: center; gap: 8px;">
788
  <button id="create-room-show-modal" class="action-btn small">
789
  <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>
790
+ <span>Новый</span>
791
  </button>
792
  </div>
793
  </div>
794
+ <div class="user-profile-summary">
795
+ <div id="user-wallet-summary"></div>
796
+ <div id="user-nickname-summary"></div>
797
+ <form class="username-form" id="username-form">
798
+ <input type="text" id="username-input" class="username-input" placeholder="Установить никнейм" autocomplete="off">
799
+ <button type="submit" class="action-btn small">✓</button>
800
+ </form>
801
+ </div>
802
  </div>
803
+ <div id="chatroom-list"></div>
804
  </div>
805
 
806
  <div id="chat-window-view">
807
  <div id="chat-placeholder" class="chat-placeholder">
808
  <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
809
  <h2>Выберите чат</h2>
810
+ <p>Начните общение в одном из существующих чатов или создайте свой собственный.</p>
811
  </div>
812
  <div id="active-chat" style="display: none; width: 100%; height: 100%; flex-direction: column;">
813
  <div class="chat-header">
 
820
  <div id="messages-container"></div>
821
  <form id="message-form" class="message-form">
822
  <input type="text" id="message-input" placeholder="Сообщение..." autocomplete="off">
823
+ <button type="submit" class="action-btn send-btn" id="send-btn">
824
+ <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>
825
  </button>
826
  </form>
827
  </div>
828
  </div>
829
  </div>
830
 
831
+ <!-- Users View -->
832
+ <div id="users-view" class="view">
833
+ <div class="view-header">
834
+ <h2>Пользователи</h2>
 
 
 
 
835
  </div>
836
+ <div id="user-list"></div>
837
  </div>
838
 
839
+ <!-- Browser View -->
840
+ <div id="browser-view" class="view">
841
+ <div class="view-header">
842
+ <h2>Браузер</h2>
843
+ </div>
844
+ <form id="browser-form" class="browser-bar">
845
+ <input type="text" id="browser-url-input" class="browser-url-input" placeholder="Введите адрес или поисковый запрос" value="https://www.google.com">
846
+ <button type="submit" class="action-btn small">Go</button>
847
+ </form>
848
+ <iframe id="browser-iframe" class="browser-iframe" src="https://www.google.com"></iframe>
849
  </div>
850
 
851
+ <!-- Profile View -->
852
+ <div id="profile-view" class="view">
853
+ <h2>Мой профиль</h2>
854
+ <div id="my-profile-avatar-container" class="avatar"></div>
855
+ <p id="my-profile-username"></p>
856
+ <p id="my-profile-address" style="word-break: break-all;"></p>
857
+ <p id="my-profile-balance" class="profile-balance"></p>
858
+
859
+ <div id="my-profile-qr-code" style="background: white; padding: 10px; margin: 20px auto; width: fit-content; border-radius: 8px;"></div>
860
+ <p class="qr-label">Отсканируйте для открытия профиля</p>
861
+
862
+ <!-- Add buttons for actions from profile? Or keep actions in modal? -->
863
+ <!-- Keeping send TON in modal for other users, maybe add other actions here later -->
864
+ <button id="disconnect-wallet-btn" class="action-btn secondary-btn" style="background-color: var(--error-color); margin-top: 30px;">
865
+ Отключить кошелек
866
+ </button>
867
  </div>
868
 
869
  </div>
870
 
871
+ <!-- Bottom Navigation -->
872
+ <div id="bottom-nav">
873
+ <div class="nav-item active" data-view="chats-view">
874
+ <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-2zM6 9h12v2H6V9zm8 6H6v-2h8v2zm4-6v6H6V9h12z"/></svg>
875
  <span>Чаты</span>
876
+ </div>
877
+ <div class="nav-item" data-view="users-view">
878
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M16.5 12c-.97 0-3.68.63-5.18 1.67-.88.61-1.41 1.35-1.82 2.33H2v3h20v-3h-8.5c.36-.74.77-1.45 1.31-2.09C14.68 12.63 15.53 12 16.5 12zM7.5 8c1.93 0 3.5-1.57 3.5-3.5S9.43 1 7.5 1 4 2.57 4 4.5 5.57 8 7.5 8zm9 0c1.93 0 3.5-1.57 3.5-3.5S18.43 1 16.5 1 13 2.57 13 4.5 14.57 8 16.5 8z"/></svg>
879
+ <span>Юзеры</span>
880
+ </div>
881
+ <div class="nav-item" id="nav-scan">
882
+ <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>
883
+ <span>Scan</span>
884
+ </div>
885
+ <div class="nav-item" data-view="browser-view">
886
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-5-5h2v-2H7v2zm3 0h2v-2h-2v2zm3 0h2v-2h-2v2z"/></svg>
887
  <span>Браузер</span>
888
+ </div>
889
+ <div class="nav-item" data-view="profile-view">
890
+ <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="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>
891
  <span>Профиль</span>
892
+ </div>
893
+ </div>
894
  </div>
895
 
896
+ <!-- Modals -->
897
  <div id="create-room-modal" class="modal-overlay">
898
  <div class="modal-content">
899
  <h3>Создать новый чат</h3>
900
  <form id="create-room-form">
901
  <label for="room-name">Название чата</label>
902
+ <input type="text" id="room-name" required autocomplete="off">
903
  <label for="room-password">Пароль (оставьте пустым для открытого)</label>
904
+ <input type="password" id="room-password" autocomplete="new-password">
905
  <div class="modal-actions">
906
  <button type="button" id="create-room-cancel" class="modal-btn secondary-btn">Отмена</button>
907
  <button type="submit" class="modal-btn action-btn">Создать</button>
 
915
  <h3>Вход в приватный чат</h3>
916
  <form id="password-form">
917
  <label for="password-input">Введите пароль</label>
918
+ <input type="password" id="password-input" required autocomplete="current-password">
919
  <div class="modal-actions">
920
  <button type="button" id="password-cancel" class="modal-btn secondary-btn">Отмена</button>
921
  <button type="submit" class="modal-btn action-btn">Войти</button>
 
924
  </div>
925
  </div>
926
 
927
+ <!-- Profile Modal (for others' profiles) -->
928
  <div id="profile-modal" class="modal-overlay">
929
  <div class="modal-content" style="text-align: center;">
930
  <h3 id="profile-modal-title">Профиль пользователя</h3>
 
955
  <script>
956
  document.addEventListener('DOMContentLoaded', () => {
957
  const tonConnectUI = new TON_CONNECT_UI.TonConnectUI({
958
+ manifestUrl: 'https://huggingface.co/spaces/Aleksmorshen/MorshenGroup/resolve/main/tonconnect-manifest.json', // Or your manifest URL
959
  buttonRootId: 'ton-connect-button'
960
  });
961
 
962
+ let currentUser = { address: null, username: null, balance: '...' };
963
  let activeChatroomId = null;
964
  let messagePollingInterval = null;
965
  let chatroomsData = {};
966
+ let usersData = [];
967
  let html5QrCode = null;
968
  let profileQrCode = null;
969
  let myProfileQrCode = null;
970
 
971
  const loginView = document.getElementById('login-view');
972
  const appView = document.getElementById('app-view');
973
+ const chatsView = document.getElementById('chats-view');
974
+ const usersView = document.getElementById('users-view');
975
+ const browserView = document.getElementById('browser-view');
976
+ const profileView = document.getElementById('profile-view');
977
+
978
  const chatroomListView = document.getElementById('chatroom-list-view');
979
  const chatWindowView = document.getElementById('chat-window-view');
980
  const chatPlaceholder = document.getElementById('chat-placeholder');
981
  const activeChat = document.getElementById('active-chat');
982
+
983
  const profileModal = document.getElementById('profile-modal');
984
  const scannerModal = document.getElementById('scanner-modal');
985
 
986
+ const bottomNav = document.getElementById('bottom-nav');
987
+ const navItems = bottomNav.querySelectorAll('.nav-item');
988
+
989
+ const browserIframe = document.getElementById('browser-iframe');
990
+ const browserUrlInput = document.getElementById('browser-url-input');
991
+ const browserForm = document.getElementById('browser-form');
992
 
993
+ const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292'];
994
+
995
+ const getAvatar = (name) => {
996
  const initial = (name ? name[0] : '?').toUpperCase();
997
  const charCode = initial.charCodeAt(0);
998
  const color = AVATAR_COLORS[charCode % AVATAR_COLORS.length];
999
  const avatar = document.createElement('div');
1000
+ avatar.className = 'avatar';
1001
  avatar.style.backgroundColor = color;
1002
  avatar.textContent = initial;
1003
+ // Basic placeholder for TON symbol if it's the app's profile/placeholder
1004
+ if (!name || name === 'Virton') {
1005
+ avatar.innerHTML = `<img src="https://ton.org/download/ton_symbol.svg" style="filter: invert(100%);">`;
1006
+ avatar.style.backgroundColor = 'transparent'; // Or a subtle background
1007
+ }
1008
  return avatar;
1009
  };
1010
 
 
1029
  if (response.status === 204) return null;
1030
  return await response.json();
1031
  } catch (error) {
1032
+ showStatus(`Ошибка API: ${error.message}`, 'error');
1033
  throw error;
1034
  }
1035
  };
1036
 
1037
  const truncateAddress = (address) => address ? `${address.substring(0, 4)}...${address.substring(address.length - 4)}` : '';
1038
 
1039
+ const updateUserInfoSummary = () => {
1040
+ document.getElementById('user-wallet-summary').textContent = `Кошелек: ${truncateAddress(currentUser.address)}`;
1041
+ const nicknameEl = document.getElementById('user-nickname-summary');
1042
+ const usernameInput = document.getElementById('username-input');
1043
+ nicknameEl.textContent = currentUser.username ? `Ник: ${currentUser.username}` : `Никнейм не установлен`;
1044
+ usernameInput.value = currentUser.username || '';
 
 
 
 
1045
  };
1046
 
1047
+ const updateMyProfileView = () => {
1048
+ const avatarContainer = document.getElementById('my-profile-avatar-container');
1049
+ const usernameEl = document.getElementById('my-profile-username');
1050
+ const addressEl = document.getElementById('my-profile-address');
1051
+ const balanceEl = document.getElementById('my-profile-balance');
1052
+ const qrCodeEl = document.getElementById('my-profile-qr-code');
1053
+
 
1054
  avatarContainer.innerHTML = '';
1055
+ avatarContainer.appendChild(getAvatar(currentUser.username || currentUser.address));
1056
+
1057
+ usernameEl.textContent = currentUser.username || `Пользователь ${truncateAddress(currentUser.address)}`;
1058
+ addressEl.textContent = currentUser.address;
1059
+ balanceEl.textContent = `Баланс: ${currentUser.balance}`;
1060
 
 
1061
  qrCodeEl.innerHTML = '';
1062
+ if (myProfileQrCode) {
1063
+ myProfileQrCode.clear();
1064
+ myProfileQrCode = null;
1065
+ }
1066
+ if (currentUser.address) {
1067
+ myProfileQrCode = new QRCode(qrCodeEl, {
1068
+ text: currentUser.address,
1069
+ width: 150,
1070
+ height: 150,
1071
+ colorDark : "#000000",
1072
+ colorLight : "#ffffff",
1073
+ correctLevel : QRCode.CorrectLevel.H
1074
+ });
1075
+ }
1076
+ }
1077
 
1078
  document.getElementById('username-form').addEventListener('submit', async (e) => {
1079
  e.preventDefault();
1080
  const newUsername = document.getElementById('username-input').value.trim();
1081
+ if (!newUsername) {
1082
+ showStatus('Никнейм не может быть пустым.', 'error');
1083
+ return;
1084
+ }
1085
+ if (newUsername.length < 3 || newUsername.length > 20) {
1086
+ showStatus('Никнейм должен быть от 3 до 20 символов.', 'error');
1087
  return;
1088
  }
1089
+
1090
+ const submitBtn = e.target.querySelector('button[type="submit"]');
1091
+ submitBtn.disabled = true;
1092
+
1093
  try {
1094
  await apiCall('/api/set_username', {
1095
  method: 'POST',
 
1097
  body: JSON.stringify({ address: currentUser.address, username: newUsername })
1098
  });
1099
  currentUser.username = newUsername;
1100
+ updateUserInfoSummary();
1101
+ updateMyProfileView();
1102
  showStatus('Никнейм успешно обновлен!', 'success');
1103
+ fetchChatrooms(); // Update chat list with new name
1104
+ if (activeChatroomId) fetchMessages(activeChatroomId); // Update chat messages with new name
1105
+ fetchUsers(); // Update user list
1106
+ } catch (err) {
1107
+ // Error handled by apiCall
1108
+ } finally {
1109
+ submitBtn.disabled = false;
1110
+ }
1111
  });
1112
 
1113
  const initializeUser = async (address) => {
1114
  currentUser.address = address;
1115
+ if (tonConnectUI.wallet) {
1116
+ // TON Connect UI >= 2.0 provides balance directly in wallet.account
1117
+ // Balance is in nanoTON, need to convert
1118
+ const balanceNano = tonConnectUI.wallet.account.balance;
1119
+ if (balanceNano !== undefined && balanceNano !== null) {
1120
+ currentUser.balance = (Number(balanceNano) / 1_000_000_000).toFixed(2) + ' TON';
1121
+ } else {
1122
+ currentUser.balance = 'Баланс недоступен';
1123
+ }
1124
+ } else {
1125
+ currentUser.balance = '...';
1126
+ }
1127
+
1128
  try {
1129
  const data = await apiCall('/api/user_data', {
1130
  method: 'POST',
 
1133
  });
1134
  currentUser.username = data.username;
1135
  } catch (err) {
1136
+ currentUser.username = null; // User not found or error, treat as new/unknown
1137
  }
1138
+
1139
+ updateUserInfoSummary();
1140
+ updateMyProfileView();
1141
+
1142
  loginView.style.display = 'none';
1143
  appView.style.display = 'flex';
1144
+
1145
+ // Initial data fetches
1146
  fetchChatrooms();
1147
  fetchUsers();
1148
+
1149
+ // Show default view (chats)
1150
+ showView('chats-view');
1151
  };
1152
 
1153
  const renderChatrooms = (rooms) => {
 
1157
  rooms.forEach(room => {
1158
  chatroomsData[room.id] = room;
1159
  const item = document.createElement('div');
1160
+ item.className = 'chatroom-item';
1161
  item.dataset.id = room.id;
1162
 
1163
  item.appendChild(getAvatar(room.name));
1164
 
1165
  const infoDiv = document.createElement('div');
1166
+ infoDiv.className = 'chatroom-info';
1167
  const nameSpan = document.createElement('div');
1168
+ nameSpan.className = 'chatroom-name';
1169
  nameSpan.textContent = room.name;
1170
  infoDiv.appendChild(nameSpan);
1171
  item.appendChild(infoDiv);
 
1175
  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>`;
1176
  item.appendChild(lockIcon);
1177
  }
1178
+
1179
  item.addEventListener('click', () => selectChatroom(room.id, room.is_private));
1180
  list.appendChild(item);
1181
  });
1182
  };
1183
 
1184
  const fetchChatrooms = async () => {
1185
+ if (!currentUser.address) return;
1186
  try {
1187
  const data = await apiCall('/api/chatrooms');
1188
  renderChatrooms(data.chatrooms);
1189
+ } catch (err) {
1190
+ // Error handled by apiCall
1191
+ }
1192
  };
1193
 
1194
+ const renderUsers = (users) => {
1195
+ const list = document.getElementById('user-list');
1196
+ list.innerHTML = '';
1197
+ usersData = users; // Store for potential future use or filtering
1198
+ users.forEach(user => {
1199
+ if (user.address === currentUser.address) return; // Don't list self
1200
+
1201
+ const item = document.createElement('div');
1202
+ item.className = 'user-item';
1203
+ item.dataset.address = user.address;
1204
+
1205
+ item.appendChild(getAvatar(user.username || user.address));
1206
+
1207
+ const infoDiv = document.createElement('div');
1208
+ infoDiv.className = 'user-item-info';
1209
+ const nameSpan = document.createElement('div');
1210
+ nameSpan.className = 'user-item-name';
1211
+ nameSpan.textContent = user.username || `Пользователь ${truncateAddress(user.address)}`;
1212
+ infoDiv.appendChild(nameSpan);
1213
+ const addressSpan = document.createElement('div');
1214
+ addressSpan.className = 'user-item-address';
1215
+ addressSpan.textContent = user.address;
1216
+ infoDiv.appendChild(addressSpan);
1217
+
1218
+ item.appendChild(infoDiv);
1219
+
1220
+ item.addEventListener('click', () => showProfileModal(user.address));
1221
+ list.appendChild(item);
1222
+ });
1223
+ };
1224
+
1225
+ const fetchUsers = async () => {
1226
+ if (!currentUser.address) return;
1227
+ try {
1228
+ const data = await apiCall('/api/users');
1229
+ renderUsers(data.users);
1230
+ } catch (err) {
1231
+ // Error handled by apiCall
1232
+ }
1233
+ };
1234
 
1235
  const renderMessages = (messages) => {
1236
  const container = document.getElementById('messages-container');
1237
+ // Check if user is near bottom before rendering to decide whether to auto-scroll
1238
+ const isNearBottom = container.scrollHeight - container.scrollTop - container.clientHeight <= 50;
1239
+
1240
  container.innerHTML = '';
1241
  messages.forEach(msg => {
1242
  const msgDiv = document.createElement('div');
1243
  msgDiv.className = 'message ' + (msg.sender_address === currentUser.address ? 'sent' : 'received');
1244
 
1245
+ const avatar = getAvatar(msg.display_name);
1246
+ avatar.classList.add('message-avatar');
1247
  avatar.style.cursor = 'pointer';
1248
+ avatar.onclick = () => showProfileModal(msg.sender_address);
1249
 
1250
  const contentDiv = document.createElement('div');
1251
  contentDiv.className = 'message-content';
 
1253
  const senderDiv = document.createElement('div');
1254
  senderDiv.className = 'message-sender';
1255
  senderDiv.textContent = msg.display_name;
1256
+ senderDiv.onclick = () => showProfileModal(msg.sender_address);
1257
 
1258
  const bubbleDiv = document.createElement('div');
1259
  bubbleDiv.className = 'message-bubble';
 
1263
  contentDiv.appendChild(bubbleDiv);
1264
 
1265
  msgDiv.appendChild(contentDiv);
1266
+ msgDiv.insertBefore(avatar, contentDiv); // Avatar comes first in structure
1267
  container.appendChild(msgDiv);
1268
  });
1269
 
1270
+ if(isNearBottom) {
1271
+ container.scrollTop = container.scrollHeight;
1272
  }
1273
  };
1274
 
1275
  const fetchMessages = async (roomId) => {
1276
+ if (!currentUser.address || !roomId) return;
1277
  try {
1278
  const data = await apiCall(`/api/messages/${roomId}`);
1279
  renderMessages(data.messages);
1280
  } catch (err) {
1281
+ // Error handled by apiCall
1282
  if (messagePollingInterval) clearInterval(messagePollingInterval);
1283
+ messagePollingInterval = null;
1284
+ showStatus('Не удалось загрузить сообщения.', 'error');
1285
  }
1286
  };
1287
+
1288
+ const showChatWindow = () => {
1289
+ chatroomListView.style.display = 'none';
1290
  chatWindowView.style.display = 'flex';
 
1291
  };
1292
 
1293
+ const showChatroomList = () => {
1294
+ chatroomListView.style.display = 'flex';
1295
+ chatWindowView.style.display = 'none';
1296
+ // Clear active chat state
1297
+ activeChat.style.display = 'none';
1298
+ chatPlaceholder.style.display = 'flex';
1299
+ activeChatroomId = null;
1300
+ if (messagePollingInterval) {
1301
+ clearInterval(messagePollingInterval);
1302
+ messagePollingInterval = null;
1303
+ }
1304
  };
1305
 
1306
  const selectChatroom = (roomId, isPrivate) => {
 
1318
 
1319
  chatPlaceholder.style.display = 'none';
1320
  activeChat.style.display = 'flex';
 
1321
 
1322
+ showChatWindow(); // Switch view to chat window on mobile
1323
+
1324
  fetchMessages(roomId);
1325
  messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000);
1326
  };
 
1336
  const formSubmitHandler = async (e) => {
1337
  e.preventDefault();
1338
  passwordForm.removeEventListener('submit', formSubmitHandler);
1339
+ document.getElementById('password-cancel').removeEventListener('click', cancelHandler);
1340
+
1341
  const password = passwordInput.value;
1342
  passwordModal.style.display = 'none';
1343
+
1344
+ const submitBtn = e.target.querySelector('button[type="submit"]');
1345
+ submitBtn.disabled = true;
1346
+
1347
  try {
1348
  await apiCall('/api/join_chatroom', {
1349
  method: 'POST',
 
1351
  body: JSON.stringify({ chatroom_id: roomId, password })
1352
  });
1353
  proceedToRoom();
1354
+ } catch (err) {
1355
+ // Error handled by apiCall (Invalid password, etc.)
1356
+ } finally {
1357
+ submitBtn.disabled = false;
1358
+ }
1359
  };
1360
  passwordForm.addEventListener('submit', formSubmitHandler);
1361
 
1362
+ const cancelHandler = () => {
1363
  passwordModal.style.display = 'none';
1364
  passwordForm.removeEventListener('submit', formSubmitHandler);
1365
+ document.getElementById('password-cancel').removeEventListener('click', cancelHandler);
1366
  };
1367
+ document.getElementById('password-cancel').addEventListener('click', cancelHandler);
1368
+
1369
  } else {
1370
  proceedToRoom();
1371
  }
 
1376
  const input = document.getElementById('message-input');
1377
  const sendBtn = document.getElementById('send-btn');
1378
  const text = input.value.trim();
1379
+ if (text && activeChatroomId && currentUser.address) {
1380
  input.value = '';
1381
  input.disabled = true;
1382
  sendBtn.disabled = true;
 
1390
  text: text
1391
  })
1392
  });
1393
+ // Fetch messages immediately after sending for faster UI update
1394
  await fetchMessages(activeChatroomId);
1395
  document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight;
1396
+ } catch (err) {
1397
+ // Error handled by apiCall
1398
+ showStatus('Не удалось отправить сообщение.', 'error');
1399
  } finally {
1400
  input.disabled = false;
1401
  sendBtn.disabled = false;
 
1416
  e.preventDefault();
1417
  const name = document.getElementById('room-name').value.trim();
1418
  const password = document.getElementById('room-password').value;
1419
+ if (!name) {
1420
+ showStatus('Название чата обязательно.', 'error');
1421
+ return;
1422
+ }
1423
+ if (!currentUser.address) {
1424
+ showStatus('Необходимо подключить кошелек для создания чата.', 'error');
1425
+ return;
1426
+ }
1427
+
1428
+ const submitBtn = e.target.querySelector('button[type="submit"]');
1429
+ submitBtn.disabled = true;
1430
 
1431
  try {
1432
  await apiCall('/api/create_chatroom', {
 
1437
  createRoomModal.style.display = 'none';
1438
  showStatus('Чат успешно создан!', 'success');
1439
  fetchChatrooms();
1440
+ } catch (err) {
1441
+ // Error handled by apiCall
1442
+ } finally {
1443
+ submitBtn.disabled = false;
1444
+ }
1445
  });
1446
 
1447
+ const showProfileModal = async (address) => {
1448
+ if (!address) return;
 
 
 
1449
  try {
1450
  const userData = await apiCall('/api/user_data', {
1451
  method: 'POST',
 
1453
  body: JSON.stringify({ address: address })
1454
  });
1455
 
1456
+ const username = userData.username || `Пользователь ${truncateAddress(address)}`;
 
 
 
 
 
1457
 
1458
+ const avatarContainer = document.getElementById('profile-avatar-container');
1459
+ const usernameEl = document.getElementById('profile-username');
1460
+ const addressEl = document.getElementById('profile-address');
1461
+ const qrCodeEl = document.getElementById('profile-qr-code');
1462
+ const sendTonBtn = document.getElementById('send-ton-btn');
1463
+ const modalTitle = document.getElementById('profile-modal-title');
1464
+
1465
+ modalTitle.textContent = (address === currentUser.address) ? 'Мой профиль' : 'Профиль пользователя';
1466
+
1467
  avatarContainer.innerHTML = '';
1468
+ avatarContainer.appendChild(getAvatar(username));
1469
+
1470
+ usernameEl.textContent = username;
1471
+ addressEl.textContent = address;
1472
 
 
1473
  qrCodeEl.innerHTML = '';
1474
+ if (profileQrCode) {
1475
+ profileQrCode.clear();
1476
+ }
1477
+ // Generate QR code only if address is valid
1478
+ if (address) {
1479
+ profileQrCode = new QRCode(qrCodeEl, {
1480
+ text: address,
1481
+ width: 150,
1482
+ height: 150,
1483
+ colorDark : "#000000",
1484
+ colorLight : "#ffffff",
1485
+ correctLevel : QRCode.CorrectLevel.H
1486
+ });
1487
+ }
1488
 
1489
+ // Show/hide Send TON button
1490
+ sendTonBtn.style.display = (address === currentUser.address) ? 'none' : 'block';
1491
+ if (address !== currentUser.address) {
1492
+ sendTonBtn.onclick = async () => {
1493
+ if (!tonConnectUI.connected || !currentUser.address) {
1494
+ showStatus('Подключите кошелек для отправки TON.', 'error');
1495
+ return;
1496
+ }
1497
+ const amountString = prompt(`Введите сумму в TON для отправки на адрес ${truncateAddress(address)}:`, "0.1");
1498
+ if (amountString === null) return;
1499
+
1500
+ const amount = parseFloat(amountString);
1501
+ if (isNaN(amount) || amount <= 0) {
1502
+ showStatus('Неверная сумма.', 'error');
1503
+ return;
1504
+ }
1505
+
1506
+ const amountInNanoTon = Math.floor(amount * 1_000_000_000).toString();
1507
+
1508
+ const transaction = {
1509
+ validUntil: Math.floor(Date.now() / 1000) + 600,
1510
+ messages: [ { address: address, amount: amountInNanoTon } ]
1511
+ };
1512
+
1513
+ try {
1514
+ await tonConnectUI.sendTransaction(transaction);
1515
+ showStatus(`Транзакция отправлена успешно!`, 'success');
1516
+ profileModal.style.display = 'none';
1517
+ } catch (error) {
1518
+ showStatus('Транзакция отклонена или произошла ошибка.', 'error');
1519
+ console.error("Send transaction failed:", error);
1520
+ }
1521
  };
1522
+ }
1523
+
 
 
 
 
 
 
 
1524
  profileModal.style.display = 'flex';
1525
  } catch (err) {
1526
+ showStatus('Не удалось загрузить профиль пользователя.', 'error');
1527
  }
1528
  };
1529
 
1530
  const showScanner = () => {
1531
  scannerModal.style.display = 'flex';
1532
+ if (!html5QrCode) {
1533
+ html5QrCode = new Html5Qrcode("qr-reader");
1534
+ }
1535
  const qrCodeSuccessCallback = (decodedText, decodedResult) => {
1536
  hideScanner();
1537
+ // Basic check for TON address format (starts with EQ or UQ, length around 48 chars)
1538
  if (decodedText && decodedText.length > 40 && (decodedText.startsWith('EQ') || decodedText.startsWith('UQ'))) {
1539
+ showProfileModal(decodedText);
1540
  } else {
1541
+ showStatus('Отсканирован недействительный QR-код (не похож на адрес TON).', 'error');
1542
  }
1543
  };
1544
+ const qrCodeErrorCallback = (errorMessage) => {
1545
+ // console.warn(`QR scan error: ${errorMessage}`); // Log errors but don't show modal if it's just scanning issues
1546
+ };
1547
+
1548
+ const config = { fps: 10, qrbox: { width: 250, height: 250 }, rememberLastUsedCamera: true };
1549
+ html5QrCode.start({ facingMode: "environment" }, config, qrCodeSuccessCallback, qrCodeErrorCallback)
1550
  .catch(err => {
1551
+ showStatus('Не удалось запустить сканер QR-кодов. Проверьте разрешения камеры.', 'error');
1552
+ console.error("QR scanner start failed:", err);
1553
+ hideScanner(); // Hide modal if start fails
1554
  });
1555
  };
1556
 
 
1561
  scannerModal.style.display = 'none';
1562
  };
1563
 
 
1564
  document.getElementById('profile-close-btn').addEventListener('click', () => profileModal.style.display = 'none');
1565
  document.getElementById('scanner-close-btn').addEventListener('click', hideScanner);
1566
+ document.getElementById('back-to-list-btn').addEventListener('click', showChatroomList);
1567
 
1568
+ const handleResize = () => {
1569
+ const isMobile = window.innerWidth < 768;
1570
+ const backBtn = document.getElementById('back-to-list-btn');
1571
+
1572
+ if (isMobile) {
1573
+ backBtn.style.display = 'block';
1574
+ // On mobile, only the active view should be displayed
1575
+ // Handled by showView function
1576
+ // If chats is the active view, manage list/window visibility
1577
+ if (chatsView.classList.contains('active')) {
1578
+ if (activeChatroomId) {
1579
+ showChatWindow();
1580
+ } else {
1581
+ showChatroomList();
1582
+ }
1583
+ } else {
1584
+ // Ensure chat views are hidden if another view is active
1585
+ chatroomListView.style.display = 'none';
1586
+ chatWindowView.style.display = 'none';
1587
+ }
1588
+
1589
+ } else { // Desktop
1590
+ backBtn.style.display = 'none';
1591
+ // On desktop, chats view should always show both list and window side-by-side
1592
+ // Other views take full space when active.
1593
+ // The `display: flex !important` in CSS handles the side-by-side for chats-view
1594
+ // The `display: flex !important` on other views handles them taking full space
1595
+ // The `position: static` and overflow styles in desktop media query are key
1596
+ if (chatsView.classList.contains('active')) {
1597
+ chatroomListView.style.display = 'flex';
1598
+ chatWindowView.style.display = 'flex';
1599
+ } else {
1600
+ chatroomListView.style.display = 'none';
1601
+ chatWindowView.style.display = 'none';
1602
+ }
1603
 
 
 
 
 
1604
  }
1605
+ };
1606
 
1607
+ // --- View Switching Logic ---
1608
+ const views = document.querySelectorAll('.view');
1609
+ const showView = (viewId) => {
1610
+ views.forEach(view => {
1611
+ view.classList.remove('active');
1612
+ });
1613
+ document.getElementById(viewId).classList.add('active');
1614
+
1615
+ navItems.forEach(item => {
1616
+ item.classList.remove('active');
1617
+ if (item.dataset.view === viewId) {
1618
+ item.classList.add('active');
1619
+ }
1620
+ });
1621
+
1622
+ // Special handling for Chats view layout on mobile resize
1623
+ if (viewId === 'chats-view') {
1624
+ handleResize(); // Re-apply chat list/window visibility based on size and active chat
1625
+ } else {
1626
+ // Ensure chat views are hidden when other tabs are active
1627
+ chatroomListView.style.display = 'none';
1628
+ chatWindowView.style.display = 'none';
1629
+ // Stop message polling if switching away from chat
1630
+ if (messagePollingInterval) {
1631
+ clearInterval(messagePollingInterval);
1632
+ messagePollingInterval = null;
1633
+ }
1634
+ activeChatroomId = null; // Clear active chat when leaving chat view
1635
+ }
1636
+ };
1637
+
1638
+ navItems.forEach(item => {
1639
+ if (item.id === 'nav-scan') {
1640
+ item.addEventListener('click', showScanner);
1641
+ } else {
1642
+ item.addEventListener('click', () => {
1643
+ const viewId = item.dataset.view;
1644
+ if (viewId) showView(viewId);
1645
+ });
1646
+ }
1647
+ });
1648
+
1649
+ // Browser Form Submission
1650
+ browserForm.addEventListener('submit', (e) => {
1651
+ e.preventDefault();
1652
+ let url = browserUrlInput.value.trim();
1653
+ if (!url) return;
1654
+
1655
+ // Simple check if it looks like a URL (contains .) or is just text
1656
+ if (url.includes('.')) {
1657
+ // Assume it's a URL, add protocol if missing
1658
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
1659
+ url = 'https://' + url; // Default to https
1660
+ }
1661
+ } else {
1662
+ // Assume it's a search query, use Google search
1663
+ url = `https://www.google.com/search?q=${encodeURIComponent(url)}`;
1664
+ }
1665
+ browserIframe.src = url;
1666
+ });
1667
+
1668
+ // Disconnect wallet button
1669
+ document.getElementById('disconnect-wallet-btn').addEventListener('click', () => {
1670
+ if (tonConnectUI.connected) {
1671
+ tonConnectUI.disconnect();
1672
+ }
1673
+ });
1674
+
1675
+
1676
+ window.addEventListener('resize', handleResize);
1677
+
1678
+ // Initial state check
1679
  tonConnectUI.onStatusChange(wallet => {
1680
  if (wallet) {
1681
  const address = TON_CONNECT_UI.toUserFriendlyAddress(wallet.account.address, false);
1682
  initializeUser(address);
1683
  } else {
1684
+ currentUser = { address: null, username: null, balance: '...' };
1685
  appView.style.display = 'none';
1686
  loginView.style.display = 'flex';
1687
+ // Ensure chats and messages clear on disconnect
1688
+ document.getElementById('chatroom-list').innerHTML = '';
1689
+ document.getElementById('messages-container').innerHTML = '';
1690
+ chatroomsData = {};
1691
+ usersData = [];
1692
  if (messagePollingInterval) clearInterval(messagePollingInterval);
1693
+ messagePollingInterval = null;
1694
  activeChatroomId = null;
1695
+ // Reset profile view
1696
+ document.getElementById('my-profile-avatar-container').innerHTML = '';
1697
+ document.getElementById('my-profile-username').textContent = '';
1698
+ document.getElementById('my-profile-address').textContent = '';
1699
+ document.getElementById('my-profile-balance').textContent = '';
1700
+ document.getElementById('my-profile-qr-code').innerHTML = '';
1701
+ if (myProfileQrCode) {
1702
+ myProfileQrCode.clear();
1703
+ myProfileQrCode = null;
1704
+ }
1705
+
1706
  }
1707
  });
1708
+
1709
+ // Handle deep links/initial wallet state on page load
1710
+ tonConnectUI.getWallet().then(wallet => {
1711
+ if (wallet) {
1712
+ const address = TON_CONNECT_UI.toUserFriendlyAddress(wallet.account.address, false);
1713
+ initializeUser(address);
1714
+ } else {
1715
+ // Show login if no wallet connected
1716
+ loginView.style.display = 'flex';
1717
+ appView.style.display = 'none';
1718
+ }
1719
+ // Set initial layout based on screen size
1720
+ handleResize();
1721
+ });
1722
+
1723
  });
1724
  </script>
1725
  </body>
 
1727
  '''
1728
  return Response(html_content, mimetype='text/html')
1729
 
 
1730
  @app.route('/api/user_data', methods=['POST'])
1731
  def get_user_data():
1732
  data = request.get_json()
 
1747
  'address': address,
1748
  'username': user_data.get('username')
1749
  })
1750
+ # Optionally sort users
1751
+ users_list.sort(key=lambda x: x['username'] if x['username'] else x['address'])
1752
  return jsonify({'users': users_list})
1753
 
1754
+
1755
  @app.route('/api/set_username', methods=['POST'])
1756
  def set_username():
1757
  data = request.get_json()
 
1761
  return jsonify({'error': 'Address and username are required'}), 400
1762
  if len(username) < 3 or len(username) > 20:
1763
  return jsonify({'error': 'Username must be between 3 and 20 characters'}), 400
1764
+
1765
+ # Check for username uniqueness (optional but good practice)
1766
  db = read_db()
1767
+ for existing_address, user_data in db['users'].items():
1768
+ if existing_address != address and user_data.get('username') == username:
1769
+ return jsonify({'error': 'Username already taken'}), 409 # Conflict
1770
+
1771
  if address not in db['users']:
1772
  db['users'][address] = {}
1773
  db['users'][address]['username'] = username
 
1794
  creator_address = data.get('creator_address')
1795
  if not name or not creator_address:
1796
  return jsonify({'error': 'Name and creator address are required'}), 400
1797
+ if len(name) < 3 or len(name) > 50:
1798
+ return jsonify({'error': 'Chatroom name must be between 3 and 50 characters'}), 400
1799
 
1800
  db = read_db()
1801
  room_id = str(uuid.uuid4())
 
1819
  if not chatroom:
1820
  return jsonify({'error': 'Chatroom not found'}), 404
1821
  if chatroom['is_private']:
1822
+ if not password or not chatroom['password_hash'] or not check_password_hash(chatroom['password_hash'], password):
1823
  return jsonify({'error': 'Invalid password'}), 403
1824
  return jsonify({'success': True})
1825
 
 
1853
 
1854
  if not all([chatroom_id, sender_address, text]):
1855
  return jsonify({'error': 'Missing data'}), 400
1856
+ if not text.strip():
1857
+ return jsonify({'error': 'Message cannot be empty'}), 400
1858
+ if len(text) > 1000: # Simple message length limit
1859
+ return jsonify({'error': 'Message is too long'}), 400
1860
 
1861
  db = read_db()
1862
  if chatroom_id not in db['messages']:
 
1865
  message = {
1866
  'id': str(uuid.uuid4()),
1867
  'sender_address': sender_address,
1868
+ 'text': text.strip(),
1869
  'timestamp': datetime.utcnow().isoformat() + "Z"
1870
  }
1871
 
1872
+ # Keep only the last 100 messages per chatroom
1873
  if len(db['messages'][chatroom_id]) >= 100:
1874
  db['messages'][chatroom_id].pop(0)
1875