AstraOS commited on
Commit
814ddd5
·
verified ·
1 Parent(s): 4db902d

Upload 4 files

Browse files
Files changed (4) hide show
  1. css/styles.css +980 -0
  2. index.html +146 -19
  3. js/irc.js +285 -0
  4. js/main.js +621 -0
css/styles.css ADDED
@@ -0,0 +1,980 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* CSS Reset */
2
+ * {
3
+ margin: 0;
4
+ padding: 0;
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ /* Variables (Telegram Web Colors - Refined) */
9
+ :root {
10
+ --tg-bg-color: #f2f2f2; /* Slightly lighter background */
11
+ --tg-sidebar-bg-color: #ffffff;
12
+ --tg-chat-bg-color: #e5ddd5;
13
+ --tg-text-color: #2a2a2a;
14
+ --tg-link-color: #0088cc;
15
+ --tg-sent-message-bg: #d9f0fd;
16
+ --tg-received-message-bg: #ffffff;
17
+ --tg-border-color: #e0e0e0;
18
+ --tg-input-bg-color: #f9f9f9;
19
+ --tg-input-border-color: #d9d9d9;
20
+ --tg-input-placeholder-color: #9a9a9a;
21
+ --tg-active-item-bg: #e8f4fd;
22
+ --tg-hover-item-bg: #f8f8f8;
23
+
24
+ /* Telegram Web Specific Refinements */
25
+ --tg-message-radius: 12px; /* More rounded message corners */
26
+ --tg-message-padding-vertical: 10px; /* Adjusted message padding */
27
+ --tg-message-padding-horizontal: 14px;
28
+ --tg-chat-item-avatar-size: 42px; /* Slightly larger avatars in chat list */
29
+ --tg-chat-item-padding-vertical: 10px; /* Adjusted chat item padding */
30
+ --tg-chat-item-padding-horizontal: 16px;
31
+ --tg-header-height: 56px; /* Standard Telegram header height */
32
+ --tg-input-height: 48px; /* Standard Telegram input height */
33
+ --tg-scrollbar-width: 8px; /* Thinner scrollbar */
34
+ --tg-scrollbar-color: rgba(0, 0, 0, 0.2); /* Scrollbar color */
35
+ --tg-scrollbar-track-color: transparent; /* Scrollbar track color */
36
+ }
37
+
38
+ body {
39
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
40
+ background: var(--tg-bg-color);
41
+ overflow: hidden;
42
+ }
43
+
44
+ /* App Layout */
45
+ .app {
46
+ display: flex;
47
+ height: 100vh;
48
+ }
49
+
50
+ /* Chat List (Left Sidebar) */
51
+ .chat-list {
52
+ width: 320px; /* Wider sidebar for better Telegram clone */
53
+ background: var(--tg-sidebar-bg-color);
54
+ border-right: 1px solid var(--tg-border-color);
55
+ display: flex;
56
+ flex-direction: column;
57
+ }
58
+
59
+ .chat-list-header {
60
+ padding: 14px 16px;
61
+ background: var(--tg-sidebar-bg-color);
62
+ border-bottom: 1px solid var(--tg-border-color);
63
+ display: flex;
64
+ flex-direction: column;
65
+ }
66
+
67
+ .chat-list-header h2 {
68
+ margin-bottom: 7px;
69
+ font-size: 1.1em;
70
+ color: var(--tg-text-color);
71
+ font-weight: 500;
72
+ }
73
+
74
+ .chat-list-header button.connect-btn {
75
+ width: 100%;
76
+ padding: 8px 14px;
77
+ border: 1px solid var(--tg-input-border-color);
78
+ border-radius: 24px;
79
+ outline: none;
80
+ font-size: 0.95em;
81
+ background-color: var(--tg-link-color);
82
+ color: white;
83
+ margin-bottom: 8px;
84
+ text-align: left;
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: center;
88
+ gap: 8px;
89
+ }
90
+
91
+ .chat-list-header input {
92
+ width: 100%;
93
+ padding: 8px 14px;
94
+ border: 1px solid var(--tg-input-border-color);
95
+ border-radius: 24px;
96
+ outline: none;
97
+ font-size: 0.95em;
98
+ background-color: var(--tg-input-bg-color);
99
+ color: var(--tg-text-color);
100
+ transition: border-color 0.2s;
101
+ }
102
+
103
+ .chat-list-header input:focus {
104
+ border-color: var(--tg-link-color);
105
+ box-shadow: 0 0 0 1px var(--tg-link-color);
106
+ }
107
+
108
+ .chat-items {
109
+ list-style: none;
110
+ overflow-y: auto;
111
+ flex-grow: 1;
112
+ scrollbar-width: thin;
113
+ scrollbar-color: var(--tg-scrollbar-color) var(--tg-scrollbar-track-color);
114
+ }
115
+ .chat-items::-webkit-scrollbar {
116
+ width: var(--tg-scrollbar-width);
117
+ }
118
+ .chat-items::-webkit-scrollbar-track {
119
+ background: var(--tg-scrollbar-track-color);
120
+ }
121
+ .chat-items::-webkit-scrollbar-thumb {
122
+ background-color: var(--tg-scrollbar-color);
123
+ border-radius: 4px;
124
+ }
125
+
126
+ .chat-item {
127
+ display: flex;
128
+ align-items: center;
129
+ justify-content: space-between;
130
+ padding: var(--tg-chat-item-padding-vertical) var(--tg-chat-item-padding-horizontal);
131
+ cursor: pointer;
132
+ transition: background 0.2s;
133
+ border-bottom: 1px solid var(--tg-border-color);
134
+ }
135
+
136
+ .chat-item.active {
137
+ background: var(--tg-active-item-bg);
138
+ }
139
+
140
+ .chat-item:hover {
141
+ background-color: var(--tg-hover-item-bg);
142
+ }
143
+
144
+ .chat-item .avatar {
145
+ margin-right: 14px;
146
+ display: flex;
147
+ align-items: center;
148
+ justify-content: center;
149
+ width: var(--tg-chat-item-avatar-size);
150
+ height: var(--tg-chat-item-avatar-size);
151
+ background: var(--tg-border-color);
152
+ border-radius: 50%;
153
+ font-weight: 600;
154
+ font-size: 18px;
155
+ color: var(--tg-chat-bg-color);
156
+ overflow: hidden; /* Ensure content inside avatar stays within bounds */
157
+ }
158
+
159
+ .chat-item .avatar svg {
160
+ width: 100%;
161
+ height: 100%;
162
+ }
163
+
164
+ .chat-item .chat-info {
165
+ display: flex;
166
+ flex-direction: column;
167
+ overflow: hidden;
168
+ flex-grow: 1;
169
+ margin-right: 10px;
170
+ }
171
+
172
+ .chat-name {
173
+ font-weight: 500;
174
+ margin-bottom: 3px;
175
+ color: var(--tg-text-color);
176
+ white-space: nowrap;
177
+ overflow: hidden;
178
+ text-overflow: ellipsis;
179
+ font-size: 1.05em;
180
+ }
181
+
182
+ .chat-snippet {
183
+ font-size: 0.9em;
184
+ color: var(--tg-input-placeholder-color);
185
+ white-space: nowrap;
186
+ overflow: hidden;
187
+ text-overflow: ellipsis;
188
+ }
189
+
190
+ .chat-item .chat-time {
191
+ font-size: 0.8em;
192
+ color: var(--tg-input-placeholder-color);
193
+ align-self: flex-start;
194
+ }
195
+
196
+ .chat-item .unread-badge {
197
+ background-color: var(--tg-link-color);
198
+ color: white;
199
+ font-size: 0.75em;
200
+ padding: 4px 8px;
201
+ border-radius: 12px;
202
+ font-weight: bold;
203
+ align-self: flex-end;
204
+ }
205
+
206
+ .chat-item .unread-badge.hidden {
207
+ display: none;
208
+ }
209
+
210
+ .chat-item .channel-info {
211
+ display: flex;
212
+ flex-direction: column;
213
+ gap: 4px;
214
+ }
215
+
216
+ .channel-users-count {
217
+ font-size: 0.8em;
218
+ color: var(--tg-input-placeholder-color);
219
+ }
220
+
221
+ /* Chat Window (Right Panel) */
222
+ .chat-window {
223
+ flex-grow: 1;
224
+ display: grid;
225
+ grid-template-columns: 1fr 300px;
226
+ grid-template-rows: var(--tg-header-height) 1fr auto;
227
+ background: var(--tg-chat-bg-color);
228
+ position: relative;
229
+ }
230
+
231
+ .chat-header {
232
+ grid-column: 1 / -1;
233
+ padding: 0 16px;
234
+ height: var(--tg-header-height);
235
+ background: var(--tg-sidebar-bg-color);
236
+ border-bottom: 1px solid var(--tg-border-color);
237
+ display: flex;
238
+ justify-content: space-between;
239
+ align-items: center;
240
+ }
241
+
242
+ .chat-header-info {
243
+ display: flex;
244
+ align-items: center;
245
+ }
246
+
247
+ .chat-header .avatar {
248
+ width: 40px;
249
+ height: 40px;
250
+ font-size: 18px;
251
+ margin-right: 12px;
252
+ display: flex;
253
+ align-items: center;
254
+ justify-content: center;
255
+ background: #5288c1;
256
+ border-radius: 50%;
257
+ font-weight: 600;
258
+ color: white;
259
+ }
260
+
261
+ .chat-header h2 {
262
+ font-size: 1.1em;
263
+ color: var(--tg-text-color);
264
+ font-weight: 500;
265
+ }
266
+
267
+ .settings-btn {
268
+ background: none;
269
+ border: none;
270
+ cursor: pointer;
271
+ padding: 8px;
272
+ color: var(--tg-input-placeholder-color);
273
+ transition: transform 0.3s;
274
+ }
275
+
276
+ .settings-btn:hover {
277
+ transform: rotate(20deg);
278
+ }
279
+
280
+ .messages {
281
+ grid-column: 1;
282
+ padding-right: 300px;
283
+ flex-grow: 1;
284
+ overflow-y: auto;
285
+ padding: 14px 16px;
286
+ display: flex;
287
+ flex-direction: column;
288
+ gap: 8px;
289
+ scrollbar-width: thin;
290
+ scrollbar-color: var(--tg-scrollbar-color) var(--tg-scrollbar-track-color);
291
+ }
292
+ .messages::-webkit-scrollbar {
293
+ width: var(--tg-scrollbar-width);
294
+ }
295
+ .messages::-webkit-scrollbar-track {
296
+ background: var(--tg-scrollbar-track-color);
297
+ }
298
+ .messages::-webkit-scrollbar-thumb {
299
+ background-color: var(--tg-scrollbar-color);
300
+ border-radius: 4px;
301
+ }
302
+
303
+ /* Message bubbles */
304
+ .message {
305
+ display: flex;
306
+ max-width: 78%;
307
+ animation: fadeIn 0.3s ease-in-out;
308
+ margin-bottom: 3px;
309
+ }
310
+ .message.sent {
311
+ align-self: flex-end;
312
+ }
313
+ .message.received {
314
+ align-self: flex-start;
315
+ }
316
+ .message-bubble {
317
+ padding: var(--tg-message-padding-vertical) var(--tg-message-padding-horizontal);
318
+ border-radius: var(--tg-message-radius) calc(var(--tg-message-radius) * 2) calc(var(--tg-message-radius) * 2) var(--tg-message-radius);
319
+ background: var(--tg-received-message-bg);
320
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
321
+ font-size: 0.98em;
322
+ line-height: 1.3;
323
+ position: relative;
324
+ word-wrap: break-word;
325
+ }
326
+ .message.sent .message-bubble {
327
+ background: var(--tg-sent-message-bg);
328
+ color: var(--tg-text-color);
329
+ border-radius: calc(var(--tg-message-radius) * 2) var(--tg-message-radius) var(--tg-message-radius) calc(var(--tg-message-radius) * 2);
330
+ }
331
+ .message.received .message-bubble {
332
+ background: var(--tg-received-message-bg);
333
+ padding: var(--tg-message-padding-vertical) var(--tg-message-padding-horizontal);
334
+ border-radius: var(--tg-message-radius);
335
+ max-width: 85%;
336
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
337
+ }
338
+
339
+ .message-time {
340
+ position: absolute;
341
+ right: 10px;
342
+ bottom: 6px;
343
+ font-size: 0.7em;
344
+ color: var(--tg-input-placeholder-color);
345
+ }
346
+ .message.sent .message-time {
347
+ right: 10px;
348
+ left: auto;
349
+ text-align: right;
350
+ }
351
+
352
+ /* Message input area */
353
+ .message-input {
354
+ grid-column: 1;
355
+ padding-right: 300px;
356
+ display: flex;
357
+ padding: 9px 16px;
358
+ background: var(--tg-sidebar-bg-color);
359
+ border-top: 1px solid var(--tg-border-color);
360
+ height: var(--tg-input-height);
361
+ }
362
+ .message-input input {
363
+ flex-grow: 1;
364
+ padding: 10px 16px;
365
+ border: 1px solid var(--tg-input-border-color);
366
+ border-radius: 24px;
367
+ outline: none;
368
+ font-size: 1em;
369
+ margin-right: 10px;
370
+ background-color: var(--tg-input-bg-color);
371
+ color: var(--tg-text-color);
372
+ height: auto;
373
+ transition: border-color 0.2s;
374
+ }
375
+
376
+ .message-input input:focus {
377
+ border-color: var(--tg-link-color);
378
+ box-shadow: 0 0 0 1px var(--tg-link-color);
379
+ }
380
+
381
+ .message-input input::placeholder {
382
+ color: var(--tg-input-placeholder-color);
383
+ }
384
+ .message-input button {
385
+ background: transparent;
386
+ border: none;
387
+ padding: 0;
388
+ width: 40px;
389
+ display: flex;
390
+ align-items: center;
391
+ justify-content: center;
392
+ cursor: pointer;
393
+ color: var(--tg-link-color);
394
+ }
395
+
396
+ /* Bot command buttons */
397
+ .bot-commands {
398
+ display: flex;
399
+ flex-wrap: wrap;
400
+ gap: 8px;
401
+ margin: 10px 0;
402
+ padding: 0 16px;
403
+ }
404
+ .bot-command {
405
+ background: var(--tg-sidebar-bg-color);
406
+ border: none;
407
+ padding: 8px 16px;
408
+ border-radius: 18px;
409
+ cursor: pointer;
410
+ color: var(--tg-link-color);
411
+ font-size: 0.95em;
412
+ transition: background-color 0.2s;
413
+ }
414
+ .bot-command:hover {
415
+ background: var(--tg-active-item-bg);
416
+ }
417
+
418
+ /* Bot keyboard and verification badge */
419
+ .bot-keyboard {
420
+ display: flex;
421
+ flex-direction: column;
422
+ gap: 8px;
423
+ margin-top: 10px;
424
+ }
425
+ .keyboard-row {
426
+ display: flex;
427
+ gap: 8px;
428
+ }
429
+ .keyboard-button {
430
+ flex: 1;
431
+ background: var(--tg-sidebar-bg-color);
432
+ border: none;
433
+ padding: 8px 16px;
434
+ border-radius: 8px;
435
+ cursor: pointer;
436
+ color: var(--tg-link-color);
437
+ font-size: 0.95em;
438
+ transition: background-color 0.2s;
439
+ text-align: center;
440
+ }
441
+ .keyboard-button:hover {
442
+ background: var(--tg-active-item-bg);
443
+ }
444
+ .bot-badge {
445
+ background: var(--tg-link-color);
446
+ color: white;
447
+ font-size: 0.7em;
448
+ padding: 2px 6px;
449
+ border-radius: 4px;
450
+ margin-left: 8px;
451
+ text-transform: uppercase;
452
+ }
453
+
454
+ /* Fade In Animation */
455
+ @keyframes fadeIn {
456
+ from {
457
+ opacity: 0;
458
+ transform: translateY(3px);
459
+ }
460
+ to {
461
+ opacity: 1;
462
+ transform: translateY(0);
463
+ }
464
+ }
465
+
466
+ /* Dialog styles */
467
+ .dialog {
468
+ display: none;
469
+ position: fixed;
470
+ top: 0;
471
+ left: 0;
472
+ right: 0;
473
+ bottom: 0;
474
+ background: rgba(0, 0, 0, 0.5);
475
+ align-items: center;
476
+ justify-content: center;
477
+ z-index: 1000;
478
+ }
479
+ .dialog-content {
480
+ background: var(--tg-sidebar-bg-color);
481
+ padding: 24px;
482
+ border-radius: 16px;
483
+ width: 100%;
484
+ max-width: 420px;
485
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.2);
486
+ }
487
+ .dialog h3 {
488
+ margin: 0 0 24px;
489
+ color: var(--tg-text-color);
490
+ font-size: 1.3em;
491
+ font-weight: 500;
492
+ }
493
+ .form-group {
494
+ margin-bottom: 18px;
495
+ }
496
+ .form-group label {
497
+ display: block;
498
+ margin-bottom: 6px;
499
+ color: var(--tg-text-color);
500
+ font-size: 0.95em;
501
+ }
502
+ .form-group input {
503
+ width: 100%;
504
+ padding: 10px 14px;
505
+ border: 1px solid var(--tg-input-border-color);
506
+ border-radius: 10px;
507
+ font-size: 1em;
508
+ background: var(--tg-input-bg-color);
509
+ color: var(--tg-text-color);
510
+ transition: border-color 0.2s;
511
+ }
512
+
513
+ .form-group input:focus {
514
+ border-color: var(--tg-link-color);
515
+ box-shadow: 0 0 0 1px var(--tg-link-color);
516
+ }
517
+
518
+ .buttons {
519
+ display: flex;
520
+ gap: 12px;
521
+ justify-content: flex-end;
522
+ margin-top: 24px;
523
+ }
524
+ .buttons button {
525
+ padding: 10px 20px;
526
+ border: none;
527
+ border-radius: 10px;
528
+ cursor: pointer;
529
+ font-size: 0.95em;
530
+ transition: background-color 0.2s;
531
+ }
532
+ .buttons button[type="submit"] {
533
+ background: var(--tg-link-color);
534
+ color: white;
535
+ }
536
+ .buttons button.cancel {
537
+ background: var(--tg-hover-item-bg);
538
+ color: var(--tg-text-color);
539
+ }
540
+ .message-sender {
541
+ display: block;
542
+ font-weight: 500;
543
+ margin-bottom: 4px;
544
+ color: var(--tg-link-color);
545
+ }
546
+ .message.action {
547
+ align-self: center;
548
+ font-style: italic;
549
+ color: var(--tg-input-placeholder-color);
550
+ }
551
+ .message.system {
552
+ align-self: center;
553
+ color: var(--tg-input-placeholder-color);
554
+ font-size: 0.9em;
555
+ }
556
+ .message.error {
557
+ align-self: center;
558
+ color: #ff4444;
559
+ font-size: 0.9em;
560
+ }
561
+ .header-actions {
562
+ display: flex;
563
+ gap: 10px;
564
+ }
565
+ .connect-btn {
566
+ background: none;
567
+ border: none;
568
+ cursor: pointer;
569
+ padding: 8px;
570
+ color: var(--tg-input-placeholder-color);
571
+ transition: transform 0.3s;
572
+ }
573
+ .connect-btn:hover {
574
+ transform: scale(1.1);
575
+ }
576
+
577
+ /* User List */
578
+ .user-list {
579
+ grid-column: 2;
580
+ grid-row: 2 / span 2;
581
+ position: relative;
582
+ width: 300px;
583
+ background: var(--tg-sidebar-bg-color);
584
+ border-left: 1px solid var(--tg-border-color);
585
+ display: flex;
586
+ flex-direction: column;
587
+ }
588
+
589
+ .user-list-header {
590
+ padding: 14px 16px;
591
+ border-bottom: 1px solid var(--tg-border-color);
592
+ display: flex;
593
+ }
594
+ .user-list-header h3 {
595
+ font-size: 1em;
596
+ font-weight: 500;
597
+ color: var(--tg-text-color);
598
+ }
599
+ .user-list-header input.user-search-input {
600
+ padding: 8px 14px;
601
+ border: 1px solid var(--tg-input-border-color);
602
+ border-radius: 24px;
603
+ outline: none;
604
+ font-size: 0.95em;
605
+ background-color: var(--tg-input-bg-color);
606
+ color: var(--tg-text-color);
607
+ margin-left: auto;
608
+ width: 60%;
609
+ }
610
+
611
+ .user-list-content {
612
+ overflow-y: auto;
613
+ flex-grow: 1;
614
+ scrollbar-width: thin;
615
+ scrollbar-color: var(--tg-scrollbar-color) var(--tg-scrollbar-track-color);
616
+ }
617
+ .user-list-content::-webkit-scrollbar {
618
+ width: var(--tg-scrollbar-width);
619
+ }
620
+ .user-list-content::-webkit-scrollbar-track {
621
+ background: var(--tg-scrollbar-track-color);
622
+ }
623
+ .user-list-content::-webkit-scrollbar-thumb {
624
+ background-color: var(--tg-scrollbar-color);
625
+ border-radius: 4px;
626
+ }
627
+
628
+ .user-item {
629
+ display: flex;
630
+ align-items: center;
631
+ padding: 10px 16px;
632
+ cursor: pointer;
633
+ transition: background 0.2s;
634
+ }
635
+ .user-item:hover {
636
+ background: var(--tg-hover-item-bg);
637
+ }
638
+ .user-avatar {
639
+ width: 36px;
640
+ height: 36px;
641
+ border-radius: 50%;
642
+ background: var(--tg-link-color);
643
+ color: white;
644
+ display: flex;
645
+ align-items: center;
646
+ justify-content: center;
647
+ margin-right: 12px;
648
+ font-weight: 500;
649
+ font-size: 1em;
650
+ overflow: hidden; /* Ensure content inside avatar stays within bounds */
651
+ }
652
+
653
+ .user-avatar svg {
654
+ width: 100%;
655
+ height: 100%;
656
+ }
657
+
658
+ .user-name {
659
+ color: var(--tg-text-color);
660
+ font-size: 0.95em;
661
+ }
662
+
663
+ /* Channel Topic */
664
+ .channel-topic {
665
+ font-size: 0.8em;
666
+ color: var(--tg-input-placeholder-color);
667
+ margin-top: 4px;
668
+ white-space: nowrap;
669
+ overflow: hidden;
670
+ text-overflow: ellipsis;
671
+ }
672
+
673
+ /* Server Info */
674
+ .server-info {
675
+ padding: 8px 16px;
676
+ font-size: 0.8em;
677
+ color: var(--tg-input-placeholder-color);
678
+ background: var(--tg-sidebar-bg-color);
679
+ border-bottom: 1px solid var(--tg-border-color);
680
+ }
681
+
682
+ /* Connection Status */
683
+ .connection-status {
684
+ position: fixed;
685
+ top: 0;
686
+ left: 50%;
687
+ transform: translateX(-50%);
688
+ padding: 10px 18px;
689
+ background: var(--tg-link-color);
690
+ color: white;
691
+ border-radius: 0 0 10px 10px;
692
+ font-size: 0.9em;
693
+ z-index: 1000;
694
+ transition: transform 0.3s;
695
+ }
696
+ .connection-status.hidden {
697
+ transform: translateX(-50%) translateY(-100%);
698
+ }
699
+
700
+ /* Private Message Dialog */
701
+ .pm-dialog {
702
+ position: fixed;
703
+ bottom: 20px;
704
+ right: 20px;
705
+ width: 320px;
706
+ background: var(--tg-sidebar-bg-color);
707
+ border-radius: 16px;
708
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.2);
709
+ z-index: 1000;
710
+ display: flex; /* Enable flexbox for PM dialog */
711
+ flex-direction: column; /* Stack header, messages, input vertically */
712
+ max-height: 80vh; /* Limit height to viewport height */
713
+ }
714
+ .pm-header {
715
+ padding: 14px 16px;
716
+ border-bottom: 1px solid var(--tg-border-color);
717
+ display: flex;
718
+ justify-content: space-between;
719
+ align-items: center;
720
+ flex-shrink: 0; /* Prevent header from shrinking */
721
+ }
722
+ .pm-messages {
723
+ height: auto; /* Adjust height automatically based on content */
724
+ overflow-y: auto;
725
+ padding: 14px 16px;
726
+ scrollbar-width: thin;
727
+ scrollbar-color: var(--tg-scrollbar-color) var(--tg-scrollbar-track-color);
728
+ flex-grow: 1; /* Allow messages to take up available space */
729
+ display: flex;
730
+ flex-direction: column-reverse; /* Newest messages at bottom */
731
+ }
732
+ .pm-messages::-webkit-scrollbar {
733
+ width: var(--tg-scrollbar-width);
734
+ }
735
+ .pm-messages::-webkit-scrollbar-track {
736
+ background: var(--tg-scrollbar-track-color);
737
+ }
738
+ .pm-messages::-webkit-scrollbar-thumb {
739
+ background-color: var(--tg-scrollbar-color);
740
+ border-radius: 4px;
741
+ }
742
+ .pm-input {
743
+ padding: 14px 16px;
744
+ border-top: 1px solid var(--tg-border-color);
745
+ flex-shrink: 0; /* Prevent input from shrinking */
746
+ }
747
+ .pm-input input {
748
+ width: 100%;
749
+ padding: 10px 14px;
750
+ border: 1px solid var(--tg-input-border-color);
751
+ border-radius: 24px;
752
+ outline: none;
753
+ font-size: 1em;
754
+ background-color: var(--tg-input-bg-color);
755
+ color: var(--tg-text-color);
756
+ transition: border-color 0.2s;
757
+ }
758
+
759
+ .pm-input input:focus {
760
+ border-color: var(--tg-link-color);
761
+ box-shadow: 0 0 0 1px var(--tg-link-color);
762
+ }
763
+ .pm-message {
764
+ margin-bottom: 8px; /* Spacing between PM messages */
765
+ }
766
+
767
+ /* Settings Panel */
768
+ .settings-panel {
769
+ position: fixed;
770
+ top: 0;
771
+ right: 0;
772
+ bottom: 0;
773
+ width: 320px;
774
+ background: var(--tg-sidebar-bg-color);
775
+ border-left: 1px solid var(--tg-border-color);
776
+ transform: translateX(100%);
777
+ transition: transform 0.3s;
778
+ z-index: 1000;
779
+ }
780
+ .settings-panel.open {
781
+ transform: translateX(0);
782
+ }
783
+ .settings-header {
784
+ padding: 16px;
785
+ border-bottom: 1px solid var(--tg-border-color);
786
+ display: flex;
787
+ justify-content: space-between;
788
+ align-items: center;
789
+ }
790
+ .settings-content {
791
+ padding: 16px;
792
+ }
793
+ .settings-group {
794
+ margin-bottom: 20px;
795
+ }
796
+ .settings-group h3 {
797
+ margin-bottom: 12px;
798
+ font-size: 1em;
799
+ font-weight: 500;
800
+ }
801
+
802
+ /* Utility class to hide elements */
803
+ .hidden {
804
+ display: none;
805
+ }
806
+
807
+ /* Channel search improvements */
808
+ .search-results {
809
+ position: absolute;
810
+ top: 100%;
811
+ left: 0;
812
+ right: 0;
813
+ background: var(--tg-sidebar-bg-color);
814
+ border: 1px solid var(--tg-border-color);
815
+ border-radius: 0 0 12px 12px;
816
+ max-height: 320px;
817
+ overflow-y: auto;
818
+ z-index: 1000;
819
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
820
+ }
821
+
822
+ .search-result-item {
823
+ padding: 12px 16px;
824
+ border-bottom: 1px solid var(--tg-border-color);
825
+ cursor: pointer;
826
+ display: flex;
827
+ align-items: center;
828
+ justify-content: space-between;
829
+ }
830
+
831
+ .search-result-item:hover {
832
+ background: var(--tg-hover-item-bg);
833
+ }
834
+
835
+ .join-button {
836
+ padding: 8px 16px;
837
+ background: var(--tg-link-color);
838
+ color: white;
839
+ border: none;
840
+ border-radius: 6px;
841
+ cursor: pointer;
842
+ }
843
+
844
+ .join-button:hover {
845
+ opacity: 0.9;
846
+ }
847
+
848
+ /* Section headers */
849
+ .section-header {
850
+ padding: 10px 16px;
851
+ font-weight: 500;
852
+ color: var(--tg-input-placeholder-color);
853
+ background: var(--tg-hover-item-bg);
854
+ font-size: 0.9em;
855
+ }
856
+
857
+ /* Channel categories */
858
+ .channel-category {
859
+ margin-bottom: 10px;
860
+ }
861
+
862
+ /* Loading indicator */
863
+ .loading-spinner {
864
+ display: inline-block;
865
+ width: 20px;
866
+ height: 20px;
867
+ border: 2px solid var(--tg-border-color);
868
+ border-radius: 50%;
869
+ border-top-color: var(--tg-link-color);
870
+ animation: spin 1s linear infinite;
871
+ }
872
+
873
+ @keyframes spin {
874
+ to { transform: rotate(360deg); }
875
+ }
876
+
877
+ /* Theme switcher */
878
+ .theme-switcher {
879
+ padding: 12px 16px;
880
+ background: var(--tg-sidebar-bg-color);
881
+ border-top: 1px solid var(--tg-border-color);
882
+ }
883
+
884
+ .theme-option {
885
+ display: flex;
886
+ align-items: center;
887
+ padding: 8px 0;
888
+ cursor: pointer;
889
+ }
890
+
891
+ .theme-option label {
892
+ font-size: 0.95em;
893
+ }
894
+
895
+ .theme-option input[type="radio"] {
896
+ margin-right: 8px;
897
+ }
898
+
899
+ /* Channel Info Styles */
900
+ .channel-info {
901
+ display: flex;
902
+ flex-direction: column;
903
+ position: relative;
904
+ background: var(--tg-sidebar-bg-color);
905
+ border-top: 1px solid var(--tg-border-color);
906
+ height: 100%;
907
+ }
908
+
909
+ .channel-info.hidden {
910
+ display: none;
911
+ }
912
+
913
+ .channel-info-header {
914
+ display: flex;
915
+ align-items: center;
916
+ padding: 14px 16px;
917
+ border-bottom: 1px solid var(--tg-border-color);
918
+ }
919
+
920
+ .channel-info-header h3 {
921
+ font-size: 1em;
922
+ font-weight: 500;
923
+ color: var(--tg-text-color);
924
+ }
925
+
926
+ .back-btn {
927
+ background: none;
928
+ border: none;
929
+ cursor: pointer;
930
+ padding: 8px;
931
+ color: var(--tg-input-placeholder-color);
932
+ transition: transform 0.3s;
933
+ }
934
+
935
+ .back-btn:hover {
936
+ transform: scale(1.1);
937
+ }
938
+
939
+ .channel-info-content {
940
+ flex-grow: 1;
941
+ display: flex;
942
+ flex-direction: column;
943
+ padding: 14px;
944
+ height: calc(100% - 72px); /* Adjust height based on header height */
945
+ overflow-y: auto;
946
+ scrollbar-width: thin;
947
+ scrollbar-color: var(--tg-scrollbar-color) var(--tg-scrollbar-track-color);
948
+ }
949
+ .channel-info-content::-webkit-scrollbar {
950
+ width: var(--tg-scrollbar-width);
951
+ }
952
+ .channel-info-content::-webkit-scrollbar-track {
953
+ background: var(--tg-scrollbar-track-color);
954
+ }
955
+ .channel-info-content::-webkit-scrollbar-thumb {
956
+ background-color: var(--tg-scrollbar-color);
957
+ border-radius: 4px;
958
+ }
959
+
960
+ .channel-topic {
961
+ margin-bottom: 10px;
962
+ font-size: 0.8em;
963
+ color: var(--tg-input-placeholder-color);
964
+ white-space: nowrap;
965
+ overflow: hidden;
966
+ text-overflow: ellipsis;
967
+ }
968
+
969
+ /* User Search Input in Channel Info */
970
+ .user-search-input {
971
+ padding: 8px 14px;
972
+ border: 1px solid var(--tg-input-border-color);
973
+ border-radius: 24px;
974
+ outline: none;
975
+ font-size: 0.95em;
976
+ background-color: var(--tg-input-bg-color);
977
+ color: var(--tg-text-color);
978
+ margin-bottom: 10px;
979
+ width: 100%;
980
+ }
index.html CHANGED
@@ -1,19 +1,146 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <html lang="en">
2
+ <head>
3
+ <meta charset="UTF-8" />
4
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
+ <title>Libera Chat - Telegram Clone</title>
6
+ <link rel="stylesheet" href="css/styles.css" />
7
+ </head>
8
+ <body>
9
+ <!-- Global Connection Status (fixed at top center) -->
10
+ <div id="connectionStatus" class="connection-status hidden"></div>
11
+
12
+ <div class="app">
13
+ <aside class="chat-list">
14
+ <div class="chat-list-header">
15
+ <h2>Libera Chat</h2>
16
+ <input type="text" placeholder="Search channels" id="chatSearch" />
17
+ </div>
18
+ <ul class="chat-items">
19
+ <!-- Joined channels will be populated by JavaScript -->
20
+ </ul>
21
+ <div class="channel-info hidden" id="channelInfo">
22
+ <div class="channel-info-header">
23
+ <button class="back-btn" id="backBtn">
24
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
25
+ <path d="M5 12h14"></path>
26
+ <path d="M12 5l7 7-7 7"></path>
27
+ </svg>
28
+ </button>
29
+ <h3 id="channelName">Channel Name</h3>
30
+ </div>
31
+ <div class="channel-info-content">
32
+ <div class="channel-topic" id="channelTopic">Topic: <span id="topicText">No topic set</span></div>
33
+ <input type="text" placeholder="Search members" id="userSearch" class="user-search-input" />
34
+ <div class="user-list" id="channelUserList">
35
+ <!-- Channel users will be populated by JavaScript -->
36
+ </div>
37
+ </div>
38
+ </div>
39
+ </aside>
40
+ <main class="chat-window">
41
+ <header class="chat-header">
42
+ <div class="chat-header-info">
43
+ <div class="avatar"></div>
44
+ <h2 id="channelHeader">Welcome to Libera Chat</h2>
45
+ </div>
46
+ <div class="header-actions">
47
+ <button class="connect-btn" id="connectBtn" title="Connect">
48
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
49
+ <path d="M5 12h14"></path>
50
+ <path d="M12 5l7 7-7 7"></path>
51
+ </svg>
52
+ </button>
53
+ <button class="settings-btn" id="settingsBtn" title="Settings">
54
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
55
+ <circle cx="12" cy="12" r="3"></circle>
56
+ <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
57
+ </svg>
58
+ </button>
59
+ </div>
60
+ </header>
61
+ <div class="messages" id="messages">
62
+ <!-- Channel messages will be populated by JavaScript -->
63
+ </div>
64
+ <form class="message-input" id="messageForm">
65
+ <input type="text" id="messageText" placeholder="Message" disabled />
66
+ <button type="submit" title="Send" disabled>
67
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
68
+ <line x1="22" y1="2" x2="11" y2="13"></line>
69
+ <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
70
+ </svg>
71
+ </button>
72
+ </form>
73
+ <!-- User list (for current channel) will be injected dynamically -->
74
+ <div class="user-list"></div>
75
+ </main>
76
+ </div>
77
+
78
+ <!-- Connection Dialog -->
79
+ <div class="dialog" id="connectionDialog">
80
+ <div class="dialog-content">
81
+ <h3>Connect to Libera Chat</h3>
82
+ <form id="connectionForm">
83
+ <div class="form-group">
84
+ <label for="nickname">Nickname</label>
85
+ <input type="text" id="nickname" required />
86
+ </div>
87
+ <div class="form-group">
88
+ <label for="channel">Channel</label>
89
+ <input type="text" id="channel" value="#libera" required />
90
+ </div>
91
+ <div class="buttons">
92
+ <button type="submit">Connect</button>
93
+ <button type="button" class="cancel">Cancel</button>
94
+ </div>
95
+ </form>
96
+ </div>
97
+ </div>
98
+
99
+ <!-- Private Message Dialog -->
100
+ <div class="pm-dialog hidden" id="pmDialog">
101
+ <div class="pm-header">
102
+ <span id="pmUserName">Private Chat</span>
103
+ <button id="pmCloseBtn" title="Close">
104
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
105
+ <line x1="18" y1="6" x2="6" y2="18"></line>
106
+ <line x1="6" y1="6" x2="18" y2="18"></line>
107
+ </svg>
108
+ </button>
109
+ </div>
110
+ <div class="pm-messages" id="pmMessages"></div>
111
+ <div class="pm-input">
112
+ <input type="text" id="pmInput" placeholder="Message" />
113
+ </div>
114
+ </div>
115
+
116
+ <!-- Settings Panel -->
117
+ <div class="settings-panel" id="settingsPanel">
118
+ <div class="settings-header">
119
+ <h3>Settings</h3>
120
+ <button id="closeSettingsBtn" title="Close">
121
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
122
+ <line x1="18" y1="6" x2="6" y2="18"></line>
123
+ <line x1="6" y1="6" x2="18" y2="18"></line>
124
+ </svg>
125
+ </button>
126
+ </div>
127
+ <div class="settings-content">
128
+ <div class="settings-group">
129
+ <h3>Server Info</h3>
130
+ <p id="serverInfoText">Loading...</p>
131
+ </div>
132
+ <div class="settings-group">
133
+ <h3>Appearance</h3>
134
+ <p>Theme: Telegram Clone</p>
135
+ </div>
136
+ <div class="settings-group">
137
+ <h3>Advanced</h3>
138
+ <p>Reconnection Status: <span id="reconnectStatus">Good</span></p>
139
+ </div>
140
+ </div>
141
+ </div>
142
+
143
+ <script src="js/irc.js"></script>
144
+ <script src="js/main.js"></script>
145
+ </body>
146
+ </html>
js/irc.js ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class IRCClient {
2
+ constructor() {
3
+ this.ws = null;
4
+ this.nickname = '';
5
+ this.channels = new Set();
6
+ this.callbacks = {
7
+ message: () => {},
8
+ join: () => {},
9
+ part: () => {},
10
+ connected: () => {},
11
+ error: () => {},
12
+ channelUsers: () => {},
13
+ topic: () => {},
14
+ serverInfo: () => {},
15
+ channelList: () => {}
16
+ };
17
+ this.reconnectAttempts = 0;
18
+ this.maxReconnectAttempts = 5;
19
+ this.channelUsers = new Map();
20
+ this.channelTopics = new Map();
21
+ this.privateMessages = new Map();
22
+ this.serverInfo = {
23
+ network: 'Libera Chat',
24
+ users: 0,
25
+ channels: 0,
26
+ operators: 0
27
+ };
28
+ this.pingInterval = null;
29
+ this.lastPing = Date.now();
30
+ this.connectionState = 'disconnected';
31
+ this.globalChannels = new Set();
32
+ }
33
+
34
+ connect(nickname) {
35
+ this.nickname = nickname;
36
+ this.connectionState = 'connecting';
37
+
38
+ try {
39
+ this.ws = new WebSocket('wss://web.libera.chat/webirc/websocket/');
40
+
41
+ this.pingInterval = setInterval(() => {
42
+ if (this.ws.readyState === WebSocket.OPEN) {
43
+ this.send('PING :' + Date.now());
44
+ }
45
+ }, 30000);
46
+
47
+ this.ws.onopen = () => {
48
+ this.connectionState = 'registering';
49
+ this.reconnectAttempts = 0;
50
+ this.send(`NICK ${this.nickname}`);
51
+ this.send(`USER ${this.nickname} 0 * :${this.nickname}`);
52
+ this.send('LUSERS');
53
+ this.send('LIST');
54
+ };
55
+
56
+ this.ws.onmessage = (event) => this.handleMessage(event.data);
57
+
58
+ this.ws.onerror = (error) => {
59
+ console.error('WebSocket error:', error);
60
+ let errorMessage = 'Connection error occurred';
61
+ if (error.message) {
62
+ errorMessage = error.message;
63
+ } else if (error.code) {
64
+ errorMessage = `Error code: ${error.code}`;
65
+ }
66
+ this.callbacks.error(errorMessage);
67
+ };
68
+
69
+ this.ws.onclose = (event) => {
70
+ let closeMessage = 'Connection closed';
71
+ if (event.code !== 1000) {
72
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
73
+ this.reconnectAttempts++;
74
+ closeMessage = `Connection lost. Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`;
75
+ this.callbacks.error(closeMessage);
76
+ setTimeout(() => this.connect(this.nickname), 2000);
77
+ return;
78
+ }
79
+ closeMessage = 'Connection closed unexpectedly. Please try reconnecting.';
80
+ }
81
+ this.callbacks.error(closeMessage);
82
+ };
83
+
84
+ } catch (error) {
85
+ this.connectionState = 'error';
86
+ console.error('Connection error:', error);
87
+ this.callbacks.error('Failed to establish connection: ' + error.message);
88
+ }
89
+ }
90
+
91
+ disconnect() {
92
+ if (this.pingInterval) {
93
+ clearInterval(this.pingInterval);
94
+ }
95
+ if (this.ws) {
96
+ this.ws.close(1000, 'User initiated disconnect');
97
+ }
98
+ this.connectionState = 'disconnected';
99
+ }
100
+
101
+ send(message) {
102
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
103
+ this.ws.send(message + '\r\n');
104
+ }
105
+ }
106
+
107
+ join(channel) {
108
+ if (!channel.startsWith('#')) channel = '#' + channel;
109
+ this.send(`JOIN ${channel}`);
110
+ this.channels.add(channel);
111
+ }
112
+
113
+ part(channel) {
114
+ this.send(`PART ${channel}`);
115
+ this.channels.delete(channel);
116
+ }
117
+
118
+ sendMessage(channel, message) {
119
+ this.send(`PRIVMSG ${channel} :${message}`);
120
+ }
121
+
122
+ getChannelUsers(channel) {
123
+ return Array.from(this.channelUsers.get(channel) || new Set());
124
+ }
125
+
126
+ getChannelTopic(channel) {
127
+ return this.channelTopics.get(channel) || '';
128
+ }
129
+
130
+ sendPrivateMessage(target, message) {
131
+ this.send(`PRIVMSG ${target} :${message}`);
132
+ if (!this.privateMessages.has(target)) {
133
+ this.privateMessages.set(target, []);
134
+ }
135
+ this.privateMessages.get(target).push({
136
+ from: this.nickname,
137
+ message,
138
+ timestamp: Date.now()
139
+ });
140
+ }
141
+
142
+ requestChannelInfo(channel) {
143
+ this.send(`MODE ${channel}`);
144
+ this.send(`WHO ${channel}`);
145
+ this.send(`TOPIC ${channel}`);
146
+ }
147
+
148
+ handleMessage(data) {
149
+ const lines = data.split('\r\n');
150
+ for (const line of lines) {
151
+ if (!line) continue;
152
+
153
+ if (line.startsWith('PING')) {
154
+ this.send('PONG' + line.substring(4));
155
+ this.lastPing = Date.now();
156
+ continue;
157
+ }
158
+
159
+ const match = line.match(/^(?::([\w.]+) )?([\w]+)(?: (?!:)(.+?))?(?: :(.+))?$/);
160
+ if (!match) continue;
161
+
162
+ const [, prefix, command, params = '', trailing] = match;
163
+ const args = params.split(' ').filter(arg => arg);
164
+ if (trailing) args.push(trailing);
165
+
166
+ switch (command) {
167
+ case '001':
168
+ this.connectionState = 'connected';
169
+ this.callbacks.connected();
170
+ break;
171
+
172
+ case '353': {
173
+ const channel = args[2];
174
+ const users = args[3].split(' ');
175
+ if (!this.channelUsers.has(channel)) {
176
+ this.channelUsers.set(channel, new Set());
177
+ }
178
+ users.forEach(user => this.channelUsers.get(channel).add(user));
179
+ this.callbacks.channelUsers?.({ channel, users: Array.from(this.channelUsers.get(channel)) });
180
+ break;
181
+ }
182
+
183
+ case '332': {
184
+ const topicChannel = args[1];
185
+ const topic = args[2];
186
+ this.channelTopics.set(topicChannel, topic);
187
+ this.callbacks.topic?.({ channel: topicChannel, topic });
188
+ break;
189
+ }
190
+ case '333':
191
+ break;
192
+
193
+ case '266': {
194
+ const match = trailing.match(/(\d+)/);
195
+ if (match) {
196
+ this.serverInfo.users = parseInt(match[1]);
197
+ this.callbacks.serverInfo?.(this.serverInfo);
198
+ }
199
+ break;
200
+ }
201
+
202
+ case '322': {
203
+ const [channel, userCount, topic] = args;
204
+ this.globalChannels.add(channel);
205
+ break;
206
+ }
207
+
208
+ case '323': {
209
+ this.callbacks.channelList?.(Array.from(this.globalChannels));
210
+ break;
211
+ }
212
+
213
+ case 'PRIVMSG': {
214
+ const [target, message] = [args[0], args[1]];
215
+ const nick = prefix.split('!')[0];
216
+ this.callbacks.message({
217
+ from: nick,
218
+ target,
219
+ message,
220
+ isAction: message.startsWith('\u0001ACTION') && message.endsWith('\u0001'),
221
+ timestamp: Date.now()
222
+ });
223
+ break;
224
+ }
225
+ case 'JOIN':
226
+ this.callbacks.join({
227
+ channel: args[0],
228
+ nick: prefix.split('!')[0]
229
+ });
230
+ this.requestChannelInfo(args[0]);
231
+ break;
232
+ case 'PART':
233
+ this.callbacks.part({
234
+ channel: args[0],
235
+ nick: prefix.split('!')[0]
236
+ });
237
+ break;
238
+ case 'QUIT':
239
+ this.channelUsers.forEach((users, channel) => {
240
+ if (users.delete(prefix.split('!')[0])) {
241
+ this.callbacks.channelUsers?.({ channel, users: Array.from(users) });
242
+ }
243
+ });
244
+ break;
245
+ case 'NICK': {
246
+ const oldNick = prefix.split('!')[0];
247
+ const newNick = args[0];
248
+ if (oldNick === this.nickname) {
249
+ this.nickname = newNick;
250
+ }
251
+ this.channelUsers.forEach((users, channel) => {
252
+ if (users.has(oldNick)) {
253
+ users.delete(oldNick);
254
+ users.add(newNick);
255
+ this.callbacks.channelUsers?.({ channel, users: Array.from(users) });
256
+ }
257
+ });
258
+ break;
259
+ }
260
+ }
261
+ }
262
+ }
263
+
264
+ on(event, callback) {
265
+ if (this.callbacks.hasOwnProperty(event)) {
266
+ this.callbacks[event] = callback;
267
+ }
268
+ }
269
+
270
+ onChannelUsers(callback) {
271
+ this.callbacks.channelUsers = callback;
272
+ }
273
+
274
+ onTopic(callback) {
275
+ this.callbacks.topic = callback;
276
+ }
277
+
278
+ onServerInfo(callback) {
279
+ this.callbacks.serverInfo = callback;
280
+ }
281
+
282
+ onChannelList(callback) {
283
+ this.callbacks.channelList = callback;
284
+ }
285
+ }
js/main.js ADDED
@@ -0,0 +1,621 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ // ...existing code...
3
+ const irc = new IRCClient();
4
+ const messageForm = document.getElementById('messageForm');
5
+ const messageInput = document.getElementById('messageText');
6
+ const messagesContainer = document.getElementById('messages');
7
+ const connectBtn = document.getElementById('connectBtn');
8
+ const connectionDialog = document.getElementById('connectionDialog');
9
+ const connectionForm = document.getElementById('connectionForm');
10
+ const chatSearchInput = document.getElementById('chatSearch');
11
+ const settingsBtn = document.getElementById('settingsBtn');
12
+ const settingsPanel = document.getElementById('settingsPanel');
13
+ const closeSettingsBtn = document.getElementById('closeSettingsBtn');
14
+ const connectionStatusEl = document.getElementById('connectionStatus');
15
+ const serverInfoText = document.getElementById('serverInfoText');
16
+ const reconnectStatusEl = document.getElementById('reconnectStatus');
17
+ const pmDialog = document.getElementById('pmDialog');
18
+ const pmCloseBtn = document.getElementById('pmCloseBtn');
19
+ const pmMessagesContainer = document.getElementById('pmMessages');
20
+ const pmInput = document.getElementById('pmInput');
21
+ const backBtn = document.getElementById('backBtn');
22
+
23
+ let activeChannels = new Set();
24
+ let selectedChannel = '';
25
+ let currentChannel = '';
26
+ let currentPMUser = '';
27
+ let channelMessages = {};
28
+ let pmMessages = {};
29
+ let globalChannels = new Set();
30
+ let searchTimeout = null;
31
+
32
+ // UI Components
33
+ const channelList = document.querySelector('.chat-items');
34
+ // Create user list if not already present
35
+ let userListEl = document.querySelector('.user-list');
36
+ if (!userListEl) {
37
+ userListEl = document.createElement('div');
38
+ userListEl.classList.add('user-list');
39
+ document.querySelector('.chat-window').appendChild(userListEl);
40
+ }
41
+
42
+ const channelInfoEl = document.getElementById('channelInfo');
43
+ const channelUserListEl = document.getElementById('channelUserList');
44
+ const channelNameEl = document.getElementById('channelName');
45
+ const channelTopicEl = document.getElementById('topicText');
46
+ const userSearchInput = document.getElementById('userSearch');
47
+
48
+ function generateAvatar(name) {
49
+ const colors = ["#1abc9c", "#2ecc71", "#3498db", "#9b59b6", "#34495e", "#16a085", "#27ae60", "#2980b9", "#8e44ad", "#2c3e50", "#f1c40f", "#e67e22", "#e74c3c", "#95a5a6", "#f39c12", "#d35400", "#c0392b", "#bdc3c7", "#77b1a9", "#94d82d", "#a327ff", "#f7e01d", "#e64a19", "#ecf0f1"];
50
+ const colorIndex = name.charCodeAt(0) % colors.length;
51
+ const color = colors[colorIndex];
52
+ const initials = name.substring(0, 2).toUpperCase();
53
+
54
+ return `
55
+ <svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg">
56
+ <circle cx="60" cy="60" r="60" fill="${color}"/>
57
+ <text
58
+ x="50%"
59
+ y="50%"
60
+ text-anchor="middle"
61
+ dominant-baseline="middle"
62
+ font-size="48"
63
+ font-family="sans-serif"
64
+ fill="white"
65
+ >${initials}</text>
66
+ </svg>
67
+ `;
68
+ }
69
+
70
+ function updateChannelList() {
71
+ const searchQuery = chatSearchInput.value.toLowerCase();
72
+ channelList.innerHTML = '';
73
+
74
+ // Connected channels section
75
+ if (activeChannels.size > 0) {
76
+ const connectedSection = document.createElement('div');
77
+ connectedSection.className = 'channel-category';
78
+ connectedSection.innerHTML = `
79
+ <div class="section-header">Connected Channels</div>
80
+ `;
81
+
82
+ Array.from(activeChannels)
83
+ .filter(channel => channel.toLowerCase().includes(searchQuery))
84
+ .forEach(channel => {
85
+ const li = createChannelListItem(channel);
86
+ connectedSection.appendChild(li);
87
+ });
88
+
89
+ channelList.appendChild(connectedSection);
90
+ }
91
+
92
+ // Update search results if query exists
93
+ if (searchQuery.length >= 2) {
94
+ searchGlobalChannels(searchQuery);
95
+ }
96
+ }
97
+
98
+ function createChannelListItem(channel) {
99
+ const li = document.createElement('li');
100
+ li.classList.add('chat-item');
101
+ if (channel === selectedChannel) li.classList.add('active');
102
+
103
+ li.innerHTML = `
104
+ <div class="avatar">${generateAvatar(channel)}</div>
105
+ <div class="chat-info">
106
+ <div class="chat-name">${channel}</div>
107
+ <div class="chat-snippet">${
108
+ channelMessages[channel]?.length ?
109
+ channelMessages[channel][channelMessages[channel].length - 1].text :
110
+ irc.getChannelTopic(channel) || 'No topic set'
111
+ }</div>
112
+ </div>
113
+ <div class="unread-badge">${irc.getChannelUsers(channel).length}</div>
114
+ `;
115
+
116
+ li.addEventListener('click', () => {
117
+ selectedChannel = channel;
118
+ currentChannel = channel;
119
+ updateChannelList();
120
+ updateUserList();
121
+ updateHeader();
122
+ displayMessagesForCurrentChannel();
123
+ channelInfoEl.classList.remove('hidden');
124
+ showChannelInfo(channel);
125
+ });
126
+
127
+ return li;
128
+ }
129
+
130
+ async function searchGlobalChannels(query) {
131
+ if (searchTimeout) clearTimeout(searchTimeout);
132
+
133
+ searchTimeout = setTimeout(async () => {
134
+ const searchResults = document.createElement('div');
135
+ searchResults.className = 'search-results';
136
+
137
+ if (query.length < 2) {
138
+ const existing = document.querySelector('.search-results');
139
+ if (existing) existing.remove();
140
+ return;
141
+ }
142
+
143
+ searchResults.innerHTML = '<div class="section-header">Loading channels...</div>';
144
+ chatSearchInput.parentElement.appendChild(searchResults);
145
+
146
+ try {
147
+ // Request channel list from server
148
+ irc.send('LIST');
149
+
150
+ // Filter channels based on query
151
+ const filteredChannels = Array.from(globalChannels)
152
+ .filter(channel => channel.toLowerCase().includes(query.toLowerCase()))
153
+ .slice(0, 20); // Limit results
154
+
155
+ searchResults.innerHTML = `
156
+ <div class="section-header">Global Channels</div>
157
+ ${filteredChannels.map(channel => `
158
+ <div class="search-result-item">
159
+ <span>${channel}</span>
160
+ ${!activeChannels.has(channel) ?
161
+ `<button class="join-button" data-channel="${channel}">Join</button>` :
162
+ '<span>Joined</span>'
163
+ }
164
+ </div>
165
+ `).join('')}
166
+ `;
167
+
168
+ // Add click handlers for join buttons
169
+ searchResults.querySelectorAll('.join-button').forEach(btn => {
170
+ btn.addEventListener('click', () => {
171
+ const channel = btn.dataset.channel;
172
+ irc.join(channel);
173
+ activeChannels.add(channel);
174
+ selectedChannel = channel;
175
+ updateChannelList();
176
+ searchResults.remove();
177
+ });
178
+ });
179
+
180
+ } catch (error) {
181
+ searchResults.innerHTML = '<div class="section-header">Error loading channels</div>';
182
+ }
183
+ }, 300);
184
+ }
185
+
186
+ function updateUserList() {
187
+ if (!selectedChannel) {
188
+ userListEl.innerHTML = '';
189
+ return;
190
+ }
191
+
192
+ const users = irc.getChannelUsers(selectedChannel);
193
+ userListEl.innerHTML = `
194
+ <div class="user-list-header">
195
+ <h3>Users (${users.length})</h3>
196
+ </div>
197
+ <div class="user-list-content">
198
+ ${users.map(user => `
199
+ <div class="user-item" data-user="${user}">
200
+ <div class="user-avatar">${generateAvatar(user)}</div>
201
+ <div class="user-name">${user}</div>
202
+ </div>
203
+ `).join('')}
204
+ </div>
205
+ `;
206
+ userListEl.querySelectorAll('.user-item').forEach(item => {
207
+ item.addEventListener('click', () => {
208
+ const user = item.getAttribute('data-user');
209
+ showPMDialog(user);
210
+ });
211
+ });
212
+ }
213
+
214
+ function updateHeader() {
215
+ const header = document.querySelector('.chat-header h2');
216
+ const avatar = document.querySelector('.chat-header .avatar');
217
+ if (selectedChannel) {
218
+ const topic = irc.getChannelTopic(selectedChannel);
219
+ header.innerHTML = `
220
+ ${selectedChannel}
221
+ ${topic ? `<div class="channel-topic">${topic}</div>` : ''}
222
+ `;
223
+ avatar.innerHTML = generateAvatar(selectedChannel);
224
+ } else {
225
+ header.textContent = 'Welcome to Libera Chat';
226
+ avatar.innerHTML = ''; // Clear avatar when no channel selected
227
+ }
228
+ }
229
+
230
+ function addChannelMessage(channel, message) {
231
+ if (!channel) return;
232
+ if (!channelMessages[channel]) {
233
+ channelMessages[channel] = [];
234
+ }
235
+ channelMessages[channel].push(message);
236
+ if (selectedChannel === channel) {
237
+ displayMessagesForCurrentChannel();
238
+ }
239
+ }
240
+
241
+ function displayMessagesForCurrentChannel() {
242
+ messagesContainer.innerHTML = '';
243
+ if (!selectedChannel || !channelMessages[selectedChannel]) return;
244
+ channelMessages[selectedChannel].forEach(msg => {
245
+ const el = createMessageElement(msg);
246
+ messagesContainer.appendChild(el);
247
+ });
248
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
249
+ }
250
+
251
+ function createMessageElement(msg) {
252
+ const messageWrapper = document.createElement('div');
253
+ messageWrapper.classList.add('message', msg.type);
254
+
255
+ const bubble = document.createElement('div');
256
+ bubble.classList.add('message-bubble');
257
+
258
+ if (msg.from && msg.type !== 'system' && msg.type !== 'error') {
259
+ const nameSpan = document.createElement('span');
260
+ nameSpan.classList.add('message-sender');
261
+ nameSpan.textContent = msg.from;
262
+ nameSpan.style.cursor = 'pointer';
263
+ nameSpan.addEventListener('click', () => {
264
+ showPMDialog(msg.from);
265
+ });
266
+ bubble.appendChild(nameSpan);
267
+ }
268
+
269
+ const textSpan = document.createElement('span');
270
+ textSpan.classList.add('message-text');
271
+ textSpan.textContent = msg.text;
272
+ bubble.appendChild(textSpan);
273
+
274
+ // Timestamp element
275
+ const timeSpan = document.createElement('span');
276
+ timeSpan.classList.add('message-time');
277
+ const date = new Date(msg.timestamp);
278
+ const hours = String(date.getHours()).padStart(2, '0');
279
+ const minutes = String(date.getMinutes()).padStart(2, '0');
280
+ timeSpan.textContent = `${hours}:${minutes}`;
281
+ bubble.appendChild(timeSpan);
282
+
283
+ messageWrapper.appendChild(bubble);
284
+ return messageWrapper;
285
+ }
286
+
287
+ function addPMMessage(user, msg) {
288
+ if (!user) return;
289
+ if (!pmMessages[user]) {
290
+ pmMessages[user] = [];
291
+ }
292
+ pmMessages[user].push(msg);
293
+ if (currentPMUser === user) {
294
+ displayPMMessages(user);
295
+ }
296
+ }
297
+
298
+ function displayPMMessages(user) {
299
+ pmMessagesContainer.innerHTML = '';
300
+ if (!pmMessages[user]) return;
301
+ // Display messages in reverse order to show newest at the bottom
302
+ pmMessages[user].slice().reverse().forEach(msg => {
303
+ const msgEl = createMessageElement(msg); // Reuse createMessageElement
304
+ msgEl.classList.add('pm-message'); // Add class for PM styling if needed
305
+ pmMessagesContainer.appendChild(msgEl);
306
+ });
307
+ pmMessagesContainer.scrollTop = pmMessagesContainer.scrollHeight;
308
+ }
309
+
310
+ function showPMDialog(user) {
311
+ currentPMUser = user;
312
+ document.getElementById('pmUserName').textContent = `Chat with ${user}`;
313
+ pmDialog.classList.remove('hidden');
314
+ displayPMMessages(user);
315
+ pmInput.focus(); // Focus on input when PM dialog opens
316
+ }
317
+
318
+ function hidePMDialog() {
319
+ pmDialog.classList.add('hidden');
320
+ currentPMUser = '';
321
+ }
322
+
323
+ function showConnectionDialog() {
324
+ connectionDialog.style.display = 'flex';
325
+ }
326
+
327
+ function hideConnectionDialog() {
328
+ connectionDialog.style.display = 'none';
329
+ }
330
+
331
+ function updateConnectionStatus(status) {
332
+ connectionStatusEl.textContent = status;
333
+ if (status.toLowerCase().includes('connected')) {
334
+ connectionStatusEl.classList.add('hidden');
335
+ } else {
336
+ connectionStatusEl.classList.remove('hidden');
337
+ }
338
+ }
339
+
340
+ function initializeUI() {
341
+ messageInput.disabled = true;
342
+ messageForm.querySelector('button').disabled = true;
343
+ }
344
+
345
+ function showChannelInfo(channel) {
346
+ channelNameEl.textContent = channel;
347
+ channelTopicEl.textContent = irc.getChannelTopic(channel) || 'No topic set';
348
+ const users = irc.getChannelUsers(channel);
349
+ channelUserListEl.innerHTML = `
350
+ <div class="user-list-header">
351
+ <h3>Users (${users.length})</h3>
352
+ </div>
353
+ <div class="user-list-content">
354
+ ${users.map(user => `
355
+ <div class="user-item" data-user="${user}">
356
+ <div class="user-avatar">${generateAvatar(user)}</div>
357
+ <div class="user-name">${user}</div>
358
+ </div>
359
+ `).join('')}
360
+ </div>
361
+ `;
362
+ channelUserListEl.querySelectorAll('.user-item').forEach(item => {
363
+ item.addEventListener('click', () => {
364
+ const user = item.getAttribute('data-user');
365
+ showPMDialog(user);
366
+ });
367
+ });
368
+ }
369
+
370
+ // Event Listeners
371
+ connectBtn.addEventListener('click', () => {
372
+ showConnectionDialog();
373
+ });
374
+
375
+ connectionDialog.querySelector('.cancel').addEventListener('click', () => {
376
+ hideConnectionDialog();
377
+ });
378
+
379
+ connectionForm.addEventListener('submit', (e) => {
380
+ e.preventDefault();
381
+ const nickname = document.getElementById('nickname').value;
382
+ currentChannel = document.getElementById('channel').value;
383
+
384
+ if (!nickname || !currentChannel) {
385
+ addChannelMessage(currentChannel, { type: 'error', text: 'Please enter both nickname and channel', timestamp: Date.now(), channel: currentChannel });
386
+ return;
387
+ }
388
+
389
+ hideConnectionDialog();
390
+ updateConnectionStatus('Connecting to Libera Chat...');
391
+ addChannelMessage(currentChannel, { type: 'system', text: 'Connecting to Libera Chat...', timestamp: Date.now(), channel: currentChannel });
392
+
393
+ try {
394
+ irc.connect(nickname);
395
+ activeChannels.add(currentChannel);
396
+ selectedChannel = currentChannel;
397
+ updateChannelList();
398
+ } catch (error) {
399
+ addChannelMessage(currentChannel, { type: 'error', text: `Failed to connect: ${error.message}`, timestamp: Date.now(), channel: currentChannel });
400
+ }
401
+ });
402
+
403
+ messageForm.addEventListener('submit', (e) => {
404
+ e.preventDefault();
405
+ const text = messageInput.value.trim();
406
+ if (!text) return;
407
+
408
+ if (text.startsWith('/')) {
409
+ const [command, ...args] = text.slice(1).split(' ');
410
+ switch (command.toLowerCase()) {
411
+ case 'join':
412
+ if (args[0]) {
413
+ let channel = args[0];
414
+ if (!channel.startsWith('#')) channel = '#' + channel;
415
+ irc.join(channel);
416
+ activeChannels.add(channel);
417
+ selectedChannel = channel;
418
+ currentChannel = channel;
419
+ updateChannelList();
420
+ }
421
+ break;
422
+ case 'part':
423
+ if (selectedChannel) {
424
+ irc.part(selectedChannel);
425
+ activeChannels.delete(selectedChannel);
426
+ selectedChannel = '';
427
+ updateChannelList();
428
+ messagesContainer.innerHTML = '';
429
+ updateHeader(); // Clear header when leaving channel
430
+ }
431
+ break;
432
+ case 'me':
433
+ if (args.length > 0 && currentChannel) {
434
+ const action = args.join(' ');
435
+ irc.send(`PRIVMSG ${currentChannel} :\u0001ACTION ${action}\u0001`);
436
+ addChannelMessage(currentChannel, { type: 'action', from: irc.nickname, text: action, timestamp: Date.now(), channel: currentChannel });
437
+ }
438
+ break;
439
+ case 'msg':
440
+ if (args.length >= 2) {
441
+ const [target, ...msgParts] = args;
442
+ const message = msgParts.join(' ');
443
+ irc.sendPrivateMessage(target, message);
444
+ addPMMessage(target, { type: 'sent', from: irc.nickname, text: message, timestamp: Date.now() });
445
+ }
446
+ break;
447
+ default:
448
+ addChannelMessage(currentChannel, { type: 'system', text: 'Unknown command', timestamp: Date.now(), channel: currentChannel });
449
+ }
450
+ } else if (currentChannel) {
451
+ irc.sendMessage(currentChannel, text);
452
+ addChannelMessage(currentChannel, { type: 'sent', from: irc.nickname, text, timestamp: Date.now(), channel: currentChannel });
453
+ }
454
+
455
+ messageInput.value = '';
456
+ });
457
+
458
+ // Settings panel toggle
459
+ settingsBtn.addEventListener('click', () => {
460
+ settingsPanel.classList.toggle('open');
461
+ });
462
+ closeSettingsBtn.addEventListener('click', () => {
463
+ settingsPanel.classList.remove('open');
464
+ });
465
+
466
+ // Channel search listener
467
+ chatSearchInput.addEventListener('input', () => {
468
+ updateChannelList();
469
+ });
470
+
471
+ // PM dialog input listener
472
+ pmInput.addEventListener('keyup', (e) => {
473
+ if (e.key === 'Enter' && currentPMUser) {
474
+ const text = pmInput.value.trim();
475
+ if (text) {
476
+ irc.sendPrivateMessage(currentPMUser, text);
477
+ addPMMessage(currentPMUser, { type: 'sent', from: irc.nickname, text, timestamp: Date.now() });
478
+ pmInput.value = '';
479
+ }
480
+ }
481
+ });
482
+ pmCloseBtn.addEventListener('click', hidePMDialog);
483
+
484
+ // Back button listener
485
+ backBtn.addEventListener('click', () => {
486
+ channelInfoEl.classList.add('hidden');
487
+ });
488
+
489
+ // IRC event handlers
490
+ irc.on('connected', () => {
491
+ addChannelMessage(currentChannel, { type: 'system', text: 'Connected to Libera Chat', timestamp: Date.now(), channel: currentChannel });
492
+ updateConnectionStatus('Connected to Libera Chat');
493
+ messageInput.disabled = false;
494
+ messageForm.querySelector('button').disabled = false;
495
+ if (currentChannel) {
496
+ irc.join(currentChannel);
497
+ }
498
+ });
499
+
500
+ irc.on('message', (data) => {
501
+ if (data.target === irc.nickname) {
502
+ // Private message
503
+ const type = data.from === irc.nickname ? 'sent' : 'received';
504
+ let text = data.message;
505
+ if (data.isAction) {
506
+ text = text.replace(/^\u0001ACTION /, '').replace(/\u0001$/, '');
507
+ addPMMessage(data.from, { type: 'action', from: data.from, text, timestamp: data.timestamp }); // Use timestamp from IRC event
508
+ } else {
509
+ addPMMessage(data.from, { type, from: data.from, text, timestamp: data.timestamp }); // Use timestamp from IRC event
510
+ }
511
+ } else {
512
+ const channel = data.target;
513
+ const type = data.from === irc.nickname ? 'sent' : 'received';
514
+ let text = data.message;
515
+ if (data.isAction) {
516
+ text = text.replace(/^\u0001ACTION /, '').replace(/\u0001$/, '');
517
+ addChannelMessage(channel, { type: 'action', from: data.from, text, timestamp: data.timestamp, channel }); // Use timestamp from IRC event
518
+ } else {
519
+ addChannelMessage(channel, { type, from: data.from, text, timestamp: data.timestamp, channel }); // Use timestamp from IRC event
520
+ }
521
+ }
522
+ });
523
+
524
+ irc.on('join', (data) => {
525
+ addChannelMessage(data.channel, { type: 'system', text: `${data.nick} joined ${data.channel}`, timestamp: Date.now(), channel: data.channel });
526
+ irc.requestChannelInfo(data.channel); // Request channel info on join
527
+ });
528
+
529
+ irc.on('part', (data) => {
530
+ addChannelMessage(data.channel, { type: 'system', text: `${data.nick} left ${data.channel}`, timestamp: Date.now(), channel: data.channel });
531
+ if (data.nick === irc.nickname && selectedChannel === data.channel) {
532
+ selectedChannel = ''; // Clear selected channel if user parts
533
+ messagesContainer.innerHTML = ''; // Clear messages
534
+ updateUserList(); // Clear user list
535
+ updateHeader(); // Update header
536
+ channelInfoEl.classList.add('hidden'); // Hide channel info
537
+ }
538
+ });
539
+
540
+ irc.on('error', (error) => {
541
+ let errorMessage = error;
542
+ if (error instanceof Event) {
543
+ errorMessage = 'Connection error occurred';
544
+ } else if (typeof error === 'object') {
545
+ errorMessage = error.message || 'Unknown error occurred';
546
+ }
547
+ addChannelMessage(currentChannel, { type: 'error', text: `Error: ${errorMessage}`, timestamp: Date.now(), channel: currentChannel });
548
+ messageInput.disabled = true;
549
+ messageForm.querySelector('button').disabled = true;
550
+ });
551
+
552
+ irc.onChannelUsers(({ channel, users }) => {
553
+ if (channel === selectedChannel) {
554
+ updateUserList();
555
+ }
556
+ updateChannelList();
557
+ });
558
+
559
+ irc.onTopic(({ channel, topic }) => {
560
+ if (channel === selectedChannel) {
561
+ updateHeader();
562
+ channelTopicEl.textContent = topic; // Update topic in channel info as well
563
+ }
564
+ updateChannelList();
565
+ });
566
+
567
+ irc.onServerInfo((info) => {
568
+ if (serverInfoText) {
569
+ serverInfoText.textContent = `Users: ${info.users} | Channels: ${info.channels}`;
570
+ }
571
+ });
572
+
573
+ irc.onChannelList((channels) => {
574
+ globalChannels = new Set(channels);
575
+ });
576
+
577
+ userSearchInput.addEventListener('input', () => {
578
+ const searchQuery = userSearchInput.value.toLowerCase();
579
+ const users = irc.getChannelUsers(selectedChannel);
580
+ const filteredUsers = users.filter(user => user.toLowerCase().includes(searchQuery));
581
+ channelUserListEl.querySelector('.user-list-content').innerHTML = `
582
+ ${filteredUsers.map(user => `
583
+ <div class="user-item" data-user="${user}">
584
+ <div class="user-avatar">${generateAvatar(user)}</div>
585
+ <div class="user-name">${user}</div>
586
+ </div>
587
+ `).join('')}
588
+ `;
589
+ channelUserListEl.querySelectorAll('.user-item').forEach(item => {
590
+ item.addEventListener('click', () => {
591
+ const user = item.getAttribute('data-user');
592
+ showPMDialog(user);
593
+ });
594
+ });
595
+ });
596
+
597
+ function initializeThemeSelector() {
598
+ const themes = [
599
+ { name: 'Light', class: 'theme-light' },
600
+ { name: 'Dark', class: 'theme-dark' },
601
+ { name: 'System', class: 'theme-system' }
602
+ ];
603
+
604
+ const themeSelector = document.createElement('div');
605
+ themeSelector.className = 'theme-switcher';
606
+ themeSelector.innerHTML = `
607
+ <div class="section-header">Theme</div>
608
+ ${themes.map(theme => `
609
+ <label class="theme-option">
610
+ <input type="radio" name="theme" value="${theme.class}">
611
+ ${theme.name}
612
+ </label>
613
+ `).join('')}
614
+ `;
615
+
616
+ document.querySelector('.settings-content').appendChild(themeSelector);
617
+ }
618
+
619
+ initializeUI();
620
+ initializeThemeSelector();
621
+ });