├── img
├── favicon-16x16.png
└── favicon-32x32.png
├── style.css
├── index.html
├── tmi.min.js
└── main.js
/img/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Glodenox/twitch-chat-monitor/HEAD/img/favicon-16x16.png
--------------------------------------------------------------------------------
/img/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Glodenox/twitch-chat-monitor/HEAD/img/favicon-32x32.png
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: black;
3 | background-color: var(--background-color);
4 | margin: 0;
5 | color: #eee;
6 | color: var(--text-color);
7 | font-size: 32px;
8 | font-size: var(--font-size);
9 | font-family: "Open Sans Condensed", sans-serif;
10 | font-weight: 300;
11 | }
12 |
13 | body.hide-cursor {
14 | cursor: none;
15 | }
16 |
17 | .hidden {
18 | display: none !important;
19 | }
20 |
21 | #chat-container {
22 | overflow: hidden;
23 | height: 100vh;
24 | }
25 |
26 | #chat {
27 | display: flex;
28 | flex-direction: column-reverse;
29 | margin-top: 0;
30 | margin-bottom: 100vh;
31 | }
32 |
33 | body.reverse-order #chat {
34 | flex-direction: column;
35 | margin-top: 100vh;
36 | margin-bottom: 0;
37 | }
38 |
39 | #chat > div {
40 | padding: 3px 1px 3px 6px;
41 | line-height: 35px;
42 | line-height: calc(var(--font-size) + 3px);
43 | border-bottom: #444 solid 1px;
44 | border-bottom-color: var(--separator-color);
45 | }
46 | #chat > div:nth-child(odd) {
47 | background-color: #111;
48 | background-color: var(--odd-background-color);
49 | }
50 |
51 | #chat .deleted {
52 | font-style: italic;
53 | }
54 |
55 | #chat:not(.hide-timestamps) .timestamp {
56 | margin-right: 5px;
57 | }
58 |
59 | #chat div.emoticon {
60 | display: inline-block;
61 | height: 28px;
62 | width: 28px;
63 | background-repeat: no-repeat;
64 | background-position: center;
65 | vertical-align: middle;
66 | }
67 |
68 | #chat img {
69 | max-width: 100%;
70 | max-height: 50vh;
71 | max-height: var(--inline-images-height);
72 | color: inherit;
73 | margin: 0;
74 | vertical-align: baseline;
75 | }
76 |
77 | #chat img.user-image {
78 | vertical-align: top;
79 | padding: 4px;
80 | }
81 |
82 | #chat img.user-image:not(.loaded) {
83 | display: none;
84 | }
85 |
86 | #chat .image-loading {
87 | font-style: italic;
88 | font-size: 80%;
89 | }
90 |
91 | #chat div.tweet-embed {
92 | display: inline-block;
93 | width: 550px;
94 | }
95 |
96 | #chat .chat-user::after {
97 | color: #eee;
98 | color: var(--text-color);
99 | font-weight: 300;
100 | }
101 |
102 | #chat .chat-user:not(.action)::after {
103 | content: ': ';
104 | }
105 |
106 | #chat .chat-user.action::after {
107 | content: ' ';
108 | }
109 |
110 | #chat .chat-user {
111 | color: green;
112 | color: var(--user-color);
113 | font-weight: 700;
114 | }
115 |
116 | #chat .chat-user.vip {
117 | color: #4686f8;
118 | color: var(--vip-color);
119 | }
120 |
121 | #chat .chat-user.admin {
122 | color: #a970ff;
123 | color: var(--admin-color);
124 | }
125 |
126 | #chat .chat-user.staff {
127 | color: #a970ff;
128 | color: var(--staff-color);
129 | }
130 |
131 | #chat .chat-user.moderator {
132 | color: #8383f9;
133 | color: var(--moderator-color);
134 | }
135 |
136 | #chat.align-messages .chat-user {
137 | flex-basis: 270px;
138 | flex-shrink: 0;
139 | text-overflow: ellipsis;
140 | overflow: hidden;
141 | text-align: right;
142 | padding-right: 5px;
143 | }
144 |
145 | #chat.align-messages > div.notice .chat-user, #chat.align-messages > div.highlight .chat-user {
146 | flex-basis: 258px;
147 | }
148 |
149 | #chat.align-messages > div {
150 | display: flex;
151 | }
152 | #chat.align-messages > div.subscription {
153 | display: block;
154 | }
155 |
156 | #chat > div.highlight, #chat > div.notice {
157 | padding-left: 12px;
158 | border-left: 6px solid;
159 | }
160 |
161 | #chat > div.highlight {
162 | border-left-color: #731180;
163 | border-left-color: var(--highlight-color);
164 | background-color: #73118030;
165 | background-color: var(--highlight-background-color);
166 | }
167 |
168 | #chat > div.notice {
169 | border-left-color: #eee;
170 | border-left-color: var(--notice-color);
171 | background-color: #eeeeee30;
172 | background-color: var(--notice-background-color);
173 | }
174 |
175 | #chat > div.channel {
176 | border-left-color: #0d86ff;
177 | border-left-color: var(--channel-color);
178 | background-color: #0d86ff30;
179 | background-color: var(--channel-background-color);
180 | }
181 |
182 | #chat a, #settings a {
183 | color: inherit;
184 | }
185 |
186 | #chat .cheer, #chat .cheer img {
187 | margin-right: 5px;
188 | font-weight: 700;
189 | }
190 |
191 | #chat .cheer-1 {
192 | color: gray;
193 | }
194 | #chat .cheer-100 {
195 | color: cyan;
196 | }
197 | #chat .cheer-500 {
198 | color: blue;
199 | }
200 | #chat .cheer-1000 {
201 | color: red;
202 | }
203 | #chat .cheer-10000 {
204 | color: yellow;
205 | }
206 |
207 | #chat .counter {
208 | font-weight: bold;
209 | float: right;
210 | font-size: 80%;
211 | transition: 1s;
212 | padding-right: 10px;
213 | }
214 | #chat .counter.bump {
215 | transition: 0.1s;
216 | font-size: 150%;
217 | }
218 |
219 | #chat.hide-timestamps .timestamps {
220 | display: none;
221 | }
222 |
223 | #commands {
224 | position: absolute;
225 | right: 5px;
226 | bottom: 5px;
227 | display: flex;
228 | }
229 |
230 | body.reverse-order #commands {
231 | bottom: auto;
232 | top: 5px;
233 | }
234 |
235 | #fullscreen, #settings-toggle {
236 | z-index: 10;
237 | opacity: 0.3;
238 | margin-left: 25px;
239 | cursor: pointer;
240 | }
241 | #fullscreen:hover, #settings-toggle:hover, #settings-toggle.open {
242 | opacity: 1;
243 | }
244 |
245 | body.reverse-order #settings-toggle {
246 | top: 5px;
247 | bottom: auto;
248 | }
249 |
250 | #fps {
251 | position: absolute;
252 | right: 15px;
253 | top: 15px;
254 | border: 2px solid #eee;
255 | border-color: var(--text-color);
256 | border-radius: 10px;
257 | width: 3em;
258 | text-align: center;
259 | background-color: black;
260 | background-color: var(--background-color);
261 | opacity: 0.9;
262 | }
263 |
264 | body.show-message-entry #fps {
265 | top: calc(15px + var(--font-size));
266 | }
267 |
268 | body.reverse-order #fps {
269 | top: auto;
270 | bottom: 15px;
271 | }
272 |
273 | body.show-message-entry.reverse-order #fps {
274 | bottom: calc(15px + var(--font-size));
275 | }
276 |
277 | .notifications {
278 | position: absolute;
279 | bottom: 10px;
280 | left: 0;
281 | right: 0;
282 | display: flex;
283 | flex-direction: column;
284 | align-items: center;
285 | }
286 |
287 | body.reverse-order .notifications {
288 | bottom: auto;
289 | top: 10px;
290 | }
291 |
292 | body.show-message-entry:not(.reverse-order) .notifications {
293 | bottom: calc(14px + var(--font-size));
294 | }
295 | body.show-message-entry.reverse-order .notifications {
296 | top: calc(14px + var(--font-size));
297 | }
298 |
299 | .notifications div {
300 | border: 2px solid #eee;
301 | border-color: var(--text-color);
302 | padding: 5px;
303 | margin: 5px;
304 | border-radius: 10px;
305 | background-color: black;
306 | background-color: var(--background-color);
307 | opacity: 0.9;
308 | }
309 |
310 | #settings {
311 | position: absolute;
312 | right: 20px;
313 | bottom: var(--font-size);
314 | left: 20px;
315 | margin-bottom: 15px;
316 | padding: 20px;
317 | max-height: 50vh;
318 | overflow-y: auto;
319 | border: 3px solid #444;
320 | border-color: var(--separator-color);
321 | background-color: black;
322 | background-color: var(--background-color);
323 | z-index: 9999;
324 | }
325 |
326 | body.reverse-order #settings {
327 | top: 60px;
328 | bottom: auto;
329 | }
330 |
331 | #settings h2 {
332 | margin-top: 0;
333 | text-align: center;
334 | }
335 |
336 | #settings h3 {
337 | padding: 5px 20px;
338 | background-color: var(--background-color);
339 | margin: 0;
340 | }
341 |
342 | #settings > div {
343 | background-color: #111;
344 | background-color: var(--odd-background-color);
345 | padding: 10px;
346 | }
347 |
348 | #settings > div > div {
349 | margin: 10px 0;
350 | }
351 |
352 | #settings .fields {
353 | display: grid;
354 | grid-gap: 10px;
355 | grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
356 | }
357 |
358 | #settings .fields.centered > div {
359 | text-align: center;
360 | }
361 |
362 | #settings .fields .style-preview {
363 | border: 2px solid var(--text-color);
364 | border-radius: 10px;
365 | padding: 10px;
366 | background-color: var(--background-color);
367 | cursor: pointer;
368 | opacity: 0.8;
369 | display: flex;
370 | flex-direction: column;
371 | align-items: center;
372 | }
373 |
374 | #settings .fields .style-preview.active {
375 | opacity: 1;
376 | border-color: var(--highlight-color);
377 | background-color: var(--highlight-background-color);
378 | }
379 |
380 | #settings .fields .style-preview:not(.active):hover {
381 | opacity: 1;
382 | }
383 |
384 | #settings .subfield {
385 | margin-top: 5px;
386 | margin-left: 40px;
387 | border-left: 5px solid #eee;
388 | border-color: var(--text-color);
389 | padding-left: 10px;
390 | }
391 |
392 | #settings label.disabled {
393 | opacity: 0.5;
394 | cursor: not-allowed;
395 | }
396 |
397 | #settings input, #settings button, #message-entry input {
398 | font-size: 0.625em;
399 | vertical-align: bottom;
400 | color: inherit;
401 | }
402 |
403 | #settings button:disabled {
404 | opacity: 0.5;
405 | cursor: default;
406 | }
407 |
408 | #settings input[type="checkbox"] {
409 | appearance: none;
410 | border: 2px solid #eee;
411 | border-color: var(--text-color);
412 | text-align: center;
413 | padding-top: 2px;
414 | width: 32px;
415 | width: calc(var(--font-size) + 4px);
416 | height: 32px;
417 | height: calc(var(--font-size) + 4px);
418 | cursor: pointer;
419 | }
420 |
421 | #settings input[type="checkbox"]:checked::after {
422 | content: "\2713";
423 | font-weight: 700;
424 | }
425 |
426 | #settings input[type="number"], #settings input[type="text"], #settings input[type="password"], #settings input[type="submit"], #settings button, #message-entry input {
427 | height: 32px;
428 | height: var(--font-size);
429 | padding: 0 5px;
430 | border: 2px solid #eee;
431 | border-color: var(--text-color);
432 | background-color: black;
433 | background-color: var(--background-color);
434 | max-width: 95%;
435 | }
436 |
437 | #settings input[type="submit"], #settings button, #message-entry input[type="submit"] {
438 | height: 38px;
439 | height: calc(var(--font-size) + 4px);
440 | padding: 0 15px;
441 | border-radius: 8px;
442 | cursor: pointer;
443 | }
444 |
445 | #settings input[type="submit"]:hover, #settings input[type="submit"]:active, #settings button:hover, #settings button:active, #message-entry input[type="submit"]:hover, #message-entry input[type="submit"]:active {
446 | background-color: #111;
447 | background-color: var(--odd-background-color);
448 | }
449 |
450 | #settings input[type="number"]:invalid, #settings input[type="text"]:invalid, #settings input[type="password"]:invalid {
451 | border-color: red;
452 | }
453 |
454 | #settings input[type="color"] {
455 | appearance: none;
456 | border: 2px solid #eee;
457 | border-color: var(--text-color);
458 | background-color: black;
459 | background-color: var(--background-color);
460 | padding: 5px;
461 | height: 32px;
462 | cursor: pointer;
463 | }
464 |
465 | #settings select {
466 | font-size: 0.625em;
467 | color: #eee;
468 | border-color: var(--text-color);
469 | background-color: black;
470 | background-color: var(--background-color);
471 | }
472 |
473 | #curtain {
474 | position: fixed;
475 | top: 0;
476 | right: 0;
477 | bottom: 0;
478 | left: 0;
479 | background-color: inherit;
480 | display: flex;
481 | align-items: center;
482 | justify-content: center;
483 | text-align: center;
484 | opacity: 1;
485 | animation: curtain 1s;
486 | }
487 | #curtain.hidden {
488 | opacity: 0;
489 | }
490 |
491 | @keyframes curtain {
492 | from { opacity: 0; }
493 | to { opacity: 1; }
494 | }
495 |
496 | body.show-message-entry:not(.reverse-order) #commands {
497 | bottom: calc(9px + var(--font-size));
498 | }
499 |
500 | body.show-message-entry.reverse-order #commands {
501 | top: calc(9px + var(--font-size));
502 | }
503 |
504 | body.show-message-entry:not(.reverse-order) #settings {
505 | bottom: calc(var(--font-size) * 2);
506 | }
507 |
508 | body.show-message-entry.reverse-order #settings {
509 | top: calc(60px + var(--font-size));
510 | }
511 |
512 | body:not(.show-message-entry) #message-entry {
513 | display: none;
514 | }
515 |
516 | #message-entry {
517 | position: fixed;
518 | bottom: 0;
519 | width: 100%;
520 | display: flex;
521 | height: calc(4px + var(--font-size));
522 | background-color: black;
523 | background-color: var(--background-color);
524 | }
525 |
526 | body.reverse-order #message-entry {
527 | bottom: auto;
528 | top: 0;
529 | }
530 |
531 | #message-entry span {
532 | font-size: 0.8em;
533 | }
534 |
535 | #message-entry input[type="text"] {
536 | flex-grow: 1;
537 | margin: 0 5px;
538 | }
539 |
540 | #message-username::after {
541 | content: ': ';
542 | }
543 |
544 | .help:before {
545 | content: '?';
546 | border-radius: 50%;
547 | background-color: #eee;
548 | background-color: var(--text-color);
549 | color: black;
550 | color: var(--background-color);
551 | font-weight: bold;
552 | padding: 0 0.5em;
553 | font-size: 0.5em;
554 | cursor: pointer;
555 | vertical-align: super;
556 | }
557 | .help::after {
558 | content: attr(data-title);
559 | display: none;
560 | border: 1px solid #eee;
561 | border-color: var(--text-color);
562 | margin: 15px;
563 | color: #eee;
564 | color: var(--text-color);
565 | background-color: black;
566 | background-color: var(--background-color);
567 | padding: 4px;
568 | }
569 | .help.visible::after {
570 | display: block;
571 | }
572 |
573 | .help:hover::after {
574 | opacity: 1;
575 | transition: all 0.1s ease 0.3s;
576 | visibility: visible;
577 | }
578 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Twitch Chat Monitor
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
😴
Chat hidden - press Ctrl+Shift+H to reveal
16 |
17 |
18 |
🔍 Fullscreen
19 |
⚙ Settings
20 |
21 |
22 |
23 |
🔥 Chat overload – 0 messages skipped
24 |
📡 Connection lost – attempting to reestablish
25 |
26 |
27 |
28 |
Twitch
29 |
30 |
35 |
36 |
37 |
38 |
42 |
43 |
44 |
45 |
Style
46 |
47 |
61 |
62 |
63 |
64 |
65 |
66 |
72 |
73 |
74 |
75 |
Chat Behavior
76 |
83 |
84 |
91 |
92 |
93 |
94 |
95 |
101 |
102 |
103 |
Message Handling
104 |
105 |
106 |
107 |
108 |
109 |
110 |
115 |
116 |
117 |
118 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
Message Types
139 |
144 |
145 |
146 |
Stats for Nerds
147 |
150 |
151 |
152 |
Hotkeys overview
153 |
154 |
Ctrl+Shift+H
Hide/show chat
155 |
Ctrl+Shift+S
Open/close settings
156 |
Ctrl+Shift+D
Toggle artificial chat delay
157 |
F11
Toggle fullscreen
158 |
159 |
160 |
161 |
164 |
169 |
170 |
171 |
175 |
176 |
180 |
181 |
185 |
186 |
189 |
190 |
194 |
195 |
199 |
200 |
204 |
205 |
209 |
210 |
214 |
215 |
216 |
217 |
218 |
269 |
270 |
271 |
272 |
--------------------------------------------------------------------------------
/tmi.min.js:
--------------------------------------------------------------------------------
1 | !function s(o,i,r){function a(t,e){if(!i[t]){if(!o[t]){var n="function"==typeof require&&require;if(!e&&n)return n(t,!0);if(c)return c(t,!0);throw(n=new Error("Cannot find module '"+t+"'")).code="MODULE_NOT_FOUND",n}n=i[t]={exports:{}},o[t][0].call(n.exports,function(e){return a(o[t][1][e]||e)},n,n.exports,s,o,i,r)}return i[t].exports}for(var c="function"==typeof require&&require,e=0;ee.length)&&(t=e.length);for(var n=0,s=new Array(t);n: ").concat(h)),V.hasOwn(t.tags,"username")||(t.tags.username=J),t.tags["message-type"]="whisper";J=V.channel(t.tags.username);this.emits(["whisper","message"],[[J,t.tags,h,!1]]);break;case"PRIVMSG":t.tags.username=t.prefix.split("!")[0],"jtv"===t.tags.username?(c=V.username(h.split(" ")[0]),u=h.includes("auto"),h.includes("hosting you for")?(a=V.extractNumber(h),this.emit("hosted",m,c,a,u)):h.includes("hosting you")&&this.emit("hosted",m,c,0,u)):(a=V.get(this.opts.options.messagesLogLevel,"info"),c=V.actionMessage(h),t.tags["message-type"]=c?"action":"chat",h=c?c[1]:h,V.hasOwn(t.tags,"bits")?this.emit("cheer",m,t.tags,h):(V.hasOwn(t.tags,"msg-id")?"highlighted-message"===t.tags["msg-id"]?(u=t.tags["msg-id"],this.emit("redeem",m,t.tags.username,u,t.tags,h)):"skip-subs-mode-message"===t.tags["msg-id"]&&(l=t.tags["msg-id"],this.emit("redeem",m,t.tags.username,l,t.tags,h)):V.hasOwn(t.tags,"custom-reward-id")&&(l=t.tags["custom-reward-id"],this.emit("redeem",m,t.tags.username,l,t.tags,h)),c?(this.log[a]("[".concat(m,"] *<").concat(t.tags.username,">: ").concat(h)),this.emits(["action","message"],[[m,t.tags,h,!1]])):(this.log[a]("[".concat(m,"] <").concat(t.tags.username,">: ").concat(h)),this.emits(["chat","message"],[[m,t.tags,h,!1]]))));break;default:this.log.warn("Could not parse message:\n".concat(JSON.stringify(t,null,4)))}}},n.prototype.connect=function(){var s=this;return new Promise(function(t,n){s.server=V.get(s.opts.connection.server,"irc-ws.chat.twitch.tv"),s.port=V.get(s.opts.connection.port,80),s.secure&&(s.port=443),443===s.port&&(s.secure=!0),s.reconnectTimer=s.reconnectTimer*s.reconnectDecay,s.reconnectTimer>=s.maxReconnectInterval&&(s.reconnectTimer=s.maxReconnectInterval),s._openConnection(),s.once("_promiseConnect",function(e){e?n(e):t([s.server,~~s.port])})})},n.prototype._openConnection=function(){var e="".concat(this.secure?"wss":"ws","://").concat(this.server,":").concat(this.port,"/"),t={};"agent"in this.opts.connection&&(t.agent=this.opts.connection.agent),this.ws=new r(e,"irc",t),this.ws.onmessage=this._onMessage.bind(this),this.ws.onerror=this._onError.bind(this),this.ws.onclose=this._onClose.bind(this),this.ws.onopen=this._onOpen.bind(this)},n.prototype._onOpen=function(){var n=this;this._isConnected()&&(this.log.info("Connecting to ".concat(this.server," on port ").concat(this.port,"..")),this.emit("connecting",this.server,~~this.port),this.username=V.get(this.opts.identity.username,V.justinfan()),this._getToken().then(function(e){var t=V.password(e);n.log.info("Sending authentication to server.."),n.emit("logon");e="twitch.tv/tags twitch.tv/commands";n._skipMembership||(e+=" twitch.tv/membership"),n.ws.send("CAP REQ :"+e),t?n.ws.send("PASS ".concat(t)):V.isJustinfan(n.username)&&n.ws.send("PASS SCHMOOPIIE"),n.ws.send("NICK ".concat(n.username))}).catch(function(e){n.emits(["_promiseConnect","disconnected"],[[e],["Could not get a token."]])}))},n.prototype._getToken=function(){var e,t=this.opts.identity.password;return"function"==typeof t?(e=t())instanceof Promise?e:Promise.resolve(e):Promise.resolve(t)},n.prototype._onMessage=function(e){var t=this;e.data.trim().split("\r\n").forEach(function(e){e=q.msg(e);e&&t.handleMessage(e)})},n.prototype._onError=function(){var t=this;this.moderators={},this.userstate={},this.globaluserstate={},clearInterval(this.pingLoop),clearTimeout(this.pingTimeout),clearTimeout(this._updateEmotesetsTimer),this.reason=null===this.ws?"Connection closed.":"Unable to connect.",this.emits(["_promiseConnect","disconnected"],[[this.reason]]),this.reconnect&&this.reconnections===this.maxReconnectAttempts&&(this.emit("maxreconnect"),this.log.error("Maximum reconnection attempts reached.")),this.reconnect&&!this.reconnecting&&this.reconnections<=this.maxReconnectAttempts-1&&(this.reconnecting=!0,this.reconnections=this.reconnections+1,this.log.error("Reconnecting in ".concat(Math.round(this.reconnectTimer/1e3)," seconds..")),this.emit("reconnect"),setTimeout(function(){t.reconnecting=!1,t.connect().catch(function(e){return t.log.error(e)})},this.reconnectTimer)),this.ws=null},n.prototype._onClose=function(){var t=this;this.moderators={},this.userstate={},this.globaluserstate={},clearInterval(this.pingLoop),clearTimeout(this.pingTimeout),clearTimeout(this._updateEmotesetsTimer),this.wasCloseCalled?(this.wasCloseCalled=!1,this.reason="Connection closed.",this.log.info(this.reason),this.emits(["_promiseConnect","_promiseDisconnect","disconnected"],[[this.reason],[null],[this.reason]])):(this.emits(["_promiseConnect","disconnected"],[[this.reason]]),this.reconnect&&this.reconnections===this.maxReconnectAttempts&&(this.emit("maxreconnect"),this.log.error("Maximum reconnection attempts reached.")),this.reconnect&&!this.reconnecting&&this.reconnections<=this.maxReconnectAttempts-1&&(this.reconnecting=!0,this.reconnections=this.reconnections+1,this.log.error("Could not connect to server. Reconnecting in ".concat(Math.round(this.reconnectTimer/1e3)," seconds..")),this.emit("reconnect"),setTimeout(function(){t.reconnecting=!1,t.connect().catch(function(e){return t.log.error(e)})},this.reconnectTimer))),this.ws=null},n.prototype._getPromiseDelay=function(){return this.currentLatency<=600?600:this.currentLatency+100},n.prototype._sendCommand=function(s,o,i,r){var a=this;return new Promise(function(e,t){if(!a._isConnected())return t("Not connected to server.");var n;null!==s&&"number"!=typeof s||(null===s&&(s=a._getPromiseDelay()),V.promiseDelay(s).then(function(){return t("No response from Twitch.")})),null!==o?(n=V.channel(o),a.log.info("[".concat(n,"] Executing command: ").concat(i)),a.ws.send("PRIVMSG ".concat(n," :").concat(i))):(a.log.info("Executing command: ".concat(i)),a.ws.send(i)),"function"==typeof r?r(e,t):e()})},n.prototype._sendMessage=function(c,u,l,m){var h=this;return new Promise(function(e,t){if(!h._isConnected())return t("Not connected to server.");if(V.isJustinfan(h.getUsername()))return t("Cannot send anonymous messages.");var n,s=V.channel(u);h.userstate[s]||(h.userstate[s]={}),500<=l.length&&(n=V.splitLine(l,500),l=n[0],setTimeout(function(){h._sendMessage(c,u,n[1],function(){})},350)),h.ws.send("PRIVMSG ".concat(s," :").concat(l));var o={};Object.keys(h.emotesets).forEach(function(e){return h.emotesets[e].forEach(function(e){return(V.isRegex(e.code)?q.emoteRegex:q.emoteString)(l,e.code,e.id,o)})});var i=Object.assign(h.userstate[s],q.emotes({emotes:q.transformEmotes(o)||null})),r=V.get(h.opts.options.messagesLogLevel,"info"),a=V.actionMessage(l);a?(i["message-type"]="action",h.log[r]("[".concat(s,"] *<").concat(h.getUsername(),">: ").concat(a[1])),h.emits(["action","message"],[[s,i,a[1],!0]])):(i["message-type"]="chat",h.log[r]("[".concat(s,"] <").concat(h.getUsername(),">: ").concat(l)),h.emits(["chat","message"],[[s,i,l,!0]])),"function"==typeof m?m(e,t):e()})},n.prototype._updateEmoteset=function(s){var t,o=this,e=void 0!==s;e&&(s===this.emotes?e=!1:this.emotes=s),this._skipUpdatingEmotesets?e&&this.emit("emotesets",s,{}):(t=function(){0n&&(this._events[e].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[e].length),"function"==typeof console.trace&&console.trace()),this},o.prototype.once=function(e,t){if(!c(t))throw TypeError("listener must be a function");var n=!1;if(this._events.hasOwnProperty(e)&&"_"===e.charAt(0)){var s,o=1,i=e;for(s in this._events)this._events.hasOwnProperty(s)&&s.startsWith(i)&&o++;e+=o}function r(){"_"!==e.charAt(0)||isNaN(e.substr(e.length-1))||(e=e.substring(0,e.length-1)),this.removeListener(e,r),n||(n=!0,t.apply(this,arguments))}return r.listener=t,this.on(e,r),this},o.prototype.removeListener=function(e,t){var n,s,o,i;if(!c(t))throw TypeError("listener must be a function");if(!this._events||!this._events[e])return this;if(o=(n=this._events[e]).length,s=-1,n===t||c(n.listener)&&n.listener===t){if(delete this._events[e],this._events.hasOwnProperty(e+"2")&&"_"===e.charAt(0)){var r,a=e;for(r in this._events)this._events.hasOwnProperty(r)&&r.startsWith(a)&&(isNaN(parseInt(r.substr(r.length-1)))||(this._events[e+parseInt(r.substr(r.length-1)-1)]=this._events[r],delete this._events[r]));this._events[e]=this._events[e+"1"],delete this._events[e+"1"]}this._events.removeListener&&this.emit("removeListener",e,t)}else if(u(n)){for(i=o;0n?(t.command=e.slice(n),t):null;for(t.command=e.slice(n,s),n=s+1;32===e.charCodeAt(n);)n++;for(;n").replace(/\\"\\;/g,'"').replace(/\\'\\;/g,"'")},unescapeIRC:function(e){return e&&"string"==typeof e&&e.includes("\\")?e.replace(i,function(e,t){return t in a?a[t]:t}):e},escapeIRC:function(e){return e&&"string"==typeof e?e.replace(r,function(e,t){return t in c?"\\".concat(c[t]):t}):e},addWord:function(e,t){return e.length?e+" "+t:e+t},splitLine:function(e,t){var n=e.substring(0,t).lastIndexOf(" ");return[e.substring(0,n=-1===n?t-1:n),e.substring(n+1)]},extractNumber:function(e){for(var t=e.split(" "),n=0;n document.body.style.setProperty('--' + key, settings[key]));
25 | transparentBackgroundKeys.forEach((key) => document.body.style.setProperty('--' + key.replace('-color', '-background-color'), settings[key] + '50'));
26 |
27 | var update = (key, value) => {
28 | settings[key] = value;
29 | localStorage.setItem('config', JSON.stringify(settings));
30 | document.body.style.setProperty('--' + key, value);
31 | // Generate transparent background color values
32 | if (transparentBackgroundKeys.indexOf(key) != -1) {
33 | document.body.style.setProperty(key.replace('-color', '-background-color'), value + '50');
34 | }
35 | };
36 | return {
37 | 'get': (key) => settings[key],
38 | 'set': update,
39 | 'toggle': (key) => update(key, !settings[key]),
40 | 'reset': () => {
41 | Object.assign(settings, defaultSettings);
42 | localStorage.setItem('config', JSON.stringify(defaultSettings));
43 | }
44 | }
45 | }();
46 |
47 | var HexCompressor = function() {
48 | const characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ=#';
49 | return {
50 | color2string: (color) => {
51 | var code = '';
52 | var firstHalf = parseInt(color.substr(1, 3), 16);
53 | code += characters.charAt(Math.floor(firstHalf / 64));
54 | code += characters.charAt(firstHalf % 64);
55 | var secondHalf = parseInt(color.substr(4, 3), 16);
56 | code += characters.charAt(Math.floor(secondHalf / 64));
57 | code += characters.charAt(secondHalf % 64);
58 | return code;
59 | },
60 | string2color: (string) => '#' +
61 | ("00" + (characters.indexOf(string.charAt(0)) * 64 + characters.indexOf(string.charAt(1))).toString(16)).slice(-3) +
62 | ("00" + (characters.indexOf(string.charAt(2)) * 64 + characters.indexOf(string.charAt(3))).toString(16)).slice(-3)
63 | };
64 | }();
65 |
66 | var highlightUsers = Settings.get('highlight-users').toLowerCase().split(',').filter((user) => user != ''),
67 | highlightKeyphrases = Settings.get('highlight-keyphrases').toLowerCase().split(',').filter((phrase) => phrase != '');
68 |
69 | // Object containing references to all relevant UI blocks
70 | var ui = {
71 | main: {
72 | curtain: document.getElementById('curtain'),
73 | fps: document.getElementById('fps')
74 | },
75 | chat: {
76 | body: document.getElementById('chat'),
77 | container: document.getElementById('chat-container')
78 | },
79 | commands: {
80 | body: document.getElementById('commands'),
81 | settings: document.getElementById('settings-toggle'),
82 | fullscreen: document.getElementById('fullscreen')
83 | },
84 | messageEntry: {
85 | body: document.getElementById('message-entry'),
86 | username: document.getElementById('message-username'),
87 | field: document.querySelector('#message-entry .message-field')
88 | },
89 | settings: {
90 | body: document.getElementById('settings'),
91 | twitch: {
92 | channel: document.getElementById('settings-channel'),
93 | channelOverride: document.getElementById('settings-channel-override'),
94 | identity: {
95 | toggle: document.getElementById('settings-twitch-messagefield'),
96 | body: document.getElementById('settings-twitch-messaging'),
97 | username: document.getElementById('settings-twitch-username'),
98 | token: document.getElementById('settings-twitch-token')
99 | }
100 | },
101 | style: {
102 | custom: {
103 | container: document.getElementById('styles'),
104 | selector: document.getElementById('settings-custom-style'),
105 | exchange: document.getElementById('settings-custom-style-exchange'),
106 | field: document.getElementById('settings-custom-style-exchange-field'),
107 | preview: document.getElementById('style-template')
108 | },
109 | fontSize: document.getElementById('settings-font-size'),
110 | hideCursor: document.getElementById('settings-hide-cursor'),
111 | adjustTitle: document.getElementById('settings-adjust-page-title'),
112 | showUnreadInTitle: document.getElementById('settings-unread-counter-in-page-title'),
113 | animateEmoji : document.getElementById('settings-animate-emoji')
114 | },
115 | behaviour: {
116 | limitRate: {
117 | toggle: document.getElementById('settings-limit-message-rate'),
118 | body: document.getElementById('settings-limit-message-rate').parentNode.nextElementSibling,
119 | field: document.getElementById('settings-message-rate')
120 | },
121 | reverseOrder: document.getElementById('settings-new-messages-on-top'),
122 | smoothScroll: {
123 | body: document.getElementById('settings-smooth-scroll').parentNode.nextElementSibling,
124 | duration: document.getElementById('settings-smooth-scroll-duration')
125 | },
126 | chatDelay: {
127 | toggle: document.getElementById('settings-enable-chat-delay'),
128 | body: document.getElementById('settings-chat-delay').parentNode.parentNode,
129 | delay: document.getElementById('settings-chat-delay')
130 | }
131 | },
132 | messageHandling: {
133 | inlineImages: {
134 | body: document.getElementById('settings-inline-images').parentNode.nextElementSibling.nextElementSibling,
135 | height: document.getElementById('settings-inline-images-height')
136 | },
137 | timestamps: document.getElementById('settings-timestamps'),
138 | highlightUsers: document.getElementById('settings-highlight-users'),
139 | keyPhrases: document.getElementById('settings-highlight-keyphrases')
140 | }
141 | },
142 | notifications: {
143 | chatOverload: {
144 | body: document.getElementById('chat-overload'),
145 | count: document.getElementById('chat-overload-count')
146 | },
147 | networkStatus: document.getElementById('network-status'),
148 | keyPhrases: document.getElementById('settings-highlight-keyphrases')
149 | }
150 | };
151 |
152 | /** Set up chat client **/
153 | var configuration = {
154 | options: {
155 | skipMembership: true,
156 | skipUpdatingEmotesets: true // the API no longer exists on Kraken
157 | }
158 | };
159 | if (Settings.get('identity')) {
160 | configuration.identity = Settings.get('identity');
161 | }
162 | var client = new tmi.client(configuration);
163 | client.on('message', handleChat);
164 | client.on('roomstate', handleRoomstate);
165 | client.on('subscription', (channel, username, method, message, userstate) => handleSubscription(username, message, userstate));
166 | client.on('resub', (channel, username, months, message, userstate, methods) => handleSubscription(username, message, userstate));
167 | client.on('submysterygift', (channel, username, numbOfSubs, methods, userstate) => handleSubscription(username, null, userstate));
168 | client.on('cheer', handleCheer);
169 | client.on('raided', (channel, username, viewers) => addNotice(`${username} raided the channel with ${viewers} viewers!`));
170 | client.on('usernotice', (msgid, channel, userstate, message) => handleChat(channel, userstate, message));
171 | client.on('slowmode', (channel, enabled, length) => addNotice(`Slowmode chat has been ${enabled ? 'activated' : 'deactivated'}.`));
172 | client.on('followersonly', (channel, enabled, length) => addNotice(`Followers-only chat has been ${enabled ? 'activated' : 'deactivated'}.`));
173 | client.on('emoteonly', (channel, enabled) => addNotice(`Emote-only chat has been ${enabled ? 'activated' : 'deactivated'}.`));
174 | client.on('hosting', (channel, target) => addNotice(`The channel is now hosting ${target}.`));
175 | client.on('unhost', (channel) => addNotice(`The channel has stopped hosting another channel.`));
176 | client.on('messagedeleted', handleMessageDeletion);
177 | client.on('ban', (channel, username, reason, userstate) => handleModAction('ban', username, null, userstate));
178 | client.on('timeout', (channel, username, reason, duration, userstate) => handleModAction('timeout', username, duration, userstate));
179 | client.on('clearchat', (channel) => {
180 | ui.chat.body.textContent = '';
181 | addNotice('Chat has been cleared by a moderator');
182 | });
183 | // Network connection monitoring
184 | client.on('disconnected', () => ui.notifications.networkStatus.classList.remove('hidden'));
185 | client.on('connected', () => {
186 | if (!ui.notifications.networkStatus.classList.contains('hidden')) {
187 | addNotice('Connection reestablished, resuming chat monitoring.');
188 | }
189 | ui.notifications.networkStatus.classList.add('hidden');
190 | });
191 | client.connect().then(() => {
192 | let channelFromPath = (document.location.href.match(/channel=([A-Za-z0-9_]+)/) || [null])[1];
193 | if (channelFromPath) {
194 | joinChannel(channelFromPath);
195 | ui.settings.twitch.channelOverride.classList.remove('hidden');
196 | } else {
197 | joinChannel(Settings.get('channel'));
198 | }
199 | });
200 |
201 | /** Interface interactions **/
202 | // Message sending
203 | ui.messageEntry.body.addEventListener('submit', (e) => {
204 | let field = ui.messageEntry.field;
205 | if (field.value.trim().length > 0) {
206 | field.disabled = true;
207 | client.say(client.channels[0], field.value.trim()).then(() => {
208 | field.value = '';
209 | field.disabled = false;
210 | field.focus();
211 | }).catch(() => {
212 | field.disabled = false;
213 | field.focus();
214 | });
215 | }
216 | e.preventDefault();
217 | });
218 | if (document.body.classList.contains('show-message-entry')) {
219 | ui.messageEntry.field.focus();
220 | }
221 | // Settings
222 | ui.commands.settings.addEventListener('click', () => {
223 | ui.settings.body.classList.toggle('hidden');
224 | ui.settings.body.scrollTop = 0;
225 | ui.commands.settings.classList.toggle('open');
226 | });
227 | document.querySelectorAll('.help').forEach((help) => help.addEventListener('click', () => help.classList.toggle('visible')));
228 | // Twitch
229 | ui.settings.twitch.channel.value = Settings.get('channel');
230 | ui.settings.twitch.channel.addEventListener('input', (e) => e.target.value = e.target.value.replaceAll('https://www.twitch.tv/', '').replaceAll('twitch.tv/', ''));
231 | ui.settings.twitch.channel.form.addEventListener('submit', (e) => {
232 | var channel = ui.settings.twitch.channel.value;
233 | if (channel != '' && client.channels.indexOf(ensureHash(channel)) == -1) {
234 | if (client.channels.length > 0) {
235 | client.leave(ensureHash(client.channels[0]));
236 | }
237 | // Fade out all previous channel messages before joining new one
238 | ui.chat.body.querySelectorAll('div').forEach((msg) => msg.style.opacity = 0.5);
239 | Settings.set('channel', channel);
240 | joinChannel(channel);
241 | ui.settings.twitch.channelOverride.classList.add('hidden');
242 | }
243 | e.preventDefault();
244 | });
245 | if (Settings.get('identity')) {
246 | let identity = ui.settings.twitch.identity;
247 | identity.username.value = Settings.get('identity').username;
248 | identity.token.value = Settings.get('identity').token;
249 | identity.body.classList.remove('disabled');
250 | identity.toggle.classList.remove('disabled');
251 | identity.toggle.disabled = false;
252 | ui.messageEntry.username.textContent = Settings.get('identity').username;
253 | document.body.classList.toggle('show-message-entry', Settings.get('twitch-messagefield'));
254 | }
255 | configureToggler('twitch-messagefield', () => {
256 | document.body.classList.toggle('show-message-entry', Settings.get('twitch-messagefield'));
257 | if (Settings.get('twitch-messagefield') && client.username.toLowerCase() != Settings.get('identity').username.toLowerCase()) {
258 | ui.messageEntry.username.textContent = Settings.get('identity').username;
259 | client.disconnect();
260 | client.opts.identity = Settings.get('identity');
261 | client.opts.username = Settings.get('identity').username;
262 | client.connect();
263 | }
264 | });
265 | ui.settings.twitch.identity.username.addEventListener('input', (e) => {
266 | let identity = ui.settings.twitch.identity;
267 | if (e.target.value.length > 0 && identity.token.value.length > 0 && identity.token.validity.valid) {
268 | Settings.set('identity', {
269 | 'username': identity.username.value,
270 | 'password': identity.token.value
271 | });
272 | identity.body.classList.remove('disabled');
273 | identity.toggle.disabled = false;
274 | } else {
275 | identity.body.classList.add('disabled');
276 | identity.toggle.disabled = false;
277 | identity.toggle.checked = false;
278 | document.body.classList.remove('show-message-entry');
279 | Settings.set('twitch-messagefield', false);
280 | Settings.set('identity', null);
281 | if (!client.username.startsWith('justinfan')) { // already logged out
282 | identity.username.textContent = '';
283 | client.disconnect();
284 | client.opts.identity = {};
285 | delete client.opts.username;
286 | client.connect();
287 | }
288 | }
289 | });
290 | ui.settings.twitch.identity.token.addEventListener('input', (e) => {
291 | let identity = ui.settings.twitch.identity;
292 | if (/^[0-9a-z]{30}$/.test(e.target.value)) {
293 | e.target.value = "oauth:" + e.target.value;
294 | }
295 | if (!/^oauth:[0-9a-z]{30}$/.test(e.target.value)) {
296 | e.target.setCustomValidity('Invalid token');
297 | e.target.reportValidity();
298 | identity.body.classList.add('disabled');
299 | identity.toggle.disabled = true;
300 | identity.toggle.checked = false;
301 | document.body.classList.remove('show-message-entry');
302 | Settings.set('twitch-messagefield', false);
303 | Settings.set('identity', null);
304 | if (!client.username.startsWith('justinfan')) { // already logged out
305 | identity.username.textContent = '';
306 | client.disconnect();
307 | client.opts.identity = {};
308 | delete client.opts.username;
309 | client.connect();
310 | }
311 | return;
312 | }
313 | e.target.setCustomValidity('');
314 | if (identity.username.value.length > 0) {
315 | Settings.set('identity', {
316 | 'username': identity.username.value,
317 | 'password': identity.token.value
318 | });
319 | identity.body.classList.remove('disabled');
320 | identity.toggle.disabled = false;
321 | }
322 | });
323 | // Style
324 | if (Settings.get('show-command-buttons')) {
325 | ui.commands.body.classList.remove('hidden');
326 | }
327 | if (document.fullscreenEnabled && Settings.get('support-fullscreen')) {
328 | ui.commands.fullscreen.addEventListener('click', () => {
329 | if (document.fullscreenElement) {
330 | document.exitFullscreen()
331 | } else {
332 | document.documentElement.requestFullscreen();
333 | }
334 | });
335 | } else {
336 | ui.commands.fullscreen.classList.add('hidden');
337 | }
338 | [
339 | {
340 | 'name': "default",
341 | 'background': "#000000",
342 | 'odd-background': "#111111",
343 | 'separator': "#444444",
344 | 'text': "#eeeeee",
345 | 'user': "#008000",
346 | 'moderator': "#8383f9",
347 | 'channel': "#0d86ff",
348 | 'notice': "#eeeeee",
349 | 'highlight': "#731180",
350 | 'vip': '#4686f8',
351 | 'admin': '#a970ff',
352 | 'staff': '#a970ff'
353 | }, {
354 | 'name': "bright",
355 | 'background': "#eeeeee",
356 | 'odd-background': "#dddddd",
357 | 'separator': "#bbbbbb",
358 | 'text': "#111111",
359 | 'user': "#00b000",
360 | 'moderator': "#8383f9",
361 | 'channel': "#0d86ff",
362 | 'notice': "#111111",
363 | 'highlight': "#731180",
364 | 'vip': '#4686f8',
365 | 'admin': '#a970ff',
366 | 'staff': '#a970ff'
367 | }, {
368 | 'name': 'LRR',
369 | 'background': "#202020",
370 | 'odd-background': "#111111",
371 | 'separator': "#464646",
372 | 'text': "#d2d2d2",
373 | 'user': "#5282ff",
374 | 'moderator': "#f15a24",
375 | 'channel': "#f15a24",
376 | 'notice': "#d2d2d2",
377 | 'highlight': "#e1480f",
378 | 'vip': '#aaf2a6',
379 | 'admin': '#a970ff',
380 | 'staff': '#a970ff'
381 | }
382 | ].forEach(createStylePreview);
383 | var colorFields = ['background', 'odd-background', 'separator', 'text', 'user', 'moderator', 'channel', 'notice', 'highlight', 'vip', 'admin', 'staff'];
384 | var customStyleValues = {
385 | 'name': "custom"
386 | };
387 | colorFields.forEach(key => customStyleValues[key] = Settings.get(`${key}-color`));
388 | var customStylePreview = createStylePreview(customStyleValues, true);
389 | colorFields.forEach(key => {
390 | document.getElementById(`settings-${key}-color`).value = Settings.get(`${key}-color`);
391 | document.getElementById(`settings-${key}-color`).addEventListener('change', (e) => {
392 | Settings.set(`${key}-color`, e.target.value);
393 | customStylePreview.style.setProperty(`--style-${key}`, e.target.value);
394 | if (['channel', 'notice', 'highlight'].indexOf(key) != -1) {
395 | customStylePreview.style.setProperty(`--style-${key}-background`, e.target.value + '50');
396 | }
397 | updateImportExport();
398 | });
399 | });
400 | updateImportExport();
401 | ui.settings.style.custom.field.addEventListener('input', (e) => {
402 | let code = e.target.value;
403 | if (code.length == 36) { // Allow old exports to work
404 | code += ['vip', 'admin', 'staff'].map((name) => HexCompressor.color2string(Settings.get(`${name}-color`))).join('');
405 | }
406 | if (!/^[0-9a-zA-Z+#]{48}$/.test(code)) {
407 | e.target.setCustomValidity('Invalid code');
408 | e.target.reportValidity();
409 | return;
410 | }
411 | e.target.setCustomValidity('');
412 | colorFields.forEach((key, index) => {
413 | var newColor = HexCompressor.string2color(code.substring(index * 4, (index * 4) + 4));
414 | Settings.set(key + '-color', newColor);
415 | document.getElementById('settings-' + key + '-color').value = newColor;
416 | });
417 | });
418 | ui.settings.style.custom.selector.classList.toggle('hidden', Settings.get('style-preset') != 'custom');
419 | ui.settings.style.custom.exchange.classList.toggle('hidden', Settings.get('style-preset') != 'custom');
420 |
421 | ui.settings.style.fontSize.value = Settings.get('font-size').slice(0, -2); // remove pixel unit
422 | ui.settings.style.fontSize.addEventListener('change', (e) => Settings.set('font-size', e.target.value + 'px'));
423 |
424 | ui.settings.style.animateEmoji.value = Settings.get('animate-emoji');
425 | ui.settings.style.animateEmoji.addEventListener('change', (e) => {
426 | Settings.set('animate-emoji', e.target.value);
427 | if (e.target.value != 'auto') { // Adjust existing emoji for existing emoji if forced enabled or disabled
428 | Array.from(ui.chat.body.querySelectorAll('div.emoticon')).forEach(updateEmoji);
429 | }
430 | });
431 |
432 | document.body.classList.toggle('hide-cursor', Settings.get('hide-cursor'));
433 | ui.settings.style.hideCursor.checked = Settings.get('hide-cursor');
434 | configureToggler('hide-cursor', () => document.body.classList.toggle('hide-cursor', Settings.get('hide-cursor')));
435 | document.addEventListener('mousemove', () => {
436 | if (Settings.get('hide-cursor')) {
437 | document.body.classList.remove('hide-cursor');
438 | clearTimeout(lastMoveTimeoutId);
439 | lastMoveTimeoutId = setTimeout(() => Settings.get('hide-cursor') && document.body.classList.add('hide-cursor'), 4000);
440 | }
441 | });
442 |
443 | ui.settings.style.adjustTitle.checked = Settings.get('adjust-page-title');
444 | configureToggler('adjust-page-title', updateTitle);
445 | ui.settings.style.showUnreadInTitle.checked = Settings.get('unread-counter-in-page-title');
446 | configureToggler('unread-counter-in-page-title');
447 | // Monitor unread messages
448 | document.addEventListener('visibilitychange', updateUnreadCounter);
449 |
450 | // Chat Behavior
451 | document.body.classList.toggle('limit-message-rate', !Settings.get('limit-message-rate'));
452 | ui.settings.behaviour.limitRate.toggle.checked = Settings.get('limit-message-rate');
453 | configureToggler('limit-message-rate', () => {
454 | document.body.classList.toggle('limit-message-rate', !Settings.get('limit-message-rate'));
455 | ui.settings.behaviour.limitRate.body.classList.toggle('hidden', !Settings.get('limit-message-rate'));
456 | if (!Settings.get('limit-message-rate')) {
457 | flushMessageQueue();
458 | ui.notifications.chatOverload.body.classList.add('hidden');
459 | }
460 | });
461 | if (Settings.get('limit-message-rate')) {
462 | ui.settings.behaviour.limitRate.body.classList.remove('hidden');
463 | }
464 | ui.settings.behaviour.limitRate.field.value = Settings.get('message-rate');
465 | ui.settings.behaviour.limitRate.field.addEventListener('input', (e) => {
466 | var rate = parseInt(e.target.value);
467 | if (!isNaN(rate) && e.target.validity.valid) {
468 | Settings.set('message-rate', rate);
469 | }
470 | });
471 | document.body.classList.toggle('reverse-order', !Settings.get('new-messages-on-top'));
472 | configureToggler('new-messages-on-top', () => {
473 | document.body.classList.toggle('reverse-order', !Settings.get('new-messages-on-top'));
474 | scrollDistance = scrollReference = 0;
475 | ui.chat.container.scrollTop = Settings.get('new-messages-on-top') ? 0 : ui.chat.container.scrollHeight - window.innerHeight;
476 | });
477 | configureToggler('smooth-scroll', () => {
478 | scrollDistance = scrollReference = 0;
479 | ui.chat.container.scrollTop = Settings.get('new-messages-on-top') ? 0 : ui.chat.container.scrollHeight - window.innerHeight;
480 | ui.settings.behaviour.smoothScroll.body.classList.toggle('hidden', !Settings.get('smooth-scroll'));
481 | });
482 | if (Settings.get('smooth-scroll')) {
483 | ui.settings.behaviour.smoothScroll.body.classList.remove('hidden');
484 | }
485 | ui.settings.behaviour.smoothScroll.duration.value = Settings.get('smooth-scroll-duration');
486 | ui.settings.behaviour.smoothScroll.duration.addEventListener('input', (e) => {
487 | var duration = parseInt(e.target.value);
488 | if (!isNaN(duration) && e.target.validity.valid) {
489 | Settings.set('smooth-scroll-duration', duration);
490 | }
491 | });
492 |
493 | configureToggler('enable-chat-delay', () => {
494 | toggleChatDelay();
495 | ui.settings.behaviour.chatDelay.body.classList.toggle('hidden', !Settings.get('enable-chat-delay'));
496 | });
497 | if (Settings.get('enable-chat-delay')) {
498 | ui.settings.behaviour.chatDelay.body.classList.remove('hidden');
499 | }
500 | ui.settings.behaviour.chatDelay.delay.value = Settings.get('chat-delay');
501 | ui.settings.behaviour.chatDelay.delay.addEventListener('change', (e) => setChatDelay(e.target.value));
502 |
503 | // Message Handling
504 | ui.chat.body.classList.toggle('align-messages', Settings.get('align-messages'));
505 | configureToggler('align-messages', () => ui.chat.body.classList.toggle('align-messages', Settings.get('align-messages')));
506 | ['combine-messages', 'format-urls', 'shorten-urls', 'unfurl-youtube', 'show-subscriptions', 'show-bits', 'show-mod-actions'].forEach(configureToggler);
507 | configureToggler('inline-images', () => ui.settings.messageHandling.inlineImages.body.classList.toggle('hidden', !Settings.get('inline-images')));
508 | if (Settings.get('inline-images')) {
509 | ui.settings.messageHandling.inlineImages.body.classList.remove('hidden');
510 | }
511 | ui.settings.messageHandling.inlineImages.height.value = Settings.get('inline-images-height').slice(0, -2); // remove vh unit
512 | ui.settings.messageHandling.inlineImages.height.addEventListener('input', (e) => {
513 | var height = parseInt(e.target.value);
514 | if (!isNaN(height) && e.target.validity.valid) {
515 | Settings.set('inline-images-height', height + 'vh');
516 | }
517 | });
518 | configureToggler('unfurl-twitter', () => {
519 | if (typeof twttr == 'undefined') {
520 | var twitterScript = document.createElement('script');
521 | twitterScript.src = 'https://platform.twitter.com/widgets.js';
522 | document.body.appendChild(twitterScript);
523 | }
524 | });
525 | if (Settings.get('unfurl-twitter')) {
526 | var twitterScript = document.createElement('script');
527 | twitterScript.src = 'https://platform.twitter.com/widgets.js';
528 | document.body.appendChild(twitterScript);
529 | }
530 | ui.settings.messageHandling.timestamps.value = Settings.get('timestamps');
531 | ui.chat.body.classList.toggle('hide-timestamps', Settings.get('timestamps') == '');
532 | ui.settings.messageHandling.timestamps.addEventListener('change', (e) => {
533 | Settings.set('timestamps', e.target.value);
534 | ui.chat.body.classList.toggle('hide-timestamps', e.target.value == '');
535 | Array.from(ui.chat.body.querySelectorAll('.timestamp')).forEach(updateTimestamp);
536 | });
537 | ui.settings.messageHandling.highlightUsers.value = Settings.get('highlight-users');
538 | ui.settings.messageHandling.highlightUsers.addEventListener('input', (e) => {
539 | Settings.set('highlight-users', e.target.value.toLowerCase());
540 | highlightUsers = e.target.value.toLowerCase().split(',').filter((user) => user != '');
541 | });
542 | ui.settings.messageHandling.keyPhrases.value = Settings.get('highlight-keyphrases');
543 | ui.settings.messageHandling.keyPhrases.addEventListener('input', (e) => {
544 | Settings.set('highlight-keyphrases', e.target.value.toLowerCase());
545 | highlightKeyphrases = e.target.value.toLowerCase().split(',').filter((phrase) => phrase != '');
546 | });
547 | configureToggler('show-fps', (e) => handleFPS(e.target.checked));
548 | if (Settings.get('show-fps')) {
549 | handleFPS(true);
550 | }
551 |
552 | // Hotkeys
553 | document.body.addEventListener('keydown', (e) => {
554 | if (!Settings.get('support-hotkeys')) {
555 | return;
556 | }
557 | if ((e.key == 'H' || e.key == 'h') && e.shiftKey && e.ctrlKey) {
558 | ui.main.curtain.classList.toggle('hidden');
559 | e.preventDefault();
560 | } else if ((e.key == 'D' || e.key == 'd') && e.shiftKey && e.ctrlKey) {
561 | Settings.toggle('enable-chat-delay');
562 | ui.settings.behaviour.chatDelay.toggle.checked = !ui.settings.behaviour.chatDelay.toggle.checked;
563 | ui.settings.behaviour.chatDelay.body.classList.toggle('hidden');
564 | toggleChatDelay();
565 | e.preventDefault();
566 | } else if ((e.key == 'S' || e.key == 's') && e.shiftKey && e.ctrlKey) {
567 | ui.settings.body.classList.toggle('hidden');
568 | ui.settings.body.scrollTop = 0;
569 | ui.commands.settings.classList.toggle('open');
570 | e.preventDefault();
571 | } else if ((e.key == 'Escape')) {
572 | ui.settings.body.classList.add('hidden');
573 | ui.commands.settings.classList.remove('open');
574 | e.preventDefault();
575 | }
576 | });
577 |
578 | function joinChannel(channel) {
579 | client.join(ensureHash(channel)).then(() => {
580 | console.log('Joined channel ' + channel);
581 | updateTitle()
582 | }, (error) => {
583 | addNotice(`Failed to join requested channel. Reason: ${decodeMessageId(error)}`);
584 | console.error('Failed to join requested channel', error);
585 | updateTitle();
586 | });
587 | }
588 |
589 | // Decode a Message ID returned by the Twitch API to a human-readable message
590 | function decodeMessageId(messageId) {
591 | let knownMessages = {
592 | 'msg_banned': 'You are permanently banned from talking in this channel.',
593 | 'msg_channel_blocked': 'Your account is not in good standing in this channel.',
594 | 'msg_channel_suspended': 'This channel does not exist or has been suspended.',
595 | 'msg_requires_verified_phone_number': 'A verified phone number is required to chat in this channel. Please visit https://www.twitch.tv/settings/security to verify your phone number.',
596 | 'msg_suspended': 'You don\'t have permission to perform that action.',
597 | 'msg_verified_email': 'This room requires a verified account to chat. Please verify your account at https://www.twitch.tv/settings/security.'
598 | };
599 | return knownMessages[messageId] || messageId;
600 | }
601 |
602 | // Process the next frame, this is the main driver of the application
603 | var lastFrame = +new Date();
604 | function step(now) {
605 | if (Settings.get('show-fps')) {
606 | frames++;
607 | }
608 | if (Settings.get('enable-chat-delay') && Settings.get('chat-delay') != 0) {
609 | while (delayQueue.length > 0 && parseInt(delayQueue[0].dataset.timestamp) + (Settings.get('chat-delay') * 1000) < Date.now()) {
610 | addMessage(delayQueue.shift(), true);
611 | }
612 | }
613 | if (Settings.get('limit-message-rate')) {
614 | if (messageQueue.length > 40) {
615 | ui.notifications.chatOverload.body.classList.remove('hidden');
616 | // Cull the queue to a reasonable length and update the counter
617 | ui.notifications.chatOverload.count.textContent = parseInt(ui.notifications.chatOverload.count.textContent) + messageQueue.splice(-40).length;
618 | }
619 | if (messageQueue.length < 10 && !ui.notifications.chatOverload.body.classList.contains('hidden')) {
620 | ui.notifications.chatOverload.body.classList.add('hidden');
621 | ui.notifications.chatOverload.count.textContent = "0";
622 | }
623 | if (messageQueue.length > 0 && now - lastMessageTimestamp > 1000 / Settings.get('message-rate')) {
624 | processChat.apply(this, messageQueue.shift());
625 | lastMessageTimestamp = now;
626 | }
627 | }
628 | if (Settings.get('smooth-scroll') && scrollDistance > 0) {
629 | // Estimate how far along we are in scrolling in the current scroll reference
630 | var currentStep = Settings.get('smooth-scroll-duration') / (now - lastFrame);
631 | scrollDistance -= scrollReference / currentStep;
632 | scrollDistance = Math.max(scrollDistance, 0);
633 | ui.chat.container.scrollTop = Math.round(Settings.get('new-messages-on-top') ? scrollDistance : ui.chat.container.scrollHeight - window.innerHeight - scrollDistance);
634 | }
635 | lastFrame = now;
636 | window.requestAnimationFrame(step);
637 | }
638 | window.requestAnimationFrame(step);
639 |
640 | /** Chat event handling **/
641 | function handleChat(channel, userstate, message) {
642 | increaseUnreadCounter();
643 | if (Settings.get('limit-message-rate')) {
644 | messageQueue.push([ channel, userstate, message ]);
645 | } else {
646 | processChat(channel, userstate, message);
647 | }
648 | }
649 |
650 | function processChat(channel, userstate, message) {
651 | try {
652 | // If enabled, combine messages instead of adding a new message
653 | var id = 'message-' + message.toLowerCase().replace(/[^\p{Letter}]/gu, '');
654 | if (Settings.get('combine-messages') && document.getElementById(id)) {
655 | var matchedMessage = document.getElementById(id);
656 | if (!matchedMessage.counter) {
657 | var counterContainer = document.createElement('span'),
658 | counter = document.createElement('span');
659 | counterContainer.className = 'counter';
660 | counterContainer.innerHTML = '× ';
661 | counterContainer.appendChild(counter);
662 | counter.textContent = '1';
663 | matchedMessage.appendChild(counterContainer);
664 | matchedMessage.counter = counter;
665 | }
666 | ui.chat.body.appendChild(matchedMessage);
667 | matchedMessage.querySelector('.counter').classList.add('bump');
668 | matchedMessage.counter.textContent++;
669 | setTimeout(() => matchedMessage.querySelector('.counter').classList.remove('bump'), 150);
670 | return;
671 | }
672 | var chatLine = createChatLine(userstate, message);
673 | if (Settings.get('combine-messages')) {
674 | chatLine.id = id;
675 | }
676 |
677 | // Deal with loading user-provided inline images
678 | var userImages = Array.from(chatLine.querySelectorAll('img.user-image'));
679 | if (userImages.length > 0) {
680 | userImages.forEach((userImage) => {
681 | if (userImage.complete) { // most likely it was already cached
682 | userImage.classList.add('loaded');
683 | return;
684 | }
685 | userImage.addEventListener('load', () => {
686 | if (userImage.dataset.mq && userImage.naturalWidth == 120) { // Failed to load, placeholder received
687 | if (userImage.dataset.hq) {
688 | userImage.src = userImage.dataset.hq;
689 | userImage.dataset.hq = '';
690 | return;
691 | } else if (userImage.dataset.mq) {
692 | userImage.src = userImage.dataset.mq;
693 | userImage.dataset.mq = '';
694 | return;
695 | }
696 | }
697 | var oldChatLineHeight = chatLine.scrollHeight;
698 | userImage.classList.add('loaded');
699 | var loadingText = chatLine.querySelector('.image-loading');
700 | if (chatLine.querySelector('.user-image:not(.loaded)') == null && loadingText != null) {
701 | loadingText.remove();
702 | }
703 | scrollReference = scrollDistance += Math.max(0, chatLine.scrollHeight - oldChatLineHeight);
704 | });
705 | userImage.addEventListener('error', () => {
706 | var loadingText = chatLine.querySelector('.image-loading');
707 | if (loadingText) {
708 | loadingText.textContent = '[image loading failed]';
709 | }
710 | });
711 | });
712 | if (userImages.some(image => !image.complete)) {
713 | var loadingText = document.createElement('span');
714 | loadingText.className = 'image-loading';
715 | loadingText.textContent = '[Loading image...]';
716 | var firstBreakLine = chatLine.querySelector('br');
717 | firstBreakLine.insertAdjacentText('beforebegin', ' ');
718 | firstBreakLine.insertAdjacentElement('beforebegin', loadingText);
719 | }
720 | }
721 |
722 | // Load Twitter/X messages, if any
723 | var tweets = Array.from(chatLine.querySelectorAll('div.tweet-embed'));
724 | if (tweets.length > 0 && typeof twttr != 'undefined' && twttr.init) {
725 | tweets.forEach((tweet) => {
726 | twttr.widgets
727 | .createTweet(tweet.dataset.tweet, tweet, {
728 | theme: 'dark',
729 | conversation: 'none',
730 | cards: 'hidden',
731 | dnt: 'true'
732 | })
733 | .then(el => {
734 | scrollReference = scrollDistance += el.scrollHeight;
735 | })
736 | .catch(e => console.log(e));
737 | });
738 | }
739 | addMessage(chatLine);
740 | // Check whether the message we just added was a message that was already deleted
741 | if (userstate.deleted) {
742 | deleteMessage(userstate.id);
743 | }
744 | } catch (error) {
745 | console.error('Error parsing chat message: ' + message, error);
746 | }
747 | }
748 |
749 | function handleRoomstate(channel, state) {
750 | flushDelayQueue();
751 | flushMessageQueue();
752 | addNotice(`Joined ${channel}.`);
753 | if (state.slow) {
754 | addNotice(`Channel is in slow mode.`);
755 | }
756 | if (state['followers-only'] != -1) {
757 | addNotice(`Channel is in followers-only mode.`);
758 | }
759 | if (state['emote-only']) {
760 | addNotice(`Channel is in emote-only mode.`);
761 | }
762 | if (Settings.get('enable-chat-delay') && Settings.get('chat-delay') != 0) {
763 | addNotice(`Chat is set to an artificial delay of ${Settings.get('chat-delay')} second${Settings.get('chat-delay') == 1 ? '' : 's'}.`);
764 | }
765 | }
766 |
767 | function handleSubscription(username, message, userstate) {
768 | if (!Settings.get('show-subscriptions')) {
769 | return;
770 | }
771 | var chatLine = document.createElement('div');
772 | chatLine.className = 'highlight subscription';
773 |
774 | var subscriptionNotice = document.createElement('div');
775 | subscriptionNotice.textContent = userstate['system-msg'].replaceAll('\\s', ' ');
776 | chatLine.append(subscriptionNotice);
777 |
778 | if (message && message.length > 0) {
779 | chatLine.append(createChatLine(userstate, message));
780 | }
781 | addMessage(chatLine);
782 | }
783 |
784 | function handleCheer(channel, userstate, message) {
785 | // We could consider to transform the cheer emotes in the message instead of removing them (https://dev.twitch.tv/docs/irc/tags/#privmsg-twitch-tags)
786 | var chatMessage = message;
787 | bitLevels.forEach((level) => chatMessage = chatMessage.replaceAll(new RegExp(`\\b[a-zA-Z]+${level}\\b`, 'g'), ''));
788 | var chatLine = createChatLine(userstate, chatMessage),
789 | cheer = document.createElement('span'),
790 | bitLevel = bitLevels.find((level) => parseInt(userstate.bits) >= level),
791 | cheerIcon = document.createElement('img');
792 |
793 | if (Settings.get('show-bits')) {
794 | if (bitLevel == undefined) {
795 | console.warn(`Could not parse bits received from ${userstate.username}`, userstate.bits);
796 | return;
797 | }
798 | let imageStyle = Settings.get('animate-emoji') == 'yes' ? 'animated' : 'static'; // TODO: support 'auto'
799 | cheerIcon.src = `https://static-cdn.jtvnw.net/bits/dark/${imageStyle}/${bitLevel}/1.5.gif`;
800 | cheerIcon.alt = 'Bits';
801 | cheer.appendChild(cheerIcon);
802 | cheer.className = `cheer cheer-${bitLevel}`;
803 | cheer.appendChild(document.createTextNode(userstate.bits));
804 | chatLine.insertBefore(cheer, chatLine.lastChild);
805 | }
806 | addMessage(chatLine);
807 | }
808 |
809 | function handleMessageDeletion(channel, username, deletedMessage, userstate) {
810 | deleteMessage(userstate['target-msg-id']);
811 | }
812 |
813 | function handleModAction(action, username, duration, userstate) {
814 | if (Settings.get('show-mod-actions')) {
815 | if (action == 'timeout') {
816 | addNotice(`${username} was given a time-out of ${duration} second${duration == 1 ? '' : 's'}.`);
817 | } else if (action == 'ban') {
818 | addNotice(`${username} has been banned.`);
819 | }
820 | }
821 | Array.from(ui.chat.body.querySelectorAll(`span[data-user="${userstate["target-user-id"]}"]`)).map(message => message.id).forEach(deleteMessage);
822 | }
823 |
824 | function handleFPS(enable) {
825 | ui.main.fps.innerHTML = ' ';
826 | ui.main.fps.classList.toggle('hidden', !enable);
827 | lastFrameReset = Date.now();
828 | frames = 0;
829 | if (enable) {
830 | fpsInterval = setInterval(updateFPS, 1000);
831 | } else {
832 | clearInterval(fpsInterval);
833 | }
834 | }
835 |
836 | function updateFPS() {
837 | var currentFrameTime = Date.now();
838 | ui.main.fps.textContent = (frames / (currentFrameTime - lastFrameReset) * 1000).toFixed(1);
839 | lastFrameReset = currentFrameTime;
840 | frames = 0;
841 | }
842 |
843 |
844 | /** Helper functions **/
845 | function configureToggler(key, callback) {
846 | document.getElementById(`settings-${key}`).checked = Settings.get(key);
847 | document.getElementById(`settings-${key}`).addEventListener('click', (e) => {
848 | Settings.toggle(key);
849 | if (typeof callback == 'function') {
850 | callback(e);
851 | }
852 | });
853 | }
854 |
855 | function createChatLine(userstate, message) {
856 | // $timestamp$username$message
857 | var chatLine = document.createElement('div'),
858 | chatTimestamp = document.createElement('span'),
859 | chatName = document.createElement('span'),
860 | chatMessage = document.createElement('span');
861 |
862 | chatTimestamp.className = 'timestamp';
863 | chatTimestamp.dataset.timestamp = userstate['tmi-sent-ts'] || Date.now();
864 | updateTimestamp(chatTimestamp);
865 | chatLine.appendChild(chatTimestamp);
866 | chatName.className = 'chat-user';
867 | if (userstate.badges?.vip) {
868 | chatName.classList.add('vip');
869 | }
870 | if (userstate.badges?.admin) {
871 | chatName.classList.add('admin');
872 | }
873 | if (userstate.badges?.staff) {
874 | chatName.classList.add('staff');
875 | }
876 | if (userstate.mod) {
877 | chatName.classList.add('moderator');
878 | }
879 | if (userstate['message-type'] == 'action') {
880 | chatName.classList.add('action');
881 | }
882 | chatName.textContent = userstate['display-name'] || userstate.username;
883 | if (userstate['message-type'] == 'announcement') {
884 | chatName.textContent = '📢 ' + chatName.textContent;
885 | }
886 | if (chatName.textContent.toLowerCase() == removeHash(client.channels[0]).toLowerCase()) {
887 | chatLine.className = 'highlight channel';
888 | }
889 | chatMessage.innerHTML = formatMessage(message, userstate.emotes);
890 | chatMessage.id = userstate.id;
891 | if (userstate['user-id']) {
892 | chatMessage.dataset.user = userstate['user-id'];
893 | }
894 |
895 | if (highlightUsers.indexOf(chatName.textContent.toLowerCase()) != -1) {
896 | chatLine.className = 'highlight';
897 | }
898 | if (highlightKeyphrases.find((phrase) => message.toLowerCase().indexOf(phrase) != -1)) {
899 | chatLine.className = 'highlight';
900 | }
901 |
902 | chatLine.appendChild(chatName);
903 | chatLine.appendChild(chatMessage);
904 |
905 | return chatLine;
906 | }
907 |
908 | function addNotice(message) {
909 | var chatLine = document.createElement('div');
910 | chatLine.textContent = message;
911 | chatLine.className = 'notice';
912 | addMessage(chatLine);
913 | }
914 |
915 | function addMessage(chatLine, bypass) {
916 | if (chatLine.className != 'notice' && !bypass && Settings.get('enable-chat-delay') && Settings.get('chat-delay') != 0) {
917 | chatLine.dataset.timestamp = Date.now();
918 | delayQueue.push(chatLine);
919 | return;
920 | }
921 | ui.chat.body.appendChild(chatLine);
922 | // Calculate height for smooth scrolling
923 | scrollReference = scrollDistance += chatLine.scrollHeight;
924 | if (!Settings.get('new-messages-on-top') && !Settings.get('smooth-scroll')) {
925 | ui.chat.container.scrollTop = ui.chat.container.scrollHeight - window.innerHeight;
926 | }
927 |
928 | // Check whether we can remove some of the oldest messages
929 | while (chat.childNodes.length > 2 && ui.chat.body.scrollHeight - (window.innerHeight + (Settings.get('smooth-scroll') ? scrollDistance : 0)) > ui.chat.body.firstChild.scrollHeight + ui.chat.body.childNodes[1].scrollHeight) {
930 | // Always remove two elements at the same time to prevent switching the odd and even rows
931 | ui.chat.body.firstChild.remove();
932 | ui.chat.body.firstChild.remove();
933 | }
934 | }
935 |
936 | function deleteMessage(messageId) {
937 | var message = document.getElementById(messageId);
938 | if (message == null) {
939 | var messageToDelete = messageQueue.find(entry => entry[1].id == messageId);
940 | if (messageToDelete) {
941 | messageToDelete[2] = ''; // Text will be replaced, but just intended to put it back on one line
942 | messageToDelete[1].deleted = true;
943 | }
944 | return;
945 | }
946 | if (message.classList.contains('deleted')) { // Weird, but ok
947 | return;
948 | }
949 | message.parentNode.style.height = (message.parentNode.scrollHeight - 7) + 'px'; // 2 x 3px padding + 1px border = 7
950 | message.textContent = '';
951 | message.classList.add('deleted');
952 | }
953 |
954 | /*
955 | To deal with message formatting, the message gets turned into an array of characters first.
956 | Twitch provides the IDs of the emotes and from where to where they are located in the message.
957 | We replace those emote-related characters with empty strings and place an
tag as a string at the 'from' location.
958 | Other changes take place in a similar way, by calculating the 'from' and 'to' values ourselves.
959 | As a last step, all entries in the array with 1 character are transformed into HTML entities if they are potentially dangerous.
960 | At the end, we join() the character array again, forming a message safe to assign to the innerHTML property.
961 | */
962 | function formatMessage(text, emotes) {
963 | if (Settings.get('new-messages-on-top')) {
964 | text = text.replaceAll('^', '⌄');
965 | }
966 | let message = text.split('');
967 | message = formatEmotes(message, emotes);
968 | message = formatLinks(message, text);
969 | return htmlEntities(message).join('');
970 | }
971 |
972 | function formatEmotes(text, emotes) {
973 | if (!emotes) {
974 | return text;
975 | }
976 | for (var id in emotes) {
977 | emotes[id].forEach((range) => {
978 | if (typeof range == 'string') {
979 | range = range.split('-').map(index => parseInt(index));
980 | let emote = text.slice(range[0], range[1] + 1).join('');
981 | let imageStyle = Settings.get('animate-emoji') == 'yes' ? 'default' : 'static'; // TODO: support 'auto'
982 | let baseUrl = `https://static-cdn.jtvnw.net/emoticons/v2/${id}/${imageStyle}/dark`;
983 | replaceText(text, ``, range[0], range[1]);
984 | }
985 | });
986 | };
987 | return text;
988 | }
989 |
990 | function formatLinks(text, originalText) {
991 | var urlRegex = /(https?:\/\/)?(www\.)?([0-9a-zA-Z-_\.]+\.[0-9a-zA-Z]+\/)([0-9a-zA-Z-_+:;,|`%^\(\)\[\]#=&\/\.\?\|\~@]*[0-9a-zA-Z-_+:;|`%^\(\)\[\]#=&\/\.\?\|\~])?/g;
992 | var match;
993 | while ((match = urlRegex.exec(originalText)) !== null) {
994 | var urlText = url = match[0];
995 | if (!match[1]) {
996 | url = 'https://' + url;
997 | }
998 | var path = match[4] || '';
999 | if (Settings.get('inline-images')) {
1000 | var giphy = /^https?:\/\/giphy\.com\/gifs\/(.*-)?([a-zA-Z0-9]+)$/gm.exec(urlText);
1001 | if (giphy) {
1002 | url = `https://media1.giphy.com/media/${giphy[2].split("-").pop()}/giphy.gif`;
1003 | path = `media/${giphy[2].split("-").pop()}/giphy.gif`;
1004 | }
1005 | var imgur = /^https?:\/\/imgur\.com\/([a-zA-Z0-9]+)$/gm.exec(urlText);
1006 | if (imgur) {
1007 | url = `https://i.imgur.com/${imgur[1]}.gif`;
1008 | path = `${imgur[1]}.gif`;
1009 | }
1010 | var twimg = /^https?:\/\/pbs\.twimg\.com\/media\/([a-zA-Z0-9]+)\?format=([a-z]+).*$/gm.exec(urlText);
1011 | if (twimg) {
1012 | url = `https://pbs.twimg.com/media/${twimg[1]}.${twimg[2]}`;
1013 | path = `/media/${twimg[1]}.${twimg[2]}`;
1014 | }
1015 | if (match[1] && imageExtensions.some((extension) => path.endsWith(extension))) {
1016 | if (text.indexOf('
') == -1) {
1017 | text.push('
');
1018 | }
1019 | text.push(`
`);
1020 | }
1021 | }
1022 | if (Settings.get('unfurl-youtube') && (match[3] == 'youtube.com/' || match[3] == 'youtu.be/')) {
1023 | var youtube = /^https?:\/\/(www\.)?(youtu\.be\/|youtube\.com\/watch\?v=)([^&?]+).*$/gm.exec(url);
1024 | if (youtube) {
1025 | if (text.indexOf('
') == -1) {
1026 | text.push('
');
1027 | }
1028 | text.push(`
`);
1029 | }
1030 | }
1031 | if (Settings.get('unfurl-twitter') && (match[3] == 'twitter.com/' || match[3] == 'x.com/') && match[4] != undefined) {
1032 | var twitter = /^https?:\/\/(www\.)?x\.com.+\/([0-9]+)$/gm.exec(match[0]);
1033 | if (twitter) {
1034 | if (text.indexOf('
') == -1) {
1035 | text.push('
');
1036 | }
1037 | text.push(``);
1038 | }
1039 | }
1040 | if (Settings.get('shorten-urls')) {
1041 | if (path.length < 25) {
1042 | urlText = match[3] + path;
1043 | } else {
1044 | urlText = match[3] + ' … ';
1045 | if (path.lastIndexOf('/') == -1) {
1046 | urlText += path.slice(-7); // No directory structure in the URL
1047 | } else {
1048 | urlText += path.substring(path.lastIndexOf('/')).slice(-10); // Show last directory if it is not too long
1049 | }
1050 | }
1051 | }
1052 | var replacement = Settings.get('format-urls') ? `${urlText}` : urlText;
1053 | replaceText(text, replacement, match.index, match.index + match[0].length - 1);
1054 | }
1055 | return text;
1056 | }
1057 |
1058 | function flushMessageQueue() {
1059 | messageQueue.forEach((args) => processChat.apply(this, args));
1060 | messageQueue = [];
1061 | }
1062 |
1063 | function flushDelayQueue() {
1064 | delayQueue.forEach((chatLine) => addMessage(chatLine, true));
1065 | delayQueue = [];
1066 | }
1067 |
1068 | function createStylePreview(style) {
1069 | var styleContainer = document.createElement('div');
1070 | styleContainer.className = 'style-preview';
1071 | var stylePreview = ui.settings.style.custom.preview.cloneNode(true);
1072 | stylePreview.removeAttribute('id');
1073 | stylePreview.classList.remove('hidden');
1074 | if (style.name == Settings.get('style-preset')) {
1075 | styleContainer.classList.add('active');
1076 | Object.keys(style).filter(key => key != 'name').forEach(key => {
1077 | document.body.style.setProperty(`--${key}-color`, (style.name == 'custom' ? Settings.get(`${key}-color`) : style[key]));
1078 | if (['channel', 'notice', 'highlight'].indexOf(key) != -1) {
1079 | document.body.style.setProperty(`--${key}-background-color`, (style.name == 'custom' ? Settings.get(`${key}-color`) : style[key]) + '50');
1080 | }
1081 | });
1082 | }
1083 | styleContainer.addEventListener('click', () => {
1084 | Array.from(ui.settings.style.custom.container.querySelectorAll('.style-preview')).forEach(preview => preview.classList.remove('active'));
1085 | styleContainer.classList.add('active');
1086 | Settings.set('style-preset', style.name);
1087 | ui.settings.style.custom.selector.classList.toggle('hidden', style.name != 'custom');
1088 | ui.settings.style.custom.exchange.classList.toggle('hidden', style.name != 'custom');
1089 | Object.keys(style).filter(key => key != 'name').forEach(key => {
1090 | document.body.style.setProperty(`--${key}-color`, (style.name == 'custom' ? Settings.get(`${key}-color`) : style[key]));
1091 | if (['channel', 'notice', 'highlight'].indexOf(key) != -1) {
1092 | document.body.style.setProperty(`--${key}-background-color`, (style.name == 'custom' ? Settings.get(`${key}-color`) : style[key]) + '50');
1093 | }
1094 | });
1095 | });
1096 | Object.keys(style).forEach(key => stylePreview.style.setProperty(`--style-${key}`, style[key]));
1097 | ['channel', 'notice', 'highlight'].forEach(key => stylePreview.style.setProperty(`--style-${key}-background`, style[key] + '50'));
1098 | styleContainer.textContent = style.name;
1099 | styleContainer.appendChild(stylePreview);
1100 | ui.settings.style.custom.container.appendChild(styleContainer);
1101 | return stylePreview;
1102 | }
1103 |
1104 | function updateEmoji(field) {
1105 | let needle = Settings.get('animate-emoji') == 'yes' ? '/static/dark/' : '/default/dark/';
1106 | let replacement = Settings.get('animate-emoji') == 'yes' ? '/default/dark/' : '/static/dark/';
1107 | field.style.backgroundImage = field.style.backgroundImage.replace(needle, replacement);
1108 | }
1109 |
1110 | function updateTimestamp(field) {
1111 | var formats = {
1112 | 'short24': (now) => (new Date(now)).toLocaleTimeString('en-GB').replace(/:\d\d$/, ''),
1113 | 'long24': (now) => (new Date(now)).toLocaleTimeString('en-GB'),
1114 | 'short12': (now) => (new Date(now)).toLocaleTimeString('en-US').replace(/:\d\d /, ' ').replace(/^(\d):/, '0$1:'),
1115 | 'long12': (now) => (new Date(now)).toLocaleTimeString('en-US').replace(/^(\d):/, '0$1:'),
1116 | 'short': (now) => (new Date(now)).toLocaleTimeString('en-GB').replace(/^\d\d:/, ''),
1117 | '': () => {}
1118 | };
1119 | field.textContent = formats[Settings.get('timestamps')](parseInt(field.dataset.timestamp));
1120 | }
1121 |
1122 | function toggleChatDelay() {
1123 | if (Settings.get('enable-chat-delay')) {
1124 | let delay = Settings.get('chat-delay');
1125 | addNotice(`Artificial chat delay set to ${delay} second${delay == 1 ? '' : 's'}`);
1126 | } else {
1127 | addNotice('Artificial chat delay disabled');
1128 | flushDelayQueue();
1129 | }
1130 | }
1131 |
1132 | function setChatDelay(delay) {
1133 | Settings.set('chat-delay', delay);
1134 | addNotice(`Artificial chat delay set to ${delay} second${delay == 1 ? '' : 's'}`);
1135 | if (delay == 0) {
1136 | flushDelayQueue();
1137 | }
1138 | }
1139 |
1140 | function updateImportExport() {
1141 | var code = '';
1142 | colorFields.forEach(key => code += HexCompressor.color2string(Settings.get(key + '-color')));
1143 | ui.settings.style.custom.field.value = code;
1144 | }
1145 |
1146 | function updateTitle() {
1147 | var pageTitle = 'Twitch Chat Monitor';
1148 | if (Settings.get('adjust-page-title')) {
1149 | pageTitle = ensureHash(client.channels[0]) + ' - ' + pageTitle;
1150 | }
1151 | if (Settings.get('unread-counter-in-page-title') && unreadMessages > 0) {
1152 | pageTitle = '(' + (unreadMessages > 99 ? '99+' : unreadMessages) + ') ' + pageTitle;
1153 | }
1154 | document.title = pageTitle;
1155 | }
1156 |
1157 | function increaseUnreadCounter() {
1158 | if (document.visibilityState == 'hidden') {
1159 | unreadMessages++;
1160 | updateTitle();
1161 | }
1162 | }
1163 |
1164 | function updateUnreadCounter() {
1165 | if (document.visibilityState == 'visible') {
1166 | unreadMessages = 0;
1167 | updateTitle();
1168 | }
1169 | }
1170 |
1171 | function ensureHash(text) {
1172 | if (!text.startsWith('#')) {
1173 | return '#' + text;
1174 | }
1175 | return text;
1176 | }
1177 |
1178 | function removeHash(text) {
1179 | if (text != undefined && text.startsWith('#')) {
1180 | return text.substring(1);
1181 | }
1182 | return text;
1183 | }
1184 |
1185 | function replaceText(text, replacement, from, to) {
1186 | for (var i = from + 1; i <= to; i++) {
1187 | text[i] = '';
1188 | }
1189 | text.splice(from, 1, replacement);
1190 | }
1191 |
1192 | function htmlEntities(html) {
1193 | const entityRegex = /[\u00A0-\u9999<>\&]/gim;
1194 | return html.map((character) => {
1195 | if (character.length == 1) {
1196 | return character.replace(entityRegex, (match) => '' + match.charCodeAt(0) + ';');
1197 | }
1198 | return character;
1199 | });
1200 | }
1201 |
--------------------------------------------------------------------------------