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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +614 -567
app.py CHANGED
@@ -40,21 +40,24 @@ 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
- --bg-color: #000000;
47
- --secondary-bg-color: #111111;
48
- --header-bg-color: rgba(18, 18, 18, 0.75);
49
- --tertiary-bg-color: #222222;
50
- --text-color: #ffffff;
51
- --text-secondary-color: #8E8E93;
52
- --button-color: #007AFF;
53
- --button-text-color: #ffffff;
54
- --link-color: #007AFF;
55
- --separator-color: #28282A;
56
- --destructive-color: #FF3B30;
57
- --font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
 
 
 
58
  }
59
 
60
  * {
@@ -64,16 +67,19 @@ def index():
64
  -webkit-tap-highlight-color: transparent;
65
  }
66
 
67
- html { font-size: 16px; }
 
 
68
 
69
  body {
70
  font-family: var(--font-family);
71
- background-color: var(--bg-color);
72
- color: var(--text-color);
73
  overflow: hidden;
74
  height: 100vh;
75
  width: 100vw;
76
  display: flex;
 
77
  align-items: center;
78
  justify-content: center;
79
  }
@@ -94,71 +100,133 @@ def index():
94
  text-align: center;
95
  width: 100%;
96
  height: 100%;
 
97
  }
98
  #login-view img {
99
- width: 120px;
100
- height: 120px;
101
  margin-bottom: 24px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  }
103
- #login-view h1 { font-size: 2.5rem; font-weight: 700; margin-bottom: 12px; }
104
- #login-view p { font-size: 1.1rem; color: var(--text-secondary-color); margin-bottom: 40px; }
105
 
106
- #app-view {
107
  display: none;
108
- width: 100%;
109
  height: 100%;
 
 
 
 
 
 
110
  overflow: hidden;
111
  }
112
 
113
- .page {
114
- position: absolute;
115
- top: 0;
116
- left: 0;
117
- width: 100%;
118
- height: 100%;
119
  display: flex;
120
  flex-direction: column;
121
- background-color: var(--bg-color);
122
- opacity: 0;
123
- visibility: hidden;
124
- transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
125
- transform: translateX(10px);
126
- padding-bottom: 70px; /* Space for bottom nav */
127
- }
128
- .page.active {
129
- opacity: 1;
130
- visibility: visible;
131
- transform: translateX(0);
132
- z-index: 10;
133
  }
134
 
135
- .page-header {
 
 
136
  flex-shrink: 0;
137
- padding: 12px 16px;
138
- background-color: var(--header-bg-color);
139
- backdrop-filter: blur(20px);
140
- -webkit-backdrop-filter: blur(20px);
141
- border-bottom: 1px solid var(--separator-color);
142
- z-index: 20;
143
  }
144
- .page-header h2 { font-size: 1.5rem; font-weight: 700; }
145
- .page-header .header-content { display: flex; justify-content: space-between; align-items: center; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
 
147
- .page-content { flex-grow: 1; overflow-y: auto; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
- #chats-page-content .list-item, #users-page-content .list-item {
 
 
 
 
 
150
  display: flex;
151
  align-items: center;
152
  gap: 12px;
153
- padding: 10px 16px;
154
  cursor: pointer;
155
- border-bottom: 1px solid var(--separator-color);
 
156
  }
157
- #chats-page-content .list-item:hover, #users-page-content .list-item:hover { background-color: var(--tertiary-bg-color); }
158
-
159
  .avatar {
160
- width: 48px;
161
- height: 48px;
162
  border-radius: 50%;
163
  display: flex;
164
  align-items: center;
@@ -166,66 +234,56 @@ def index():
166
  font-weight: 600;
167
  color: white;
168
  flex-shrink: 0;
169
- font-size: 1.2rem;
170
  }
171
- .item-info { flex-grow: 1; overflow: hidden; }
172
- .item-name { font-weight: 600; font-size: 1.1rem; }
173
- .item-detail {
174
- font-size: 0.9rem;
175
- color: var(--text-secondary-color);
 
176
  white-space: nowrap;
177
  overflow: hidden;
178
  text-overflow: ellipsis;
179
  }
180
- .lock-icon { width: 16px; height: 16px; fill: var(--text-secondary-color); flex-shrink: 0; }
181
-
182
- .floating-action-button {
183
- position: fixed;
184
- bottom: 85px;
185
- right: 20px;
186
- width: 56px;
187
- height: 56px;
188
- background-color: var(--button-color);
189
- border-radius: 50%;
190
- display: flex;
191
- align-items: center;
192
- justify-content: center;
193
- border: none;
194
- cursor: pointer;
195
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
196
- z-index: 100;
197
  }
198
 
199
  #chat-window-view {
200
- position: fixed;
201
- top: 0;
202
- left: 0;
203
- width: 100%;
204
- height: 100%;
205
- background-color: var(--secondary-bg-color);
206
- display: flex;
207
  flex-direction: column;
208
- transform: translateX(100%);
209
- transition: transform 0.3s ease-in-out;
210
- z-index: 50;
211
  }
212
- #chat-window-view.active { transform: translateX(0); }
213
-
214
  .chat-header {
215
  display: flex;
216
  align-items: center;
217
- gap: 10px;
218
- padding: 10px 8px 10px 8px;
219
- background-color: var(--header-bg-color);
220
- backdrop-filter: blur(20px);
221
- -webkit-backdrop-filter: blur(20px);
222
- border-bottom: 1px solid var(--separator-color);
223
  flex-shrink: 0;
224
  }
225
- .back-btn { background: none; border: none; cursor: pointer; padding: 8px; }
226
- .back-btn svg { width: 28px; height: 28px; fill: var(--link-color); }
227
- #chat-header-title { font-size: 1.1rem; font-weight: 600; }
228
- #chat-header-avatar { width: 40px; height: 40px; }
 
 
 
 
 
 
 
 
 
 
 
229
 
230
  #messages-container {
231
  flex-grow: 1;
@@ -234,261 +292,238 @@ def index():
234
  display: flex;
235
  flex-direction: column;
236
  gap: 12px;
237
- background-image: url('https://ton.org/download/ton_symbol.svg');
238
- background-repeat: no-repeat;
239
- background-position: center;
240
- background-size: 50%;
241
- background-blend-mode: overlay;
242
- background-color: var(--bg-color);
243
  }
244
 
245
- .message { display: flex; gap: 10px; max-width: 85%; }
246
- .message .avatar { width: 36px; height: 36px; align-self: flex-end; }
247
- .message-content { display: flex; flex-direction: column; gap: 4px; }
248
- .message-sender { font-size: 0.85rem; font-weight: 600; color: var(--text-secondary-color); word-break: break-all; cursor: pointer; padding: 0 12px; }
249
- .message-bubble { padding: 8px 14px; border-radius: 20px; line-height: 1.4; word-wrap: break-word; font-size: 1rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  .message.sent { align-self: flex-end; flex-direction: row-reverse; }
251
- .message.sent .message-bubble { background-color: var(--button-color); color: white; border-bottom-right-radius: 4px; }
 
 
 
 
 
 
252
  .message.received { align-self: flex-start; }
253
- .message.received .message-bubble { background-color: var(--tertiary-bg-color); border-bottom-left-radius: 4px; }
254
- .message.received .message-sender { color: var(--link-color); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
  .message-form {
257
  display: flex;
258
- padding: 8px;
259
- gap: 8px;
260
- background-color: var(--secondary-bg-color);
261
- border-top: 1px solid var(--separator-color);
262
  flex-shrink: 0;
263
  }
264
  #message-input {
265
  flex-grow: 1;
266
- padding: 10px 16px;
267
- border: none;
268
- background-color: var(--tertiary-bg-color);
269
- color: var(--text-color);
270
- border-radius: 20px;
271
  outline: none;
272
  font-size: 1rem;
 
273
  }
 
 
274
  .send-btn {
275
- width: 40px;
276
- height: 40px;
277
  border-radius: 50%;
278
  flex-shrink: 0;
279
  padding: 0;
280
- background-color: var(--button-color);
281
- border: none;
282
- cursor: pointer;
283
- display: flex;
284
- align-items: center;
285
- justify-content: center;
286
  }
287
  .send-btn svg { width: 20px; height: 20px; fill: white; }
288
 
289
- #bottom-nav {
290
  position: fixed;
291
- bottom: 0;
292
  left: 0;
293
  width: 100%;
294
- height: 70px;
295
- background-color: var(--header-bg-color);
296
- backdrop-filter: blur(20px);
297
- -webkit-backdrop-filter: blur(20px);
298
- border-top: 1px solid var(--separator-color);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  display: flex;
300
  justify-content: space-around;
301
- align-items: flex-start;
302
- padding-top: 6px;
303
- z-index: 1000;
 
 
304
  }
305
  .nav-item {
306
  display: flex;
307
  flex-direction: column;
308
  align-items: center;
309
- gap: 4px;
310
- color: var(--text-secondary-color);
311
- font-size: 0.7rem;
312
- font-weight: 500;
313
  cursor: pointer;
314
- transition: color 0.2s;
315
- }
316
- .nav-item.active { color: var(--link-color); }
317
- .nav-item svg { width: 28px; height: 28px; fill: currentColor; }
318
- .nav-item#nav-scan {
319
- transform: translateY(-20px);
320
  }
321
- .nav-item#nav-scan .scan-button-wrapper {
 
 
322
  width: 60px;
323
  height: 60px;
324
  border-radius: 50%;
325
- background-color: var(--button-color);
326
  display: flex;
327
  align-items: center;
328
  justify-content: center;
329
- border: 4px solid var(--bg-color);
330
- }
331
- .nav-item#nav-scan svg { fill: white; width: 30px; height: 30px; }
332
- .nav-item#nav-scan span { display: none; }
333
-
334
- #profile-page .profile-header {
335
- padding-top: 60px;
336
- padding-bottom: 20px;
337
- text-align: center;
338
- }
339
- #profile-page .profile-header .avatar {
340
- width: 100px;
341
- height: 100px;
342
- font-size: 3rem;
343
- margin: 0 auto 16px;
344
- }
345
- #profile-page #profile-username {
346
- font-size: 1.5rem;
347
- font-weight: 600;
348
- }
349
- #profile-page #profile-address {
350
- font-size: 0.9rem;
351
- color: var(--text-secondary-color);
352
- margin-top: 4px;
353
- word-break: break-all;
354
- padding: 0 20px;
355
- }
356
- #profile-page .profile-content {
357
- padding: 16px;
358
- display: flex;
359
- flex-direction: column;
360
- align-items: center;
361
- }
362
- #profile-qr-code {
363
- background: white;
364
- padding: 16px;
365
- border-radius: 12px;
366
- margin-bottom: 12px;
367
- }
368
- .profile-actions { width: 100%; display: flex; flex-direction: column; gap: 12px; padding: 0 16px; }
369
- .action-button {
370
- padding: 14px;
371
- width: 100%;
372
- border-radius: 12px;
373
- border: none;
374
- font-size: 1rem;
375
- font-weight: 600;
376
- cursor: pointer;
377
- background-color: var(--tertiary-bg-color);
378
- color: var(--link-color);
379
- }
380
- .action-button.primary {
381
- background-color: var(--button-color);
382
- color: var(--button-text-color);
383
- }
384
- .action-button.destructive {
385
- color: var(--destructive-color);
386
- }
387
-
388
- .modal-overlay {
389
- position: fixed; top: 0; left: 0; width: 100%; height: 100%;
390
- background-color: rgba(0,0,0,0.7);
391
- display: none; align-items: center; justify-content: center;
392
- z-index: 2000;
393
- backdrop-filter: blur(5px);
394
- }
395
- .modal-content {
396
- background-color: var(--tertiary-bg-color);
397
- padding: 20px; border-radius: 14px;
398
- width: 90%; max-width: 350px;
399
- box-shadow: 0 10px 30px rgba(0,0,0,0.5);
400
  }
401
- .modal-content h3 { margin-bottom: 16px; font-weight: 600; font-size: 1.1rem; text-align: center; }
402
- .modal-content input {
403
- width: 100%; padding: 12px; margin-bottom: 16px;
404
- background-color: #3a3a3c; border: none;
405
- color: white; border-radius: 8px; font-size: 1rem;
406
  }
407
- .modal-actions { display: flex; justify-content: space-between; gap: 12px; margin-top: 8px; }
408
- .modal-btn {
409
- flex-grow: 1; padding: 12px; border-radius: 8px;
410
- border: none; cursor: pointer; font-weight: 600;
411
  }
412
- .secondary-btn { background-color: #555; color: white; }
413
- .primary-btn { background-color: var(--button-color); color: white; }
414
-
415
- #status-bar {
416
- position: fixed; top: 10px; left: 50%; transform: translateX(-50%);
417
- background-color: rgba(40, 40, 40, 0.8); backdrop-filter: blur(10px);
418
- color: white; padding: 10px 20px; border-radius: 20px;
419
- font-size: 0.9rem; opacity: 0; visibility: hidden;
420
- transition: opacity 0.3s, visibility 0.3s, transform 0.3s;
421
- z-index: 3000; transform: translate(-50%, -100px);
422
  }
423
- #status-bar.visible { opacity: 1; visibility: visible; transform: translate(-50%, 0); }
424
 
425
  @media (min-width: 768px) {
426
- body { background-color: #1c1c1e; }
427
  .main-container {
428
- max-width: 1200px; max-height: 90vh;
429
- border-radius: 12px; overflow: hidden;
 
 
430
  box-shadow: 0 10px 40px rgba(0,0,0,0.3);
431
- border: 1px solid var(--separator-color);
432
  }
433
- #app-view { flex-direction: row; }
434
- #bottom-nav { display: none; }
435
- .page {
436
- position: static;
437
- opacity: 1; visibility: visible;
438
- transform: none;
439
- width: 350px; flex-shrink: 0;
440
- border-right: 1px solid var(--separator-color);
441
- padding-bottom: 0;
442
  }
443
- .page.active { z-index: 1; }
444
- .page:not(.active) { display: none; }
445
-
446
  #chat-window-view {
447
- position: static;
448
- transform: none;
449
  width: auto;
450
  flex-grow: 1;
451
  display: flex !important;
452
  }
453
- .back-btn { display: none !important; }
454
-
455
- .floating-action-button {
456
- position: absolute; bottom: 20px; right: 20px;
457
- }
458
- #chats-page .floating-action-button { right: calc(100% - 350px + 20px); }
459
-
460
- #profile-page {
461
- position: fixed;
462
- top: 50%; left: 50%;
463
- transform: translate(-50%, -50%);
464
- width: 400px;
465
- height: auto;
466
- max-height: 90vh;
467
- border-radius: 14px;
468
- z-index: 100;
469
- padding-bottom: 0;
470
- }
471
- #profile-page .page-header {
472
- background: none; backdrop-filter: none; border: none;
473
- }
474
- #profile-page .profile-content {
475
- padding-bottom: 24px;
476
- }
477
- #profile-page-overlay {
478
- position: fixed; top: 0; left: 0; width: 100%; height: 100%;
479
- background: rgba(0,0,0,0.5);
480
- z-index: 99;
481
- }
482
- #profile-page-close-btn {
483
- position: absolute; top: 10px; right: 10px;
484
- background: rgba(100,100,100,0.5); border-radius: 50%;
485
- width: 30px; height: 30px; border: none; cursor: pointer;
486
- color: white; font-size: 1.2rem;
487
  }
 
 
488
  }
489
  </style>
490
  </head>
491
  <body>
 
492
  <div id="login-view" class="main-container">
493
  <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
494
  <h1>Virton</h1>
@@ -496,90 +531,86 @@ def index():
496
  <div id="ton-connect-button"></div>
497
  </div>
498
 
499
- <div id="app-view" class="main-container">
500
- <div id="chats-page" class="page active">
501
- <div class="page-header"><h2>Чаты</h2></div>
502
- <div id="chats-page-content" class="page-content"></div>
503
- <button id="create-room-show-modal" class="floating-action-button">
504
- <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="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
505
- </button>
506
- </div>
507
-
508
- <div id="users-page" class="page">
509
- <div class="page-header"><h2>Пользователи</h2></div>
510
- <div id="users-page-content" class="page-content"></div>
511
- </div>
512
-
513
- <div id="profile-page-overlay" style="display: none;"></div>
514
- <div id="profile-page" class="page">
515
- <button id="profile-page-close-btn" style="display: none;">×</button>
516
- <div class="profile-header">
517
- <div id="profile-avatar-container" class="avatar"></div>
518
- <h3 id="profile-username"></h3>
519
- <p id="profile-address"></p>
520
- </div>
521
- <div class="profile-content">
522
- <div id="profile-qr-code"></div>
523
- <p style="font-size: 0.8rem; color: var(--text-secondary-color); margin-top: 8px;">Отсканируйте для открытия профиля</p>
524
- </div>
525
- <div class="profile-actions">
526
- <form id="username-form" style="display: flex; gap: 8px; margin-bottom: 12px;">
527
- <input type="text" id="username-input" placeholder="Новый никнейм" style="flex-grow: 1; padding: 14px; background-color: var(--tertiary-bg-color); color: white; border: none; border-radius: 12px; font-size: 1rem;">
528
- <button type="submit" class="action-button primary" style="width: auto; padding: 0 20px;">✓</button>
529
- </form>
530
- <button id="send-ton-btn" class="action-button primary">Отправить TON</button>
531
- <button id="disconnect-btn" class="action-button destructive">Отключить кошелек</button>
532
  </div>
533
- </div>
534
 
535
- <div id="chat-window-view">
536
- <div class="chat-header">
537
- <button class="back-btn" id="back-to-list-btn">
538
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>
539
- </button>
540
- <div id="chat-header-avatar" class="avatar"></div>
541
- <span id="chat-header-title"></span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
542
  </div>
543
- <div id="messages-container"></div>
544
- <form id="message-form" class="message-form">
545
- <input type="text" id="message-input" placeholder="Сообщение" autocomplete="off">
546
- <button type="submit" class="send-btn" id="send-btn">
547
- <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>
548
- </button>
549
- </form>
550
  </div>
551
 
552
- <nav id="bottom-nav">
553
  <div class="nav-item" id="nav-chats">
554
- <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>
555
  <span>Чаты</span>
556
  </div>
557
  <div class="nav-item" id="nav-users">
558
- <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><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,8 s-1.34-3-3-3S5,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.05c1.16,0.84,1.97,1.97,1.97,3.45V19h6v-2.5C23,14.17,18.33,13,16,13z"/></g></svg>
559
- <span>Люди</span>
560
  </div>
561
- <div class="nav-item" id="nav-scan">
562
- <div class="scan-button-wrapper">
563
- <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>
564
- </div>
565
- <span>Скан</span>
566
  </div>
567
  <div class="nav-item" id="nav-profile">
568
- <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>
569
  <span>Профиль</span>
570
  </div>
571
- </nav>
572
  </div>
573
-
574
  <div id="create-room-modal" class="modal-overlay">
575
  <div class="modal-content">
576
  <h3>Создать новый чат</h3>
577
  <form id="create-room-form">
578
- <input type="text" id="room-name" placeholder="Название чата" required>
579
- <input type="password" id="room-password" placeholder="Пароль (необязательно)">
 
 
580
  <div class="modal-actions">
581
  <button type="button" id="create-room-cancel" class="modal-btn secondary-btn">Отмена</button>
582
- <button type="submit" class="modal-btn primary-btn">Создать</button>
583
  </div>
584
  </form>
585
  </div>
@@ -587,27 +618,43 @@ def index():
587
 
588
  <div id="password-modal" class="modal-overlay">
589
  <div class="modal-content">
590
- <h3 id="password-modal-title">Вход в приватный чат</h3>
591
  <form id="password-form">
592
- <input type="password" id="password-input" placeholder="Пароль" required>
 
593
  <div class="modal-actions">
594
  <button type="button" id="password-cancel" class="modal-btn secondary-btn">Отмена</button>
595
- <button type="submit" class="modal-btn primary-btn">Войти</button>
596
  </div>
597
  </form>
598
  </div>
599
  </div>
600
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
601
  <div id="scanner-modal" class="modal-overlay">
602
  <div class="modal-content">
603
  <h3>Сканировать QR-код</h3>
604
- <div id="qr-reader" style="width: 100%; border: 1px solid var(--separator-color); margin-top: 16px; border-radius: 8px; overflow: hidden;"></div>
605
- <div class="modal-actions" style="justify-content:center;">
606
  <button id="scanner-close-btn" class="modal-btn secondary-btn">Отмена</button>
607
  </div>
608
  </div>
609
  </div>
610
-
611
  <div id="status-bar"></div>
612
 
613
  <script>
@@ -623,14 +670,19 @@ def index():
623
  let chatroomsData = {};
624
  let html5QrCode = null;
625
  let profileQrCode = null;
 
626
 
627
  const loginView = document.getElementById('login-view');
628
- const appView = document.getElementById('app-view');
 
629
  const chatWindowView = document.getElementById('chat-window-view');
630
- const navItems = document.querySelectorAll('.nav-item');
631
- const pages = document.querySelectorAll('.page');
 
 
 
632
 
633
- const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292', '#a1887f', '#7986cb', '#4dd0e1'];
634
 
635
  const getAvatar = (name) => {
636
  const initial = (name ? name[0] : '?').toUpperCase();
@@ -643,9 +695,13 @@ def index():
643
  return avatar;
644
  };
645
 
646
- const showStatus = (message, duration = 3000) => {
647
  const statusBar = document.getElementById('status-bar');
648
  statusBar.textContent = message;
 
 
 
 
649
  statusBar.classList.add('visible');
650
  setTimeout(() => statusBar.classList.remove('visible'), duration);
651
  };
@@ -654,52 +710,24 @@ def index():
654
  try {
655
  const response = await fetch(endpoint, options);
656
  if (!response.ok) {
657
- const errorData = await response.json().catch(() => ({ error: 'Request failed: ' + response.status }));
658
  throw new Error(errorData.error || 'Unknown error');
659
  }
660
  if (response.status === 204) return null;
661
  return await response.json();
662
  } catch (error) {
663
- showStatus(`Ошибка: ${error.message}`);
664
  throw error;
665
  }
666
  };
667
 
668
  const truncateAddress = (address) => address ? `${address.substring(0, 4)}...${address.substring(address.length - 4)}` : '';
669
 
670
- const switchView = (viewId) => {
671
- pages.forEach(page => page.classList.remove('active'));
672
- document.getElementById(viewId).classList.add('active');
673
-
674
- navItems.forEach(item => item.classList.remove('active'));
675
- const activeNavItem = document.getElementById(`nav-${viewId.split('-')[0]}`);
676
- if (activeNavItem) activeNavItem.classList.add('active');
677
- };
678
-
679
- document.getElementById('nav-chats').addEventListener('click', () => switchView('chats-page'));
680
- document.getElementById('nav-users').addEventListener('click', () => {
681
- switchView('users-page');
682
- fetchUsers();
683
- });
684
- document.getElementById('nav-profile').addEventListener('click', () => {
685
- showProfile(currentUser.address);
686
- });
687
- document.getElementById('nav-scan').addEventListener('click', () => showScanner());
688
-
689
  const updateUserInfo = () => {
690
- if (!currentUser.address) return;
691
-
692
- const username = currentUser.username || `User ${truncateAddress(currentUser.address)}`;
693
- const avatarContainer = document.querySelector('#profile-page .profile-header .avatar');
694
- const usernameEl = document.getElementById('profile-username');
695
- const addressEl = document.getElementById('profile-address');
696
  const usernameInput = document.getElementById('username-input');
697
-
698
- avatarContainer.innerHTML = '';
699
- avatarContainer.appendChild(getAvatar(username));
700
-
701
- usernameEl.textContent = username;
702
- addressEl.textContent = currentUser.address;
703
  usernameInput.value = currentUser.username || '';
704
  };
705
 
@@ -707,7 +735,7 @@ def index():
707
  e.preventDefault();
708
  const newUsername = document.getElementById('username-input').value.trim();
709
  if (!newUsername || newUsername.length < 3) {
710
- showStatus('Никнейм должен быть не короче 3 символов.');
711
  return;
712
  }
713
  try {
@@ -718,7 +746,7 @@ def index():
718
  });
719
  currentUser.username = newUsername;
720
  updateUserInfo();
721
- showStatus('Никнейм успешно обновлен!');
722
  fetchChatrooms();
723
  if (activeChatroomId) fetchMessages(activeChatroomId);
724
  } catch (err) {}
@@ -733,77 +761,57 @@ def index():
733
  body: JSON.stringify({ address: currentUser.address })
734
  });
735
  currentUser.username = data.username;
736
- } catch (err) { currentUser.username = null; }
737
-
 
738
  updateUserInfo();
739
  loginView.style.display = 'none';
740
- appView.style.display = 'flex';
741
- switchView('chats-page');
742
  fetchChatrooms();
743
  };
744
 
745
- const renderList = (containerId, items, itemRenderer) => {
746
- const listContainer = document.getElementById(containerId);
747
- listContainer.innerHTML = '';
748
- if (items.length === 0) {
749
- listContainer.innerHTML = `<p style="text-align: center; color: var(--text-secondary-color); padding: 40px;">Список пуст</p>`;
750
- } else {
751
- items.forEach(item => {
752
- listContainer.appendChild(itemRenderer(item));
753
- });
754
- }
755
- };
756
-
757
- const createChatroomItem = (room) => {
758
- chatroomsData[room.id] = room;
759
- const item = document.createElement('div');
760
- item.className = 'list-item';
761
- item.dataset.id = room.id;
762
- item.appendChild(getAvatar(room.name));
763
-
764
- const infoDiv = document.createElement('div');
765
- infoDiv.className = 'item-info';
766
- infoDiv.innerHTML = `<div class="item-name">${room.name}</div><div class="item-detail">${room.is_private ? 'Приватный чат' : 'Публичный чат'}</div>`;
767
- item.appendChild(infoDiv);
 
 
768
 
769
- if (room.is_private) {
770
- item.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>`;
771
- }
772
- item.addEventListener('click', () => selectChatroom(room.id, room.is_private));
773
- return item;
774
  };
775
-
776
  const fetchChatrooms = async () => {
777
  try {
778
  const data = await apiCall('/api/chatrooms');
779
- renderList('chats-page-content', data.chatrooms, createChatroomItem);
780
- } catch (err) {}
781
- };
782
-
783
- const createUserItem = (user) => {
784
- const item = document.createElement('div');
785
- item.className = 'list-item';
786
- item.appendChild(getAvatar(user.username));
787
-
788
- const infoDiv = document.createElement('div');
789
- infoDiv.className = 'item-info';
790
- infoDiv.innerHTML = `<div class="item-name">${user.username || 'Без имени'}</div><div class="item-detail">${truncateAddress(user.address)}</div>`;
791
- item.appendChild(infoDiv);
792
-
793
- item.addEventListener('click', () => showProfile(user.address));
794
- return item;
795
- };
796
-
797
- const fetchUsers = async () => {
798
- try {
799
- const data = await apiCall('/api/users');
800
- renderList('users-page-content', data.users, createUserItem);
801
  } catch (err) {}
802
  };
803
 
804
  const renderMessages = (messages) => {
805
  const container = document.getElementById('messages-container');
806
- const shouldScroll = container.scrollTop + container.clientHeight >= container.scrollHeight - 50;
807
  container.innerHTML = '';
808
  messages.forEach(msg => {
809
  const msgDiv = document.createElement('div');
@@ -811,6 +819,7 @@ def index():
811
 
812
  const avatar = getAvatar(msg.display_name);
813
  avatar.classList.add('message-avatar');
 
814
  avatar.onclick = () => showProfile(msg.sender_address);
815
 
816
  const contentDiv = document.createElement('div');
@@ -827,11 +836,15 @@ def index():
827
 
828
  contentDiv.appendChild(senderDiv);
829
  contentDiv.appendChild(bubbleDiv);
 
830
  msgDiv.appendChild(contentDiv);
831
  msgDiv.insertBefore(avatar, contentDiv);
832
  container.appendChild(msgDiv);
833
  });
834
- if(shouldScroll) container.scrollTop = container.scrollHeight;
 
 
 
835
  };
836
 
837
  const fetchMessages = async (roomId) => {
@@ -843,6 +856,20 @@ def index():
843
  }
844
  };
845
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
846
  const selectChatroom = (roomId, isPrivate) => {
847
  const roomData = chatroomsData[roomId];
848
  if (!roomData) return;
@@ -855,8 +882,11 @@ def index():
855
  const headerAvatar = document.getElementById('chat-header-avatar');
856
  headerAvatar.innerHTML = '';
857
  headerAvatar.appendChild(getAvatar(roomData.name));
 
 
 
 
858
 
859
- chatWindowView.classList.add('active');
860
  fetchMessages(roomId);
861
  messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000);
862
  };
@@ -897,9 +927,12 @@ def index():
897
  document.getElementById('message-form').addEventListener('submit', async (e) => {
898
  e.preventDefault();
899
  const input = document.getElementById('message-input');
 
900
  const text = input.value.trim();
901
  if (text && activeChatroomId) {
 
902
  input.disabled = true;
 
903
  try {
904
  await apiCall('/api/send_message', {
905
  method: 'POST',
@@ -910,22 +943,23 @@ def index():
910
  text: text
911
  })
912
  });
913
- input.value = '';
914
  await fetchMessages(activeChatroomId);
915
  document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight;
916
  } finally {
917
  input.disabled = false;
 
918
  input.focus();
919
  }
920
  }
921
  });
922
 
 
923
  document.getElementById('create-room-show-modal').addEventListener('click', () => {
924
- document.getElementById('create-room-modal').style.display = 'flex';
925
  document.getElementById('create-room-form').reset();
926
  });
927
  document.getElementById('create-room-cancel').addEventListener('click', () => {
928
- document.getElementById('create-room-modal').style.display = 'none';
929
  });
930
  document.getElementById('create-room-form').addEventListener('submit', async (e) => {
931
  e.preventDefault();
@@ -939,24 +973,13 @@ def index():
939
  headers: { 'Content-Type': 'application/json' },
940
  body: JSON.stringify({ name, password: password || null, creator_address: currentUser.address })
941
  });
942
- document.getElementById('create-room-modal').style.display = 'none';
943
- showStatus('Чат успешно создан!');
944
  fetchChatrooms();
945
  } catch (err) {}
946
  });
947
 
948
  const showProfile = async (address) => {
949
- if (!address) return;
950
-
951
- const isMyProfile = address === currentUser.address;
952
- if (isMyProfile) {
953
- updateUserInfo(); // Ensure my profile is up to date
954
- switchView('profile-page');
955
- return;
956
- }
957
-
958
- // For viewing other users, we might use a modal on desktop
959
- // or a dedicated view on mobile. For simplicity, we use one component.
960
  try {
961
  const userData = await apiCall('/api/user_data', {
962
  method: 'POST',
@@ -965,82 +988,69 @@ def index():
965
  });
966
 
967
  const username = userData.username || `User ${truncateAddress(address)}`;
968
- const profilePage = document.getElementById('profile-page');
969
- const profileOverlay = document.getElementById('profile-page-overlay');
970
- const closeBtn = document.getElementById('profile-page-close-btn');
971
-
972
- // Populate the profile page with the other user's data
973
- const avatarContainer = profilePage.querySelector('.profile-header .avatar');
974
  avatarContainer.innerHTML = '';
975
  avatarContainer.appendChild(getAvatar(username));
976
- profilePage.querySelector('#profile-username').textContent = username;
977
- profilePage.querySelector('#profile-address').textContent = address;
978
 
979
- // QR Code
980
- const qrCodeEl = document.getElementById('profile-qr-code');
 
981
  qrCodeEl.innerHTML = '';
982
- if (profileQrCode) profileQrCode.clear();
983
- profileQrCode = new QRCode(qrCodeEl, { text: address, width: 180, height: 180 });
984
-
985
- // Show/hide buttons
986
- document.getElementById('username-form').style.display = 'none';
987
- document.getElementById('disconnect-btn').style.display = 'none';
988
- document.getElementById('send-ton-btn').style.display = 'block';
989
-
990
- document.getElementById('send-ton-btn').onclick = () => handleSendTon(address);
991
-
992
- profilePage.classList.add('active'); // Show it
993
- profileOverlay.style.display = 'block';
994
- closeBtn.style.display = 'block';
995
-
996
- const closeProfile = () => {
997
- profilePage.classList.remove('active');
998
- profileOverlay.style.display = 'none';
999
- closeBtn.style.display = 'none';
1000
- // Restore my profile view state
1001
- document.getElementById('username-form').style.display = 'flex';
1002
- document.getElementById('disconnect-btn').style.display = 'block';
1003
  }
1004
- closeBtn.onclick = closeProfile;
1005
- profileOverlay.onclick = closeProfile;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1006
 
 
 
 
 
 
 
 
 
 
 
 
1007
  } catch (err) {
1008
- showStatus('Не удалось загрузить профиль.');
1009
  }
1010
  };
1011
-
1012
- const handleSendTon = async (recipientAddress) => {
1013
- if (!tonConnectUI.connected) {
1014
- showStatus('Подключите кошелек для отправки TON.');
1015
- return;
1016
- }
1017
- const amountString = prompt("Введите сумму в TON для отправки:", "0.1");
1018
- if (amountString === null) return;
1019
-
1020
- const amount = parseFloat(amountString);
1021
- if (isNaN(amount) || amount <= 0) {
1022
- showStatus('Неверная сумма.');
1023
- return;
1024
- }
1025
-
1026
- const amountInNanoTon = Math.floor(amount * 1_000_000_000).toString();
1027
- const transaction = {
1028
- validUntil: Math.floor(Date.now() / 1000) + 600,
1029
- messages: [ { address: recipientAddress, amount: amountInNanoTon } ]
1030
- };
1031
- try {
1032
- await tonConnectUI.sendTransaction(transaction);
1033
- showStatus(`Транзакция отправлена успешно!`);
1034
- } catch (error) {
1035
- showStatus('Транзакция отклонена.');
1036
- }
1037
- };
1038
-
1039
- document.getElementById('send-ton-btn').onclick = () => handleSendTon(document.getElementById('profile-address').textContent);
1040
- document.getElementById('disconnect-btn').addEventListener('click', () => tonConnectUI.disconnect());
1041
 
1042
  const showScanner = () => {
1043
- const scannerModal = document.getElementById('scanner-modal');
1044
  scannerModal.style.display = 'flex';
1045
  html5QrCode = new Html5Qrcode("qr-reader");
1046
  const qrCodeSuccessCallback = (decodedText, decodedResult) => {
@@ -1048,38 +1058,85 @@ def index():
1048
  if (decodedText && decodedText.length > 40 && (decodedText.startsWith('EQ') || decodedText.startsWith('UQ'))) {
1049
  showProfile(decodedText);
1050
  } else {
1051
- showStatus('Отсканирован недействительный QR-код.');
1052
  }
1053
  };
1054
  const config = { fps: 10, qrbox: { width: 250, height: 250 } };
1055
  html5QrCode.start({ facingMode: "environment" }, config, qrCodeSuccessCallback)
1056
  .catch(err => {
1057
- showStatus('Не удалось запустить сканер.');
1058
  hideScanner();
1059
  });
1060
  };
 
1061
  const hideScanner = () => {
1062
- const scannerModal = document.getElementById('scanner-modal');
1063
  if (html5QrCode && html5QrCode.isScanning) {
1064
- html5QrCode.stop().catch(err => console.error("QR scanner stop failed.", err));
1065
  }
1066
  scannerModal.style.display = 'none';
1067
  };
1068
- document.getElementById('scanner-close-btn').addEventListener('click', hideScanner);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1069
 
1070
- document.getElementById('back-to-list-btn').addEventListener('click', () => {
1071
- chatWindowView.classList.remove('active');
1072
- if (messagePollingInterval) clearInterval(messagePollingInterval);
1073
- activeChatroomId = null;
1074
- });
 
 
 
 
 
1075
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1076
  tonConnectUI.onStatusChange(wallet => {
1077
  if (wallet) {
1078
  const address = TON_CONNECT_UI.toUserFriendlyAddress(wallet.account.address, false);
1079
  initializeUser(address);
1080
  } else {
1081
  currentUser = { address: null, username: null };
1082
- appView.style.display = 'none';
1083
  loginView.style.display = 'flex';
1084
  if (messagePollingInterval) clearInterval(messagePollingInterval);
1085
  activeChatroomId = null;
@@ -1092,16 +1149,6 @@ def index():
1092
  '''
1093
  return Response(html_content, mimetype='text/html')
1094
 
1095
- @app.route('/api/users', methods=['GET'])
1096
- def get_users():
1097
- db = read_db()
1098
- users_list = []
1099
- for address, user_data in db['users'].items():
1100
- users_list.append({
1101
- 'address': address,
1102
- 'username': user_data.get('username')
1103
- })
1104
- return jsonify({'users': sorted(users_list, key=lambda x: x.get('username') or '')})
1105
 
1106
  @app.route('/api/user_data', methods=['POST'])
1107
  def get_user_data():
 
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: #0F0F11;
47
+ --bg-secondary: #18191C;
48
+ --bg-tertiary: #212328;
49
+ --bg-hover: #2A2C33;
50
+ --text-primary: #EAECEF;
51
+ --text-secondary: #8E9297;
52
+ --accent-blue: #0088CC;
53
+ --accent-blue-light: #33AADD;
54
+ --border-color: #2D2F34;
55
+ --success-color: #28a745;
56
+ --error-color: #dc3545;
57
+ --font-family: 'Inter', sans-serif;
58
+ --nav-bg: #18191C;
59
+ --nav-icon-color: #8E9297;
60
+ --nav-icon-active-color: #FFFFFF;
61
  }
62
 
63
  * {
 
67
  -webkit-tap-highlight-color: transparent;
68
  }
69
 
70
+ html {
71
+ font-size: 16px;
72
+ }
73
 
74
  body {
75
  font-family: var(--font-family);
76
+ background-color: var(--bg-primary);
77
+ color: var(--text-primary);
78
  overflow: hidden;
79
  height: 100vh;
80
  width: 100vw;
81
  display: flex;
82
+ flex-direction: column;
83
  align-items: center;
84
  justify-content: center;
85
  }
 
100
  text-align: center;
101
  width: 100%;
102
  height: 100%;
103
+ background: radial-gradient(circle, #1a2a3a 0%, var(--bg-primary) 70%);
104
  }
105
  #login-view img {
106
+ width: 100px;
107
+ height: 100px;
108
  margin-bottom: 24px;
109
+ filter: drop-shadow(0 0 15px rgba(0, 136, 204, 0.5));
110
+ }
111
+ #login-view h1 {
112
+ font-size: 2.8rem;
113
+ font-weight: 700;
114
+ background: linear-gradient(45deg, var(--accent-blue-light), var(--accent-blue));
115
+ -webkit-background-clip: text;
116
+ -webkit-text-fill-color: transparent;
117
+ margin-bottom: 12px;
118
+ }
119
+ #login-view p {
120
+ font-size: 1.1rem;
121
+ color: var(--text-secondary);
122
+ margin-bottom: 40px;
123
  }
 
 
124
 
125
+ #app-wrapper {
126
  display: none;
127
+ flex-direction: column;
128
  height: 100%;
129
+ width: 100%;
130
+ }
131
+
132
+ #app-content {
133
+ display: flex;
134
+ flex-grow: 1;
135
  overflow: hidden;
136
  }
137
 
138
+ #chatroom-list-view {
 
 
 
 
 
139
  display: flex;
140
  flex-direction: column;
141
+ height: 100%;
142
+ width: 100%;
143
+ background-color: var(--bg-secondary);
144
+ overflow: hidden;
 
 
 
 
 
 
 
 
145
  }
146
 
147
+ .list-header {
148
+ padding: 16px;
149
+ border-bottom: 1px solid var(--border-color);
150
  flex-shrink: 0;
 
 
 
 
 
 
151
  }
152
+ .list-header-top {
153
+ display: flex;
154
+ justify-content: space-between;
155
+ align-items: center;
156
+ margin-bottom: 16px;
157
+ }
158
+ .list-header-top h2 {
159
+ font-size: 1.5rem;
160
+ font-weight: 600;
161
+ }
162
+
163
+ .user-profile-section {
164
+ padding: 12px;
165
+ background-color: var(--bg-tertiary);
166
+ border-radius: 8px;
167
+ margin-bottom: 16px;
168
+ }
169
+ #user-wallet, #user-nickname {
170
+ font-size: 0.9rem;
171
+ color: var(--text-secondary);
172
+ margin-bottom: 8px;
173
+ word-break: break-all;
174
+ }
175
+
176
+ .username-form { display: flex; gap: 8px; }
177
+ .username-input {
178
+ flex-grow: 1;
179
+ background-color: var(--bg-primary);
180
+ border: 1px solid var(--border-color);
181
+ color: var(--text-primary);
182
+ border-radius: 6px;
183
+ padding: 8px 12px;
184
+ font-size: 0.9rem;
185
+ }
186
+ .username-input:focus { outline: none; border-color: var(--accent-blue); }
187
 
188
+ .action-btn {
189
+ background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
190
+ color: white;
191
+ border: none;
192
+ padding: 10px 16px;
193
+ border-radius: 6px;
194
+ cursor: pointer;
195
+ font-weight: 500;
196
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
197
+ display: flex;
198
+ align-items: center;
199
+ justify-content: center;
200
+ gap: 8px;
201
+ }
202
+ .action-btn:hover {
203
+ transform: translateY(-2px);
204
+ box-shadow: 0 4px 15px rgba(0, 136, 204, 0.3);
205
+ }
206
+ .action-btn.small {
207
+ padding: 8px 12px;
208
+ font-size: 0.9rem;
209
+ }
210
 
211
+ #chatroom-list {
212
+ flex-grow: 1;
213
+ overflow-y: auto;
214
+ padding-bottom: 60px;
215
+ }
216
+ .chatroom-item {
217
  display: flex;
218
  align-items: center;
219
  gap: 12px;
220
+ padding: 12px 16px;
221
  cursor: pointer;
222
+ border-bottom: 1px solid var(--border-color);
223
+ transition: background-color 0.2s ease;
224
  }
225
+ .chatroom-item:hover { background-color: var(--bg-hover); }
226
+
227
  .avatar {
228
+ width: 40px;
229
+ height: 40px;
230
  border-radius: 50%;
231
  display: flex;
232
  align-items: center;
 
234
  font-weight: 600;
235
  color: white;
236
  flex-shrink: 0;
 
237
  }
238
+ .chatroom-info {
239
+ flex-grow: 1;
240
+ overflow: hidden;
241
+ }
242
+ .chatroom-name {
243
+ font-weight: 500;
244
  white-space: nowrap;
245
  overflow: hidden;
246
  text-overflow: ellipsis;
247
  }
248
+ .lock-icon {
249
+ width: 16px;
250
+ height: 16px;
251
+ fill: var(--text-secondary);
252
+ flex-shrink: 0;
 
 
 
 
 
 
 
 
 
 
 
 
253
  }
254
 
255
  #chat-window-view {
256
+ display: none;
 
 
 
 
 
 
257
  flex-direction: column;
258
+ height: 100%;
259
+ width: 100%;
260
+ background-color: var(--bg-primary);
261
  }
262
+
 
263
  .chat-header {
264
  display: flex;
265
  align-items: center;
266
+ gap: 12px;
267
+ padding: 12px 16px;
268
+ background-color: var(--bg-secondary);
269
+ border-bottom: 1px solid var(--border-color);
 
 
270
  flex-shrink: 0;
271
  }
272
+ .back-btn {
273
+ background: none;
274
+ border: none;
275
+ cursor: pointer;
276
+ display: none;
277
+ }
278
+ .back-btn svg {
279
+ width: 24px;
280
+ height: 24px;
281
+ fill: var(--text-primary);
282
+ }
283
+ #chat-header-title {
284
+ font-size: 1.2rem;
285
+ font-weight: 600;
286
+ }
287
 
288
  #messages-container {
289
  flex-grow: 1;
 
292
  display: flex;
293
  flex-direction: column;
294
  gap: 12px;
 
 
 
 
 
 
295
  }
296
 
297
+ .message {
298
+ display: flex;
299
+ gap: 10px;
300
+ max-width: 80%;
301
+ }
302
+ .message .avatar {
303
+ width: 36px;
304
+ height: 36px;
305
+ align-self: flex-end;
306
+ }
307
+ .message-content {
308
+ display: flex;
309
+ flex-direction: column;
310
+ gap: 4px;
311
+ }
312
+ .message-sender {
313
+ font-size: 0.8rem;
314
+ font-weight: 500;
315
+ color: var(--text-secondary);
316
+ word-break: break-all;
317
+ cursor: pointer;
318
+ }
319
+ .message-bubble {
320
+ padding: 10px 14px;
321
+ border-radius: 18px;
322
+ line-height: 1.4;
323
+ word-wrap: break-word;
324
+ }
325
+
326
  .message.sent { align-self: flex-end; flex-direction: row-reverse; }
327
+ .message.sent .message-sender { text-align: right; color: var(--accent-blue-light); }
328
+ .message.sent .message-bubble {
329
+ background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
330
+ color: white;
331
+ border-bottom-right-radius: 4px;
332
+ }
333
+
334
  .message.received { align-self: flex-start; }
335
+ .message.received .message-bubble {
336
+ background-color: var(--bg-tertiary);
337
+ border-bottom-left-radius: 4px;
338
+ }
339
+
340
+ .chat-placeholder {
341
+ display: flex;
342
+ flex-direction: column;
343
+ align-items: center;
344
+ justify-content: center;
345
+ height: 100%;
346
+ text-align: center;
347
+ color: var(--text-secondary);
348
+ padding: 20px;
349
+ }
350
+ .chat-placeholder img { width: 80px; margin-bottom: 20px; opacity: 0.5; }
351
 
352
  .message-form {
353
  display: flex;
354
+ padding: 16px;
355
+ gap: 12px;
356
+ background-color: var(--bg-secondary);
357
+ border-top: 1px solid var(--border-color);
358
  flex-shrink: 0;
359
  }
360
  #message-input {
361
  flex-grow: 1;
362
+ padding: 12px 16px;
363
+ border: 1px solid var(--border-color);
364
+ background-color: var(--bg-tertiary);
365
+ color: var(--text-primary);
366
+ border-radius: 22px;
367
  outline: none;
368
  font-size: 1rem;
369
+ transition: border-color 0.2s;
370
  }
371
+ #message-input:focus { border-color: var(--accent-blue); }
372
+
373
  .send-btn {
374
+ width: 44px;
375
+ height: 44px;
376
  border-radius: 50%;
377
  flex-shrink: 0;
378
  padding: 0;
 
 
 
 
 
 
379
  }
380
  .send-btn svg { width: 20px; height: 20px; fill: white; }
381
 
382
+ .modal-overlay {
383
  position: fixed;
384
+ top: 0;
385
  left: 0;
386
  width: 100%;
387
+ height: 100%;
388
+ background-color: rgba(0,0,0,0.7);
389
+ display: none;
390
+ align-items: center;
391
+ justify-content: center;
392
+ z-index: 1000;
393
+ -webkit-backdrop-filter: blur(5px);
394
+ backdrop-filter: blur(5px);
395
+ }
396
+ .modal-content {
397
+ background-color: var(--bg-secondary);
398
+ padding: 24px;
399
+ border-radius: 12px;
400
+ width: 90%;
401
+ max-width: 400px;
402
+ border: 1px solid var(--border-color);
403
+ box-shadow: 0 10px 30px rgba(0,0,0,0.5);
404
+ }
405
+ .modal-content h3 { margin-bottom: 20px; font-weight: 600; font-size: 1.3rem; }
406
+ .modal-content label { display: block; margin-bottom: 8px; font-size: 0.9rem; color: var(--text-secondary); }
407
+ .modal-content input {
408
+ width: 100%;
409
+ padding: 12px;
410
+ margin-bottom: 16px;
411
+ background-color: var(--bg-tertiary);
412
+ border: 1px solid var(--border-color);
413
+ color: white;
414
+ border-radius: 6px;
415
+ font-size: 1rem;
416
+ }
417
+ .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
418
+ .modal-btn {
419
+ padding: 10px 20px;
420
+ border-radius: 6px;
421
+ border: none;
422
+ cursor: pointer;
423
+ font-weight: 500;
424
+ }
425
+ .secondary-btn { background-color: var(--bg-hover); color: white; }
426
+
427
+ #status-bar {
428
+ position: fixed;
429
+ bottom: 20px;
430
+ left: 50%;
431
+ transform: translateX(-50%);
432
+ background-color: var(--bg-tertiary);
433
+ color: white;
434
+ padding: 12px 20px;
435
+ border-radius: 8px;
436
+ font-size: 0.9rem;
437
+ opacity: 0;
438
+ visibility: hidden;
439
+ transition: opacity 0.3s, visibility 0.3s;
440
+ z-index: 2000;
441
+ box-shadow: 0 5px 15px rgba(0,0,0,0.3);
442
+ }
443
+ #status-bar.success { background-color: var(--success-color); }
444
+ #status-bar.error { background-color: var(--error-color); }
445
+ #status-bar.visible { opacity: 1; visibility: visible; }
446
+
447
+ .bottom-nav {
448
  display: flex;
449
  justify-content: space-around;
450
+ align-items: center;
451
+ background-color: var(--nav-bg);
452
+ padding: 12px 0;
453
+ border-top: 1px solid var(--border-color);
454
+ flex-shrink: 0;
455
  }
456
  .nav-item {
457
  display: flex;
458
  flex-direction: column;
459
  align-items: center;
 
 
 
 
460
  cursor: pointer;
461
+ color: var(--nav-icon-color);
462
+ transition: color 0.2s ease;
463
+ font-size: 0.75rem;
464
+ width: 25%;
465
+ text-align: center;
 
466
  }
467
+ .nav-item.center {
468
+ transform: translateY(-10px);
469
+ background-color: var(--accent-blue);
470
  width: 60px;
471
  height: 60px;
472
  border-radius: 50%;
 
473
  display: flex;
474
  align-items: center;
475
  justify-content: center;
476
+ color: white;
477
+ box-shadow: 0 5px 20px rgba(0, 136, 204, 0.4);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
  }
479
+ .nav-item svg {
480
+ width: 24px;
481
+ height: 24px;
482
+ margin-bottom: 4px;
483
+ fill: currentColor;
484
  }
485
+ .nav-item.center svg {
486
+ width: 30px;
487
+ height: 30px;
488
+ margin-bottom: 0;
489
  }
490
+ .nav-item.active {
491
+ color: var(--nav-icon-active-color);
 
 
 
 
 
 
 
 
492
  }
 
493
 
494
  @media (min-width: 768px) {
 
495
  .main-container {
496
+ max-width: 1100px;
497
+ max-height: 800px;
498
+ border-radius: 12px;
499
+ overflow: hidden;
500
  box-shadow: 0 10px 40px rgba(0,0,0,0.3);
501
+ border: 1px solid var(--border-color);
502
  }
503
+ #app-wrapper {
504
+ flex-direction: row;
505
+ }
506
+ #chatroom-list-view {
507
+ width: 320px;
508
+ flex-shrink: 0;
509
+ border-right: 1px solid var(--border-color);
510
+ display: flex !important;
 
511
  }
 
 
 
512
  #chat-window-view {
 
 
513
  width: auto;
514
  flex-grow: 1;
515
  display: flex !important;
516
  }
517
+ #chat-window-view.hidden-on-desktop {
518
+ display: flex !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
519
  }
520
+ .back-btn { display: none !important; }
521
+ .bottom-nav { display: none; }
522
  }
523
  </style>
524
  </head>
525
  <body>
526
+
527
  <div id="login-view" class="main-container">
528
  <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
529
  <h1>Virton</h1>
 
531
  <div id="ton-connect-button"></div>
532
  </div>
533
 
534
+ <div id="app-wrapper" class="main-container">
535
+ <div id="app-content">
536
+ <div id="chatroom-list-view">
537
+ <div class="list-header">
538
+ <div class="list-header-top">
539
+ <h2>Чаты</h2>
540
+ <div style="display: flex; align-items: center; gap: 8px;">
541
+ <button id="create-room-show-modal" class="action-btn small">
542
+ <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>
543
+ <span>Новый</span>
544
+ </button>
545
+ </div>
546
+ </div>
547
+ <div class="user-profile-section">
548
+ <div id="user-wallet"></div>
549
+ <div id="user-nickname"></div>
550
+ <form class="username-form" id="username-form">
551
+ <input type="text" id="username-input" class="username-input" placeholder="Установить никнейм" autocomplete="off">
552
+ <button type="submit" class="action-btn small">✓</button>
553
+ </form>
554
+ </div>
555
+ </div>
556
+ <div id="chatroom-list"></div>
 
 
 
 
 
 
 
 
 
 
557
  </div>
 
558
 
559
+ <div id="chat-window-view" class="hidden-on-desktop">
560
+ <div id="chat-placeholder" class="chat-placeholder">
561
+ <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
562
+ <h2>Выберите чат</h2>
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="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></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" width="24px" height="24px"><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
 
584
+ <div class="bottom-nav">
585
  <div class="nav-item" id="nav-chats">
586
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 13l-2-2V4H4v11h14z"/><path fill="none" d="M0 0h24v24H0V0z"/><path d="M16 8a2 2 0 100-4 2 2 0 000 4zM8 12a2 2 0 100-4 2 2 0 000 4zM16 12a2 2 0 100-4 2 2 0 000 4z"/></svg>
587
  <span>Чаты</span>
588
  </div>
589
  <div class="nav-item" id="nav-users">
590
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
591
+ <span>Пользователи</span>
592
  </div>
593
+ <div class="nav-item center" id="nav-scan">
594
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 4h-4v2h2v14h-14v-2h-2v4h20V4z"/><path d="M8 13a5 5 0 110-10 5 5 0 010 10zm0-6a3 3 0 100 6 3 3 0 000-6z"/></svg>
 
 
 
595
  </div>
596
  <div class="nav-item" id="nav-profile">
597
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
598
  <span>Профиль</span>
599
  </div>
600
+ </div>
601
  </div>
602
+
603
  <div id="create-room-modal" class="modal-overlay">
604
  <div class="modal-content">
605
  <h3>Создать новый чат</h3>
606
  <form id="create-room-form">
607
+ <label for="room-name">Название чата</label>
608
+ <input type="text" id="room-name" required>
609
+ <label for="room-password">Пароль (оставьте пустым для открытого)</label>
610
+ <input type="password" id="room-password">
611
  <div class="modal-actions">
612
  <button type="button" id="create-room-cancel" class="modal-btn secondary-btn">Отмена</button>
613
+ <button type="submit" class="modal-btn action-btn">Создать</button>
614
  </div>
615
  </form>
616
  </div>
 
618
 
619
  <div id="password-modal" class="modal-overlay">
620
  <div class="modal-content">
621
+ <h3>Вход в приватный чат</h3>
622
  <form id="password-form">
623
+ <label for="password-input">Введите пароль</label>
624
+ <input type="password" id="password-input" required>
625
  <div class="modal-actions">
626
  <button type="button" id="password-cancel" class="modal-btn secondary-btn">Отмена</button>
627
+ <button type="submit" class="modal-btn action-btn">Войти</button>
628
  </div>
629
  </form>
630
  </div>
631
  </div>
632
 
633
+ <div id="profile-modal" class="modal-overlay">
634
+ <div class="modal-content" style="text-align: center;">
635
+ <h3 id="profile-modal-title">Профиль пользователя</h3>
636
+ <div id="profile-avatar-container" style="margin: 20px auto; display: inline-block;"></div>
637
+ <p id="profile-username" style="font-size: 1.2rem; font-weight: 600;"></p>
638
+ <p id="profile-address" style="color: var(--text-secondary); font-size: 0.9rem; word-break: break-all; margin-top: 8px;"></p>
639
+ <div id="profile-qr-code" style="background: white; padding: 10px; margin: 20px auto; width: fit-content; border-radius: 8px;"></div>
640
+ <p style="text-align: center; color: var(--text-secondary); font-size: 0.8rem; margin-top: -10px; margin-bottom: 20px;">Отсканируйте для открытия профиля</p>
641
+ <div class="modal-actions" style="flex-direction: column; gap: 12px; align-items: stretch;">
642
+ <button id="send-ton-btn" class="modal-btn action-btn">Отправить TON</button>
643
+ <button id="profile-close-btn" class="modal-btn secondary-btn">Закрыть</button>
644
+ </div>
645
+ </div>
646
+ </div>
647
+
648
  <div id="scanner-modal" class="modal-overlay">
649
  <div class="modal-content">
650
  <h3>Сканировать QR-код</h3>
651
+ <div id="qr-reader" style="width: 100%; border: 1px solid var(--border-color); margin-top: 16px; border-radius: 8px; overflow: hidden;"></div>
652
+ <div class="modal-actions">
653
  <button id="scanner-close-btn" class="modal-btn secondary-btn">Отмена</button>
654
  </div>
655
  </div>
656
  </div>
657
+
658
  <div id="status-bar"></div>
659
 
660
  <script>
 
670
  let chatroomsData = {};
671
  let html5QrCode = null;
672
  let profileQrCode = null;
673
+ let currentNavView = 'chats';
674
 
675
  const loginView = document.getElementById('login-view');
676
+ const appWrapper = document.getElementById('app-wrapper');
677
+ const chatroomListView = document.getElementById('chatroom-list-view');
678
  const chatWindowView = document.getElementById('chat-window-view');
679
+ const chatPlaceholder = document.getElementById('chat-placeholder');
680
+ const activeChat = document.getElementById('active-chat');
681
+ const profileModal = document.getElementById('profile-modal');
682
+ const scannerModal = document.getElementById('scanner-modal');
683
+ const bottomNav = document.querySelector('.bottom-nav');
684
 
685
+ const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292'];
686
 
687
  const getAvatar = (name) => {
688
  const initial = (name ? name[0] : '?').toUpperCase();
 
695
  return avatar;
696
  };
697
 
698
+ const showStatus = (message, type = 'info', duration = 3000) => {
699
  const statusBar = document.getElementById('status-bar');
700
  statusBar.textContent = message;
701
+ statusBar.className = 'status-bar';
702
+ if (type === 'success') statusBar.classList.add('success');
703
+ else if (type === 'error') statusBar.classList.add('error');
704
+
705
  statusBar.classList.add('visible');
706
  setTimeout(() => statusBar.classList.remove('visible'), duration);
707
  };
 
710
  try {
711
  const response = await fetch(endpoint, options);
712
  if (!response.ok) {
713
+ const errorData = await response.json().catch(() => ({ error: 'Request failed with status ' + response.status }));
714
  throw new Error(errorData.error || 'Unknown error');
715
  }
716
  if (response.status === 204) return null;
717
  return await response.json();
718
  } catch (error) {
719
+ showStatus(`Ошибка: ${error.message}`, 'error');
720
  throw error;
721
  }
722
  };
723
 
724
  const truncateAddress = (address) => address ? `${address.substring(0, 4)}...${address.substring(address.length - 4)}` : '';
725
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
726
  const updateUserInfo = () => {
727
+ document.getElementById('user-wallet').textContent = `Кошелек: ${truncateAddress(currentUser.address)}`;
728
+ const nicknameEl = document.getElementById('user-nickname');
 
 
 
 
729
  const usernameInput = document.getElementById('username-input');
730
+ nicknameEl.textContent = currentUser.username ? `Ник: ${currentUser.username}` : `Никнейм не установлен`;
 
 
 
 
 
731
  usernameInput.value = currentUser.username || '';
732
  };
733
 
 
735
  e.preventDefault();
736
  const newUsername = document.getElementById('username-input').value.trim();
737
  if (!newUsername || newUsername.length < 3) {
738
+ showStatus('Никнейм должен быть не короче 3 символов.', 'error');
739
  return;
740
  }
741
  try {
 
746
  });
747
  currentUser.username = newUsername;
748
  updateUserInfo();
749
+ showStatus('Никнейм успешно обновлен!', 'success');
750
  fetchChatrooms();
751
  if (activeChatroomId) fetchMessages(activeChatroomId);
752
  } catch (err) {}
 
761
  body: JSON.stringify({ address: currentUser.address })
762
  });
763
  currentUser.username = data.username;
764
+ } catch (err) {
765
+ currentUser.username = null;
766
+ }
767
  updateUserInfo();
768
  loginView.style.display = 'none';
769
+ appWrapper.style.display = 'flex';
770
+ showView('chats');
771
  fetchChatrooms();
772
  };
773
 
774
+ const renderChatrooms = (rooms) => {
775
+ const list = document.getElementById('chatroom-list');
776
+ list.innerHTML = '';
777
+ chatroomsData = {};
778
+ rooms.forEach(room => {
779
+ chatroomsData[room.id] = room;
780
+ const item = document.createElement('div');
781
+ item.className = 'chatroom-item';
782
+ item.dataset.id = room.id;
783
+
784
+ item.appendChild(getAvatar(room.name));
785
+
786
+ const infoDiv = document.createElement('div');
787
+ infoDiv.className = 'chatroom-info';
788
+ const nameSpan = document.createElement('div');
789
+ nameSpan.className = 'chatroom-name';
790
+ nameSpan.textContent = room.name;
791
+ infoDiv.appendChild(nameSpan);
792
+ item.appendChild(infoDiv);
793
+
794
+ if (room.is_private) {
795
+ const lockIcon = document.createElement('div');
796
+ lockIcon.innerHTML = `<svg class="lock-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zM9 8V6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9z"/></svg>`;
797
+ item.appendChild(lockIcon);
798
+ }
799
 
800
+ item.addEventListener('click', () => selectChatroom(room.id, room.is_private));
801
+ list.appendChild(item);
802
+ });
 
 
803
  };
804
+
805
  const fetchChatrooms = async () => {
806
  try {
807
  const data = await apiCall('/api/chatrooms');
808
+ renderChatrooms(data.chatrooms);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
809
  } catch (err) {}
810
  };
811
 
812
  const renderMessages = (messages) => {
813
  const container = document.getElementById('messages-container');
814
+ const shouldScroll = container.scrollTop + container.clientHeight >= container.scrollHeight - 30;
815
  container.innerHTML = '';
816
  messages.forEach(msg => {
817
  const msgDiv = document.createElement('div');
 
819
 
820
  const avatar = getAvatar(msg.display_name);
821
  avatar.classList.add('message-avatar');
822
+ avatar.style.cursor = 'pointer';
823
  avatar.onclick = () => showProfile(msg.sender_address);
824
 
825
  const contentDiv = document.createElement('div');
 
836
 
837
  contentDiv.appendChild(senderDiv);
838
  contentDiv.appendChild(bubbleDiv);
839
+
840
  msgDiv.appendChild(contentDiv);
841
  msgDiv.insertBefore(avatar, contentDiv);
842
  container.appendChild(msgDiv);
843
  });
844
+
845
+ if(shouldScroll) {
846
+ container.scrollTop = container.scrollHeight;
847
+ }
848
  };
849
 
850
  const fetchMessages = async (roomId) => {
 
856
  }
857
  };
858
 
859
+ const showChatView = () => {
860
+ chatWindowView.style.display = 'flex';
861
+ if (window.innerWidth < 768) {
862
+ chatroomListView.style.display = 'none';
863
+ }
864
+ };
865
+
866
+ const showListView = () => {
867
+ chatroomListView.style.display = 'flex';
868
+ if (window.innerWidth < 768) {
869
+ chatWindowView.style.display = 'none';
870
+ }
871
+ };
872
+
873
  const selectChatroom = (roomId, isPrivate) => {
874
  const roomData = chatroomsData[roomId];
875
  if (!roomData) return;
 
882
  const headerAvatar = document.getElementById('chat-header-avatar');
883
  headerAvatar.innerHTML = '';
884
  headerAvatar.appendChild(getAvatar(roomData.name));
885
+
886
+ chatPlaceholder.style.display = 'none';
887
+ activeChat.style.display = 'flex';
888
+ showChatView();
889
 
 
890
  fetchMessages(roomId);
891
  messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000);
892
  };
 
927
  document.getElementById('message-form').addEventListener('submit', async (e) => {
928
  e.preventDefault();
929
  const input = document.getElementById('message-input');
930
+ const sendBtn = document.getElementById('send-btn');
931
  const text = input.value.trim();
932
  if (text && activeChatroomId) {
933
+ input.value = '';
934
  input.disabled = true;
935
+ sendBtn.disabled = true;
936
  try {
937
  await apiCall('/api/send_message', {
938
  method: 'POST',
 
943
  text: text
944
  })
945
  });
 
946
  await fetchMessages(activeChatroomId);
947
  document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight;
948
  } finally {
949
  input.disabled = false;
950
+ sendBtn.disabled = false;
951
  input.focus();
952
  }
953
  }
954
  });
955
 
956
+ const createRoomModal = document.getElementById('create-room-modal');
957
  document.getElementById('create-room-show-modal').addEventListener('click', () => {
958
+ createRoomModal.style.display = 'flex';
959
  document.getElementById('create-room-form').reset();
960
  });
961
  document.getElementById('create-room-cancel').addEventListener('click', () => {
962
+ createRoomModal.style.display = 'none';
963
  });
964
  document.getElementById('create-room-form').addEventListener('submit', async (e) => {
965
  e.preventDefault();
 
973
  headers: { 'Content-Type': 'application/json' },
974
  body: JSON.stringify({ name, password: password || null, creator_address: currentUser.address })
975
  });
976
+ createRoomModal.style.display = 'none';
977
+ showStatus('Чат успешно создан!', 'success');
978
  fetchChatrooms();
979
  } catch (err) {}
980
  });
981
 
982
  const showProfile = async (address) => {
 
 
 
 
 
 
 
 
 
 
 
983
  try {
984
  const userData = await apiCall('/api/user_data', {
985
  method: 'POST',
 
988
  });
989
 
990
  const username = userData.username || `User ${truncateAddress(address)}`;
991
+ const avatarContainer = document.getElementById('profile-avatar-container');
992
+ const usernameEl = document.getElementById('profile-username');
993
+ const addressEl = document.getElementById('profile-address');
994
+ const qrCodeEl = document.getElementById('profile-qr-code');
995
+ const sendTonBtn = document.getElementById('send-ton-btn');
996
+
997
  avatarContainer.innerHTML = '';
998
  avatarContainer.appendChild(getAvatar(username));
 
 
999
 
1000
+ usernameEl.textContent = username;
1001
+ addressEl.textContent = address;
1002
+
1003
  qrCodeEl.innerHTML = '';
1004
+ if (profileQrCode) {
1005
+ profileQrCode.clear();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1006
  }
1007
+ profileQrCode = new QRCode(qrCodeEl, {
1008
+ text: address,
1009
+ width: 150,
1010
+ height: 150,
1011
+ colorDark : "#000000",
1012
+ colorLight : "#ffffff",
1013
+ correctLevel : QRCode.CorrectLevel.H
1014
+ });
1015
+
1016
+ sendTonBtn.onclick = async () => {
1017
+ if (!tonConnectUI.connected) {
1018
+ showStatus('Подключите кошелек для отправки TON.', 'error');
1019
+ return;
1020
+ }
1021
+ const amountString = prompt("Введите сумму в TON для отправки:", "0.1");
1022
+ if (amountString === null) return;
1023
+
1024
+ const amount = parseFloat(amountString);
1025
+ if (isNaN(amount) || amount <= 0) {
1026
+ showStatus('Неверная сумма.', 'error');
1027
+ return;
1028
+ }
1029
+
1030
+ const amountInNanoTon = Math.floor(amount * 1_000_000_000).toString();
1031
+
1032
+ const transaction = {
1033
+ validUntil: Math.floor(Date.now() / 1000) + 600,
1034
+ messages: [ { address: address, amount: amountInNanoTon } ]
1035
+ };
1036
 
1037
+ try {
1038
+ await tonConnectUI.sendTransaction(transaction);
1039
+ showStatus(`Транзакция отправлена успешно!`, 'success');
1040
+ profileModal.style.display = 'none';
1041
+ } catch (error) {
1042
+ showStatus('Транзакция отклонена.', 'error');
1043
+ }
1044
+ };
1045
+
1046
+ sendTonBtn.style.display = (address === currentUser.address) ? 'none' : 'block';
1047
+ profileModal.style.display = 'flex';
1048
  } catch (err) {
1049
+ showStatus('Не удалось загрузить профиль.', 'error');
1050
  }
1051
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1052
 
1053
  const showScanner = () => {
 
1054
  scannerModal.style.display = 'flex';
1055
  html5QrCode = new Html5Qrcode("qr-reader");
1056
  const qrCodeSuccessCallback = (decodedText, decodedResult) => {
 
1058
  if (decodedText && decodedText.length > 40 && (decodedText.startsWith('EQ') || decodedText.startsWith('UQ'))) {
1059
  showProfile(decodedText);
1060
  } else {
1061
+ showStatus('Отсканирован недействительный QR-код.', 'error');
1062
  }
1063
  };
1064
  const config = { fps: 10, qrbox: { width: 250, height: 250 } };
1065
  html5QrCode.start({ facingMode: "environment" }, config, qrCodeSuccessCallback)
1066
  .catch(err => {
1067
+ showStatus('Не удалось запустить сканер.', 'error');
1068
  hideScanner();
1069
  });
1070
  };
1071
+
1072
  const hideScanner = () => {
 
1073
  if (html5QrCode && html5QrCode.isScanning) {
1074
+ html5QrCode.stop().catch(err => console.error("Failed to stop QR scanner.", err));
1075
  }
1076
  scannerModal.style.display = 'none';
1077
  };
1078
+
1079
+ const showView = (viewName) => {
1080
+ currentNavView = viewName;
1081
+ document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active'));
1082
+ document.getElementById(`nav-${viewName}`).classList.add('active');
1083
+
1084
+ if (viewName === 'chats') {
1085
+ if (window.innerWidth < 768) {
1086
+ chatroomListView.style.display = 'flex';
1087
+ chatWindowView.style.display = 'none';
1088
+ }
1089
+ } else if (viewName === 'users') {
1090
+ showStatus('Функционал списка пользователей в разработке.', 'info');
1091
+ } else if (viewName === 'scan') {
1092
+ showScanner();
1093
+ } else if (viewName === 'profile') {
1094
+ if (currentUser.address) showProfile(currentUser.address);
1095
+ }
1096
+ };
1097
 
1098
+ document.getElementById('nav-chats').addEventListener('click', () => showView('chats'));
1099
+ document.getElementById('nav-users').addEventListener('click', () => showView('users'));
1100
+ document.getElementById('nav-scan').addEventListener('click', () => showView('scan'));
1101
+ document.getElementById('nav-profile').addEventListener('click', () => showView('profile'));
1102
+
1103
+ document.getElementById('my-profile-btn').addEventListener('click', () => showView('profile'));
1104
+ document.getElementById('scan-qr-btn').addEventListener('click', () => showView('scan'));
1105
+ document.getElementById('profile-close-btn').addEventListener('click', () => profileModal.style.display = 'none');
1106
+ document.getElementById('scanner-close-btn').addEventListener('click', hideScanner);
1107
+ document.getElementById('back-to-list-btn').addEventListener('click', () => showView('chats'));
1108
 
1109
+ const handleResize = () => {
1110
+ const isMobile = window.innerWidth < 768;
1111
+ bottomNav.style.display = isMobile ? 'flex' : 'none';
1112
+
1113
+ if (!isMobile) {
1114
+ chatroomListView.style.display = 'flex';
1115
+ chatWindowView.style.display = 'flex';
1116
+ } else {
1117
+ if (currentNavView === 'chats') {
1118
+ chatroomListView.style.display = 'flex';
1119
+ chatWindowView.style.display = 'none';
1120
+ } else if (activeChat.style.display === 'flex') {
1121
+ chatroomListView.style.display = 'none';
1122
+ chatWindowView.style.display = 'flex';
1123
+ } else {
1124
+ chatroomListView.style.display = 'flex';
1125
+ chatWindowView.style.display = 'none';
1126
+ }
1127
+ }
1128
+ };
1129
+
1130
+ window.addEventListener('resize', handleResize);
1131
+ handleResize();
1132
+
1133
  tonConnectUI.onStatusChange(wallet => {
1134
  if (wallet) {
1135
  const address = TON_CONNECT_UI.toUserFriendlyAddress(wallet.account.address, false);
1136
  initializeUser(address);
1137
  } else {
1138
  currentUser = { address: null, username: null };
1139
+ appWrapper.style.display = 'none';
1140
  loginView.style.display = 'flex';
1141
  if (messagePollingInterval) clearInterval(messagePollingInterval);
1142
  activeChatroomId = null;
 
1149
  '''
1150
  return Response(html_content, mimetype='text/html')
1151
 
 
 
 
 
 
 
 
 
 
 
1152
 
1153
  @app.route('/api/user_data', methods=['POST'])
1154
  def get_user_data():