Aleksmorshen commited on
Commit
b21ff9e
·
verified ·
1 Parent(s): 3f920af

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +531 -630
app.py CHANGED
@@ -40,24 +40,20 @@ def index():
40
  <script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
41
  <link rel="preconnect" href="https://fonts.googleapis.com">
42
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
43
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
44
  <style>
45
  :root {
46
- --tg-bg-color: #18222d;
47
- --tg-secondary-bg-color: #1f2b36;
48
- --tg-header-bg-color: #222e3a;
49
- --tg-text-color: #ffffff;
50
- --tg-hint-color: #8c98a3;
51
- --tg-link-color: #52a0e2;
52
- --tg-button-color: #52a0e2;
53
- --tg-button-text-color: #ffffff;
54
- --tg-seperator-color: #2a3742;
55
- --tg-accent-blue: #3390ec;
56
- --tg-sent-bubble: #3390ec;
57
- --tg-received-bubble: #27333e;
58
- --tg-icon-color: #8c98a3;
59
- --success-color: #28a745;
60
- --error-color: #dc3545;
61
  --font-family: 'Inter', sans-serif;
62
  }
63
 
@@ -74,23 +70,22 @@ def index():
74
 
75
  body {
76
  font-family: var(--font-family);
77
- background-color: var(--tg-bg-color);
78
- color: var(--tg-text-color);
79
  overflow: hidden;
80
- height: 100vh;
81
  width: 100vw;
82
  display: flex;
83
  align-items: center;
84
  justify-content: center;
85
  }
86
-
87
- .main-container {
88
  width: 100%;
89
  height: 100%;
90
  display: flex;
91
  flex-direction: column;
92
  transition: opacity 0.3s ease;
93
- background-color: var(--tg-bg-color);
94
  }
95
 
96
  #login-view {
@@ -99,70 +94,76 @@ def index():
99
  align-items: center;
100
  justify-content: center;
101
  text-align: center;
 
 
 
102
  }
103
  #login-view img {
104
  width: 120px;
105
  height: 120px;
106
  margin-bottom: 24px;
107
- filter: drop-shadow(0 0 20px rgba(82, 160, 226, 0.4));
108
  }
109
  #login-view h1 {
110
  font-size: 3rem;
111
  font-weight: 700;
 
 
 
112
  margin-bottom: 12px;
113
  }
114
  #login-view p {
115
  font-size: 1.1rem;
116
- color: var(--tg-hint-color);
117
  margin-bottom: 40px;
118
  }
119
 
120
- #app-view {
121
  display: none;
122
- width: 100%;
123
- height: 100%;
124
- flex-direction: column;
 
 
 
125
  }
126
 
127
- .view {
128
- display: none;
 
 
129
  width: 100%;
130
- height: calc(100% - 65px); /* Height minus bottom nav */
 
 
131
  flex-direction: column;
132
  }
133
- .view.active {
134
  display: flex;
135
  }
136
 
137
- .view-header {
138
- display: flex;
139
- justify-content: space-between;
140
- align-items: center;
141
- padding: 12px 16px;
142
- background-color: var(--tg-header-bg-color);
143
- border-bottom: 1px solid var(--tg-seperator-color);
144
  flex-shrink: 0;
145
- height: 56px;
 
 
 
146
  }
147
- .view-header h2 {
148
- font-size: 1.3rem;
149
- font-weight: 600;
150
  }
151
-
152
- .header-action-btn {
153
- background: none;
154
- border: none;
155
- cursor: pointer;
156
  padding: 8px;
157
  }
158
- .header-action-btn svg {
159
- width: 24px;
160
- height: 24px;
161
- fill: var(--tg-icon-color);
162
- }
163
 
164
- #chats-view {
165
- flex-direction: row;
 
 
166
  }
167
 
168
  #chatroom-list-view {
@@ -170,23 +171,33 @@ def index():
170
  flex-direction: column;
171
  height: 100%;
172
  width: 100%;
173
- background-color: var(--tg-secondary-bg-color);
174
  }
175
-
176
- #chatroom-list {
177
- flex-grow: 1;
178
- overflow-y: auto;
 
 
 
 
 
 
 
 
179
  }
180
- .chatroom-item {
 
 
181
  display: flex;
182
  align-items: center;
183
  gap: 12px;
184
- padding: 10px 16px;
185
  cursor: pointer;
186
- border-bottom: 1px solid var(--tg-seperator-color);
187
  transition: background-color 0.2s ease;
188
  }
189
- .chatroom-item:hover { background-color: var(--tg-header-bg-color); }
190
 
191
  .avatar {
192
  width: 48px;
@@ -195,28 +206,32 @@ def index():
195
  display: flex;
196
  align-items: center;
197
  justify-content: center;
 
198
  font-weight: 600;
199
  color: white;
200
- font-size: 1.4rem;
201
  flex-shrink: 0;
202
  }
203
- .chatroom-info {
204
  flex-grow: 1;
205
  overflow: hidden;
206
  }
207
- .chatroom-name {
208
- font-weight: 600;
209
- font-size: 1.1rem;
 
 
 
 
 
 
210
  white-space: nowrap;
211
  overflow: hidden;
212
  text-overflow: ellipsis;
213
  }
214
  .lock-icon {
215
- width: 16px;
216
- height: 16px;
217
- fill: var(--tg-hint-color);
218
  flex-shrink: 0;
219
- margin-left: 8px;
220
  }
221
 
222
  #chat-window-view {
@@ -224,41 +239,28 @@ def index():
224
  flex-direction: column;
225
  height: 100%;
226
  width: 100%;
227
- background-color: var(--tg-bg-color);
228
- }
229
- #chat-window-view.active {
230
- display: flex;
231
  }
232
 
233
  .chat-header {
234
  display: flex;
235
  align-items: center;
236
  gap: 12px;
237
- padding: 8px 16px;
238
- background-color: var(--tg-header-bg-color);
239
- border-bottom: 1px solid var(--tg-seperator-color);
 
 
240
  flex-shrink: 0;
241
- height: 56px;
 
242
  }
243
  .back-btn {
244
- background: none;
245
- border: none;
246
- cursor: pointer;
247
- }
248
- .back-btn svg {
249
- width: 24px;
250
- height: 24px;
251
- fill: var(--tg-text-color);
252
- }
253
- #chat-header-avatar {
254
- width: 40px;
255
- height: 40px;
256
- font-size: 1.2rem;
257
- }
258
- #chat-header-title {
259
- font-size: 1.2rem;
260
- font-weight: 600;
261
  }
 
 
262
 
263
  #messages-container {
264
  flex-grow: 1;
@@ -272,330 +274,242 @@ def index():
272
  .message {
273
  display: flex;
274
  gap: 10px;
275
- max-width: 80%;
276
- }
277
- .message .avatar {
278
- width: 36px;
279
- height: 36px;
280
- align-self: flex-end;
281
  }
282
  .message-content {
283
  display: flex;
284
  flex-direction: column;
285
- gap: 4px;
286
- }
287
- .message-sender {
288
- font-size: 0.9rem;
289
- font-weight: 600;
290
- color: var(--tg-link-color);
291
- word-break: break-all;
292
- cursor: pointer;
293
  }
294
  .message-bubble {
295
  padding: 10px 14px;
296
- border-radius: 18px;
297
  line-height: 1.4;
298
  word-wrap: break-word;
 
 
 
 
 
 
 
 
 
299
  }
300
 
301
- .message.sent { align-self: flex-end; flex-direction: row-reverse; }
302
  .message.sent .message-bubble {
303
- background-color: var(--tg-sent-bubble);
304
  color: white;
305
- border-bottom-right-radius: 4px;
306
  }
307
 
308
  .message.received { align-self: flex-start; }
309
  .message.received .message-bubble {
310
- background-color: var(--tg-received-bubble);
311
- border-bottom-left-radius: 4px;
 
 
 
312
  }
313
 
314
  .chat-placeholder {
315
- display: flex;
316
- flex-direction: column;
317
- align-items: center;
318
- justify-content: center;
319
- height: 100%;
320
- text-align: center;
321
- color: var(--tg-hint-color);
322
- padding: 20px;
323
  }
324
  .chat-placeholder img { width: 80px; margin-bottom: 20px; opacity: 0.5; }
325
 
326
  .message-form {
327
- display: flex;
328
- padding: 8px 12px;
329
- gap: 12px;
330
- background-color: var(--tg-header-bg-color);
331
- border-top: 1px solid var(--tg-seperator-color);
332
  flex-shrink: 0;
333
- align-items: center;
334
  }
335
  #message-input {
336
- flex-grow: 1;
337
- padding: 12px 16px;
338
- border: none;
339
- background-color: var(--tg-secondary-bg-color);
340
- color: var(--tg-text-color);
341
- border-radius: 22px;
342
- outline: none;
343
- font-size: 1rem;
344
- }
345
- #message-input:focus {
346
- box-shadow: 0 0 0 2px var(--tg-link-color);
347
- }
348
-
349
- .send-btn {
350
- width: 44px;
351
- height: 44px;
352
- border-radius: 50%;
353
- flex-shrink: 0;
354
- padding: 0;
355
- background-color: var(--tg-button-color);
356
- border: none;
357
- cursor: pointer;
358
- display: flex;
359
- align-items: center;
360
- justify-content: center;
361
- }
362
- .send-btn svg { width: 22px; height: 22px; fill: white; }
363
-
364
- .modal-overlay {
365
- position: fixed;
366
- top: 0;
367
- left: 0;
368
- width: 100%;
369
- height: 100%;
370
- background-color: rgba(0,0,0,0.7);
371
- display: none;
372
- align-items: center;
373
- justify-content: center;
374
- z-index: 1000;
375
- -webkit-backdrop-filter: blur(5px);
376
- backdrop-filter: blur(5px);
377
- }
378
- .modal-content {
379
- background-color: var(--tg-secondary-bg-color);
380
- padding: 24px;
381
- border-radius: 12px;
382
- width: 90%;
383
- max-width: 400px;
384
- border: 1px solid var(--tg-seperator-color);
385
- box-shadow: 0 10px 30px rgba(0,0,0,0.5);
386
- }
387
- .modal-content h3 { margin-bottom: 20px; font-weight: 600; font-size: 1.3rem; }
388
- .modal-content label { display: block; margin-bottom: 8px; font-size: 0.9rem; color: var(--tg-hint-color); }
389
- .modal-content input {
390
- width: 100%;
391
- padding: 12px;
392
- margin-bottom: 16px;
393
- background-color: var(--tg-bg-color);
394
- border: 1px solid var(--tg-seperator-color);
395
- color: white;
396
- border-radius: 6px;
397
- font-size: 1rem;
398
- }
399
- .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
400
- .modal-btn {
401
- padding: 10px 20px;
402
- border-radius: 8px;
403
- border: none;
404
- cursor: pointer;
405
- font-weight: 600;
406
- font-size: 1rem;
407
  }
408
- .secondary-btn { background-color: var(--tg-header-bg-color); color: white; }
409
- .action-btn { background-color: var(--tg-button-color); color: var(--tg-button-text-color); }
410
 
411
- #status-bar {
412
- position: fixed;
413
- bottom: 80px;
414
- left: 50%;
415
- transform: translateX(-50%);
416
- background-color: var(--tg-secondary-bg-color);
417
- color: white;
418
- padding: 12px 20px;
419
- border-radius: 8px;
420
- font-size: 0.9rem;
421
- opacity: 0;
422
- visibility: hidden;
423
- transition: opacity 0.3s, visibility 0.3s, bottom 0.3s;
424
- z-index: 2000;
425
- box-shadow: 0 5px 15px rgba(0,0,0,0.3);
426
  }
427
- #status-bar.success { background-color: var(--success-color); }
428
- #status-bar.error { background-color: var(--error-color); }
429
- #status-bar.visible { opacity: 1; visibility: visible; }
430
 
431
- #bottom-nav {
432
- position: fixed;
433
- bottom: 0;
434
- left: 0;
435
- right: 0;
436
- height: 65px;
437
- background-color: var(--tg-header-bg-color);
438
- border-top: 1px solid var(--tg-seperator-color);
439
  display: flex;
440
- justify-content: space-around;
441
- align-items: center;
442
- z-index: 100;
443
  }
444
- .nav-btn {
 
445
  display: flex;
446
  flex-direction: column;
447
  align-items: center;
448
  justify-content: center;
449
- background: none;
450
- border: none;
451
- color: var(--tg-icon-color);
452
  cursor: pointer;
453
- padding: 4px 0;
454
- flex-grow: 1;
455
- height: 100%;
456
  transition: color 0.2s;
457
  }
458
- .nav-btn.active { color: var(--tg-link-color); }
459
- .nav-btn svg {
460
- width: 28px;
461
- height: 28px;
462
- fill: currentColor;
463
- margin-bottom: 2px;
464
  }
465
- .nav-btn span { font-size: 0.75rem; font-weight: 500; }
466
- #scan-qr-nav-btn {
467
- transform: translateY(-20px);
468
- background-color: var(--tg-button-color);
469
- color: white;
470
- width: 60px;
471
- height: 60px;
472
- border-radius: 50%;
473
- box-shadow: 0 4px 15px rgba(82, 160, 226, 0.4);
474
- border: 4px solid var(--tg-secondary-bg-color);
475
  }
476
- #scan-qr-nav-btn svg { width: 32px; height: 32px; margin: 0; }
477
- #scan-qr-nav-btn.active { background-color: #3b8ac9; }
478
 
479
- .content-list {
480
- flex-grow: 1;
481
- overflow-y: auto;
482
  }
483
- .list-item {
484
  display: flex;
485
- align-items: center;
486
- gap: 12px;
487
- padding: 10px 16px;
488
- cursor: pointer;
489
- border-bottom: 1px solid var(--tg-seperator-color);
490
- transition: background-color 0.2s ease;
491
  }
492
- .list-item:hover { background-color: var(--tg-header-bg-color); }
493
- .list-item .info { overflow: hidden; }
494
- .list-item .name { font-weight: 600; font-size: 1.1rem; }
495
- .list-item .details { color: var(--tg-hint-color); font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
496
-
497
- #browser-view .view-header { padding: 8px; }
498
- #browser-controls { display: flex; align-items: center; gap: 8px; width: 100%; }
499
- #browser-url-input {
500
- flex-grow: 1;
501
- padding: 8px 12px;
502
- border: 1px solid var(--tg-seperator-color);
503
- background-color: var(--tg-secondary-bg-color);
504
- color: var(--tg-text-color);
505
- border-radius: 18px;
506
- outline: none;
507
- font-size: 0.9rem;
508
  }
509
- #browser-iframe {
510
- width: 100%;
511
- height: 100%;
512
  border: none;
 
513
  }
514
-
515
- #profile-view {
516
- align-items: center;
517
  padding: 24px;
 
518
  text-align: center;
519
- overflow-y: auto;
520
  }
521
- #profile-view-avatar { width: 100px; height: 100px; font-size: 3rem; margin-bottom: 16px; }
522
- #profile-view-username { font-size: 1.5rem; font-weight: 700; }
523
- #profile-view-address {
524
- color: var(--tg-hint-color);
525
- font-size: 0.9rem;
526
- word-break: break-all;
527
- margin: 8px 0 24px 0;
528
- }
529
- #profile-view-qr { background: white; padding: 10px; margin: 0 auto 24px auto; width: fit-content; border-radius: 8px; }
530
- #profile-view .username-form { display: flex; gap: 8px; max-width: 320px; width: 100%; margin-bottom: 24px; }
531
- #profile-view .username-input {
532
- flex-grow: 1;
533
- background-color: var(--tg-secondary-bg-color);
534
- border: 1px solid var(--tg-seperator-color);
535
- color: var(--tg-text-color);
536
- border-radius: 8px;
537
- padding: 12px;
538
- font-size: 1rem;
539
  }
540
- #profile-view .username-submit { padding: 12px 16px; border-radius: 8px; }
541
 
542
- .full-width-btn {
543
- width: 100%;
544
- max-width: 320px;
545
- padding: 14px;
546
- border-radius: 8px;
547
- font-size: 1rem;
548
- font-weight: 600;
549
- margin-top: 10px;
 
 
550
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
 
552
  @media (min-width: 768px) {
553
- .main-container {
554
- max-width: 1100px;
555
- max-height: 800px;
 
 
 
 
556
  border-radius: 12px;
557
  overflow: hidden;
558
  box-shadow: 0 10px 40px rgba(0,0,0,0.3);
559
- border: 1px solid var(--tg-seperator-color);
560
- flex-direction: row;
561
  }
562
- #app-view {
563
- flex-direction: row;
564
- }
565
- #bottom-nav {
566
- position: relative;
567
  flex-direction: column;
568
- width: 80px;
569
- height: 100%;
570
- justify-content: flex-start;
571
- padding-top: 20px;
572
- gap: 15px;
573
- border-top: none;
574
- border-right: 1px solid var(--tg-seperator-color);
575
  }
576
- #scan-qr-nav-btn {
577
- transform: none;
578
- position: relative;
579
- margin: 15px 0;
580
  }
581
- .nav-btn svg { margin-bottom: 5px; }
582
- .nav-btn span { font-size: 0.8rem; }
583
- .view {
584
- height: 100%;
585
- width: calc(100% - 80px);
586
  }
587
- #status-bar { bottom: 20px; }
588
- #chats-view { display: flex !important; }
 
 
 
 
 
589
  #chatroom-list-view {
590
- width: 340px;
591
  flex-shrink: 0;
592
- border-right: 1px solid var(--tg-seperator-color);
 
593
  }
594
  #chat-window-view {
595
- display: flex !important;
596
  flex-grow: 1;
 
597
  }
598
  .back-btn { display: none !important; }
 
 
599
  }
600
  </style>
601
  </head>
@@ -607,99 +521,111 @@ def index():
607
  <p>Децентрализованный и анонимный мессенджер</p>
608
  <div id="ton-connect-button"></div>
609
  </div>
610
-
611
- <div id="app-view" class="main-container">
612
-
613
- <div id="chats-view" class="view active">
614
- <div id="chatroom-list-view">
615
- <div class="view-header">
616
- <h2>Чаты</h2>
617
- <button id="create-room-show-modal" class="header-action-btn">
618
- <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-5 7h-2v2h-2v-2H9v-2h2V9h2v2h2v2z"/></svg>
619
- </button>
620
- </div>
621
- <div id="chatroom-list" class="content-list"></div>
622
- </div>
623
-
624
- <div id="chat-window-view">
625
- <div id="chat-placeholder" class="chat-placeholder">
626
- <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
627
- <h2>Выберите чат</h2>
628
- <p>Начните общение в одном из существующих чатов или создайте свой собственный.</p>
629
- </div>
630
- <div id="active-chat" style="display: none; width: 100%; height: 100%; flex-direction: column;">
631
- <div class="chat-header">
632
- <button class="back-btn" id="back-to-list-btn">
633
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
634
- </button>
635
- <div id="chat-header-avatar" class="avatar"></div>
636
- <span id="chat-header-title"></span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
637
  </div>
638
- <div id="messages-container"></div>
639
- <form id="message-form" class="message-form">
640
- <input type="text" id="message-input" placeholder="Сообщение..." autocomplete="off">
641
- <button type="submit" class="send-btn" id="send-btn">
642
- <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>
643
- </button>
644
- </form>
645
  </div>
646
  </div>
647
- </div>
648
-
649
- <div id="users-view" class="view">
650
- <div class="view-header">
651
- <h2>Пользователи</h2>
652
  </div>
653
- <div id="user-list" class="content-list"></div>
654
- </div>
655
-
656
- <div id="browser-view" class="view">
657
- <div class="view-header">
658
- <div id="browser-controls">
659
- <button id="browser-back-btn" class="header-action-btn"><svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12l4.58-4.59z"/></svg></button>
660
- <button id="browser-forward-btn" class="header-action-btn"><svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12l-4.58 4.59z"/></svg></button>
661
- <form id="browser-form" style="display: contents;">
662
- <input type="text" id="browser-url-input" placeholder="https://">
 
 
 
 
 
 
 
663
  </form>
664
- <button id="browser-reload-btn" class="header-action-btn"><svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg></button>
665
  </div>
666
  </div>
667
- <iframe id="browser-iframe" sandbox="allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts" src="https://www.google.com/webhp?igu=1"></iframe>
668
- </div>
669
-
670
- <div id="profile-view" class="view">
671
- <div id="profile-view-avatar" class="avatar"></div>
672
- <h3 id="profile-view-username"></h3>
673
- <p id="profile-view-address"></p>
674
- <div id="profile-view-qr"></div>
675
- <form class="username-form" id="username-form">
676
- <input type="text" id="username-input" class="username-input" placeholder="Новый никнейм" autocomplete="off">
677
- <button type="submit" class="action-btn username-submit">✓</button>
678
- </form>
679
- <p style="color: var(--tg-hint-color); font-size: 0.9rem; margin-bottom: 24px;">Баланс кошелька: <strong id="user-balance">недоступен</strong><br/><small>(Проверяйте в вашем кошельке)</small></p>
680
- <button id="disconnect-btn" class="full-width-btn secondary-btn">Отключить кошелек</button>
681
  </div>
682
-
683
- <nav id="bottom-nav">
684
- <button class="nav-btn active" data-view="chats-view">
685
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg>
686
  <span>Чаты</span>
687
- </button>
688
- <button class="nav-btn" data-view="users-view">
689
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>
690
- <span>Контакты</span>
691
- </button>
692
- <button class="nav-btn" id="scan-qr-nav-btn">
693
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><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>
694
- </button>
695
- <button class="nav-btn" data-view="browser-view">
696
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 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>
697
  <span>Браузер</span>
698
- </button>
699
- <button class="nav-btn" data-view="profile-view">
700
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
701
  <span>Профиль</span>
702
- </button>
703
  </nav>
704
  </div>
705
 
@@ -732,14 +658,15 @@ def index():
732
  </form>
733
  </div>
734
  </div>
735
-
736
  <div id="profile-modal" class="modal-overlay">
737
  <div class="modal-content" style="text-align: center;">
738
  <h3 id="profile-modal-title">Профиль пользователя</h3>
739
- <div id="profile-modal-avatar-container" style="margin: 20px auto; display: inline-block;"></div>
740
  <p id="profile-username" style="font-size: 1.2rem; font-weight: 600;"></p>
741
- <p id="profile-address" style="color: var(--tg-hint-color); font-size: 0.9rem; word-break: break-all; margin-top: 8px;"></p>
742
  <div id="profile-qr-code" style="background: white; padding: 10px; margin: 20px auto; width: fit-content; border-radius: 8px;"></div>
 
743
  <div class="modal-actions" style="flex-direction: column; gap: 12px; align-items: stretch;">
744
  <button id="send-ton-btn" class="modal-btn action-btn">Отправить TON</button>
745
  <button id="profile-close-btn" class="modal-btn secondary-btn">Закрыть</button>
@@ -750,7 +677,7 @@ def index():
750
  <div id="scanner-modal" class="modal-overlay">
751
  <div class="modal-content">
752
  <h3>Сканировать QR-код</h3>
753
- <div id="qr-reader" style="width: 100%; border: 1px solid var(--tg-seperator-color); margin-top: 16px; border-radius: 8px; overflow: hidden;"></div>
754
  <div class="modal-actions">
755
  <button id="scanner-close-btn" class="modal-btn secondary-btn">Отмена</button>
756
  </div>
@@ -772,29 +699,27 @@ def index():
772
  let chatroomsData = {};
773
  let html5QrCode = null;
774
  let profileQrCode = null;
775
- let userProfileQrCode = null;
776
- let browserHistory = [];
777
- let browserHistoryIndex = -1;
778
 
779
  const loginView = document.getElementById('login-view');
780
- const appView = document.getElementById('app-view');
781
  const chatroomListView = document.getElementById('chatroom-list-view');
782
  const chatWindowView = document.getElementById('chat-window-view');
 
 
783
  const profileModal = document.getElementById('profile-modal');
784
  const scannerModal = document.getElementById('scanner-modal');
 
 
785
 
786
- const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292', '#a1887f'];
787
 
788
- const getAvatar = (name, size) => {
789
  const initial = (name ? name[0] : '?').toUpperCase();
790
  const charCode = initial.charCodeAt(0);
791
  const color = AVATAR_COLORS[charCode % AVATAR_COLORS.length];
792
  const avatar = document.createElement('div');
793
- avatar.className = 'avatar';
794
- if (size) {
795
- avatar.style.width = size;
796
- avatar.style.height = size;
797
- }
798
  avatar.style.backgroundColor = color;
799
  avatar.textContent = initial;
800
  return avatar;
@@ -806,6 +731,7 @@ def index():
806
  statusBar.className = 'status-bar';
807
  if (type === 'success') statusBar.classList.add('success');
808
  else if (type === 'error') statusBar.classList.add('error');
 
809
  statusBar.classList.add('visible');
810
  setTimeout(() => statusBar.classList.remove('visible'), duration);
811
  };
@@ -826,30 +752,11 @@ def index():
826
  };
827
 
828
  const truncateAddress = (address) => address ? `${address.substring(0, 6)}...${address.substring(address.length - 6)}` : '';
829
-
830
- const updateProfileView = () => {
831
- if (!currentUser.address) return;
832
-
833
- const username = currentUser.username || `User ${truncateAddress(currentUser.address)}`;
834
- const avatarContainer = document.getElementById('profile-view-avatar');
835
- avatarContainer.innerHTML = '';
836
- avatarContainer.appendChild(getAvatar(username));
837
 
838
- document.getElementById('profile-view-username').textContent = username;
839
- document.getElementById('profile-view-address').textContent = currentUser.address;
840
  document.getElementById('username-input').value = currentUser.username || '';
841
-
842
- const qrEl = document.getElementById('profile-view-qr');
843
- qrEl.innerHTML = '';
844
- if (userProfileQrCode) userProfileQrCode.clear();
845
- userProfileQrCode = new QRCode(qrEl, {
846
- text: currentUser.address,
847
- width: 180,
848
- height: 180,
849
- colorDark: "#000000",
850
- colorLight: "#ffffff",
851
- correctLevel: QRCode.CorrectLevel.H
852
- });
853
  };
854
 
855
  document.getElementById('username-form').addEventListener('submit', async (e) => {
@@ -866,7 +773,7 @@ def index():
866
  body: JSON.stringify({ address: currentUser.address, username: newUsername })
867
  });
868
  currentUser.username = newUsername;
869
- updateProfileView();
870
  showStatus('Никнейм успешно обновлен!', 'success');
871
  fetchChatrooms();
872
  if (activeChatroomId) fetchMessages(activeChatroomId);
@@ -885,9 +792,10 @@ def index():
885
  } catch (err) {
886
  currentUser.username = null;
887
  }
 
888
  loginView.style.display = 'none';
889
- appView.style.display = 'flex';
890
- switchTab('chats-view');
891
  fetchChatrooms();
892
  };
893
 
@@ -904,9 +812,9 @@ def index():
904
  item.appendChild(getAvatar(room.name));
905
 
906
  const infoDiv = document.createElement('div');
907
- infoDiv.className = 'chatroom-info';
908
  const nameSpan = document.createElement('div');
909
- nameSpan.className = 'chatroom-name';
910
  nameSpan.textContent = room.name;
911
  infoDiv.appendChild(nameSpan);
912
  item.appendChild(infoDiv);
@@ -921,36 +829,6 @@ def index():
921
  list.appendChild(item);
922
  });
923
  };
924
-
925
- const fetchAndRenderUsers = async () => {
926
- try {
927
- const data = await apiCall('/api/users');
928
- const list = document.getElementById('user-list');
929
- list.innerHTML = '';
930
- data.users.forEach(user => {
931
- const item = document.createElement('div');
932
- item.className = 'list-item';
933
- item.onclick = () => showProfileModal(user.address);
934
-
935
- item.appendChild(getAvatar(user.username || user.address));
936
-
937
- const infoDiv = document.createElement('div');
938
- infoDiv.className = 'info';
939
- const nameSpan = document.createElement('div');
940
- nameSpan.className = 'name';
941
- nameSpan.textContent = user.username || 'Без имени';
942
- const addressSpan = document.createElement('div');
943
- addressSpan.className = 'details';
944
- addressSpan.textContent = truncateAddress(user.address);
945
-
946
- infoDiv.appendChild(nameSpan);
947
- infoDiv.appendChild(addressSpan);
948
- item.appendChild(infoDiv);
949
-
950
- list.appendChild(item);
951
- });
952
- } catch (err) {}
953
- };
954
 
955
  const fetchChatrooms = async () => {
956
  try {
@@ -961,40 +839,44 @@ def index():
961
 
962
  const renderMessages = (messages) => {
963
  const container = document.getElementById('messages-container');
964
- const shouldScroll = container.scrollTop + container.clientHeight >= container.scrollHeight - 30;
965
  container.innerHTML = '';
966
  messages.forEach(msg => {
 
967
  const msgDiv = document.createElement('div');
968
- msgDiv.className = 'message ' + (msg.sender_address === currentUser.address ? 'sent' : 'received');
969
 
970
- const avatar = getAvatar(msg.display_name);
971
- avatar.classList.add('message-avatar');
972
- avatar.style.cursor = 'pointer';
973
- avatar.onclick = () => showProfileModal(msg.sender_address);
974
-
975
  const contentDiv = document.createElement('div');
976
  contentDiv.className = 'message-content';
977
 
978
- const senderDiv = document.createElement('div');
979
- senderDiv.className = 'message-sender';
980
- if (msg.sender_address !== currentUser.address) {
981
- senderDiv.textContent = msg.display_name;
982
- senderDiv.onclick = () => showProfileModal(msg.sender_address);
983
- }
984
-
985
  const bubbleDiv = document.createElement('div');
986
  bubbleDiv.className = 'message-bubble';
987
  bubbleDiv.textContent = msg.text;
988
 
989
- if (msg.sender_address !== currentUser.address) {
 
 
 
 
990
  contentDiv.appendChild(senderDiv);
991
  }
 
992
  contentDiv.appendChild(bubbleDiv);
993
  msgDiv.appendChild(contentDiv);
994
- msgDiv.insertBefore(avatar, contentDiv);
 
 
 
 
 
 
 
995
  container.appendChild(msgDiv);
996
  });
997
- if(shouldScroll) container.scrollTop = container.scrollHeight;
 
 
 
998
  };
999
 
1000
  const fetchMessages = async (roomId) => {
@@ -1006,20 +888,18 @@ def index():
1006
  }
1007
  };
1008
 
1009
- const showChatWindow = () => {
1010
- chatWindowView.classList.add('active');
1011
  if (window.innerWidth < 768) {
1012
  chatroomListView.style.display = 'none';
1013
  }
1014
  };
1015
 
1016
- const hideChatWindow = () => {
1017
- chatWindowView.classList.remove('active');
1018
  if (window.innerWidth < 768) {
1019
- chatroomListView.style.display = 'flex';
1020
  }
1021
- if (messagePollingInterval) clearInterval(messagePollingInterval);
1022
- activeChatroomId = null;
1023
  };
1024
 
1025
  const selectChatroom = (roomId, isPrivate) => {
@@ -1035,9 +915,9 @@ def index():
1035
  headerAvatar.innerHTML = '';
1036
  headerAvatar.appendChild(getAvatar(roomData.name));
1037
 
1038
- document.getElementById('chat-placeholder').style.display = 'none';
1039
- document.getElementById('active-chat').style.display = 'flex';
1040
- showChatWindow();
1041
 
1042
  fetchMessages(roomId);
1043
  messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000);
@@ -1050,6 +930,7 @@ def index():
1050
  passwordModal.style.display = 'flex';
1051
  passwordInput.value = '';
1052
  passwordInput.focus();
 
1053
  const formSubmitHandler = async (e) => {
1054
  e.preventDefault();
1055
  passwordForm.removeEventListener('submit', formSubmitHandler);
@@ -1065,6 +946,7 @@ def index():
1065
  } catch (err) {}
1066
  };
1067
  passwordForm.addEventListener('submit', formSubmitHandler);
 
1068
  document.getElementById('password-cancel').onclick = () => {
1069
  passwordModal.style.display = 'none';
1070
  passwordForm.removeEventListener('submit', formSubmitHandler);
@@ -1080,7 +962,7 @@ def index():
1080
  const sendBtn = document.getElementById('send-btn');
1081
  const text = input.value.trim();
1082
  if (text && activeChatroomId) {
1083
- const originalText = input.value;
1084
  input.value = '';
1085
  input.disabled = true;
1086
  sendBtn.disabled = true;
@@ -1089,14 +971,17 @@ def index():
1089
  method: 'POST',
1090
  headers: { 'Content-Type': 'application/json' },
1091
  body: JSON.stringify({
1092
- chatroom_id: activeChatroomId, sender_address: currentUser.address, text: text
 
 
1093
  })
1094
  });
1095
  await fetchMessages(activeChatroomId);
1096
  document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight;
1097
- } catch (err) {
1098
  input.value = originalText;
1099
- } finally {
 
1100
  input.disabled = false;
1101
  sendBtn.disabled = false;
1102
  input.focus();
@@ -1109,12 +994,15 @@ def index():
1109
  createRoomModal.style.display = 'flex';
1110
  document.getElementById('create-room-form').reset();
1111
  });
1112
- document.getElementById('create-room-cancel').addEventListener('click', () => createRoomModal.style.display = 'none');
 
 
1113
  document.getElementById('create-room-form').addEventListener('submit', async (e) => {
1114
  e.preventDefault();
1115
  const name = document.getElementById('room-name').value.trim();
1116
  const password = document.getElementById('room-password').value;
1117
  if (!name) return;
 
1118
  try {
1119
  await apiCall('/api/create_chatroom', {
1120
  method: 'POST',
@@ -1127,43 +1015,54 @@ def index():
1127
  } catch (err) {}
1128
  });
1129
 
1130
- const showProfileModal = async (address) => {
1131
- if (address === currentUser.address) {
1132
- switchTab('profile-view');
1133
- return;
1134
- }
1135
  try {
1136
  const userData = await apiCall('/api/user_data', {
1137
  method: 'POST',
1138
  headers: { 'Content-Type': 'application/json' },
1139
  body: JSON.stringify({ address: address })
1140
  });
 
1141
  const username = userData.username || `User ${truncateAddress(address)}`;
1142
- document.getElementById('profile-modal-title').textContent = username;
1143
- const avatarContainer = document.getElementById('profile-modal-avatar-container');
1144
- avatarContainer.innerHTML = '';
1145
- avatarContainer.appendChild(getAvatar(username, '80px'));
1146
  document.getElementById('profile-username').textContent = username;
1147
  document.getElementById('profile-address').textContent = address;
 
 
 
 
1148
  const qrCodeEl = document.getElementById('profile-qr-code');
1149
  qrCodeEl.innerHTML = '';
1150
  if (profileQrCode) profileQrCode.clear();
1151
- profileQrCode = new QRCode(qrCodeEl, { text: address, width: 150, height: 150 });
 
 
 
 
1152
 
1153
- document.getElementById('send-ton-btn').onclick = async () => {
 
1154
  if (!tonConnectUI.connected) {
1155
- showStatus('Подключите кошелек для отправки TON.', 'error'); return;
 
1156
  }
1157
  const amountString = prompt("Введите сумму в TON для отправки:", "0.1");
1158
  if (amountString === null) return;
 
1159
  const amount = parseFloat(amountString);
1160
  if (isNaN(amount) || amount <= 0) {
1161
- showStatus('Неверная сумма.', 'error'); return;
 
1162
  }
 
 
 
1163
  const transaction = {
1164
  validUntil: Math.floor(Date.now() / 1000) + 600,
1165
- messages: [{ address: address, amount: Math.floor(amount * 1e9).toString() }]
1166
  };
 
1167
  try {
1168
  await tonConnectUI.sendTransaction(transaction);
1169
  showStatus(`Транзакция отправлена успешно!`, 'success');
@@ -1172,6 +1071,7 @@ def index():
1172
  showStatus('Транзакция отклонена.', 'error');
1173
  }
1174
  };
 
1175
  profileModal.style.display = 'flex';
1176
  } catch (err) {
1177
  showStatus('Не удалось загрузить профиль.', 'error');
@@ -1180,13 +1080,11 @@ def index():
1180
 
1181
  const showScanner = () => {
1182
  scannerModal.style.display = 'flex';
1183
- if (!html5QrCode) {
1184
- html5QrCode = new Html5Qrcode("qr-reader");
1185
- }
1186
  const qrCodeSuccessCallback = (decodedText, decodedResult) => {
1187
  hideScanner();
1188
  if (decodedText && decodedText.length > 40 && (decodedText.startsWith('EQ') || decodedText.startsWith('UQ'))) {
1189
- showProfileModal(decodedText);
1190
  } else {
1191
  showStatus('Отсканирован недействительный QR-код.', 'error');
1192
  }
@@ -1194,127 +1092,128 @@ def index():
1194
  const config = { fps: 10, qrbox: { width: 250, height: 250 } };
1195
  html5QrCode.start({ facingMode: "environment" }, config, qrCodeSuccessCallback)
1196
  .catch(err => {
1197
- showStatus('Не удалось запустить сканер. Проверьте разрешения камеры.', 'error');
1198
  hideScanner();
1199
  });
1200
  };
1201
 
1202
  const hideScanner = () => {
1203
  if (html5QrCode && html5QrCode.isScanning) {
1204
- html5QrCode.stop().catch(err => {});
1205
  }
1206
  scannerModal.style.display = 'none';
1207
  };
1208
 
 
1209
  document.getElementById('profile-close-btn').addEventListener('click', () => profileModal.style.display = 'none');
1210
- document.getElementById('scan-qr-nav-btn').addEventListener('click', showScanner);
1211
  document.getElementById('scanner-close-btn').addEventListener('click', hideScanner);
1212
- document.getElementById('back-to-list-btn').addEventListener('click', hideChatWindow);
1213
- document.getElementById('disconnect-btn').addEventListener('click', () => tonConnectUI.disconnect());
1214
-
1215
- const switchTab = (viewId) => {
1216
- document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
1217
- document.getElementById(viewId).classList.add('active');
1218
 
1219
- document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
1220
- const navBtn = document.querySelector(`.nav-btn[data-view="${viewId}"]`);
1221
- if (navBtn) navBtn.classList.add('active');
1222
 
1223
- if (viewId === 'users-view') fetchAndRenderUsers();
1224
- if (viewId === 'profile-view') updateProfileView();
1225
  };
1226
 
1227
- document.querySelectorAll('.nav-btn[data-view]').forEach(btn => {
1228
- btn.addEventListener('click', () => switchTab(btn.dataset.view));
1229
  });
1230
 
1231
- const browserIframe = document.getElementById('browser-iframe');
1232
- const browserUrlInput = document.getElementById('browser-url-input');
1233
-
1234
- const navigateBrowser = (url, isFromHistory = false) => {
1235
- let fullUrl = url;
1236
- if (!url.startsWith('http://') && !url.startsWith('https://')) {
1237
- fullUrl = 'https://' + url;
1238
- }
1239
- browserUrlInput.value = fullUrl;
1240
- browserIframe.src = fullUrl;
 
1241
 
1242
- if (!isFromHistory) {
1243
- if (browserHistoryIndex < browserHistory.length - 1) {
1244
- browserHistory = browserHistory.slice(0, browserHistoryIndex + 1);
1245
- }
1246
- browserHistory.push(fullUrl);
1247
- browserHistoryIndex = browserHistory.length - 1;
1248
- }
1249
- updateBrowserNavButtons();
1250
- };
1251
 
1252
- const updateBrowserNavButtons = () => {
1253
- document.getElementById('browser-back-btn').disabled = browserHistoryIndex <= 0;
1254
- document.getElementById('browser-forward-btn').disabled = browserHistoryIndex >= browserHistory.length - 1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1255
  };
1256
 
1257
  document.getElementById('browser-form').addEventListener('submit', (e) => {
1258
  e.preventDefault();
1259
- const url = browserUrlInput.value.trim();
1260
- if(url) navigateBrowser(url);
1261
- });
1262
- document.getElementById('browser-reload-btn').addEventListener('click', () => {
1263
- if (browserIframe.src) browserIframe.src = browserIframe.src;
1264
- });
1265
- document.getElementById('browser-back-btn').addEventListener('click', () => {
1266
- if (browserHistoryIndex > 0) {
1267
- browserHistoryIndex--;
1268
- navigateBrowser(browserHistory[browserHistoryIndex], true);
1269
- }
1270
- });
1271
- document.getElementById('browser-forward-btn').addEventListener('click', () => {
1272
- if (browserHistoryIndex < browserHistory.length - 1) {
1273
- browserHistoryIndex++;
1274
- navigateBrowser(browserHistory[browserHistoryIndex], true);
1275
  }
 
1276
  });
1277
 
1278
- const handleResize = () => {
1279
- const isDesktop = window.innerWidth >= 768;
1280
- if (isDesktop) {
1281
- document.getElementById('chatroom-list-view').style.display = 'flex';
1282
- if (!activeChatroomId) {
1283
- document.getElementById('chat-placeholder').style.display = 'flex';
1284
- document.getElementById('active-chat').style.display = 'none';
1285
- }
1286
- const activeView = document.querySelector('.view.active');
1287
- if (activeView.id !== 'chats-view') {
1288
- document.getElementById('chats-view').classList.remove('active');
1289
- }
1290
  } else {
1291
- if (activeChatroomId) {
1292
- document.getElementById('chatroom-list-view').style.display = 'none';
1293
- document.getElementById('chat-window-view').classList.add('active');
1294
  } else {
1295
- document.getElementById('chatroom-list-view').style.display = 'flex';
1296
- document.getElementById('chat-window-view').classList.remove('active');
1297
  }
1298
  }
1299
- };
1300
- window.addEventListener('resize', handleResize);
1301
-
1302
  tonConnectUI.onStatusChange(wallet => {
1303
  if (wallet) {
1304
  const address = TON_CONNECT_UI.toUserFriendlyAddress(wallet.account.address, false);
1305
  initializeUser(address);
1306
  } else {
1307
  currentUser = { address: null, username: null };
1308
- appView.style.display = 'none';
1309
  loginView.style.display = 'flex';
1310
  if (messagePollingInterval) clearInterval(messagePollingInterval);
1311
  activeChatroomId = null;
1312
- browserHistory = [];
1313
- browserHistoryIndex = -1;
1314
  }
1315
  });
1316
-
1317
- navigateBrowser('https://www.google.com/webhp?igu=1');
1318
  });
1319
  </script>
1320
  </body>
@@ -1331,7 +1230,8 @@ def get_users():
1331
  'address': address,
1332
  'username': user_data.get('username')
1333
  })
1334
- return jsonify({'users': sorted(users_list, key=lambda x: x.get('username') or '')})
 
1335
 
1336
  @app.route('/api/user_data', methods=['POST'])
1337
  def get_user_data():
@@ -1457,6 +1357,7 @@ def send_message():
1457
  write_db(db)
1458
  return jsonify({'success': True})
1459
 
 
1460
  if __name__ == '__main__':
1461
  init_db()
1462
  app.run(host='0.0.0.0', port=7860)
 
40
  <script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
41
  <link rel="preconnect" href="https://fonts.googleapis.com">
42
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
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: #1C1C1E;
48
+ --bg-tertiary: #2C2C2E;
49
+ --bg-hover: #3A3A3C;
50
+ --text-primary: #FFFFFF;
51
+ --text-secondary: #8E8E93;
52
+ --accent-blue: #007AFF;
53
+ --accent-blue-light: #3395FF;
54
+ --border-color: #38383A;
55
+ --success-color: #34C759;
56
+ --error-color: #FF3B30;
 
 
 
 
57
  --font-family: 'Inter', sans-serif;
58
  }
59
 
 
70
 
71
  body {
72
  font-family: var(--font-family);
73
+ background-color: var(--bg-primary);
74
+ color: var(--text-primary);
75
  overflow: hidden;
76
+ height: 100dvh;
77
  width: 100vw;
78
  display: flex;
79
  align-items: center;
80
  justify-content: center;
81
  }
82
+
83
+ .app-container, .main-container {
84
  width: 100%;
85
  height: 100%;
86
  display: flex;
87
  flex-direction: column;
88
  transition: opacity 0.3s ease;
 
89
  }
90
 
91
  #login-view {
 
94
  align-items: center;
95
  justify-content: center;
96
  text-align: center;
97
+ width: 100%;
98
+ height: 100%;
99
+ background: radial-gradient(circle, #1a2a3a 0%, var(--bg-primary) 70%);
100
  }
101
  #login-view img {
102
  width: 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: 3rem;
109
  font-weight: 700;
110
+ background: linear-gradient(45deg, var(--accent-blue-light), var(--accent-blue));
111
+ -webkit-background-clip: text;
112
+ -webkit-text-fill-color: transparent;
113
  margin-bottom: 12px;
114
  }
115
  #login-view p {
116
  font-size: 1.1rem;
117
+ color: var(--text-secondary);
118
  margin-bottom: 40px;
119
  }
120
 
121
+ #app-container {
122
  display: none;
123
+ }
124
+
125
+ .main-content {
126
+ flex-grow: 1;
127
+ position: relative;
128
+ overflow: hidden;
129
  }
130
 
131
+ .page {
132
+ position: absolute;
133
+ top: 0;
134
+ left: 0;
135
  width: 100%;
136
+ height: 100%;
137
+ background-color: var(--bg-primary);
138
+ display: none;
139
  flex-direction: column;
140
  }
141
+ .page.active {
142
  display: flex;
143
  }
144
 
145
+ .page-header {
 
 
 
 
 
 
146
  flex-shrink: 0;
147
+ padding: 16px;
148
+ padding-top: 24px;
149
+ background-color: var(--bg-secondary);
150
+ border-bottom: 1px solid var(--border-color);
151
  }
152
+ .page-header h2 {
153
+ font-size: 1.8rem;
154
+ font-weight: 700;
155
  }
156
+
157
+ .content-area {
158
+ flex-grow: 1;
159
+ overflow-y: auto;
 
160
  padding: 8px;
161
  }
 
 
 
 
 
162
 
163
+ #chats-page-content {
164
+ display: flex;
165
+ width: 100%;
166
+ height: 100%;
167
  }
168
 
169
  #chatroom-list-view {
 
171
  flex-direction: column;
172
  height: 100%;
173
  width: 100%;
174
+ background-color: var(--bg-primary);
175
  }
176
+
177
+ .list-header {
178
+ display: flex;
179
+ justify-content: space-between;
180
+ align-items: center;
181
+ padding: 12px 16px;
182
+ border-bottom: 1px solid var(--border-color);
183
+ flex-shrink: 0;
184
+ }
185
+ .list-header h2 {
186
+ font-size: 1.5rem;
187
+ font-weight: 600;
188
  }
189
+
190
+ #chatroom-list { flex-grow: 1; overflow-y: auto; }
191
+ .list-item, .chatroom-item {
192
  display: flex;
193
  align-items: center;
194
  gap: 12px;
195
+ padding: 12px 16px;
196
  cursor: pointer;
197
+ border-bottom: 1px solid var(--border-color);
198
  transition: background-color 0.2s ease;
199
  }
200
+ .list-item:hover, .chatroom-item:hover { background-color: var(--bg-hover); }
201
 
202
  .avatar {
203
  width: 48px;
 
206
  display: flex;
207
  align-items: center;
208
  justify-content: center;
209
+ font-size: 1.2rem;
210
  font-weight: 600;
211
  color: white;
 
212
  flex-shrink: 0;
213
  }
214
+ .item-info {
215
  flex-grow: 1;
216
  overflow: hidden;
217
  }
218
+ .item-name {
219
+ font-weight: 500;
220
+ white-space: nowrap;
221
+ overflow: hidden;
222
+ text-overflow: ellipsis;
223
+ }
224
+ .item-subtext {
225
+ font-size: 0.9rem;
226
+ color: var(--text-secondary);
227
  white-space: nowrap;
228
  overflow: hidden;
229
  text-overflow: ellipsis;
230
  }
231
  .lock-icon {
232
+ width: 16px; height: 16px;
233
+ fill: var(--text-secondary);
 
234
  flex-shrink: 0;
 
235
  }
236
 
237
  #chat-window-view {
 
239
  flex-direction: column;
240
  height: 100%;
241
  width: 100%;
242
+ background-color: var(--bg-primary);
 
 
 
243
  }
244
 
245
  .chat-header {
246
  display: flex;
247
  align-items: center;
248
  gap: 12px;
249
+ padding: 12px 16px;
250
+ background-color: rgba(28, 28, 30, 0.8);
251
+ -webkit-backdrop-filter: blur(10px);
252
+ backdrop-filter: blur(10px);
253
+ border-bottom: 1px solid var(--border-color);
254
  flex-shrink: 0;
255
+ position: relative;
256
+ z-index: 10;
257
  }
258
  .back-btn {
259
+ background: none; border: none; cursor: pointer;
260
+ display: flex; align-items: center; justify-content: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  }
262
+ .back-btn svg { width: 28px; height: 28px; fill: var(--accent-blue); }
263
+ #chat-header-title { font-size: 1.1rem; font-weight: 600; }
264
 
265
  #messages-container {
266
  flex-grow: 1;
 
274
  .message {
275
  display: flex;
276
  gap: 10px;
277
+ max-width: 85%;
278
+ align-items: flex-end;
 
 
 
 
279
  }
280
  .message-content {
281
  display: flex;
282
  flex-direction: column;
 
 
 
 
 
 
 
 
283
  }
284
  .message-bubble {
285
  padding: 10px 14px;
286
+ border-radius: 20px;
287
  line-height: 1.4;
288
  word-wrap: break-word;
289
+ font-size: 1.05rem;
290
+ }
291
+ .message-sender {
292
+ font-size: 0.8rem;
293
+ font-weight: 600;
294
+ color: var(--text-secondary);
295
+ word-break: break-all;
296
+ cursor: pointer;
297
+ padding: 0 12px 4px;
298
  }
299
 
300
+ .message.sent { align-self: flex-end; }
301
  .message.sent .message-bubble {
302
+ background: var(--accent-blue);
303
  color: white;
304
+ border-bottom-right-radius: 5px;
305
  }
306
 
307
  .message.received { align-self: flex-start; }
308
  .message.received .message-bubble {
309
+ background-color: var(--bg-tertiary);
310
+ border-bottom-left-radius: 5px;
311
+ }
312
+ .message.received .message-sender {
313
+ color: var(--accent-blue-light);
314
  }
315
 
316
  .chat-placeholder {
317
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
318
+ height: 100%; text-align: center; color: var(--text-secondary); padding: 20px;
 
 
 
 
 
 
319
  }
320
  .chat-placeholder img { width: 80px; margin-bottom: 20px; opacity: 0.5; }
321
 
322
  .message-form {
323
+ display: flex; padding: 12px; gap: 12px;
324
+ background-color: var(--bg-secondary);
325
+ border-top: 1px solid var(--border-color);
 
 
326
  flex-shrink: 0;
 
327
  }
328
  #message-input {
329
+ flex-grow: 1; padding: 12px 16px;
330
+ border: 1px solid var(--border-color);
331
+ background-color: var(--bg-tertiary);
332
+ color: var(--text-primary);
333
+ border-radius: 22px; outline: none; font-size: 1rem;
334
+ transition: border-color 0.2s;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  }
336
+ #message-input:focus { border-color: var(--accent-blue); }
 
337
 
338
+ .action-btn {
339
+ background: var(--accent-blue);
340
+ color: white; border: none; padding: 10px 16px; border-radius: 8px;
341
+ cursor: pointer; font-weight: 500; font-size: 1rem;
342
+ transition: background-color 0.2s ease, transform 0.2s ease;
343
+ display: flex; align-items: center; justify-content: center; gap: 8px;
 
 
 
 
 
 
 
 
 
344
  }
345
+ .action-btn:hover { background-color: var(--accent-blue-light); }
346
+ .action-btn:active { transform: scale(0.95); }
 
347
 
348
+ .send-btn { width: 44px; height: 44px; border-radius: 50%; flex-shrink: 0; padding: 0; }
349
+ .send-btn svg { width: 22px; height: 22px; fill: white; margin-left: 2px; }
350
+
351
+ .bottom-nav {
 
 
 
 
352
  display: flex;
353
+ flex-shrink: 0;
354
+ background-color: var(--bg-secondary);
355
+ border-top: 1px solid var(--border-color);
356
  }
357
+ .nav-item {
358
+ flex: 1;
359
  display: flex;
360
  flex-direction: column;
361
  align-items: center;
362
  justify-content: center;
363
+ padding: 6px 0;
 
 
364
  cursor: pointer;
365
+ color: var(--text-secondary);
 
 
366
  transition: color 0.2s;
367
  }
368
+ .nav-item.active { color: var(--accent-blue); }
369
+ .nav-item svg { width: 26px; height: 26px; fill: currentColor; }
370
+ .nav-item span { font-size: 0.7rem; margin-top: 2px; }
371
+ .nav-item#nav-scan-qr {
372
+ transform: translateY(-15px);
 
373
  }
374
+ .nav-item#nav-scan-qr .scan-button {
375
+ width: 60px; height: 60px; border-radius: 50%;
376
+ background: linear-gradient(45deg, var(--accent-blue-light), var(--accent-blue));
377
+ display: flex; align-items: center; justify-content: center;
378
+ box-shadow: 0 0 20px rgba(0, 122, 255, 0.5);
379
+ border: 4px solid var(--bg-primary);
 
 
 
 
380
  }
381
+ .nav-item#nav-scan-qr .scan-button svg { width: 32px; height: 32px; fill: white; }
 
382
 
383
+ #browser-view {
384
+ background-color: var(--bg-secondary);
 
385
  }
386
+ .browser-header {
387
  display: flex;
388
+ gap: 8px;
389
+ padding: 12px;
390
+ background-color: var(--bg-tertiary);
391
+ border-bottom: 1px solid var(--border-color);
 
 
392
  }
393
+ #url-input {
394
+ flex-grow: 1; padding: 8px 12px;
395
+ background-color: var(--bg-primary);
396
+ border: 1px solid var(--border-color);
397
+ color: var(--text-primary); border-radius: 6px; font-size: 0.9rem;
 
 
 
 
 
 
 
 
 
 
 
398
  }
399
+ #browser-frame {
400
+ flex-grow: 1;
 
401
  border: none;
402
+ background-color: #fff;
403
  }
404
+
405
+ #profile-page .content-area {
 
406
  padding: 24px;
407
+ display: flex; flex-direction: column; align-items: center;
408
  text-align: center;
 
409
  }
410
+ #profile-page .avatar { width: 100px; height: 100px; font-size: 3rem; margin-bottom: 16px; }
411
+ #profile-page-username { font-size: 1.5rem; font-weight: 600; margin-bottom: 8px; }
412
+ #profile-page-address { font-size: 0.9rem; color: var(--text-secondary); word-break: break-all; margin-bottom: 24px; }
413
+ #profile-page-qr { background: white; padding: 10px; margin-bottom: 24px; border-radius: 8px; }
414
+ .username-form { display: flex; gap: 8px; width: 100%; max-width: 300px; margin-top: 20px;}
415
+ .username-input {
416
+ flex-grow: 1; background-color: var(--bg-tertiary);
417
+ border: 1px solid var(--border-color); color: var(--text-primary);
418
+ border-radius: 6px; padding: 10px 12px; font-size: 1rem;
 
 
 
 
 
 
 
 
 
419
  }
420
+ .username-input:focus { outline: none; border-color: var(--accent-blue); }
421
 
422
+ .modal-overlay {
423
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
424
+ background-color: rgba(0,0,0,0.7); display: none;
425
+ align-items: center; justify-content: center; z-index: 1000;
426
+ -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px);
427
+ }
428
+ .modal-content {
429
+ background-color: var(--bg-secondary); padding: 24px; border-radius: 12px;
430
+ width: 90%; max-width: 400px; border: 1px solid var(--border-color);
431
+ box-shadow: 0 10px 30px rgba(0,0,0,0.5);
432
  }
433
+ .modal-content h3 { margin-bottom: 20px; font-weight: 600; font-size: 1.3rem; }
434
+ .modal-content label { display: block; margin-bottom: 8px; font-size: 0.9rem; color: var(--text-secondary); }
435
+ .modal-content input {
436
+ width: 100%; padding: 12px; margin-bottom: 16px; background-color: var(--bg-tertiary);
437
+ border: 1px solid var(--border-color); color: white; border-radius: 6px; font-size: 1rem;
438
+ }
439
+ .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
440
+ .modal-btn {
441
+ padding: 10px 20px; border-radius: 6px; border: none; cursor: pointer;
442
+ font-weight: 500;
443
+ }
444
+ .secondary-btn { background-color: var(--bg-hover); color: white; }
445
+
446
+ #status-bar {
447
+ position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%);
448
+ background-color: var(--bg-tertiary); color: white; padding: 12px 20px;
449
+ border-radius: 8px; font-size: 0.9rem; opacity: 0; visibility: hidden;
450
+ transition: opacity 0.3s, visibility 0.3s, bottom 0.3s; z-index: 2000;
451
+ box-shadow: 0 5px 15px rgba(0,0,0,0.3);
452
+ }
453
+ #status-bar.success { background-color: var(--success-color); }
454
+ #status-bar.error { background-color: var(--error-color); }
455
+ #status-bar.visible { opacity: 1; visibility: visible; }
456
+
457
+ .side-nav { display: none; }
458
 
459
  @media (min-width: 768px) {
460
+ body {
461
+ background-color: #0F0F11;
462
+ }
463
+ .app-container, .main-container {
464
+ flex-direction: row;
465
+ max-width: 1200px;
466
+ max-height: 90vh;
467
  border-radius: 12px;
468
  overflow: hidden;
469
  box-shadow: 0 10px 40px rgba(0,0,0,0.3);
470
+ border: 1px solid var(--border-color);
 
471
  }
472
+ .bottom-nav { display: none; }
473
+ .side-nav {
474
+ display: flex;
 
 
475
  flex-direction: column;
476
+ width: 70px;
477
+ background-color: var(--bg-secondary);
478
+ border-right: 1px solid var(--border-color);
479
+ padding: 16px 0;
480
+ align-items: center;
481
+ gap: 16px;
 
482
  }
483
+ .side-nav .nav-item {
484
+ width: 100%;
485
+ color: var(--text-secondary);
486
+ padding: 12px 0;
487
  }
488
+ .side-nav .nav-item.active {
489
+ color: var(--accent-blue);
490
+ background-color: var(--bg-hover);
 
 
491
  }
492
+ .side-nav .nav-item#nav-scan-qr { transform: none; }
493
+ .side-nav .nav-item#nav-scan-qr .scan-button {
494
+ width: 48px; height: 48px; border: none;
495
+ }
496
+ .side-nav .nav-item#nav-scan-qr .scan-button svg { width: 24px; height: 24px; }
497
+
498
+ #chats-page-content { flex-direction: row; }
499
  #chatroom-list-view {
500
+ width: 320px;
501
  flex-shrink: 0;
502
+ border-right: 1px solid var(--border-color);
503
+ display: flex !important;
504
  }
505
  #chat-window-view {
506
+ width: auto;
507
  flex-grow: 1;
508
+ display: flex !important;
509
  }
510
  .back-btn { display: none !important; }
511
+
512
+ #status-bar { bottom: 20px; }
513
  }
514
  </style>
515
  </head>
 
521
  <p>Децентрализованный и анонимный мессенджер</p>
522
  <div id="ton-connect-button"></div>
523
  </div>
524
+
525
+ <div id="app-container">
526
+ <nav class="side-nav">
527
+ <a class="nav-item" data-view="chats">
528
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg>
529
+ <span>Чаты</span>
530
+ </a>
531
+ <a class="nav-item" data-view="users">
532
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>
533
+ <span>Люди</span>
534
+ </a>
535
+ <a class="nav-item" id="nav-scan-qr">
536
+ <div class="scan-button"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><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></div>
537
+ </a>
538
+ <a class="nav-item" data-view="browser">
539
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 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>
540
+ <span>Браузер</span>
541
+ </a>
542
+ <a class="nav-item" data-view="profile">
543
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><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>
544
+ <span>Профиль</span>
545
+ </a>
546
+ </nav>
547
+ <div class="main-content">
548
+ <div id="chats-page" class="page active">
549
+ <div id="chats-page-content">
550
+ <div id="chatroom-list-view">
551
+ <div class="list-header">
552
+ <h2>Чаты</h2>
553
+ <button id="create-room-show-modal" class="action-btn">
554
+ <svg xmlns="http://www.w3.org/2000/svg" height="18" viewBox="0 0 24 24" width="18" fill="white"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
555
+ </button>
556
+ </div>
557
+ <div id="chatroom-list"></div>
558
+ </div>
559
+ <div id="chat-window-view">
560
+ <div id="chat-placeholder" class="chat-placeholder">
561
+ <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
562
+ <h2>Выберите чат</h2>
563
+ <p>Начните общение в одном из существующих чатов или создайте свой собственный.</p>
564
+ </div>
565
+ <div id="active-chat" style="display: none; width: 100%; height: 100%; flex-direction: column;">
566
+ <div class="chat-header">
567
+ <button class="back-btn" id="back-to-list-btn">
568
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15.41 7.41 14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>
569
+ </button>
570
+ <div id="chat-header-avatar" class="avatar"></div>
571
+ <span id="chat-header-title"></span>
572
+ </div>
573
+ <div id="messages-container"></div>
574
+ <form id="message-form" class="message-form">
575
+ <input type="text" id="message-input" placeholder="Сообщение..." autocomplete="off">
576
+ <button type="submit" class="action-btn send-btn" id="send-btn">
577
+ <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>
578
+ </button>
579
+ </form>
580
+ </div>
581
  </div>
 
 
 
 
 
 
 
582
  </div>
583
  </div>
584
+ <div id="users-page" class="page">
585
+ <div class="page-header"><h2>Люди</h2></div>
586
+ <div id="users-list-container" class="content-area"></div>
 
 
587
  </div>
588
+ <div id="browser-page" class="page">
589
+ <form class="browser-header" id="browser-form">
590
+ <input type="text" id="url-input" class="username-input" placeholder="Введите URL или поисковый запрос">
591
+ <button type="submit" class="action-btn">Go</button>
592
+ </form>
593
+ <iframe id="browser-frame" src="https://duckduckgo.com/html"></iframe>
594
+ </div>
595
+ <div id="profile-page" class="page">
596
+ <div class="page-header"><h2>Профиль</h2></div>
597
+ <div class="content-area">
598
+ <div id="profile-page-avatar" class="avatar"></div>
599
+ <h3 id="profile-page-username"></h3>
600
+ <p id="profile-page-address"></p>
601
+ <div id="profile-page-qr"></div>
602
+ <form class="username-form" id="username-form">
603
+ <input type="text" id="username-input" class="username-input" placeholder="Ваш никнейм" autocomplete="off">
604
+ <button type="submit" class="action-btn">✓</button>
605
  </form>
 
606
  </div>
607
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
608
  </div>
609
+ <nav class="bottom-nav">
610
+ <a class="nav-item" data-view="chats">
 
611
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg>
612
  <span>Чаты</span>
613
+ </a>
614
+ <a class="nav-item" data-view="users">
615
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>
616
+ <span>Люди</span>
617
+ </a>
618
+ <a class="nav-item" id="nav-scan-qr">
619
+ <div class="scan-button"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><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></div>
620
+ </a>
621
+ <a class="nav-item" data-view="browser">
622
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 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>
623
  <span>Браузер</span>
624
+ </a>
625
+ <a class="nav-item" data-view="profile">
626
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><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>
627
  <span>Профиль</span>
628
+ </a>
629
  </nav>
630
  </div>
631
 
 
658
  </form>
659
  </div>
660
  </div>
661
+
662
  <div id="profile-modal" class="modal-overlay">
663
  <div class="modal-content" style="text-align: center;">
664
  <h3 id="profile-modal-title">Профиль пользователя</h3>
665
+ <div id="profile-avatar-container" style="margin: 20px auto; display: inline-block;"></div>
666
  <p id="profile-username" style="font-size: 1.2rem; font-weight: 600;"></p>
667
+ <p id="profile-address" style="color: var(--text-secondary); font-size: 0.9rem; word-break: break-all; margin-top: 8px;"></p>
668
  <div id="profile-qr-code" style="background: white; padding: 10px; margin: 20px auto; width: fit-content; border-radius: 8px;"></div>
669
+ <p style="text-align: center; color: var(--text-secondary); font-size: 0.8rem; margin-top: -10px; margin-bottom: 20px;">Отсканируйте для открытия профиля</p>
670
  <div class="modal-actions" style="flex-direction: column; gap: 12px; align-items: stretch;">
671
  <button id="send-ton-btn" class="modal-btn action-btn">Отправить TON</button>
672
  <button id="profile-close-btn" class="modal-btn secondary-btn">Закрыть</button>
 
677
  <div id="scanner-modal" class="modal-overlay">
678
  <div class="modal-content">
679
  <h3>Сканировать QR-код</h3>
680
+ <div id="qr-reader" style="width: 100%; border: 1px solid var(--border-color); margin-top: 16px; border-radius: 8px; overflow: hidden;"></div>
681
  <div class="modal-actions">
682
  <button id="scanner-close-btn" class="modal-btn secondary-btn">Отмена</button>
683
  </div>
 
699
  let chatroomsData = {};
700
  let html5QrCode = null;
701
  let profileQrCode = null;
702
+ let myProfileQrCode = null;
 
 
703
 
704
  const loginView = document.getElementById('login-view');
705
+ const appContainer = document.getElementById('app-container');
706
  const chatroomListView = document.getElementById('chatroom-list-view');
707
  const chatWindowView = document.getElementById('chat-window-view');
708
+ const chatPlaceholder = document.getElementById('chat-placeholder');
709
+ const activeChat = document.getElementById('active-chat');
710
  const profileModal = document.getElementById('profile-modal');
711
  const scannerModal = document.getElementById('scanner-modal');
712
+ const navItems = document.querySelectorAll('.nav-item[data-view]');
713
+ const pages = document.querySelectorAll('.page');
714
 
715
+ const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292', '#ff8a65', '#a1887f', '#90a4ae'];
716
 
717
+ const getAvatar = (name, sizeClass = '') => {
718
  const initial = (name ? name[0] : '?').toUpperCase();
719
  const charCode = initial.charCodeAt(0);
720
  const color = AVATAR_COLORS[charCode % AVATAR_COLORS.length];
721
  const avatar = document.createElement('div');
722
+ avatar.className = 'avatar ' + sizeClass;
 
 
 
 
723
  avatar.style.backgroundColor = color;
724
  avatar.textContent = initial;
725
  return avatar;
 
731
  statusBar.className = 'status-bar';
732
  if (type === 'success') statusBar.classList.add('success');
733
  else if (type === 'error') statusBar.classList.add('error');
734
+
735
  statusBar.classList.add('visible');
736
  setTimeout(() => statusBar.classList.remove('visible'), duration);
737
  };
 
752
  };
753
 
754
  const truncateAddress = (address) => address ? `${address.substring(0, 6)}...${address.substring(address.length - 6)}` : '';
 
 
 
 
 
 
 
 
755
 
756
+ const updateUserInfo = () => {
757
+ if (!currentUser.address) return;
758
  document.getElementById('username-input').value = currentUser.username || '';
759
+ renderMyProfilePage();
 
 
 
 
 
 
 
 
 
 
 
760
  };
761
 
762
  document.getElementById('username-form').addEventListener('submit', async (e) => {
 
773
  body: JSON.stringify({ address: currentUser.address, username: newUsername })
774
  });
775
  currentUser.username = newUsername;
776
+ updateUserInfo();
777
  showStatus('Никнейм успешно обновлен!', 'success');
778
  fetchChatrooms();
779
  if (activeChatroomId) fetchMessages(activeChatroomId);
 
792
  } catch (err) {
793
  currentUser.username = null;
794
  }
795
+ updateUserInfo();
796
  loginView.style.display = 'none';
797
+ appContainer.style.display = 'flex';
798
+ switchView('chats');
799
  fetchChatrooms();
800
  };
801
 
 
812
  item.appendChild(getAvatar(room.name));
813
 
814
  const infoDiv = document.createElement('div');
815
+ infoDiv.className = 'item-info';
816
  const nameSpan = document.createElement('div');
817
+ nameSpan.className = 'item-name';
818
  nameSpan.textContent = room.name;
819
  infoDiv.appendChild(nameSpan);
820
  item.appendChild(infoDiv);
 
829
  list.appendChild(item);
830
  });
831
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
832
 
833
  const fetchChatrooms = async () => {
834
  try {
 
839
 
840
  const renderMessages = (messages) => {
841
  const container = document.getElementById('messages-container');
842
+ const shouldScroll = container.scrollTop + container.clientHeight >= container.scrollHeight - 50;
843
  container.innerHTML = '';
844
  messages.forEach(msg => {
845
+ const isSent = msg.sender_address === currentUser.address;
846
  const msgDiv = document.createElement('div');
847
+ msgDiv.className = 'message ' + (isSent ? 'sent' : 'received');
848
 
 
 
 
 
 
849
  const contentDiv = document.createElement('div');
850
  contentDiv.className = 'message-content';
851
 
 
 
 
 
 
 
 
852
  const bubbleDiv = document.createElement('div');
853
  bubbleDiv.className = 'message-bubble';
854
  bubbleDiv.textContent = msg.text;
855
 
856
+ if (!isSent) {
857
+ const senderDiv = document.createElement('div');
858
+ senderDiv.className = 'message-sender';
859
+ senderDiv.textContent = msg.display_name;
860
+ senderDiv.onclick = () => showProfile(msg.sender_address);
861
  contentDiv.appendChild(senderDiv);
862
  }
863
+
864
  contentDiv.appendChild(bubbleDiv);
865
  msgDiv.appendChild(contentDiv);
866
+
867
+ if (!isSent) {
868
+ const avatar = getAvatar(msg.display_name);
869
+ avatar.style.cursor = 'pointer';
870
+ avatar.onclick = () => showProfile(msg.sender_address);
871
+ msgDiv.appendChild(avatar);
872
+ }
873
+
874
  container.appendChild(msgDiv);
875
  });
876
+
877
+ if(shouldScroll) {
878
+ container.scrollTop = container.scrollHeight;
879
+ }
880
  };
881
 
882
  const fetchMessages = async (roomId) => {
 
888
  }
889
  };
890
 
891
+ const showChatView = () => {
892
+ chatWindowView.style.display = 'flex';
893
  if (window.innerWidth < 768) {
894
  chatroomListView.style.display = 'none';
895
  }
896
  };
897
 
898
+ const showListView = () => {
899
+ chatroomListView.style.display = 'flex';
900
  if (window.innerWidth < 768) {
901
+ chatWindowView.style.display = 'none';
902
  }
 
 
903
  };
904
 
905
  const selectChatroom = (roomId, isPrivate) => {
 
915
  headerAvatar.innerHTML = '';
916
  headerAvatar.appendChild(getAvatar(roomData.name));
917
 
918
+ chatPlaceholder.style.display = 'none';
919
+ activeChat.style.display = 'flex';
920
+ showChatView();
921
 
922
  fetchMessages(roomId);
923
  messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000);
 
930
  passwordModal.style.display = 'flex';
931
  passwordInput.value = '';
932
  passwordInput.focus();
933
+
934
  const formSubmitHandler = async (e) => {
935
  e.preventDefault();
936
  passwordForm.removeEventListener('submit', formSubmitHandler);
 
946
  } catch (err) {}
947
  };
948
  passwordForm.addEventListener('submit', formSubmitHandler);
949
+
950
  document.getElementById('password-cancel').onclick = () => {
951
  passwordModal.style.display = 'none';
952
  passwordForm.removeEventListener('submit', formSubmitHandler);
 
962
  const sendBtn = document.getElementById('send-btn');
963
  const text = input.value.trim();
964
  if (text && activeChatroomId) {
965
+ const originalText = text;
966
  input.value = '';
967
  input.disabled = true;
968
  sendBtn.disabled = true;
 
971
  method: 'POST',
972
  headers: { 'Content-Type': 'application/json' },
973
  body: JSON.stringify({
974
+ chatroom_id: activeChatroomId,
975
+ sender_address: currentUser.address,
976
+ text: text
977
  })
978
  });
979
  await fetchMessages(activeChatroomId);
980
  document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight;
981
+ } catch(err) {
982
  input.value = originalText;
983
+ }
984
+ finally {
985
  input.disabled = false;
986
  sendBtn.disabled = false;
987
  input.focus();
 
994
  createRoomModal.style.display = 'flex';
995
  document.getElementById('create-room-form').reset();
996
  });
997
+ document.getElementById('create-room-cancel').addEventListener('click', () => {
998
+ createRoomModal.style.display = 'none';
999
+ });
1000
  document.getElementById('create-room-form').addEventListener('submit', async (e) => {
1001
  e.preventDefault();
1002
  const name = document.getElementById('room-name').value.trim();
1003
  const password = document.getElementById('room-password').value;
1004
  if (!name) return;
1005
+
1006
  try {
1007
  await apiCall('/api/create_chatroom', {
1008
  method: 'POST',
 
1015
  } catch (err) {}
1016
  });
1017
 
1018
+ const showProfile = async (address) => {
 
 
 
 
1019
  try {
1020
  const userData = await apiCall('/api/user_data', {
1021
  method: 'POST',
1022
  headers: { 'Content-Type': 'application/json' },
1023
  body: JSON.stringify({ address: address })
1024
  });
1025
+
1026
  const username = userData.username || `User ${truncateAddress(address)}`;
1027
+ const avatarContainer = document.getElementById('profile-avatar-container');
1028
+
 
 
1029
  document.getElementById('profile-username').textContent = username;
1030
  document.getElementById('profile-address').textContent = address;
1031
+
1032
+ avatarContainer.innerHTML = '';
1033
+ avatarContainer.appendChild(getAvatar(username));
1034
+
1035
  const qrCodeEl = document.getElementById('profile-qr-code');
1036
  qrCodeEl.innerHTML = '';
1037
  if (profileQrCode) profileQrCode.clear();
1038
+ profileQrCode = new QRCode(qrCodeEl, {
1039
+ text: address, width: 150, height: 150,
1040
+ colorDark : "#000000", colorLight : "#ffffff",
1041
+ correctLevel : QRCode.CorrectLevel.H
1042
+ });
1043
 
1044
+ const sendTonBtn = document.getElementById('send-ton-btn');
1045
+ sendTonBtn.onclick = async () => {
1046
  if (!tonConnectUI.connected) {
1047
+ showStatus('Подключите кошелек для отправки TON.', 'error');
1048
+ return;
1049
  }
1050
  const amountString = prompt("Введите сумму в TON для отправки:", "0.1");
1051
  if (amountString === null) return;
1052
+
1053
  const amount = parseFloat(amountString);
1054
  if (isNaN(amount) || amount <= 0) {
1055
+ showStatus('Неверная сумма.', 'error');
1056
+ return;
1057
  }
1058
+
1059
+ const amountInNanoTon = Math.floor(amount * 1_000_000_000).toString();
1060
+
1061
  const transaction = {
1062
  validUntil: Math.floor(Date.now() / 1000) + 600,
1063
+ messages: [ { address: address, amount: amountInNanoTon } ]
1064
  };
1065
+
1066
  try {
1067
  await tonConnectUI.sendTransaction(transaction);
1068
  showStatus(`Транзакция отправлена успешно!`, 'success');
 
1071
  showStatus('Транзакция отклонена.', 'error');
1072
  }
1073
  };
1074
+
1075
  profileModal.style.display = 'flex';
1076
  } catch (err) {
1077
  showStatus('Не удалось загрузить профиль.', 'error');
 
1080
 
1081
  const showScanner = () => {
1082
  scannerModal.style.display = 'flex';
1083
+ html5QrCode = new Html5Qrcode("qr-reader");
 
 
1084
  const qrCodeSuccessCallback = (decodedText, decodedResult) => {
1085
  hideScanner();
1086
  if (decodedText && decodedText.length > 40 && (decodedText.startsWith('EQ') || decodedText.startsWith('UQ'))) {
1087
+ showProfile(decodedText);
1088
  } else {
1089
  showStatus('Отсканирован недействительный QR-код.', 'error');
1090
  }
 
1092
  const config = { fps: 10, qrbox: { width: 250, height: 250 } };
1093
  html5QrCode.start({ facingMode: "environment" }, config, qrCodeSuccessCallback)
1094
  .catch(err => {
1095
+ showStatus('Не удалось запустить сканер.', 'error');
1096
  hideScanner();
1097
  });
1098
  };
1099
 
1100
  const hideScanner = () => {
1101
  if (html5QrCode && html5QrCode.isScanning) {
1102
+ html5QrCode.stop().catch(err => console.error("Failed to stop QR scanner.", err));
1103
  }
1104
  scannerModal.style.display = 'none';
1105
  };
1106
 
1107
+ document.querySelectorAll('#nav-scan-qr').forEach(el => el.addEventListener('click', showScanner));
1108
  document.getElementById('profile-close-btn').addEventListener('click', () => profileModal.style.display = 'none');
 
1109
  document.getElementById('scanner-close-btn').addEventListener('click', hideScanner);
1110
+ document.getElementById('back-to-list-btn').addEventListener('click', showListView);
1111
+
1112
+ const switchView = (viewName) => {
1113
+ pages.forEach(p => p.classList.remove('active'));
1114
+ document.getElementById(`${viewName}-page`).classList.add('active');
 
1115
 
1116
+ navItems.forEach(n => n.classList.remove('active'));
1117
+ document.querySelectorAll(`.nav-item[data-view="${viewName}"]`).forEach(n => n.classList.add('active'));
 
1118
 
1119
+ if(viewName === 'users') fetchAndRenderUsers();
1120
+ if(viewName === 'profile') renderMyProfilePage();
1121
  };
1122
 
1123
+ navItems.forEach(item => {
1124
+ item.addEventListener('click', () => switchView(item.dataset.view));
1125
  });
1126
 
1127
+ const fetchAndRenderUsers = async () => {
1128
+ try {
1129
+ const data = await apiCall('/api/users');
1130
+ const container = document.getElementById('users-list-container');
1131
+ container.innerHTML = '';
1132
+ data.users.forEach(user => {
1133
+ if (user.address === currentUser.address) return;
1134
+
1135
+ const item = document.createElement('div');
1136
+ item.className = 'list-item';
1137
+ item.onclick = () => showProfile(user.address);
1138
 
1139
+ item.appendChild(getAvatar(user.username || user.address));
1140
+ const infoDiv = document.createElement('div');
1141
+ infoDiv.className = 'item-info';
 
 
 
 
 
 
1142
 
1143
+ const nameSpan = document.createElement('div');
1144
+ nameSpan.className = 'item-name';
1145
+ nameSpan.textContent = user.username || 'Без имени';
1146
+ infoDiv.appendChild(nameSpan);
1147
+
1148
+ const subtextSpan = document.createElement('div');
1149
+ subtextSpan.className = 'item-subtext';
1150
+ subtextSpan.textContent = truncateAddress(user.address);
1151
+ infoDiv.appendChild(subtextSpan);
1152
+
1153
+ item.appendChild(infoDiv);
1154
+ container.appendChild(item);
1155
+ });
1156
+ } catch (err) {}
1157
+ };
1158
+
1159
+ const renderMyProfilePage = () => {
1160
+ const name = currentUser.username || 'Без имени';
1161
+ document.getElementById('profile-page-avatar').innerHTML = '';
1162
+ document.getElementById('profile-page-avatar').appendChild(getAvatar(name));
1163
+ document.getElementById('profile-page-username').textContent = name;
1164
+ document.getElementById('profile-page-address').textContent = currentUser.address;
1165
+
1166
+ const qrEl = document.getElementById('profile-page-qr');
1167
+ qrEl.innerHTML = '';
1168
+ if (myProfileQrCode) myProfileQrCode.clear();
1169
+ myProfileQrCode = new QRCode(qrEl, {
1170
+ text: currentUser.address, width: 200, height: 200,
1171
+ colorDark : "#000000", colorLight : "#ffffff",
1172
+ correctLevel : QRCode.CorrectLevel.H
1173
+ });
1174
  };
1175
 
1176
  document.getElementById('browser-form').addEventListener('submit', (e) => {
1177
  e.preventDefault();
1178
+ const input = document.getElementById('url-input').value.trim();
1179
+ const frame = document.getElementById('browser-frame');
1180
+ let url = input;
1181
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
1182
+ if(url.includes('.') && !url.includes(' ')) {
1183
+ url = 'https://' + url;
1184
+ } else {
1185
+ url = `https://duckduckgo.com/html?q=${encodeURIComponent(url)}`;
1186
+ }
 
 
 
 
 
 
 
1187
  }
1188
+ frame.src = url;
1189
  });
1190
 
1191
+ window.addEventListener('resize', () => {
1192
+ if (window.innerWidth >= 768) {
1193
+ chatroomListView.style.display = 'flex';
1194
+ chatWindowView.style.display = 'flex';
 
 
 
 
 
 
 
 
1195
  } else {
1196
+ if (activeChat.style.display === 'flex') {
1197
+ chatroomListView.style.display = 'none';
 
1198
  } else {
1199
+ chatroomListView.style.display = 'flex';
1200
+ chatWindowView.style.display = 'none';
1201
  }
1202
  }
1203
+ });
1204
+
 
1205
  tonConnectUI.onStatusChange(wallet => {
1206
  if (wallet) {
1207
  const address = TON_CONNECT_UI.toUserFriendlyAddress(wallet.account.address, false);
1208
  initializeUser(address);
1209
  } else {
1210
  currentUser = { address: null, username: null };
1211
+ appContainer.style.display = 'none';
1212
  loginView.style.display = 'flex';
1213
  if (messagePollingInterval) clearInterval(messagePollingInterval);
1214
  activeChatroomId = null;
 
 
1215
  }
1216
  });
 
 
1217
  });
1218
  </script>
1219
  </body>
 
1230
  'address': address,
1231
  'username': user_data.get('username')
1232
  })
1233
+ users_list.sort(key=lambda u: (u['username'] is None, u['username']))
1234
+ return jsonify({'users': users_list})
1235
 
1236
  @app.route('/api/user_data', methods=['POST'])
1237
  def get_user_data():
 
1357
  write_db(db)
1358
  return jsonify({'success': True})
1359
 
1360
+
1361
  if __name__ == '__main__':
1362
  init_db()
1363
  app.run(host='0.0.0.0', port=7860)