Aleksmorshen commited on
Commit
a74cdc1
·
verified ·
1 Parent(s): d00a31f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +633 -819
app.py CHANGED
@@ -30,7 +30,7 @@ def write_db(data):
30
  def index():
31
  html_content = '''
32
  <!DOCTYPE html>
33
- <html lang="ru">
34
  <head>
35
  <meta charset="UTF-8">
36
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
@@ -44,45 +44,44 @@ def index():
44
  <style>
45
  :root {
46
  --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
47
- --radius-s: 4px;
48
- --radius-m: 8px;
49
- --radius-l: 12px;
50
- --radius-xl: 20px;
51
- --transition-speed: 0.3s;
52
  }
53
 
54
- body {
55
- --bg-primary: #000000;
56
- --bg-secondary: #121212;
57
- --bg-tertiary: #1d1d1d;
58
- --bg-elevated: #282828;
59
- --bg-modal-overlay: rgba(0, 0, 0, 0.7);
60
- --text-primary: #ffffff;
61
- --text-secondary: #a8a8a8;
62
- --text-tertiary: #757575;
63
- --border-primary: #2d2d2d;
64
- --accent-blue: #007aff;
65
- --accent-blue-rgb: 0, 122, 255;
66
- --accent-gradient: linear-gradient(135deg, #007aff, #5856d6);
67
- --success-color: #34c759;
68
- --error-color: #ff3b30;
69
- --white: #ffffff;
70
- --black: #000000;
71
  }
72
 
73
- body.light-theme {
74
- --bg-primary: #f2f2f7;
75
- --bg-secondary: #ffffff;
76
- --bg-tertiary: #f2f2f7;
77
- --bg-elevated: #ffffff;
78
- --bg-modal-overlay: rgba(0, 0, 0, 0.4);
79
- --text-primary: #000000;
80
- --text-secondary: #636366;
81
- --text-tertiary: #aeaeb2;
82
- --border-primary: #e5e5ea;
83
- --accent-blue: #007aff;
84
- --accent-blue-rgb: 0, 122, 255;
85
- --accent-gradient: linear-gradient(135deg, #007aff, #5856d6);
 
 
 
86
  }
87
 
88
  * {
@@ -92,127 +91,97 @@ def index():
92
  -webkit-tap-highlight-color: transparent;
93
  }
94
 
95
- html, body {
96
- height: 100vh;
97
- width: 100vw;
98
- overflow: hidden;
99
- }
100
-
101
  body {
102
  font-family: var(--font-family);
103
  background-color: var(--bg-primary);
104
  color: var(--text-primary);
 
 
 
105
  display: flex;
106
  align-items: center;
107
  justify-content: center;
108
- transition: background-color var(--transition-speed), color var(--transition-speed);
 
109
  }
110
 
111
- .app-wrapper {
112
  width: 100%;
113
  height: 100%;
114
  display: flex;
115
  flex-direction: column;
116
- transition: opacity 0.5s ease;
117
  }
118
-
119
  #login-view {
120
  display: flex;
121
  flex-direction: column;
122
  align-items: center;
123
  justify-content: center;
124
  text-align: center;
125
- width: 100%;
126
- height: 100%;
127
  background: var(--bg-primary);
128
- z-index: 100;
129
- }
130
- #login-view img {
131
- width: 120px;
132
- height: 120px;
133
- margin-bottom: 24px;
134
- filter: drop-shadow(0 0 25px rgba(var(--accent-blue-rgb), 0.4));
135
- }
136
- #login-view h1 {
137
- font-size: 3rem;
138
- font-weight: 700;
139
- background: var(--accent-gradient);
140
- -webkit-background-clip: text;
141
- -webkit-text-fill-color: transparent;
142
- margin-bottom: 12px;
143
- }
144
- #login-view p {
145
- font-size: 1.1rem;
146
- color: var(--text-secondary);
147
- margin-bottom: 40px;
148
  }
 
 
 
149
 
150
- #app-view {
151
- display: none;
 
152
  width: 100%;
153
  height: 100%;
154
- flex-direction: column-reverse;
155
  }
156
-
157
  .main-content {
158
  flex-grow: 1;
 
 
159
  position: relative;
 
160
  overflow: hidden;
161
  }
162
- .view {
163
- position: absolute;
164
- top: 0;
165
- left: 0;
166
- width: 100%;
167
- height: 100%;
168
- display: flex;
169
  flex-direction: column;
170
- opacity: 0;
171
- visibility: hidden;
172
- transition: opacity var(--transition-speed) ease-in-out, visibility var(--transition-speed);
173
  background-color: var(--bg-primary);
 
174
  }
175
- .view.active {
176
- opacity: 1;
177
- visibility: visible;
178
- z-index: 10;
179
- }
180
 
181
  .view-header {
182
- padding: 12px 16px;
183
  flex-shrink: 0;
184
- background-color: rgba(var(--bg-secondary-rgb), 0.8);
185
- backdrop-filter: blur(10px);
186
- -webkit-backdrop-filter: blur(10px);
187
- border-bottom: 1px solid var(--border-primary);
188
  display: flex;
189
  align-items: center;
190
  justify-content: space-between;
191
- z-index: 5;
192
- }
193
- .view-header h2 {
194
- font-size: 1.5rem;
195
- font-weight: 700;
196
  }
197
-
198
- .list-container {
 
199
  flex-grow: 1;
200
  overflow-y: auto;
201
  padding: 8px 0;
202
  }
 
203
  .list-item {
204
  display: flex;
205
  align-items: center;
206
  gap: 12px;
207
- padding: 12px 16px;
208
  cursor: pointer;
209
- transition: background-color 0.2s ease;
210
  }
211
- .list-item:hover { background-color: var(--bg-tertiary); }
212
- .item-info { flex-grow: 1; overflow: hidden; }
213
- .item-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
214
- .item-detail { font-size: 0.9rem; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
215
- .lock-icon { width: 16px; height: 16px; fill: var(--text-tertiary); flex-shrink: 0; }
216
 
217
  .avatar {
218
  width: 48px;
@@ -223,380 +192,217 @@ def index():
223
  justify-content: center;
224
  font-size: 1.2rem;
225
  font-weight: 600;
226
- color: var(--white);
227
  flex-shrink: 0;
228
  }
229
 
230
- #chat-window-view {
231
- display: none;
232
- flex-direction: column;
233
- height: 100%;
234
- width: 100%;
235
- background-color: var(--bg-primary);
236
- position: fixed;
237
- top: 0;
238
- left: 100%;
239
- z-index: 50;
240
- transition: transform var(--transition-speed) ease-in-out;
241
- }
242
- #chat-window-view.visible { transform: translateX(-100%); }
243
-
244
  .chat-header {
245
- display: flex;
246
- align-items: center;
247
- gap: 12px;
248
- padding: 12px;
249
- background-color: rgba(var(--bg-secondary-rgb), 0.8);
250
- backdrop-filter: blur(10px);
251
- border-bottom: 1px solid var(--border-primary);
252
- flex-shrink: 0;
253
  }
254
- .back-btn { background: none; border: none; cursor: pointer; padding: 4px; }
255
  .back-btn svg { width: 28px; height: 28px; fill: var(--accent-blue); }
 
256
  #chat-header-title { font-size: 1.1rem; font-weight: 600; }
257
-
258
  #messages-container {
259
- flex-grow: 1;
260
- padding: 16px;
261
- overflow-y: auto;
262
- display: flex;
263
- flex-direction: column;
264
- gap: 16px;
265
  }
266
  .message { display: flex; gap: 10px; max-width: 85%; }
267
- .message .avatar { width: 36px; height: 36px; font-size: 1rem; align-self: flex-end; }
268
  .message-content { display: flex; flex-direction: column; gap: 4px; }
269
- .message-sender { font-size: 0.8rem; font-weight: 500; color: var(--text-secondary); padding: 0 12px; cursor: pointer; }
270
- .message-bubble { padding: 10px 14px; border-radius: var(--radius-xl); line-height: 1.4; word-wrap: break-word; }
271
-
272
- .message.sent { align-self: flex-end; flex-direction: row-reverse; }
273
- .message.sent .message-sender { text-align: right; color: var(--accent-blue); }
274
- .message.sent .message-bubble { background: var(--accent-gradient); color: var(--white); border-bottom-right-radius: var(--radius-s); }
275
-
276
  .message.received { align-self: flex-start; }
277
- .message.received .message-bubble { background-color: var(--bg-elevated); border-bottom-left-radius: var(--radius-s); }
278
-
279
- .message-form {
280
- display: flex;
281
- padding: 8px 12px;
282
- gap: 12px;
283
- background-color: var(--bg-secondary);
284
- border-top: 1px solid var(--border-primary);
285
- flex-shrink: 0;
286
  }
287
  #message-input {
288
- flex-grow: 1;
289
- padding: 10px 18px;
290
- border: none;
291
- background-color: var(--bg-tertiary);
292
- color: var(--text-primary);
293
- border-radius: 20px;
294
- outline: none;
295
- font-size: 1rem;
296
- font-family: var(--font-family);
297
- }
298
- .send-btn {
299
- width: 40px;
300
- height: 40px;
301
- border-radius: 50%;
302
- flex-shrink: 0;
303
- padding: 0;
304
- background: var(--accent-blue);
305
- color: var(--white);
306
- border: none;
307
- cursor: pointer;
308
- display: flex;
309
- align-items: center;
310
- justify-content: center;
311
  }
312
- .send-btn svg { width: 20px; height: 20px; fill: var(--white); }
313
-
314
- #my-profile-view .profile-card {
315
- background-color: var(--bg-secondary);
316
- margin: 16px;
317
- padding: 24px;
318
- border-radius: var(--radius-l);
319
- text-align: center;
320
- box-shadow: 0 4px 20px rgba(0,0,0,0.1);
321
- }
322
- #my-profile-view .avatar {
323
- width: 100px;
324
- height: 100px;
325
- font-size: 3rem;
326
- margin: 0 auto 16px;
327
- }
328
- #my-profile-view #profile-username-display { font-size: 1.5rem; font-weight: 600; }
329
- #my-profile-view #profile-address-display { font-size: 0.9rem; color: var(--text-secondary); word-break: break-all; margin-top: 4px; }
330
- #my-profile-view #profile-balance-display { font-size: 1.1rem; margin-top: 12px; color: var(--text-tertiary); }
331
- #my-profile-view .username-form { display: flex; gap: 8px; margin-top: 24px; }
332
  .action-btn {
333
- background: var(--accent-blue);
334
- color: var(--white);
335
- border: none;
336
- padding: 12px 18px;
337
- border-radius: var(--radius-m);
338
- cursor: pointer;
339
- font-weight: 500;
340
- font-size: 1rem;
341
- transition: transform 0.2s ease, box-shadow 0.2s ease;
342
- display: flex;
343
- align-items: center;
344
- justify-content: center;
345
- gap: 8px;
346
  }
347
- .username-input {
348
- flex-grow: 1;
349
- background-color: var(--bg-tertiary);
350
- border: 1px solid var(--border-primary);
351
- color: var(--text-primary);
352
- border-radius: var(--radius-m);
353
- padding: 10px 14px;
354
- font-size: 1rem;
355
- }
356
- .username-input:focus { outline: none; border-color: var(--accent-blue); }
357
-
358
  .bottom-nav {
359
- display: flex;
360
- justify-content: space-around;
361
- align-items: center;
362
- height: 84px;
363
- padding-bottom: 30px;
364
- background-color: rgba(var(--bg-secondary-rgb), 0.8);
365
- backdrop-filter: blur(10px);
366
- -webkit-backdrop-filter: blur(10px);
367
- border-top: 1px solid var(--border-primary);
368
- flex-shrink: 0;
369
  }
370
  .nav-item {
371
- display: flex;
372
- flex-direction: column;
373
- align-items: center;
374
- justify-content: center;
375
- gap: 4px;
376
- cursor: pointer;
377
- color: var(--text-tertiary);
378
- transition: color 0.2s;
379
- padding: 8px 12px;
380
- border-radius: var(--radius-l);
381
  }
382
  .nav-item.active { color: var(--accent-blue); }
383
- .nav-item:hover:not(.active) { color: var(--text-secondary); }
384
- .nav-item svg {
385
- width: 26px;
386
- height: 26px;
387
- fill: currentColor;
388
  }
389
- .nav-item span { font-size: 0.7rem; font-weight: 500; }
390
- #nav-scan-btn {
391
- transform: translateY(-20px);
392
- background: var(--accent-gradient);
393
- width: 64px;
394
- height: 64px;
395
- border-radius: 50%;
396
- color: var(--white);
397
- box-shadow: 0 5px 15px rgba(var(--accent-blue-rgb), 0.4);
398
  }
399
- #nav-scan-btn svg { width: 32px; height: 32px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
 
401
  .modal-overlay {
402
- position: fixed;
403
- top: 0;
404
- left: 0;
405
- width: 100%;
406
- height: 100%;
407
- background-color: var(--bg-modal-overlay);
408
- display: none;
409
- align-items: center;
410
- justify-content: center;
411
- z-index: 1000;
412
- backdrop-filter: blur(5px);
413
- -webkit-backdrop-filter: blur(5px);
414
  }
415
  .modal-content {
416
- background-color: var(--bg-secondary);
417
- padding: 24px;
418
- border-radius: var(--radius-l);
419
- width: 90%;
420
- max-width: 400px;
421
- border: 1px solid var(--border-primary);
422
- box-shadow: 0 10px 30px rgba(0,0,0,0.2);
423
  }
424
  .modal-content h3 { margin-bottom: 20px; font-weight: 600; font-size: 1.3rem; }
425
  .modal-content label { display: block; margin-bottom: 8px; font-size: 0.9rem; color: var(--text-secondary); }
426
  .modal-content input {
427
- width: 100%;
428
- padding: 12px;
429
- margin-bottom: 16px;
430
- background-color: var(--bg-tertiary);
431
- border: 1px solid var(--border-primary);
432
- color: var(--text-primary);
433
- border-radius: var(--radius-m);
434
- font-size: 1rem;
435
  }
436
  .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
437
- .modal-btn {
438
- padding: 10px 20px;
439
- border-radius: var(--radius-m);
440
- border: none;
441
- cursor: pointer;
442
- font-weight: 500;
443
- }
444
- .secondary-btn { background-color: var(--bg-elevated); color: var(--text-primary); }
445
 
446
- #profile-qr-code { background: var(--white); padding: 10px; margin: 20px auto; width: fit-content; border-radius: var(--radius-m); }
447
-
448
  #status-bar {
449
- position: fixed;
450
- top: 20px;
451
- left: 50%;
452
- transform: translateX(-50%);
453
- background-color: var(--bg-elevated);
454
- color: var(--text-primary);
455
- padding: 12px 20px;
456
- border-radius: var(--radius-xl);
457
- font-size: 0.9rem;
458
- opacity: 0;
459
- visibility: hidden;
460
- transition: opacity 0.3s, visibility 0.3s, transform 0.3s;
461
- z-index: 2000;
462
- box-shadow: 0 5px 15px rgba(0,0,0,0.2);
463
  }
464
- #status-bar.visible { opacity: 1; visibility: visible; transform: translate(-50%, 10px); }
465
- #status-bar.success { background-color: var(--success-color); color: var(--white); }
466
- #status-bar.error { background-color: var(--error-color); color: var(--white); }
467
-
468
- .theme-toggle-btn {
469
- background-color: var(--bg-tertiary);
470
- border: 1px solid var(--border-primary);
471
- border-radius: 50%;
472
- width: 40px;
473
- height: 40px;
474
- cursor: pointer;
475
- display: flex;
476
- align-items: center;
477
- justify-content: center;
478
- }
479
- .theme-toggle-btn svg { width: 20px; height: 20px; fill: var(--text-secondary); }
480
 
481
  @media (min-width: 768px) {
482
- #login-view { background: var(--bg-secondary); }
483
- .app-wrapper { max-width: 1200px; max-height: 900px; border-radius: var(--radius-xl); overflow: hidden; box-shadow: 0 10px 50px rgba(0,0,0,0.2); border: 1px solid var(--border-primary); }
484
- #app-view { flex-direction: row; }
485
- .bottom-nav {
486
- flex-direction: column;
487
- width: 80px;
488
- height: 100%;
489
- padding-bottom: 0;
490
- padding-top: 20px;
491
- justify-content: flex-start;
492
- gap: 16px;
493
- border-top: none;
494
- border-right: 1px solid var(--border-primary);
495
  }
496
- .nav-item { gap: 6px; }
497
- .nav-item span { font-size: 0.75rem; }
498
- #nav-scan-btn { transform: none; margin-top: 16px; }
499
- .main-content { display: flex; }
500
- .view { position: static; opacity: 1; visibility: visible; width: 350px; flex-shrink: 0; border-right: 1px solid var(--border-primary); }
501
- .view.full-width { width: 100%; border-right: none; }
502
- .view.hidden-desktop { display: none; }
503
- .view.active { display: flex; }
504
- #chat-window-view { position: static; transform: none !important; width: 100%; display: flex !important; }
505
- #chat-window-view .back-btn { display: none; }
506
- }
 
 
 
507
 
 
 
 
 
 
 
 
508
  </style>
509
  </head>
510
  <body>
511
-
512
- <div id="login-view" class="app-wrapper">
513
  <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
514
  <h1>Virton</h1>
515
- <p>Децентрализованный и анонимный мессенджер</p>
516
  <div id="ton-connect-button"></div>
517
  </div>
518
 
519
- <div id="app-view" class="app-wrapper">
520
- <div class="main-content">
521
- <div id="chats-view" class="view active">
522
- <div class="view-header">
523
- <h2>Чаты</h2>
524
- <button id="create-room-show-modal" class="action-btn">
525
- <svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 0 24 24" width="20" fill="currentColor"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
526
- </button>
527
- </div>
528
- <div id="chatroom-list" class="list-container"></div>
529
- </div>
530
-
531
- <div id="users-view" class="view hidden-desktop">
532
- <div class="view-header">
533
- <h2>Пользователи</h2>
534
  </div>
535
- <div id="user-list" class="list-container"></div>
536
- </div>
537
 
538
- <div id="my-profile-view" class="view hidden-desktop full-width">
539
- <div class="view-header">
540
- <h2>Профиль</h2>
541
- <button id="theme-toggle-btn" class="theme-toggle-btn">
542
- <svg id="theme-icon-sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.64 5.64c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41l1.06 1.06c.39.39 1.02.39 1.41 0s.39-1.02 0-1.41L5.64 5.64zm12.73 12.73c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41l1.06 1.06c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41l-1.06-1.06zM5.64 18.36l1.06-1.06c.39-.39.39-1.02 0-1.41s-1.02-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.02 0 1.41s1.02.39 1.41 0zM18.36 5.64l1.06-1.06c.39-.39.39-1.02 0-1.41s-1.02-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.02 0 1.41s1.02.39 1.41 0z"/></svg>
543
- <svg id="theme-icon-moon" style="display: none;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12.3 4.88c.15.42.21.87.18 1.32-.1 1.54-1.09 2.87-2.48 3.48-2.73 1.2-5.73-.55-5.73-3.53 0-.52.08-1.03.23-1.52.2-.64.55-1.22 1-1.72.45-.49 1-.9 1.58-1.22.58-.32 1.2-.5 1.83-.53.43-.02.86.03 1.28.14.36.09.7.24 1.01.44.31.2.59.45.82.74s.42.61.56.95zM9.5 2c-1.82 0-3.53.5-5 1.35C2.39 3.97 1.25 5.26 1.03 6.8c-.21 1.48.19 2.98 1.12 4.24.93 1.27 2.36 2.14 3.95 2.42 2.73.48 5.3-1.12 6.33-3.65.8-1.95.42-4.24-1.01-5.83C12.52 2.69 11.04 2 9.5 2z"/></svg>
544
- </button>
545
- </div>
546
- <div class="profile-card">
547
- <div id="profile-avatar-placeholder" class="avatar"></div>
548
- <h3 id="profile-username-display"></h3>
549
- <p id="profile-address-display"></p>
550
- <p id="profile-balance-display">Баланс: <span id="ton-balance">N/A</span> TON</p>
551
- <form id="username-form" class="username-form">
552
- <input type="text" id="username-input" class="username-input" placeholder="Новый никнейм" autocomplete="off">
553
- <button type="submit" class="action-btn">✓</button>
554
- </form>
555
- <button id="show-my-qr-btn" class="action-btn" style="margin-top: 16px; width: 100%;">Мой QR-код</button>
556
  </div>
557
- </div>
558
 
559
- <div id="chat-window-view">
560
- <div id="chat-placeholder" class="chat-placeholder" style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; text-align: center; color: var(--text-secondary); padding: 20px;">
561
- <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol" style="width: 80px; margin-bottom: 20px; opacity: 0.5;">
562
- <h2>Выберите чат для начала общения</h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
563
  </div>
564
- <div id="active-chat" style="display: none; width: 100%; height: 100%; flex-direction: column;">
565
- <div class="chat-header">
566
- <button class="back-btn" id="back-to-list-btn">
567
- <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>
568
- </button>
569
- <div id="chat-header-avatar" class="avatar"></div>
570
- <span id="chat-header-title"></span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
  </div>
572
- <div id="messages-container"></div>
573
- <form id="message-form" class="message-form">
574
- <input type="text" id="message-input" placeholder="Сообщение" autocomplete="off">
575
- <button type="submit" class="send-btn" id="send-btn">
576
- <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>
577
- </button>
578
- </form>
579
  </div>
580
  </div>
581
  </div>
582
-
583
- <nav class="bottom-nav">
584
- <div id="nav-chats-btn" class="nav-item active">
585
- <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>
586
- <span>Чаты</span>
587
- </div>
588
- <div id="nav-users-btn" class="nav-item">
589
- <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>
590
- <span>Люди</span>
591
- </div>
592
- <div id="nav-scan-btn" class="nav-item">
593
- <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-4V5h4v4zM13 21h8v-2h-8v2zm4-4h4v-2h-4v2z"/></svg>
594
- </div>
595
- <div id="nav-profile-btn" class="nav-item">
596
- <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>
597
- <span>Профиль</span>
598
- </div>
599
- </nav>
600
  </div>
601
 
602
  <div id="create-room-modal" class="modal-overlay">
@@ -608,7 +414,7 @@ def index():
608
  <label for="room-password">Пароль (оставьте пустым для открытого)</label>
609
  <input type="password" id="room-password">
610
  <div class="modal-actions">
611
- <button type="button" class="modal-btn secondary-btn" data-close>Отмена</button>
612
  <button type="submit" class="modal-btn action-btn">Создать</button>
613
  </div>
614
  </form>
@@ -622,7 +428,7 @@ def index():
622
  <label for="password-input">Введите пароль</label>
623
  <input type="password" id="password-input" required>
624
  <div class="modal-actions">
625
- <button type="button" class="modal-btn secondary-btn" data-close>Отмена</button>
626
  <button type="submit" class="modal-btn action-btn">Войти</button>
627
  </div>
628
  </form>
@@ -631,15 +437,15 @@ def index():
631
 
632
  <div id="profile-modal" class="modal-overlay">
633
  <div class="modal-content" style="text-align: center;">
634
- <h3 id="profile-modal-title">Профиль пользователя</h3>
635
  <div id="profile-avatar-container" style="margin: 20px auto; display: inline-block;"></div>
636
  <p id="profile-username" style="font-size: 1.2rem; font-weight: 600;"></p>
637
  <p id="profile-address" style="color: var(--text-secondary); font-size: 0.9rem; word-break: break-all; margin-top: 8px;"></p>
638
  <div id="profile-qr-code" style="background: white; padding: 10px; margin: 20px auto; width: fit-content; border-radius: 8px;"></div>
639
- <p style="text-align: center; color: var(--text-secondary); font-size: 0.8rem; margin-top: -10px; margin-bottom: 20px;">Отсканируйте для открытия профиля</p>
640
  <div class="modal-actions" style="flex-direction: column; gap: 12px; align-items: stretch;">
641
  <button id="send-ton-btn" class="modal-btn action-btn">Отправить TON</button>
642
- <button class="modal-btn secondary-btn" data-close>Закрыть</button>
643
  </div>
644
  </div>
645
  </div>
@@ -647,7 +453,7 @@ def index():
647
  <div id="scanner-modal" class="modal-overlay">
648
  <div class="modal-content">
649
  <h3>Сканировать QR-код</h3>
650
- <div id="qr-reader" style="width: 100%; border-radius: var(--radius-l); overflow: hidden; margin-top: 16px;"></div>
651
  <div class="modal-actions">
652
  <button id="scanner-close-btn" class="modal-btn secondary-btn">Отмена</button>
653
  </div>
@@ -657,426 +463,427 @@ def index():
657
  <div id="status-bar"></div>
658
 
659
  <script>
660
- document.addEventListener('DOMContentLoaded', () => {
661
- const tonConnectUI = new TON_CONNECT_UI.TonConnectUI({
662
- manifestUrl: 'https://huggingface.co/spaces/Aleksmorshen/MorshenGroup/resolve/main/tonconnect-manifest.json',
663
- buttonRootId: 'ton-connect-button'
664
- });
 
 
 
 
 
 
 
665
 
666
- let currentUser = { address: null, username: null };
667
- let activeChatroomId = null;
668
- let messagePollingInterval = null;
669
- let chatroomsData = {};
670
- let html5QrCode = null;
671
- let profileQrCode = null;
672
- let isMobile = window.innerWidth < 768;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
673
 
674
- const loginView = document.getElementById('login-view');
675
- const appView = document.getElementById('app-view');
676
- const chatWindowView = document.getElementById('chat-window-view');
 
 
 
 
677
 
678
- const body = document.body;
679
- const themeToggleBtn = document.getElementById('theme-toggle-btn');
680
- const sunIcon = document.getElementById('theme-icon-sun');
681
- const moonIcon = document.getElementById('theme-icon-moon');
682
-
683
- const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292'];
684
-
685
- const getAvatar = (name, size) => {
686
- const initial = (name ? name[0] : '?').toUpperCase();
687
- const charCode = initial.charCodeAt(0);
688
- const color = AVATAR_COLORS[charCode % AVATAR_COLORS.length];
689
- const avatar = document.createElement('div');
690
- avatar.className = 'avatar';
691
- if(size) {
692
- avatar.style.width = `${size}px`;
693
- avatar.style.height = `${size}px`;
694
- avatar.style.fontSize = `${size*0.4}px`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
695
  }
696
- avatar.style.backgroundColor = color;
697
- avatar.textContent = initial;
698
- return avatar;
699
  };
700
 
701
- const showStatus = (message, type = 'info', duration = 3000) => {
702
- const statusBar = document.getElementById('status-bar');
703
- statusBar.textContent = message;
704
- statusBar.className = 'status-bar';
705
- if (type === 'success') statusBar.classList.add('success');
706
- else if (type === 'error') statusBar.classList.add('error');
707
- statusBar.classList.add('visible');
708
- setTimeout(() => statusBar.classList.remove('visible'), duration);
709
- };
710
 
711
- const apiCall = async (endpoint, options = {}) => {
712
- try {
713
- const response = await fetch(endpoint, options);
714
- if (!response.ok) {
715
- const errorData = await response.json().catch(() => ({ error: 'Request failed with status ' + response.status }));
716
- throw new Error(errorData.error || 'Unknown error');
717
- }
718
- if (response.status === 204) return null;
719
- return await response.json();
720
- } catch (error) {
721
- showStatus(`Ошибка: ${error.message}`, 'error');
722
- throw error;
723
- }
724
- };
 
 
 
 
 
 
725
 
726
- const truncateAddress = (address) => address ? `${address.substring(0, 6)}...${address.substring(address.length - 6)}` : '';
727
-
728
- const updateCurrentUserInfo = () => {
729
- document.getElementById('profile-username-display').textContent = currentUser.username || 'Аноним';
730
- document.getElementById('profile-address-display').textContent = truncateAddress(currentUser.address);
731
- document.getElementById('username-input').value = currentUser.username || '';
732
- const avatarPlaceholder = document.getElementById('profile-avatar-placeholder');
733
- avatarPlaceholder.innerHTML = '';
734
- avatarPlaceholder.appendChild(getAvatar(currentUser.username || currentUser.address, 100));
735
- };
 
 
 
 
 
 
 
 
 
 
 
 
736
 
737
- document.getElementById('username-form').addEventListener('submit', async (e) => {
738
- e.preventDefault();
739
- const newUsername = document.getElementById('username-input').value.trim();
740
- if (!newUsername || newUsername.length < 3) {
741
- showStatus('Никнейм должен быть не короче 3 символов.', 'error'); return;
742
- }
743
- try {
744
- await apiCall('/api/set_username', {
745
- method: 'POST',
746
- headers: { 'Content-Type': 'application/json' },
747
- body: JSON.stringify({ address: currentUser.address, username: newUsername })
748
- });
749
- currentUser.username = newUsername;
750
- updateCurrentUserInfo();
751
- showStatus('Никнейм успешно обновлен!', 'success');
752
- fetchChatrooms();
753
- fetchUsers();
754
- if (activeChatroomId) fetchMessages(activeChatroomId);
755
- } catch (err) {}
756
  });
 
 
 
 
 
 
 
757
 
758
- const initializeUser = async (address) => {
759
- currentUser.address = address;
760
- try {
761
- const data = await apiCall('/api/user_data', {
762
- method: 'POST', headers: { 'Content-Type': 'application/json' },
763
- body: JSON.stringify({ address: currentUser.address })
764
- });
765
- currentUser.username = data.username;
766
- } catch (err) { currentUser.username = null; }
767
- updateCurrentUserInfo();
768
- loginView.style.display = 'none';
769
- appView.style.display = isMobile ? 'flex' : 'flex';
770
- showView('chats-view');
771
- fetchChatrooms();
772
- fetchUsers();
773
- };
774
-
775
- const renderChatrooms = (rooms) => {
776
- const list = document.getElementById('chatroom-list');
777
- list.innerHTML = '';
778
- chatroomsData = {};
779
- rooms.forEach(room => {
780
- chatroomsData[room.id] = room;
781
- const item = document.createElement('div');
782
- item.className = 'list-item';
783
- item.dataset.id = room.id;
784
- item.appendChild(getAvatar(room.name));
785
- const infoDiv = document.createElement('div');
786
- infoDiv.className = 'item-info';
787
- infoDiv.innerHTML = `<div class="item-name">${room.name}</div>`;
788
- item.appendChild(infoDiv);
789
- if (room.is_private) {
790
- 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>`;
791
- }
792
- item.addEventListener('click', () => selectChatroom(room.id, room.is_private));
793
- list.appendChild(item);
794
  });
795
- };
796
-
797
- const fetchChatrooms = async () => { try { const data = await apiCall('/api/chatrooms'); renderChatrooms(data.chatrooms); } catch (err) {} };
798
-
799
- const renderUsers = (users) => {
800
- const list = document.getElementById('user-list');
801
- list.innerHTML = '';
802
- users.forEach(user => {
803
- const item = document.createElement('div');
804
- item.className = 'list-item';
805
- item.appendChild(getAvatar(user.username || user.address));
806
- const infoDiv = document.createElement('div');
807
- infoDiv.className = 'item-info';
808
- infoDiv.innerHTML = `<div class="item-name">${user.username || 'Аноним'}</div><div class="item-detail">${truncateAddress(user.address)}</div>`;
809
- item.appendChild(infoDiv);
810
- item.addEventListener('click', () => showProfile(user.address));
811
- list.appendChild(item);
812
  });
813
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
814
 
815
- const fetchUsers = async () => { try { const data = await apiCall('/api/users'); renderUsers(data.users); } catch (err) {} };
816
-
817
- const renderMessages = (messages) => {
818
- const container = document.getElementById('messages-container');
819
- const shouldScroll = container.scrollTop + container.clientHeight >= container.scrollHeight - 50;
820
- container.innerHTML = '';
821
- messages.forEach(msg => {
822
- const msgDiv = document.createElement('div');
823
- msgDiv.className = 'message ' + (msg.sender_address === currentUser.address ? 'sent' : 'received');
824
- const avatar = getAvatar(msg.display_name);
825
- avatar.onclick = () => showProfile(msg.sender_address);
826
- const contentDiv = document.createElement('div');
827
- contentDiv.className = 'message-content';
828
- const senderDiv = document.createElement('div');
829
- senderDiv.className = 'message-sender';
830
- senderDiv.textContent = msg.display_name;
831
- senderDiv.onclick = () => showProfile(msg.sender_address);
832
- const bubbleDiv = document.createElement('div');
833
- bubbleDiv.className = 'message-bubble';
834
- bubbleDiv.textContent = msg.text;
835
- contentDiv.appendChild(senderDiv); contentDiv.appendChild(bubbleDiv);
836
- msgDiv.appendChild(contentDiv); msgDiv.insertBefore(avatar, contentDiv);
837
- container.appendChild(msgDiv);
838
- });
839
- if(shouldScroll) { container.scrollTop = container.scrollHeight; }
840
- };
841
-
842
- const fetchMessages = async (roomId) => {
843
- try {
844
- const data = await apiCall(`/api/messages/${roomId}`);
845
- renderMessages(data.messages);
846
- } catch (err) {
847
- if (messagePollingInterval) clearInterval(messagePollingInterval);
848
- }
849
- };
850
 
851
- const selectChatroom = (roomId, isPrivate) => {
852
- const roomData = chatroomsData[roomId];
853
- if (!roomData) return;
854
- const proceedToRoom = () => {
855
- if (messagePollingInterval) clearInterval(messagePollingInterval);
856
- activeChatroomId = roomId;
857
- document.getElementById('chat-header-title').textContent = roomData.name;
858
- const headerAvatar = document.getElementById('chat-header-avatar');
859
- headerAvatar.innerHTML = ''; headerAvatar.appendChild(getAvatar(roomData.name));
860
- document.getElementById('chat-placeholder').style.display = 'none';
861
- document.getElementById('active-chat').style.display = 'flex';
862
- if (isMobile) chatWindowView.classList.add('visible');
863
- fetchMessages(roomId);
864
- messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000);
865
- };
866
 
867
- if (isPrivate) {
868
- const passwordModal = document.getElementById('password-modal');
869
- const passwordForm = document.getElementById('password-form');
870
- const passwordInput = document.getElementById('password-input');
871
- passwordModal.style.display = 'flex';
872
- passwordInput.value = ''; passwordInput.focus();
873
-
874
- const formSubmitHandler = async (e) => {
875
- e.preventDefault();
876
- passwordForm.removeEventListener('submit', formSubmitHandler);
877
- const password = passwordInput.value;
878
- passwordModal.style.display = 'none';
879
- try {
880
- await apiCall('/api/join_chatroom', {
881
- method: 'POST', headers: { 'Content-Type': 'application/json' },
882
- body: JSON.stringify({ chatroom_id: roomId, password })
883
- });
884
- proceedToRoom();
885
- } catch (err) {}
886
- };
887
- passwordForm.addEventListener('submit', formSubmitHandler);
888
- } else {
889
- proceedToRoom();
890
- }
891
  };
892
- document.getElementById('back-to-list-btn').addEventListener('click', () => {
893
- chatWindowView.classList.remove('visible');
894
- if (messagePollingInterval) clearInterval(messagePollingInterval);
895
- activeChatroomId = null;
896
- });
897
-
898
- document.getElementById('message-form').addEventListener('submit', async (e) => {
899
- e.preventDefault();
900
- const input = document.getElementById('message-input');
901
- const text = input.value.trim();
902
- if (text && activeChatroomId) {
903
- const originalText = text;
904
- input.value = '';
905
  try {
906
- await apiCall('/api/send_message', {
907
  method: 'POST', headers: { 'Content-Type': 'application/json' },
908
- body: JSON.stringify({ chatroom_id: activeChatroomId, sender_address: currentUser.address, text: originalText })
909
  });
910
- await fetchMessages(activeChatroomId);
911
- document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight;
912
- } catch (err) {
913
- input.value = originalText;
914
- }
915
- }
916
- });
917
-
918
- const showModal = (modalId) => document.getElementById(modalId).style.display = 'flex';
919
- const hideModal = (modalId) => document.getElementById(modalId).style.display = 'none';
920
-
921
- document.getElementById('create-room-show-modal').addEventListener('click', () => showModal('create-room-modal'));
922
- document.getElementById('create-room-form').addEventListener('submit', async (e) => {
923
- e.preventDefault();
924
- const name = document.getElementById('room-name').value.trim();
925
- const password = document.getElementById('room-password').value;
926
- if (!name) return;
927
  try {
928
- await apiCall('/api/create_chatroom', {
929
- method: 'POST', headers: { 'Content-Type': 'application/json' },
930
- body: JSON.stringify({ name, password: password || null, creator_address: currentUser.address })
 
 
 
 
931
  });
932
- hideModal('create-room-modal');
933
- showStatus('Чат успешно создан!', 'success');
934
- fetchChatrooms();
935
- } catch (err) {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
936
  });
 
 
 
 
937
 
938
- const showProfile = async (address) => {
939
- const profileModal = document.getElementById('profile-modal');
940
- try {
941
- const userData = await apiCall('/api/user_data', {
942
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address })
943
- });
944
- const username = userData.username || `User ${truncateAddress(address)}`;
945
- document.getElementById('profile-modal-title').textContent = (address === currentUser.address) ? "Мой профиль" : "Профиль пользователя";
946
- const avatarContainer = document.getElementById('profile-avatar-container');
947
- avatarContainer.innerHTML = ''; avatarContainer.appendChild(getAvatar(username, 80));
948
- document.getElementById('profile-username').textContent = username;
949
- document.getElementById('profile-address').textContent = address;
950
- const qrCodeEl = document.getElementById('profile-qr-code');
951
- qrCodeEl.innerHTML = '';
952
- if (profileQrCode) profileQrCode.clear();
953
- profileQrCode = new QRCode(qrCodeEl, { text: address, width: 150, height: 150, correctLevel : QRCode.CorrectLevel.H });
 
 
 
 
 
954
 
955
- const sendTonBtn = document.getElementById('send-ton-btn');
956
- sendTonBtn.onclick = async () => {
957
- if (!tonConnectUI.connected) { showStatus('Подключите кошелек для отправки TON.', 'error'); return; }
958
- const amountString = prompt("Введите сумму в TON для отправки:", "0.1");
959
- if (amountString === null) return;
960
- const amount = parseFloat(amountString);
961
- if (isNaN(amount) || amount <= 0) { showStatus('Неверная сумма.', 'error'); return; }
962
- const transaction = {
963
- validUntil: Math.floor(Date.now() / 1000) + 600,
964
- messages: [ { address: address, amount: Math.floor(amount * 1e9).toString() } ]
965
- };
966
- try {
967
- await tonConnectUI.sendTransaction(transaction);
968
- showStatus(`Транзакция отправлена успешно!`, 'success');
969
- hideModal('profile-modal');
970
- } catch (error) { showStatus('Транзакция отклонена.', 'error'); }
971
  };
972
- sendTonBtn.style.display = (address === currentUser.address) ? 'none' : 'flex';
973
- showModal('profile-modal');
974
- } catch (err) { showStatus('Не удалось загрузить профиль.', 'error'); }
975
- };
976
-
977
- document.getElementById('show-my-qr-btn').addEventListener('click', () => showProfile(currentUser.address));
978
-
979
- const showScanner = () => {
980
- showModal('scanner-modal');
981
- html5QrCode = new Html5Qrcode("qr-reader");
982
- html5QrCode.start({ facingMode: "environment" }, { fps: 10, qrbox: { width: 250, height: 250 } },
983
- (decodedText) => {
984
- hideScanner();
985
- if (decodedText && decodedText.length > 40 && (decodedText.startsWith('EQ') || decodedText.startsWith('UQ'))) {
986
- showProfile(decodedText);
987
- } else { showStatus('Отсканирован недействительный QR-код.', 'error'); }
988
- })
989
- .catch(() => { showStatus('Не удалось запустить сканер.', 'error'); hideScanner(); });
990
- };
991
- const hideScanner = () => {
992
- if (html5QrCode && html5QrCode.isScanning) {
993
- html5QrCode.stop().catch(err => console.error("Failed to stop QR scanner.", err));
994
- }
995
- hideModal('scanner-modal');
996
- };
997
- document.getElementById('nav-scan-btn').addEventListener('click', showScanner);
998
- document.getElementById('scanner-close-btn').addEventListener('click', hideScanner);
999
-
1000
- document.querySelectorAll('[data-close]').forEach(btn => btn.addEventListener('click', (e) => {
1001
- e.target.closest('.modal-overlay').style.display = 'none';
1002
- }));
1003
-
1004
- const showView = (viewId) => {
1005
- document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
1006
- document.getElementById(viewId).classList.add('active');
1007
-
1008
- document.querySelectorAll('.nav-item').forEach(v => v.classList.remove('active'));
1009
- let navBtnId;
1010
- if (viewId === 'chats-view') navBtnId = 'nav-chats-btn';
1011
- else if (viewId === 'users-view') navBtnId = 'nav-users-btn';
1012
- else if (viewId === 'my-profile-view') navBtnId = 'nav-profile-btn';
1013
- if(navBtnId) document.getElementById(navBtnId).classList.add('active');
1014
-
1015
- if(!isMobile) {
1016
- document.querySelectorAll('.view').forEach(v => {
1017
- v.classList.remove('hidden-desktop');
1018
- v.classList.remove('full-width');
1019
- });
1020
- document.getElementById(viewId).classList.add('active');
1021
- if(viewId === 'my-profile-view' || viewId === 'users-view') {
1022
- document.getElementById('chats-view').classList.remove('active');
1023
- document.getElementById(viewId).classList.add('full-width');
1024
  } else {
1025
- document.getElementById('chats-view').classList.add('active');
1026
  }
1027
- }
1028
- };
1029
-
1030
- document.getElementById('nav-chats-btn').addEventListener('click', () => showView('chats-view'));
1031
- document.getElementById('nav-users-btn').addEventListener('click', () => showView('users-view'));
1032
- document.getElementById('nav-profile-btn').addEventListener('click', () => showView('my-profile-view'));
1033
-
1034
- const applyTheme = (theme) => {
1035
- if (theme === 'light') {
1036
- body.classList.add('light-theme');
1037
- sunIcon.style.display = 'none';
1038
- moonIcon.style.display = 'block';
1039
- } else {
1040
- body.classList.remove('light-theme');
1041
- sunIcon.style.display = 'block';
1042
- moonIcon.style.display = 'none';
1043
- }
1044
- localStorage.setItem('theme', theme);
1045
- const themeColor = getComputedStyle(body).getPropertyValue('--bg-secondary').trim();
1046
- body.style.backgroundColor = themeColor;
1047
- document.querySelector('meta[name="theme-color"]')?.setAttribute('content', themeColor);
1048
- };
1049
- themeToggleBtn.addEventListener('click', () => {
1050
- const currentTheme = body.classList.contains('light-theme') ? 'light' : 'dark';
1051
- applyTheme(currentTheme === 'light' ? 'dark' : 'light');
1052
- });
1053
- applyTheme(localStorage.getItem('theme') || 'dark');
1054
 
1055
- window.addEventListener('resize', () => {
1056
- const newIsMobile = window.innerWidth < 768;
1057
- if (newIsMobile !== isMobile) {
1058
- isMobile = newIsMobile;
1059
- location.reload();
1060
- }
1061
- });
1062
 
1063
- tonConnectUI.onStatusChange(wallet => {
1064
- if (wallet) {
1065
- const address = TON_CONNECT_UI.toUserFriendlyAddress(wallet.account.address, false);
1066
- initializeUser(address);
1067
- } else {
1068
- currentUser = { address: null, username: null };
1069
- appView.style.display = 'none';
1070
- loginView.style.display = 'flex';
1071
- if (messagePollingInterval) clearInterval(messagePollingInterval);
1072
- activeChatroomId = null;
1073
- }
1074
- });
1075
-
1076
- document.body.style.setProperty('--bg-secondary-rgb', body.classList.contains('light-theme') ? '255, 255, 255' : '18, 18, 18');
1077
-
1078
- showView('chats-view');
1079
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1080
  </script>
1081
  </body>
1082
  </html>
@@ -1086,8 +893,13 @@ def index():
1086
  @app.route('/api/users', methods=['GET'])
1087
  def get_users():
1088
  db = read_db()
1089
- users_list = [{'address': addr, 'username': data.get('username')} for addr, data in db['users'].items()]
1090
- return jsonify({'users': users_list})
 
 
 
 
 
1091
 
1092
  @app.route('/api/user_data', methods=['POST'])
1093
  def get_user_data():
@@ -1194,6 +1006,8 @@ def send_message():
1194
 
1195
  if not all([chatroom_id, sender_address, text]):
1196
  return jsonify({'error': 'Missing data'}), 400
 
 
1197
 
1198
  db = read_db()
1199
  if chatroom_id not in db['messages']:
 
30
  def index():
31
  html_content = '''
32
  <!DOCTYPE html>
33
+ <html lang="ru" data-theme="dark">
34
  <head>
35
  <meta charset="UTF-8">
36
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
 
44
  <style>
45
  :root {
46
  --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
47
+ --transition-fast: 0.2s ease;
48
+ --transition-medium: 0.3s ease;
 
 
 
49
  }
50
 
51
+ html[data-theme='light'] {
52
+ --bg-primary: #FFFFFF;
53
+ --bg-secondary: #F0F0F0;
54
+ --bg-tertiary: #E3E3E3;
55
+ --bg-hover: #DCDCDC;
56
+ --bg-modal: rgba(255, 255, 255, 0.8);
57
+ --text-primary: #000000;
58
+ --text-secondary: #6D6D72;
59
+ --text-tertiary: #AEAEB2;
60
+ --border-color: #D1D1D6;
61
+ --accent-blue: #007AFF;
62
+ --accent-blue-light: #3395FF;
63
+ --accent-blue-gradient: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
64
+ --success-color: #34C759;
65
+ --error-color: #FF3B30;
66
+ --shadow-color: rgba(0, 0, 0, 0.1);
 
67
  }
68
 
69
+ html[data-theme='dark'] {
70
+ --bg-primary: #000000;
71
+ --bg-secondary: #1C1C1E;
72
+ --bg-tertiary: #2C2C2E;
73
+ --bg-hover: #3A3A3C;
74
+ --bg-modal: rgba(28, 28, 30, 0.8);
75
+ --text-primary: #FFFFFF;
76
+ --text-secondary: #8E8E93;
77
+ --text-tertiary: #636366;
78
+ --border-color: #38383A;
79
+ --accent-blue: #0A84FF;
80
+ --accent-blue-light: #339dff;
81
+ --accent-blue-gradient: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
82
+ --success-color: #30D158;
83
+ --error-color: #FF453A;
84
+ --shadow-color: rgba(0, 0, 0, 0.5);
85
  }
86
 
87
  * {
 
91
  -webkit-tap-highlight-color: transparent;
92
  }
93
 
 
 
 
 
 
 
94
  body {
95
  font-family: var(--font-family);
96
  background-color: var(--bg-primary);
97
  color: var(--text-primary);
98
+ overflow: hidden;
99
+ height: 100vh;
100
+ width: 100vw;
101
  display: flex;
102
  align-items: center;
103
  justify-content: center;
104
+ -webkit-font-smoothing: antialiased;
105
+ -moz-osx-font-smoothing: grayscale;
106
  }
107
 
108
+ .main-container {
109
  width: 100%;
110
  height: 100%;
111
  display: flex;
112
  flex-direction: column;
113
+ transition: opacity 0.3s ease;
114
  }
115
+
116
  #login-view {
117
  display: flex;
118
  flex-direction: column;
119
  align-items: center;
120
  justify-content: center;
121
  text-align: center;
122
+ padding: 20px;
 
123
  background: var(--bg-primary);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  }
125
+ #login-view img { width: 120px; height: 120px; margin-bottom: 24px; filter: drop-shadow(0 0 20px rgba(10, 132, 255, 0.5)); }
126
+ #login-view h1 { font-size: 3rem; font-weight: 700; background: var(--accent-blue-gradient); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 12px; }
127
+ #login-view p { font-size: 1.1rem; color: var(--text-secondary); margin-bottom: 40px; max-width: 300px; }
128
 
129
+ #app-container { display: none; width: 100%; height: 100%; }
130
+
131
+ .app-layout {
132
  width: 100%;
133
  height: 100%;
134
+ display: flex;
135
  }
136
+
137
  .main-content {
138
  flex-grow: 1;
139
+ display: flex;
140
+ flex-direction: column;
141
  position: relative;
142
+ height: 100%;
143
  overflow: hidden;
144
  }
145
+ .content-view {
146
+ display: none;
 
 
 
 
 
147
  flex-direction: column;
148
+ height: 100%;
149
+ width: 100%;
 
150
  background-color: var(--bg-primary);
151
+ animation: fadeIn 0.3s ease;
152
  }
153
+ .content-view.active { display: flex; }
154
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
 
 
 
155
 
156
  .view-header {
157
+ padding: 16px;
158
  flex-shrink: 0;
 
 
 
 
159
  display: flex;
160
  align-items: center;
161
  justify-content: space-between;
162
+ border-bottom: 1px solid var(--border-color);
 
 
 
 
163
  }
164
+ .view-header h2 { font-size: 1.8rem; font-weight: 700; }
165
+
166
+ .item-list {
167
  flex-grow: 1;
168
  overflow-y: auto;
169
  padding: 8px 0;
170
  }
171
+
172
  .list-item {
173
  display: flex;
174
  align-items: center;
175
  gap: 12px;
176
+ padding: 10px 16px;
177
  cursor: pointer;
178
+ transition: background-color var(--transition-fast);
179
  }
180
+ .list-item:hover { background-color: var(--bg-hover); }
181
+ .list-item .item-info { flex-grow: 1; overflow: hidden; }
182
+ .list-item .item-name { font-weight: 500; font-size: 1.1rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
183
+ .list-item .item-subtext { font-size: 0.9rem; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
184
+ .list-item .lock-icon { width: 16px; height: 16px; fill: var(--text-secondary); flex-shrink: 0; }
185
 
186
  .avatar {
187
  width: 48px;
 
192
  justify-content: center;
193
  font-size: 1.2rem;
194
  font-weight: 600;
195
+ color: white;
196
  flex-shrink: 0;
197
  }
198
 
199
+ #chat-window-view { display: none; }
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  .chat-header {
201
+ display: flex; align-items: center; gap: 12px; padding: 12px 16px; background-color: var(--bg-secondary); border-bottom: 1px solid var(--border-color); flex-shrink: 0;
 
 
 
 
 
 
 
202
  }
203
+ .back-btn { background: none; border: none; cursor: pointer; display: none; }
204
  .back-btn svg { width: 28px; height: 28px; fill: var(--accent-blue); }
205
+ .chat-header .avatar { width: 40px; height: 40px; }
206
  #chat-header-title { font-size: 1.1rem; font-weight: 600; }
207
+
208
  #messages-container {
209
+ flex-grow: 1; padding: 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px;
 
 
 
 
 
210
  }
211
  .message { display: flex; gap: 10px; max-width: 85%; }
212
+ .message .avatar { width: 36px; height: 36px; align-self: flex-end; }
213
  .message-content { display: flex; flex-direction: column; gap: 4px; }
214
+ .message-sender { font-size: 0.85rem; font-weight: 600; color: var(--text-secondary); word-break: break-all; cursor: pointer; padding: 0 12px; }
215
+ .message-bubble { padding: 10px 14px; border-radius: 20px; line-height: 1.4; word-wrap: break-word; font-size: 1.05rem; }
216
+ .message.sent { align-self: flex-end; }
217
+ .message.sent .message-bubble { background: var(--accent-blue-gradient); color: white; border-bottom-right-radius: 4px; }
 
 
 
218
  .message.received { align-self: flex-start; }
219
+ .message.received .message-sender { color: var(--accent-blue); }
220
+ .message.received .message-bubble { background-color: var(--bg-tertiary); border-bottom-left-radius: 4px; }
221
+
222
+ .message-form-container {
223
+ display: flex; padding: 8px 16px; gap: 12px; background-color: var(--bg-secondary); border-top: 1px solid var(--border-color); flex-shrink: 0;
 
 
 
 
224
  }
225
  #message-input {
226
+ flex-grow: 1; padding: 12px 18px; border: none; background-color: var(--bg-tertiary); color: var(--text-primary); border-radius: 22px; outline: none; font-size: 1rem; resize: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  }
228
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  .action-btn {
230
+ background: var(--accent-blue-gradient); color: white; border: none; padding: 12px 18px; border-radius: 12px; cursor: pointer; font-weight: 600; font-size: 1rem; transition: transform var(--transition-fast), box-shadow var(--transition-fast);
 
 
 
 
 
 
 
 
 
 
 
 
231
  }
232
+ .action-btn:hover { transform: scale(1.03); box-shadow: 0 4px 15px var(--shadow-color); }
233
+ .action-btn.send-btn { width: 44px; height: 44px; border-radius: 50%; flex-shrink: 0; padding: 0; display: flex; align-items: center; justify-content: center; }
234
+ .action-btn.send-btn svg { width: 22px; height: 22px; fill: white; }
235
+
 
 
 
 
 
 
 
236
  .bottom-nav {
237
+ position: fixed; bottom: 0; left: 0; right: 0; height: 84px;
238
+ background: var(--bg-modal); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
239
+ border-top: 1px solid var(--border-color); display: flex; justify-content: space-around; align-items: flex-start; padding-top: 10px; z-index: 100;
 
 
 
 
 
 
 
240
  }
241
  .nav-item {
242
+ display: flex; flex-direction: column; align-items: center; gap: 4px; color: var(--text-secondary); cursor: pointer; transition: color var(--transition-fast);
 
 
 
 
 
 
 
 
 
243
  }
244
  .nav-item.active { color: var(--accent-blue); }
245
+ .nav-item.qr-btn { margin-top: -30px; }
246
+ .nav-item .nav-icon { fill: currentColor; width: 28px; height: 28px; }
247
+ .nav-item .nav-label { font-size: 0.7rem; font-weight: 500; }
248
+ .nav-item .qr-icon-wrapper {
249
+ width: 60px; height: 60px; border-radius: 50%; background: var(--accent-blue-gradient); display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 15px var(--shadow-color);
250
  }
251
+ .nav-item .qr-icon-wrapper .nav-icon { fill: white; width: 32px; height: 32px; }
252
+
253
+ .main-content-wrapper {
254
+ width: 100%; height: 100%; display: flex;
 
 
 
 
 
255
  }
256
+ .sidebar { display: none; }
257
+
258
+ #my-profile-view {
259
+ align-items: center; justify-content: center; text-align: center; padding: 20px;
260
+ }
261
+ #my-profile-view .avatar { width: 120px; height: 120px; font-size: 4rem; margin-bottom: 20px; }
262
+ #my-profile-username { font-size: 1.8rem; font-weight: 600; }
263
+ #my-profile-address { font-size: 0.9rem; color: var(--text-secondary); word-break: break-all; margin-top: 8px; max-width: 90%; }
264
+ #my-profile-wallet-info { font-size: 1rem; color: var(--text-secondary); margin-top: 16px; }
265
+ .profile-actions { margin-top: 30px; display: flex; flex-direction: column; gap: 15px; width: 100%; max-width: 300px; }
266
+ .theme-switcher { display: flex; justify-content: space-between; align-items: center; background: var(--bg-secondary); padding: 10px 16px; border-radius: 12px; }
267
+
268
+ .switch { position: relative; display: inline-block; width: 51px; height: 31px; }
269
+ .switch input { opacity: 0; width: 0; height: 0; }
270
+ .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--bg-tertiary); transition: var(--transition-medium); border-radius: 34px; }
271
+ .slider:before { position: absolute; content: ""; height: 27px; width: 27px; left: 2px; bottom: 2px; background-color: white; transition: var(--transition-medium); border-radius: 50%; }
272
+ input:checked + .slider { background-color: var(--success-color); }
273
+ input:checked + .slider:before { transform: translateX(20px); }
274
 
275
  .modal-overlay {
276
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 1000;
277
+ backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); animation: fadeIn 0.3s;
 
 
 
 
 
 
 
 
 
 
278
  }
279
  .modal-content {
280
+ background-color: var(--bg-modal); padding: 24px; border-radius: 16px; width: 90%; max-width: 400px; border: 1px solid var(--border-color); box-shadow: 0 10px 40px var(--shadow-color);
 
 
 
 
 
 
281
  }
282
  .modal-content h3 { margin-bottom: 20px; font-weight: 600; font-size: 1.3rem; }
283
  .modal-content label { display: block; margin-bottom: 8px; font-size: 0.9rem; color: var(--text-secondary); }
284
  .modal-content input {
285
+ width: 100%; padding: 12px; margin-bottom: 16px; background-color: var(--bg-tertiary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 8px; font-size: 1rem;
 
 
 
 
 
 
 
286
  }
287
  .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
288
+ .modal-btn { padding: 10px 20px; border-radius: 8px; border: none; cursor: pointer; font-weight: 500; }
289
+ .secondary-btn { background-color: var(--bg-hover); color: var(--text-primary); }
 
 
 
 
 
 
290
 
 
 
291
  #status-bar {
292
+ position: fixed; bottom: 90px; left: 50%; transform: translateX(-50%); background-color: rgba(44, 44, 46, 0.85); color: white; padding: 12px 20px; border-radius: 12px; font-size: 0.9rem;
293
+ opacity: 0; visibility: hidden; transition: opacity var(--transition-medium), visibility var(--transition-medium), transform var(--transition-medium); z-index: 2000;
294
+ box-shadow: 0 5px 15px var(--shadow-color); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
 
 
 
 
 
 
 
 
 
 
 
295
  }
296
+ #status-bar.visible { opacity: 1; visibility: visible; transform: translate(-50%, -10px); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
 
298
  @media (min-width: 768px) {
299
+ body { padding: 20px; }
300
+ .app-layout { max-width: 1200px; max-height: 900px; margin: auto; border-radius: 20px; overflow: hidden; box-shadow: 0 20px 60px var(--shadow-color); border: 1px solid var(--border-color); }
301
+ .bottom-nav { display: none; }
302
+ .sidebar {
303
+ display: flex; flex-direction: column; align-items: center; width: 80px; flex-shrink: 0; padding: 20px 0;
304
+ background-color: var(--bg-secondary); border-right: 1px solid var(--border-color); z-index: 1;
 
 
 
 
 
 
 
305
  }
306
+ .sidebar .nav-item { gap: 8px; padding: 15px 0; width: 100%; }
307
+ .sidebar .nav-item .nav-icon { width: 24px; height: 24px; }
308
+ .sidebar .nav-item .nav-label { font-size: 0.75rem; }
309
+ .sidebar .nav-item.qr-btn { margin-top: 0; order: -1; margin-bottom: 20px; }
310
+ .sidebar .nav-item .qr-icon-wrapper { width: 52px; height: 52px; }
311
+ .sidebar .nav-item .qr-icon-wrapper .nav-icon { width: 28px; height: 28px; }
312
+
313
+ .content-view { border-right: 1px solid var(--border-color); }
314
+ #chat-list-view, #user-list-view, #my-profile-view { width: 340px; flex-shrink: 0; }
315
+ #chat-list-view.active, #user-list-view.active, #my-profile-view.active { display: flex; }
316
+ .main-content {
317
+ flex-direction: row;
318
+ }
319
+ .back-btn { display: none !important; }
320
 
321
+ #chat-window-view { display: flex; flex-grow: 1; }
322
+ .placeholder-view { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; text-align: center; color: var(--text-secondary); padding: 20px; background: var(--bg-primary); }
323
+ .placeholder-view img { width: 80px; margin-bottom: 20px; opacity: 0.5; }
324
+
325
+ #app-container { padding-bottom: 0; }
326
+ #status-bar { bottom: 30px; }
327
+ }
328
  </style>
329
  </head>
330
  <body>
331
+ <div id="login-view" class="main-container">
 
332
  <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
333
  <h1>Virton</h1>
334
+ <p>Децентрализованный и анонимный мессенджер на TON</p>
335
  <div id="ton-connect-button"></div>
336
  </div>
337
 
338
+ <div id="app-container">
339
+ <div class="app-layout">
340
+ <nav class="sidebar" id="sidebar-nav"></nav>
341
+ <div class="main-content">
342
+ <div id="chat-list-view" class="content-view">
343
+ <div class="view-header">
344
+ <h2>Чаты</h2>
345
+ <button id="create-room-show-modal" class="action-btn" style="padding: 8px 14px; font-size: 0.9rem;">Новый чат</button>
346
+ </div>
347
+ <div class="item-list" id="chatroom-list"></div>
 
 
 
 
 
348
  </div>
 
 
349
 
350
+ <div id="user-list-view" class="content-view">
351
+ <div class="view-header">
352
+ <h2>Пользователи</h2>
353
+ </div>
354
+ <div class="item-list" id="user-list"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  </div>
 
356
 
357
+ <div id="my-profile-view" class="content-view">
358
+ <div id="my-profile-content" style="width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; padding: 20px;">
359
+ <div class="avatar" id="my-profile-avatar"></div>
360
+ <h2 id="my-profile-username"></h2>
361
+ <p id="my-profile-address"></p>
362
+ <p id="my-profile-wallet-info"></p>
363
+ <div class="profile-actions">
364
+ <form id="username-form" style="display: flex; gap: 8px;">
365
+ <input type="text" id="username-input" class="username-input" placeholder="Новый никнейм" autocomplete="off" style="width: 100%; padding: 12px; background-color: var(--bg-tertiary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 8px; font-size: 1rem;">
366
+ <button type="submit" class="action-btn" style="padding: 0 16px;">✓</button>
367
+ </form>
368
+ <button id="my-qr-code-btn" class="action-btn secondary-btn" style="background: var(--bg-tertiary);">Мой QR-код</button>
369
+ <div class="theme-switcher">
370
+ <span>Тёмная тема</span>
371
+ <label class="switch">
372
+ <input type="checkbox" id="theme-toggle">
373
+ <span class="slider"></span>
374
+ </label>
375
+ </div>
376
+ </div>
377
+ </div>
378
  </div>
379
+
380
+ <div id="chat-window-view">
381
+ <div class="placeholder-view" id="placeholder-view">
382
+ <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
383
+ <h2>Выберите чат</h2>
384
+ <p>Начните общение или просмотрите список пользователей</p>
385
+ </div>
386
+ <div id="active-chat-content" style="display: none; width: 100%; height: 100%; flex-direction: column;">
387
+ <div class="chat-header">
388
+ <button class="back-btn" id="back-to-list-btn">
389
+ <svg viewBox="0 0 24 24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>
390
+ </button>
391
+ <div id="chat-header-avatar" class="avatar"></div>
392
+ <span id="chat-header-title"></span>
393
+ </div>
394
+ <div id="messages-container"></div>
395
+ <div class="message-form-container">
396
+ <input id="message-input" placeholder="Сообщение..." autocomplete="off">
397
+ <button type="button" class="action-btn send-btn" id="send-btn">
398
+ <svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
399
+ </button>
400
+ </div>
401
  </div>
 
 
 
 
 
 
 
402
  </div>
403
  </div>
404
  </div>
405
+ <nav class="bottom-nav" id="bottom-nav-bar"></nav>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  </div>
407
 
408
  <div id="create-room-modal" class="modal-overlay">
 
414
  <label for="room-password">Пароль (оставьте пустым для открытого)</label>
415
  <input type="password" id="room-password">
416
  <div class="modal-actions">
417
+ <button type="button" class="modal-btn secondary-btn" data-modal-close>Отмена</button>
418
  <button type="submit" class="modal-btn action-btn">Создать</button>
419
  </div>
420
  </form>
 
428
  <label for="password-input">Введите пароль</label>
429
  <input type="password" id="password-input" required>
430
  <div class="modal-actions">
431
+ <button type="button" class="modal-btn secondary-btn" data-modal-close>Отмена</button>
432
  <button type="submit" class="modal-btn action-btn">Войти</button>
433
  </div>
434
  </form>
 
437
 
438
  <div id="profile-modal" class="modal-overlay">
439
  <div class="modal-content" style="text-align: center;">
440
+ <h3>Профиль</h3>
441
  <div id="profile-avatar-container" style="margin: 20px auto; display: inline-block;"></div>
442
  <p id="profile-username" style="font-size: 1.2rem; font-weight: 600;"></p>
443
  <p id="profile-address" style="color: var(--text-secondary); font-size: 0.9rem; word-break: break-all; margin-top: 8px;"></p>
444
  <div id="profile-qr-code" style="background: white; padding: 10px; margin: 20px auto; width: fit-content; border-radius: 8px;"></div>
445
+ <p style="text-align: center; color: var(--text-secondary); font-size: 0.8rem; margin-top: -10px; margin-bottom: 20px;">QR для открытия профиля</p>
446
  <div class="modal-actions" style="flex-direction: column; gap: 12px; align-items: stretch;">
447
  <button id="send-ton-btn" class="modal-btn action-btn">Отправить TON</button>
448
+ <button data-modal-close class="modal-btn secondary-btn">Закрыть</button>
449
  </div>
450
  </div>
451
  </div>
 
453
  <div id="scanner-modal" class="modal-overlay">
454
  <div class="modal-content">
455
  <h3>Сканировать QR-код</h3>
456
+ <div id="qr-reader" style="width: 100%; border: 1px solid var(--border-color); margin-top: 16px; border-radius: 8px; overflow: hidden;"></div>
457
  <div class="modal-actions">
458
  <button id="scanner-close-btn" class="modal-btn secondary-btn">Отмена</button>
459
  </div>
 
463
  <div id="status-bar"></div>
464
 
465
  <script>
466
+ document.addEventListener('DOMContentLoaded', () => {
467
+ const tonConnectUI = new TON_CONNECT_UI.TonConnectUI({
468
+ manifestUrl: 'https://huggingface.co/spaces/Aleksmorshen/MorshenGroup/resolve/main/tonconnect-manifest.json',
469
+ buttonRootId: 'ton-connect-button'
470
+ });
471
+
472
+ let currentUser = { address: null, username: null, wallet: null };
473
+ let activeChatroomId = null;
474
+ let messagePollingInterval = null;
475
+ let chatroomsData = {};
476
+ let html5QrCode = null;
477
+ let profileQrCode = null;
478
 
479
+ const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292'];
480
+
481
+ const getAvatar = (name, size = '48px') => {
482
+ const initial = (name ? name[0] : '?').toUpperCase();
483
+ const charCode = initial.charCodeAt(0);
484
+ const color = AVATAR_COLORS[charCode % AVATAR_COLORS.length];
485
+ const avatar = document.createElement('div');
486
+ avatar.className = 'avatar';
487
+ avatar.style.backgroundColor = color;
488
+ avatar.style.width = size;
489
+ avatar.style.height = size;
490
+ avatar.textContent = initial;
491
+ return avatar;
492
+ };
493
+
494
+ const apiCall = async (endpoint, options = {}) => {
495
+ try {
496
+ const response = await fetch(endpoint, options);
497
+ if (!response.ok) {
498
+ const errorData = await response.json().catch(() => ({ error: 'Request failed: ' + response.status }));
499
+ throw new Error(errorData.error || 'Unknown error');
500
+ }
501
+ if (response.status === 204) return null;
502
+ return await response.json();
503
+ } catch (error) {
504
+ showStatus(`Ошибка: ${error.message}`, 'error');
505
+ throw error;
506
+ }
507
+ };
508
+
509
+ const truncateAddress = (address) => address ? `${address.substring(0, 4)}...${address.substring(address.length - 4)}` : '';
510
+
511
+ const showStatus = (message, type = 'info', duration = 3000) => {
512
+ const statusBar = document.getElementById('status-bar');
513
+ statusBar.textContent = message;
514
+ statusBar.className = 'status-bar';
515
+ if (type === 'success') statusBar.style.backgroundColor = 'var(--success-color)';
516
+ else if (type === 'error') statusBar.style.backgroundColor = 'var(--error-color)';
517
+ else statusBar.style.backgroundColor = 'rgba(44, 44, 46, 0.85)';
518
 
519
+ statusBar.classList.add('visible');
520
+ setTimeout(() => statusBar.classList.remove('visible'), duration);
521
+ };
522
+
523
+ const showView = (viewId) => {
524
+ document.querySelectorAll('.content-view').forEach(v => v.classList.remove('active'));
525
+ document.getElementById(viewId)?.classList.add('active');
526
 
527
+ document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active'));
528
+ document.querySelectorAll(`[data-view='${viewId}']`).forEach(item => item.classList.add('active'));
529
+
530
+ const isMobile = window.innerWidth < 768;
531
+ if (isMobile) {
532
+ document.getElementById('app-container').scrollTo(0,0);
533
+ if (viewId === 'chat-window-view') {
534
+ document.querySelector('.app-layout').style.transform = 'translateX(-100%)';
535
+ } else {
536
+ document.querySelector('.app-layout').style.transform = 'translateX(0%)';
537
+ document.querySelectorAll('.content-view').forEach(v => v.style.display = 'none');
538
+ const targetView = document.getElementById(viewId);
539
+ if(targetView) targetView.style.display = 'flex';
540
+ }
541
+ }
542
+ };
543
+
544
+ const renderNav = () => {
545
+ const navItems = [
546
+ { id: 'chat-list-view', label: 'Чаты', icon: '<path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/>' },
547
+ { id: 'user-list-view', label: 'Контакты', icon: '<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"/>' },
548
+ { id: 'scanner', label: 'Scan', icon: '<path d="M3 11h8V3H3v8zm2-6h4v4H5V5zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm8-12v8h8V3h-8zm6 6h-4V5h4v4zm-2 10a2 2 0 100-4 2 2 0 000 4z"/>', isQr: true },
549
+ { id: 'my-profile-view', label: 'Профиль', icon: '<path d="M12 5.9c1.16 0 2.1.94 2.1 2.1s-.94 2.1-2.1 2.1S9.9 9.16 9.9 8s.94-2.1 2.1-2.1m0 9c2.97 0 6.1 1.46 6.1 2.1v1.1H5.9V17c0-.64 3.13-2.1 6.1-2.1M12 4C9.79 4 8 5.79 8 8s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 9c-2.67 0-8 1.34-8 4v3h16v-3c0-2.66-5.33-4-8-4z"/>' }
550
+ ];
551
+
552
+ const createNavItem = (item) => {
553
+ const el = document.createElement('div');
554
+ el.className = 'nav-item';
555
+ if (item.isQr) el.classList.add('qr-btn');
556
+ el.dataset.view = item.id;
557
+ if (item.isQr) {
558
+ el.innerHTML = `<div class="qr-icon-wrapper"><svg class="nav-icon" viewBox="0 0 24 24">${item.icon}</svg></div>`;
559
+ el.onclick = showScanner;
560
+ } else {
561
+ el.innerHTML = `<svg class="nav-icon" viewBox="0 0 24 24">${item.icon}</svg><span class="nav-label">${item.label}</span>`;
562
+ el.onclick = () => showView(item.id);
563
  }
564
+ return el;
 
 
565
  };
566
 
567
+ const bottomNav = document.getElementById('bottom-nav-bar');
568
+ const sidebarNav = document.getElementById('sidebar-nav');
569
+ bottomNav.innerHTML = '';
570
+ sidebarNav.innerHTML = '';
 
 
 
 
 
571
 
572
+ navItems.forEach(item => {
573
+ bottomNav.appendChild(createNavItem(item));
574
+ sidebarNav.appendChild(createNavItem(item));
575
+ });
576
+ };
577
+
578
+ const initializeUser = async (wallet) => {
579
+ currentUser.address = TON_CONNECT_UI.toUserFriendlyAddress(wallet.account.address, false);
580
+ currentUser.wallet = wallet;
581
+ try {
582
+ const data = await apiCall('/api/user_data', {
583
+ method: 'POST',
584
+ headers: { 'Content-Type': 'application/json' },
585
+ body: JSON.stringify({ address: currentUser.address })
586
+ });
587
+ currentUser.username = data.username;
588
+ } catch (err) { currentUser.username = null; }
589
+
590
+ document.getElementById('login-view').style.display = 'none';
591
+ document.getElementById('app-container').style.display = 'block';
592
 
593
+ renderNav();
594
+ renderMyProfile();
595
+ fetchChatrooms();
596
+ fetchAndRenderUsers();
597
+ showView('chat-list-view');
598
+ };
599
+
600
+ const renderMyProfile = () => {
601
+ const username = currentUser.username || `User ${truncateAddress(currentUser.address)}`;
602
+ document.getElementById('my-profile-avatar').innerHTML = getAvatar(username, '120px').innerHTML;
603
+ document.getElementById('my-profile-username').textContent = username;
604
+ document.getElementById('my-profile-address').textContent = currentUser.address;
605
+ document.getElementById('username-input').value = currentUser.username || '';
606
+ if(currentUser.wallet?.device) {
607
+ document.getElementById('my-profile-wallet-info').textContent = `Кошелек: ${currentUser.wallet.device.appName}`;
608
+ }
609
+ };
610
+
611
+ document.getElementById('username-form').addEventListener('submit', async (e) => {
612
+ e.preventDefault();
613
+ const newUsername = document.getElementById('username-input').value.trim();
614
+ if (!newUsername || newUsername.length < 3) return showStatus('Никнейм должен быть не короче 3 символов.', 'error');
615
 
616
+ await apiCall('/api/set_username', {
617
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
618
+ body: JSON.stringify({ address: currentUser.address, username: newUsername })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
  });
620
+ currentUser.username = newUsername;
621
+ renderMyProfile();
622
+ fetchChatrooms();
623
+ fetchAndRenderUsers();
624
+ if (activeChatroomId) fetchMessages(activeChatroomId);
625
+ showStatus('Никнейм успешно обновлен!', 'success');
626
+ });
627
 
628
+ const renderList = (containerId, items, clickHandler, itemRenderer) => {
629
+ const list = document.getElementById(containerId);
630
+ list.innerHTML = '';
631
+ items.forEach(item => {
632
+ const itemEl = itemRenderer(item);
633
+ itemEl.onclick = () => clickHandler(item);
634
+ list.appendChild(itemEl);
635
+ });
636
+ };
637
+
638
+ const fetchChatrooms = async () => {
639
+ try {
640
+ const data = await apiCall('/api/chatrooms');
641
+ chatroomsData = data.chatrooms.reduce((acc, room) => ({...acc, [room.id]: room}), {});
642
+ renderList('chatroom-list', data.chatrooms, (room) => selectChatroom(room.id, room.is_private), (room) => {
643
+ const itemEl = document.createElement('div');
644
+ itemEl.className = 'list-item';
645
+ itemEl.appendChild(getAvatar(room.name, '48px'));
646
+ itemEl.innerHTML += `<div class="item-info"><div class="item-name">${room.name}</div><div class="item-subtext">${room.is_private ? "Приватный чат" : "Открытый чат"}</div></div>`;
647
+ if (room.is_private) itemEl.innerHTML += `<svg class="lock-icon" 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>`;
648
+ return itemEl;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
649
  });
650
+ } catch (err) {}
651
+ };
652
+
653
+ const fetchAndRenderUsers = async () => {
654
+ try {
655
+ const data = await apiCall('/api/users');
656
+ renderList('user-list', data.users, (user) => showProfile(user.address), (user) => {
657
+ const itemEl = document.createElement('div');
658
+ itemEl.className = 'list-item';
659
+ itemEl.appendChild(getAvatar(user.username, '48px'));
660
+ itemEl.innerHTML += `<div class="item-info"><div class="item-name">${user.username || 'Без имени'}</div><div class="item-subtext">${truncateAddress(user.address)}</div></div>`;
661
+ return itemEl;
 
 
 
 
 
662
  });
663
+ } catch (err) {}
664
+ };
665
+
666
+ const renderMessages = (messages) => {
667
+ const container = document.getElementById('messages-container');
668
+ const shouldScroll = container.scrollTop + container.clientHeight >= container.scrollHeight - 50;
669
+ container.innerHTML = '';
670
+ messages.forEach(msg => {
671
+ const msgDiv = document.createElement('div');
672
+ const isSent = msg.sender_address === currentUser.address;
673
+ msgDiv.className = 'message ' + (isSent ? 'sent' : 'received');
674
+ const avatar = getAvatar(msg.display_name, '36px');
675
+ avatar.style.cursor = 'pointer';
676
+ avatar.onclick = () => showProfile(msg.sender_address);
677
+
678
+ msgDiv.innerHTML = `
679
+ <div class="message-content">
680
+ <div class="message-sender" onclick="showProfile('${msg.sender_address}')">${isSent ? 'Вы' : msg.display_name}</div>
681
+ <div class="message-bubble">${msg.text.replace(/</g, "<").replace(/>/g, ">")}</div>
682
+ </div>
683
+ `;
684
+ msgDiv.insertBefore(avatar, msgDiv.firstChild);
685
+ container.appendChild(msgDiv);
686
+ });
687
+ if(shouldScroll) container.scrollTop = container.scrollHeight;
688
+ };
689
+
690
+ const fetchMessages = async (roomId) => {
691
+ try {
692
+ const data = await apiCall(`/api/messages/${roomId}`);
693
+ renderMessages(data.messages);
694
+ } catch (err) {
695
+ if (messagePollingInterval) clearInterval(messagePollingInterval);
696
+ }
697
+ };
698
+
699
+ const selectChatroom = (roomId, isPrivate) => {
700
+ const roomData = chatroomsData[roomId];
701
+ if (!roomData) return;
702
 
703
+ const proceedToRoom = () => {
704
+ if (messagePollingInterval) clearInterval(messagePollingInterval);
705
+ activeChatroomId = roomId;
706
+
707
+ document.getElementById('chat-header-title').textContent = roomData.name;
708
+ document.getElementById('chat-header-avatar').innerHTML = getAvatar(roomData.name, '40px').innerHTML;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
709
 
710
+ document.getElementById('placeholder-view').style.display = 'none';
711
+ document.getElementById('active-chat-content').style.display = 'flex';
 
 
 
 
 
 
 
 
 
 
 
 
 
712
 
713
+ showView('chat-window-view');
714
+
715
+ fetchMessages(roomId).then(() => {
716
+ document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight;
717
+ });
718
+ messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
719
  };
720
+
721
+ if (isPrivate) {
722
+ const passwordModal = document.getElementById('password-modal');
723
+ const passwordForm = document.getElementById('password-form');
724
+ const passwordInput = document.getElementById('password-input');
725
+ passwordModal.style.display = 'flex';
726
+ passwordInput.value = '';
727
+ passwordInput.focus();
728
+
729
+ passwordForm.onsubmit = async (e) => {
730
+ e.preventDefault();
731
+ const password = passwordInput.value;
732
+ passwordModal.style.display = 'none';
733
  try {
734
+ await apiCall('/api/join_chatroom', {
735
  method: 'POST', headers: { 'Content-Type': 'application/json' },
736
+ body: JSON.stringify({ chatroom_id: roomId, password })
737
  });
738
+ proceedToRoom();
739
+ } catch (err) {}
740
+ };
741
+ } else {
742
+ proceedToRoom();
743
+ }
744
+ };
745
+
746
+ const sendMessage = async () => {
747
+ const input = document.getElementById('message-input');
748
+ const text = input.value.trim();
749
+ if (text && activeChatroomId) {
750
+ const tempText = input.value;
751
+ input.value = '';
 
 
 
752
  try {
753
+ await apiCall('/api/send_message', {
754
+ method: 'POST',
755
+ headers: { 'Content-Type': 'application/json' },
756
+ body: JSON.stringify({ chatroom_id: activeChatroomId, sender_address: currentUser.address, text: text })
757
+ });
758
+ fetchMessages(activeChatroomId).then(() => {
759
+ document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight;
760
  });
761
+ } catch {
762
+ input.value = tempText;
763
+ }
764
+ }
765
+ };
766
+ document.getElementById('send-btn').addEventListener('click', sendMessage);
767
+ document.getElementById('message-input').addEventListener('keypress', (e) => {
768
+ if(e.key === 'Enter' && !e.shiftKey) {
769
+ e.preventDefault();
770
+ sendMessage();
771
+ }
772
+ });
773
+
774
+ const showModal = (modalId) => document.getElementById(modalId).style.display = 'flex';
775
+ const hideAllModals = () => document.querySelectorAll('.modal-overlay').forEach(m => m.style.display = 'none');
776
+
777
+ document.getElementById('create-room-show-modal').onclick = () => showModal('create-room-modal');
778
+ document.querySelectorAll('[data-modal-close]').forEach(btn => btn.onclick = hideAllModals);
779
+
780
+ document.getElementById('create-room-form').addEventListener('submit', async (e) => {
781
+ e.preventDefault();
782
+ const name = document.getElementById('room-name').value.trim();
783
+ const password = document.getElementById('room-password').value;
784
+ if (!name) return;
785
+ await apiCall('/api/create_chatroom', {
786
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
787
+ body: JSON.stringify({ name, password: password || null, creator_address: currentUser.address })
788
  });
789
+ hideAllModals();
790
+ showStatus('Чат успешно создан!', 'success');
791
+ fetchChatrooms();
792
+ });
793
 
794
+ const showProfile = async (address) => {
795
+ try {
796
+ const userData = await apiCall('/api/user_data', {
797
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address })
798
+ });
799
+
800
+ const username = userData.username || `User ${truncateAddress(address)}`;
801
+ document.getElementById('profile-avatar-container').innerHTML = getAvatar(username, '80px').outerHTML;
802
+ document.getElementById('profile-username').textContent = username;
803
+ document.getElementById('profile-address').textContent = address;
804
+ const qrCodeEl = document.getElementById('profile-qr-code');
805
+ qrCodeEl.innerHTML = '';
806
+ if (profileQrCode) profileQrCode.clear();
807
+ profileQrCode = new QRCode(qrCodeEl, { text: address, width: 150, height: 150, colorDark : "#000000", colorLight : "#ffffff", correctLevel : QRCode.CorrectLevel.H });
808
+
809
+ const sendTonBtn = document.getElementById('send-ton-btn');
810
+ sendTonBtn.onclick = async () => {
811
+ const amountString = prompt("Введите сумму в TON:", "0.1");
812
+ if (amountString === null) return;
813
+ const amount = parseFloat(amountString);
814
+ if (isNaN(amount) || amount <= 0) return showStatus('Неверная сумма.', 'error');
815
 
816
+ const transaction = {
817
+ validUntil: Math.floor(Date.now() / 1000) + 600,
818
+ messages: [ { address: address, amount: Math.floor(amount * 1e9).toString() } ]
 
 
 
 
 
 
 
 
 
 
 
 
 
819
  };
820
+ try {
821
+ await tonConnectUI.sendTransaction(transaction);
822
+ showStatus(`Транзакция отправлена!`, 'success');
823
+ hideAllModals();
824
+ } catch (error) { showStatus('Транзакция отклонена.', 'error'); }
825
+ };
826
+ sendTonBtn.style.display = (address === currentUser.address) ? 'none' : 'block';
827
+ showModal('profile-modal');
828
+ } catch (err) {}
829
+ };
830
+
831
+ document.getElementById('my-qr-code-btn').onclick = () => {
832
+ if (currentUser.address) showProfile(currentUser.address);
833
+ };
834
+
835
+ const showScanner = () => {
836
+ showModal('scanner-modal');
837
+ html5QrCode = new Html5Qrcode("qr-reader");
838
+ html5QrCode.start({ facingMode: "environment" }, { fps: 10, qrbox: { width: 250, height: 250 } },
839
+ (decodedText) => {
840
+ hideScanner();
841
+ if (decodedText && (decodedText.startsWith('EQ') || decodedText.startsWith('UQ'))) {
842
+ showProfile(decodedText);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
843
  } else {
844
+ showStatus('Отсканирован недействительный QR-код.', 'error');
845
  }
846
+ })
847
+ .catch(err => showStatus('Не удалось запустить сканер.', 'error'));
848
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
849
 
850
+ const hideScanner = () => {
851
+ if (html5QrCode && html5QrCode.isScanning) {
852
+ html5QrCode.stop().catch(err => {});
853
+ }
854
+ hideAllModals();
855
+ };
856
+ document.getElementById('scanner-close-btn').onclick = hideScanner;
857
 
858
+ document.getElementById('back-to-list-btn').addEventListener('click', () => {
859
+ document.querySelector('.app-layout').style.transform = 'translateX(0%)';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
860
  });
861
+
862
+ const themeToggle = document.getElementById('theme-toggle');
863
+ const setTeam = (theme) => {
864
+ document.documentElement.setAttribute('data-theme', theme);
865
+ localStorage.setItem('theme', theme);
866
+ themeToggle.checked = theme === 'dark';
867
+ };
868
+ themeToggle.addEventListener('change', () => {
869
+ setTeam(themeToggle.checked ? 'dark' : 'light');
870
+ });
871
+ const savedTheme = localStorage.getItem('theme');
872
+ const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
873
+ setTeam(savedTheme || (prefersDark ? 'dark' : 'light'));
874
+
875
+ tonConnectUI.onStatusChange(wallet => {
876
+ if (wallet) {
877
+ initializeUser(wallet);
878
+ } else {
879
+ currentUser = { address: null, username: null, wallet: null };
880
+ document.getElementById('app-container').style.display = 'none';
881
+ document.getElementById('login-view').style.display = 'flex';
882
+ if (messagePollingInterval) clearInterval(messagePollingInterval);
883
+ activeChatroomId = null;
884
+ }
885
+ });
886
+ });
887
  </script>
888
  </body>
889
  </html>
 
893
  @app.route('/api/users', methods=['GET'])
894
  def get_users():
895
  db = read_db()
896
+ users_list = []
897
+ for address, user_data in db['users'].items():
898
+ users_list.append({
899
+ 'address': address,
900
+ 'username': user_data.get('username')
901
+ })
902
+ return jsonify({'users': sorted(users_list, key=lambda x: x.get('username') or 'zzzz')})
903
 
904
  @app.route('/api/user_data', methods=['POST'])
905
  def get_user_data():
 
1006
 
1007
  if not all([chatroom_id, sender_address, text]):
1008
  return jsonify({'error': 'Missing data'}), 400
1009
+ if len(text) > 1000:
1010
+ return jsonify({'error': 'Message is too long'}), 400
1011
 
1012
  db = read_db()
1013
  if chatroom_id not in db['messages']: