2022-03-07 23:51:59 +09:00
/ * *
2022-06-17 10:05:36 +09:00
* SPDX - FileCopyrightText : 2022 n9k < https : //git.076.ne.jp/ninya9k>
2022-03-07 23:51:59 +09:00
* SPDX - License - Identifier : AGPL - 3.0 - or - later
* * /
2022-02-14 19:16:09 +09:00
/* token */
2022-02-27 13:24:33 +09:00
const TOKEN = document . body . dataset . token ;
const TOKEN _HASH = document . body . dataset . tokenHash ;
2022-02-14 19:16:09 +09:00
2022-07-29 17:04:54 +09:00
/* language */
const LANG = document . firstElementChild . lang ;
2022-03-07 16:11:49 +09:00
/* Content Security Policy nonce */
const CSP = document . body . dataset . csp ;
2022-02-13 13:00:10 +09:00
/* insert js-only markup */
2022-03-30 17:41:42 +09:00
const jsmarkup _stream _video = '<video id="stream__video" autoplay controls></video>'
2022-07-28 19:48:33 +09:00
const jsmarkup _stream _offline = '<header id="stream__offline"><h1 data-string="offline">[offline]</h1></header>'
2022-02-22 19:36:59 +09:00
const jsmarkup _info = '<div id="info_js" data-js="true"></div>' ;
2022-02-26 08:06:36 +09:00
const jsmarkup _info _float = '<aside id="info_js__float"></aside>' ;
2022-07-28 19:48:33 +09:00
const jsmarkup _info _float _button = '<button id="info_js__float__button" accesskey="r" data-string="reload_stream">Reload stream</button>' ;
2022-02-26 08:06:36 +09:00
const jsmarkup _info _float _viewership = '<div id="info_js__float__viewership"></div>' ;
const jsmarkup _info _float _uptime = '<div id="info_js__float__uptime"></div>' ;
2022-02-22 19:36:59 +09:00
const jsmarkup _info _title = '<header id="info_js__title"></header>' ;
2022-03-10 17:21:21 +09:00
const jsmarkup _chat _messages = ` \
< ol id = "chat-messages_js" data - js = "true" > < / o l >
2022-07-28 19:48:33 +09:00
< button id = "chat-messages-unlock" data - string = "chat_scroll_paused" > Chat scroll paused . Click to resume . < / b u t t o n > ` ;
2022-02-27 13:24:33 +09:00
const jsmarkup _chat _users = ` \
2022-02-27 21:12:37 +09:00
< article id = "chat-users_js" >
< h5 id = "chat-users_js__watching-header" > < / h 5 >
< ul id = "chat-users_js__watching" > < / u l >
< br >
< h5 id = "chat-users_js__notwatching-header" > < / h 5 >
< ul id = "chat-users_js__notwatching" > < / u l >
< / a r t i c l e > ` ;
2022-02-13 13:00:10 +09:00
const jsmarkup _chat _form = ` \
2022-02-21 11:05:19 +09:00
< form id = "chat-form_js" data - js = "true" action = "/chat" method = "post" >
2022-02-15 19:16:10 +09:00
< input id = "chat-form_js__nonce" type = "hidden" name = "nonce" value = "" >
2022-06-17 09:06:45 +09:00
< textarea id = "chat-form_js__comment" name = "comment" required placeholder = "Send a message..." rows = "1" autofocus > < / t e x t a r e a >
2022-02-15 19:16:10 +09:00
< div id = "chat-live" >
< span id = "chat-live__ball" > < / s p a n >
2022-03-07 14:39:06 +09:00
< span id = "chat-live__status" >
2022-07-28 19:48:33 +09:00
< span data - verbose = "true" data - string = "not_connected_to_chat" > Not connected to chat < / s p a n >
2022-03-07 14:39:06 +09:00
< span data - verbose = "false" > & times ; < / s p a n >
< / s p a n >
2022-02-15 19:16:10 +09:00
< / d i v >
2022-07-28 19:48:33 +09:00
< input id = "chat-form_js__submit" type = "submit" value = "Chat" accesskey = "p" disabled data - string = "chat" data - string - attr = "value" >
2022-02-21 11:05:19 +09:00
< input id = "chat-form_js__captcha-digest" type = "hidden" name = "captcha-digest" disabled >
2022-03-05 19:32:21 +09:00
< input id = "chat-form_js__captcha-image" type = "image" width = "72" height = "30" >
2022-07-28 19:48:33 +09:00
< input id = "chat-form_js__captcha-answer" name = "captcha-answer" placeholder = "Captcha" disabled data - string = "captcha" data - string - attr = "placeholder" >
< input id = "chat-form_js__settings" type = "image" src = "/static/settings.svg" width = "28" height = "28" alt = "Settings" data - string = "settings" data - string - attr = "alt" >
2022-03-05 19:17:29 +09:00
< article id = "chat-form_js__notice" >
< button id = "chat-form_js__notice__button" type = "button" >
< header id = "chat-form_js__notice__button__header" > < / h e a d e r >
2022-07-28 19:48:33 +09:00
< small data - string = "click_to_dismiss" > Click to dismiss < / s m a l l >
2022-03-05 19:17:29 +09:00
< / b u t t o n >
< / a r t i c l e >
2022-03-07 14:39:06 +09:00
< / f o r m >
< form id = "appearance-form_js" data - hidden = "" >
2022-07-28 19:48:33 +09:00
< span id = "appearance-form_js__label-name" data - string = "name" > Name : < / s p a n >
2022-06-17 09:06:45 +09:00
< input id = "appearance-form_js__name" name = "name" >
2022-03-07 14:39:06 +09:00
< input id = "appearance-form_js__color" type = "color" name = "color" >
2022-07-28 19:48:33 +09:00
< span id = "appearance-form_js__label-tripcode" data - string = "tripcode" > Tripcode : < / s p a n >
< input id = "appearance-form_js__password" type = "password" name = "password" placeholder = "(tripcode password)" data - string = "tripcode_password" data - string - attr = "placeholder" >
2022-03-07 14:39:06 +09:00
< div id = "appearance-form_js__row" >
< article id = "appearance-form_js__row__result" > < / a r t i c l e >
2022-07-28 19:48:33 +09:00
< input id = "appearance-form_js__row__submit" type = "submit" value = "Update" data - string = "update" data - string - attr = "value" >
2022-03-07 14:39:06 +09:00
< / d i v >
2022-02-21 11:05:19 +09:00
< / f o r m > ` ;
2022-02-13 13:00:10 +09:00
2022-03-07 16:11:49 +09:00
const insert _jsmarkup = ( ) => {
2022-02-18 14:16:54 +09:00
if ( document . getElementById ( "style-color" ) === null ) {
2022-03-07 16:11:49 +09:00
const style _color = document . createElement ( "style" ) ;
style _color . id = "style-color" ;
style _color . nonce = CSP ;
document . head . insertAdjacentElement ( "beforeend" , style _color ) ;
2022-02-18 14:16:54 +09:00
}
if ( document . getElementById ( "style-tripcode-display" ) === null ) {
2022-03-07 16:11:49 +09:00
const style _tripcode _display = document . createElement ( "style" ) ;
style _tripcode _display . id = "style-tripcode-display" ;
style _tripcode _display . nonce = CSP ;
document . head . insertAdjacentElement ( "beforeend" , style _tripcode _display ) ;
2022-02-18 14:16:54 +09:00
}
if ( document . getElementById ( "style-tripcode-colors" ) === null ) {
2022-03-07 16:11:49 +09:00
const style _tripcode _colors = document . createElement ( "style" ) ;
style _tripcode _colors . id = "style-tripcode-colors" ;
style _tripcode _colors . nonce = CSP ;
document . head . insertAdjacentElement ( "beforeend" , style _tripcode _colors ) ;
2022-02-16 18:55:30 +09:00
}
2022-03-30 17:41:42 +09:00
if ( document . getElementById ( "stream__video" ) === null ) {
2022-03-02 19:13:07 +09:00
const parent = document . getElementById ( "stream" ) ;
2022-03-30 17:41:42 +09:00
parent . insertAdjacentHTML ( "beforeend" , jsmarkup _stream _video ) ;
}
if ( document . getElementById ( "stream__offline" ) === null ) {
const parent = document . getElementById ( "stream" ) ;
parent . insertAdjacentHTML ( "beforeend" , jsmarkup _stream _offline ) ;
2022-03-02 19:13:07 +09:00
}
2022-02-15 19:16:10 +09:00
if ( document . getElementById ( "info_js" ) === null ) {
const parent = document . getElementById ( "info" ) ;
parent . insertAdjacentHTML ( "beforeend" , jsmarkup _info ) ;
}
2022-02-26 08:06:36 +09:00
if ( document . getElementById ( "info_js__float" ) === null ) {
2022-02-22 19:36:59 +09:00
const parent = document . getElementById ( "info_js" ) ;
2022-02-26 08:06:36 +09:00
parent . insertAdjacentHTML ( "beforeend" , jsmarkup _info _float ) ;
}
2022-03-01 11:06:48 +09:00
if ( document . getElementById ( "info_js__float__button" ) === null ) {
const parent = document . getElementById ( "info_js__float" ) ;
parent . insertAdjacentHTML ( "beforeend" , jsmarkup _info _float _button ) ;
}
2022-02-26 08:06:36 +09:00
if ( document . getElementById ( "info_js__float__viewership" ) === null ) {
const parent = document . getElementById ( "info_js__float" ) ;
parent . insertAdjacentHTML ( "beforeend" , jsmarkup _info _float _viewership ) ;
}
if ( document . getElementById ( "info_js__float__uptime" ) === null ) {
const parent = document . getElementById ( "info_js__float" ) ;
parent . insertAdjacentHTML ( "beforeend" , jsmarkup _info _float _uptime ) ;
2022-02-22 19:36:59 +09:00
}
2022-02-15 19:16:10 +09:00
if ( document . getElementById ( "info_js__title" ) === null ) {
const parent = document . getElementById ( "info_js" ) ;
parent . insertAdjacentHTML ( "beforeend" , jsmarkup _info _title ) ;
}
2022-02-27 13:24:33 +09:00
if ( document . getElementById ( "chat-users_js" ) === null ) {
2022-02-27 21:12:37 +09:00
const parent = document . getElementById ( "chat__body__users" ) ;
2022-02-27 13:24:33 +09:00
parent . insertAdjacentHTML ( "beforeend" , jsmarkup _chat _users ) ;
}
2022-02-15 19:16:10 +09:00
if ( document . getElementById ( "chat-messages_js" ) === null ) {
2022-02-27 21:12:37 +09:00
const parent = document . getElementById ( "chat__body__messages" ) ;
2022-02-15 19:16:10 +09:00
parent . insertAdjacentHTML ( "beforeend" , jsmarkup _chat _messages ) ;
}
if ( document . getElementById ( "chat-form_js" ) === null ) {
const parent = document . getElementById ( "chat__form" ) ;
parent . insertAdjacentHTML ( "beforeend" , jsmarkup _chat _form ) ;
}
2022-02-13 13:00:10 +09:00
}
insert _jsmarkup ( ) ;
2022-02-18 14:16:54 +09:00
const stylesheet _color = document . styleSheets [ 1 ] ;
const stylesheet _tripcode _display = document . styleSheets [ 2 ] ;
const stylesheet _tripcode _colors = document . styleSheets [ 3 ] ;
2022-02-13 13:00:10 +09:00
2022-03-05 19:17:29 +09:00
/* override chat form notice button */
const chat _form = document . getElementById ( "chat-form_js" ) ;
const chat _form _notice _button = document . getElementById ( "chat-form_js__notice__button" ) ;
const chat _form _notice _header = document . getElementById ( "chat-form_js__notice__button__header" ) ;
chat _form _notice _button . addEventListener ( "click" , ( event ) => {
chat _form . removeAttribute ( "data-notice" ) ;
chat _form _notice _header . innerText = "" ;
} ) ;
const show _notice = ( text ) => {
chat _form _notice _header . innerText = text ;
chat _form . dataset . notice = "" ;
}
2022-03-07 14:39:06 +09:00
/* override chat form settings input */
const chat _appearance _form = document . getElementById ( "appearance-form_js" ) ;
const chat _appearance _form _result = document . getElementById ( "appearance-form_js__row__result" ) ;
const chat _form _settings = document . getElementById ( "chat-form_js__settings" ) ;
chat _form _settings . addEventListener ( "click" , ( event ) => {
event . preventDefault ( ) ;
if ( chat _appearance _form . dataset . hidden === undefined ) {
chat _appearance _form . dataset . hidden = "" ;
chat _form _settings . style . backgroundColor = "" ;
chat _appearance _form _result . innerText = "" ;
if ( ! chat _appearance _form _submit . disabled ) {
chat _appearance _form . reset ( ) ;
}
} else {
chat _appearance _form . removeAttribute ( "data-hidden" ) ;
chat _form _settings . style . backgroundColor = "#4f4f53" ;
}
} ) ;
/* appearance form */
const chat _appearance _form _name = document . getElementById ( "appearance-form_js__name" ) ;
const chat _appearance _form _color = document . getElementById ( "appearance-form_js__color" ) ;
const chat _appearance _form _password = document . getElementById ( "appearance-form_js__password" ) ;
2022-02-13 13:00:10 +09:00
/* create websocket */
2022-02-14 19:16:09 +09:00
const info _title = document . getElementById ( "info_js__title" ) ;
2022-02-26 08:06:36 +09:00
const info _viewership = document . getElementById ( "info_js__float__viewership" ) ;
const info _uptime = document . getElementById ( "info_js__float__uptime" ) ;
2022-02-14 19:16:09 +09:00
const chat _messages = document . getElementById ( "chat-messages_js" ) ;
2022-02-27 21:12:37 +09:00
const chat _users _watching = document . getElementById ( "chat-users_js__watching" ) ;
const chat _users _watching _header = document . getElementById ( "chat-users_js__watching-header" ) ;
const chat _users _notwatching = document . getElementById ( "chat-users_js__notwatching" ) ;
const chat _users _notwatching _header = document . getElementById ( "chat-users_js__notwatching-header" ) ;
2022-02-16 18:55:30 +09:00
const create _chat _message = ( object ) => {
const user = users [ object . token _hash ] ;
const chat _message = document . createElement ( "li" ) ;
chat _message . classList . add ( "chat-message" ) ;
2022-02-18 10:32:34 +09:00
chat _message . dataset . seq = object . seq ;
2022-02-16 18:55:30 +09:00
chat _message . dataset . tokenHash = object . token _hash ;
2022-02-22 14:27:42 +09:00
const chat _message _time = document . createElement ( "time" ) ;
chat _message _time . classList . add ( "chat-message__time" ) ;
chat _message _time . dateTime = ` ${ object . date } T ${ object . time _seconds } Z ` ;
chat _message _time . title = ` ${ object . date } ${ object . time _seconds } ` ;
chat _message _time . innerText = object . time _minutes ;
2022-03-04 22:23:28 +09:00
const chat _message _user _components = create _chat _user _components ( user ) ;
2022-02-18 14:16:54 +09:00
2022-02-16 18:55:30 +09:00
const chat _message _markup = document . createElement ( "span" ) ;
chat _message _markup . classList . add ( "chat-message__markup" ) ;
chat _message _markup . innerHTML = object . markup ;
2022-02-22 14:27:42 +09:00
chat _message . insertAdjacentElement ( "beforeend" , chat _message _time ) ;
chat _message . insertAdjacentHTML ( "beforeend" , " " ) ;
2022-03-04 22:23:28 +09:00
for ( const chat _message _user _component of chat _message _user _components ) {
chat _message . insertAdjacentElement ( "beforeend" , chat _message _user _component ) ;
}
2022-03-04 15:18:47 +09:00
chat _message . insertAdjacentHTML ( "beforeend" , ": " ) ;
2022-02-16 18:55:30 +09:00
chat _message . insertAdjacentElement ( "beforeend" , chat _message _markup ) ;
2022-02-22 18:30:23 +09:00
return chat _message ;
}
2022-02-28 21:40:56 +09:00
const create _chat _user _name = ( user ) => {
const chat _user _name = document . createElement ( "span" ) ;
chat _user _name . classList . add ( "chat-name" ) ;
2022-06-15 18:38:45 +09:00
chat _user _name . innerText = get _user _name ( { user } ) . replaceAll ( /\r?\n/g , " " ) ;
2022-02-28 21:40:56 +09:00
//chat_user_name.dataset.color = user.color; // not working in any browser
2022-02-22 18:30:23 +09:00
if ( ! user . broadcaster && user . name === null ) {
2022-03-04 22:23:28 +09:00
const b = document . createElement ( "b" ) ;
b . innerText = user . tag ;
2022-02-28 21:40:56 +09:00
const chat _user _name _tag = document . createElement ( "sup" ) ;
chat _user _name _tag . classList . add ( "chat-name__tag" ) ;
2022-03-04 22:23:28 +09:00
chat _user _name _tag . innerHTML = b . outerHTML ;
2022-02-28 21:40:56 +09:00
chat _user _name . insertAdjacentElement ( "beforeend" , chat _user _name _tag ) ;
2022-02-22 18:30:23 +09:00
}
2022-02-28 21:40:56 +09:00
return chat _user _name ;
}
const create _chat _user _components = ( user ) => {
const chat _user _name = create _chat _user _name ( user ) ;
const chat _user _tripcode _nbsp = document . createElement ( "span" ) ;
chat _user _tripcode _nbsp . classList . add ( "for-tripcode" ) ;
chat _user _tripcode _nbsp . innerHTML = " " ;
const chat _user _tripcode = document . createElement ( "span" ) ;
chat _user _tripcode . classList . add ( "tripcode" ) ;
chat _user _tripcode . classList . add ( "for-tripcode" ) ;
if ( user . tripcode !== null ) {
chat _user _tripcode . innerHTML = user . tripcode . digest ;
}
2022-03-04 22:23:28 +09:00
let result ;
if ( ! user . broadcaster ) {
result = [ ] ;
} else {
const chat _user _insignia = document . createElement ( "b" ) ;
chat _user _insignia . classList . add ( "chat-insignia" )
2022-07-28 19:48:33 +09:00
chat _user _insignia . title = locale . broadcaster || "Broadcaster" ;
2022-03-04 22:23:28 +09:00
chat _user _insignia . innerText = "##" ;
const chat _user _insignia _nbsp = document . createElement ( "span" ) ;
chat _user _insignia _nbsp . innerHTML = " "
result = [ chat _user _insignia , chat _user _insignia _nbsp ] ;
}
result . push ( ... [ chat _user _name , chat _user _tripcode _nbsp , chat _user _tripcode ] ) ;
return result ;
2022-02-16 18:55:30 +09:00
}
2022-02-18 21:24:19 +09:00
const create _and _add _chat _message = ( object ) => {
const chat _message = create _chat _message ( object ) ;
chat _messages . insertAdjacentElement ( "beforeend" , chat _message ) ;
while ( chat _messages . children . length > max _chat _scrollback ) {
chat _messages . children [ 0 ] . remove ( ) ;
}
}
2022-06-14 05:43:51 +09:00
const delete _chat _messages = ( seqs ) => {
string _seqs = new Set ( seqs . map ( n => n . toString ( ) ) ) ;
to _delete = [ ] ;
for ( const chat _message of chat _messages . children ) {
if ( string _seqs . has ( chat _message . dataset . seq ) )
to _delete . push ( chat _message ) ;
}
for ( const chat _message of to _delete ) {
chat _message . remove ( ) ;
}
}
2022-02-16 18:55:30 +09:00
2022-07-28 19:48:33 +09:00
let locale = { } ;
2022-02-16 18:55:30 +09:00
let users = { } ;
2022-02-28 20:01:24 +09:00
let stats = null ;
let stats _received = null ;
2022-02-16 18:55:30 +09:00
let default _name = { true : "Broadcaster" , false : "Anonymous" } ;
2022-02-18 21:24:19 +09:00
let max _chat _scrollback = 256 ;
2022-04-02 13:46:24 +09:00
let pingpong _period = 8.0 ;
let ping = null ;
const pingpong _timeout = ( ) => pingpong _period * 1.5 + 4.0 ;
2022-06-14 07:04:51 +09:00
const pingpong _timeout _ms = ( ) => pingpong _timeout ( ) * 1000 ;
2022-02-18 15:06:23 +09:00
const tidy _stylesheet = ( { stylesheet , selector _regex , ignore _condition } ) => {
2022-02-16 18:55:30 +09:00
const to _delete = [ ] ;
const to _ignore = new Set ( ) ;
for ( let index = 0 ; index < stylesheet . cssRules . length ; index ++ ) {
2022-02-18 15:06:23 +09:00
const css _rule = stylesheet . cssRules [ index ] ;
2022-02-18 14:16:54 +09:00
const match = css _rule . selectorText . match ( selector _regex ) ;
2022-02-16 18:55:30 +09:00
const token _hash = match === null ? null : match [ 1 ] ;
const user = token _hash === null ? null : users [ token _hash ] ;
if ( user === null || user === undefined ) {
to _delete . push ( index ) ;
2022-02-18 14:16:54 +09:00
} else if ( ! ignore _condition ( token _hash , user , css _rule ) ) {
2022-02-16 18:55:30 +09:00
to _delete . push ( index ) ;
} else {
to _ignore . add ( token _hash ) ;
}
}
2022-02-18 14:16:54 +09:00
return { to _delete , to _ignore } ;
}
const equal = ( color1 , color2 ) => {
/* comparing css colors is annoying */
// when this is working, remove `ignore_other_token_hashes` from functions below
return false ;
}
const update _user _colors = ( token _hash = null ) => {
ignore _other _token _hashes = token _hash !== null ;
token _hashes = token _hash === null ? Object . keys ( users ) : [ token _hash ] ;
2022-02-18 15:06:23 +09:00
const { to _delete , to _ignore } = tidy _stylesheet ( {
stylesheet : stylesheet _color ,
2022-02-27 13:24:33 +09:00
selector _regex : /\[data-token-hash="([a-z2-7]{26})"\] > \.chat-name/ ,
2022-02-18 15:06:23 +09:00
ignore _condition : ( this _token _hash , this _user , css _rule ) => {
const irrelevant = ignore _other _token _hashes && this _token _hash !== token _hash ;
const correct _color = equal ( css _rule . style . color , this _user . color ) ;
2022-02-18 14:16:54 +09:00
return irrelevant || correct _color ;
} ,
2022-02-18 15:06:23 +09:00
} ) ;
2022-02-18 14:16:54 +09:00
// update colors
for ( const this _token _hash of token _hashes ) {
if ( ! to _ignore . has ( this _token _hash ) ) {
const user = users [ this _token _hash ] ;
stylesheet _color . insertRule (
2022-02-27 13:24:33 +09:00
` [data-token-hash=" ${ this _token _hash } "] > .chat-name { color: ${ user . color } ; } ` ,
2022-02-18 14:16:54 +09:00
stylesheet _color . cssRules . length ,
2022-02-16 18:55:30 +09:00
) ;
}
}
2022-02-18 14:16:54 +09:00
// delete css rules
2022-02-16 18:55:30 +09:00
for ( const index of to _delete . reverse ( ) ) {
2022-02-18 14:16:54 +09:00
stylesheet _color . deleteRule ( index ) ;
}
}
2022-02-19 12:07:53 +09:00
const get _user _name = ( { user = null , token _hash } ) => {
user = user || users [ token _hash ] ;
2022-02-18 21:24:19 +09:00
return user . name || default _name [ user . broadcaster ] ;
}
const update _user _names = ( token _hash = null ) => {
const token _hashes = token _hash === null ? Object . keys ( users ) : [ token _hash ] ;
2022-02-18 14:16:54 +09:00
for ( const chat _message of chat _messages . children ) {
2022-02-18 21:24:19 +09:00
const this _token _hash = chat _message . dataset . tokenHash ;
2022-02-19 12:07:53 +09:00
if ( token _hashes . includes ( this _token _hash ) ) {
2022-02-22 18:30:23 +09:00
const user = users [ this _token _hash ] ;
2022-02-27 13:24:33 +09:00
const chat _message _name = chat _message . querySelector ( ".chat-name" ) ;
2022-02-28 21:40:56 +09:00
chat _message _name . innerHTML = create _chat _user _name ( user ) . innerHTML ;
2022-02-18 14:16:54 +09:00
}
}
}
const update _user _tripcodes = ( token _hash = null ) => {
ignore _other _token _hashes = token _hash !== null ;
token _hashes = token _hash === null ? Object . keys ( users ) : [ token _hash ] ;
2022-02-18 15:06:23 +09:00
const { to _delete : to _delete _display , to _ignore : to _ignore _display } = tidy _stylesheet ( {
stylesheet : stylesheet _tripcode _display ,
2022-02-28 21:40:56 +09:00
selector _regex : /\[data-token-hash="([a-z2-7]{26})"\] > \.for-tripcode/ ,
2022-02-18 15:06:23 +09:00
ignore _condition : ( this _token _hash , this _user , css _rule ) => {
const irrelevant = ignore _other _token _hashes && this _token _hash !== token _hash ;
const correctly _hidden = this _user . tripcode === null && css _rule . style . display === "none" ;
const correctly _showing = this _user . tripcode !== null && css _rule . style . display === "inline" ;
2022-02-18 14:16:54 +09:00
return irrelevant || correctly _hidden || correctly _showing ;
} ,
2022-02-18 15:06:23 +09:00
} ) ;
const { to _delete : to _delete _colors , to _ignore : to _ignore _colors } = tidy _stylesheet ( {
stylesheet : stylesheet _tripcode _colors ,
2022-02-28 21:40:56 +09:00
selector _regex : /\[data-token-hash="([a-z2-7]{26})"\] > \.tripcode/ ,
2022-02-18 15:06:23 +09:00
ignore _condition : ( this _token _hash , this _user , css _rule ) => {
const irrelevant = ignore _other _token _hashes && this _token _hash !== token _hash ;
const correctly _blank = (
2022-02-18 14:16:54 +09:00
this _user . tripcode === null
&& css _rule . style . backgroundColor === "initial"
&& css _rule . style . color === "initial"
) ;
2022-02-18 15:06:23 +09:00
const correctly _colored = (
2022-02-18 14:16:54 +09:00
this _user . tripcode !== null
&& equal ( css _rule . style . backgroundColor , this _user . tripcode . background _color )
&& equal ( css _rule . style . color , this _user . tripcode . foreground _color )
) ;
return irrelevant || correctly _blank || correctly _colored ;
} ,
2022-02-18 15:06:23 +09:00
} ) ;
2022-02-18 14:16:54 +09:00
// update colors
for ( const this _token _hash of token _hashes ) {
const tripcode = users [ this _token _hash ] . tripcode ;
if ( tripcode === null ) {
2022-03-07 12:19:44 +09:00
if ( ! to _ignore _display . has ( this _token _hash ) ) {
2022-02-18 14:16:54 +09:00
stylesheet _tripcode _display . insertRule (
2022-02-28 21:40:56 +09:00
` [data-token-hash=" ${ this _token _hash } "] > .for-tripcode { display: none; } ` ,
2022-02-18 14:16:54 +09:00
stylesheet _tripcode _display . cssRules . length ,
) ;
}
2022-03-07 12:19:44 +09:00
if ( ! to _ignore _colors . has ( this _token _hash ) ) {
2022-02-18 14:16:54 +09:00
stylesheet _tripcode _colors . insertRule (
2022-02-28 21:40:56 +09:00
` [data-token-hash=" ${ this _token _hash } "] > .tripcode { background-color: initial; color: initial; } ` ,
2022-02-18 14:16:54 +09:00
stylesheet _tripcode _colors . cssRules . length ,
) ;
}
} else {
2022-03-07 12:19:44 +09:00
if ( ! to _ignore _display . has ( this _token _hash ) ) {
2022-02-18 14:16:54 +09:00
stylesheet _tripcode _display . insertRule (
2022-02-28 21:40:56 +09:00
` [data-token-hash=" ${ this _token _hash } "] > .for-tripcode { display: inline; } ` ,
2022-02-18 14:16:54 +09:00
stylesheet _tripcode _display . cssRules . length ,
) ;
}
2022-03-07 12:19:44 +09:00
if ( ! to _ignore _colors . has ( this _token _hash ) ) {
2022-02-18 14:16:54 +09:00
stylesheet _tripcode _colors . insertRule (
2022-02-28 21:40:56 +09:00
` [data-token-hash=" ${ this _token _hash } "] > .tripcode { background-color: ${ tripcode . background _color } ; color: ${ tripcode . foreground _color } ; } ` ,
2022-02-18 14:16:54 +09:00
stylesheet _tripcode _colors . cssRules . length ,
) ;
}
}
}
// delete css rules
for ( const index of to _delete _display . reverse ( ) ) {
stylesheet _tripcode _display . deleteRule ( index ) ;
}
for ( const index of to _delete _colors . reverse ( ) ) {
stylesheet _tripcode _colors . deleteRule ( index ) ;
}
// update inner texts
for ( const chat _message of chat _messages . children ) {
const this _token _hash = chat _message . dataset . tokenHash ;
const tripcode = users [ this _token _hash ] . tripcode ;
if ( token _hashes . includes ( this _token _hash ) ) {
2022-02-18 21:24:19 +09:00
const chat _message _tripcode = chat _message . querySelector ( ".tripcode" ) ;
chat _message _tripcode . innerText = tripcode === null ? "" : tripcode . digest ;
2022-02-18 14:16:54 +09:00
}
2022-02-16 18:55:30 +09:00
}
}
2022-02-21 11:05:19 +09:00
const chat _form _captcha _digest = document . getElementById ( "chat-form_js__captcha-digest" ) ;
const chat _form _captcha _image = document . getElementById ( "chat-form_js__captcha-image" ) ;
const chat _form _captcha _answer = document . getElementById ( "chat-form_js__captcha-answer" ) ;
chat _form _captcha _image . addEventListener ( "loadstart" , ( event ) => {
2022-02-26 08:44:54 +09:00
chat _form _captcha _image . removeAttribute ( "title" ) ;
chat _form _captcha _image . removeAttribute ( "data-reloadable" ) ;
2022-07-28 19:48:33 +09:00
chat _form _captcha _image . alt = locale . loading || "Loading..." ;
2022-02-21 11:05:19 +09:00
} ) ;
chat _form _captcha _image . addEventListener ( "load" , ( event ) => {
chat _form _captcha _image . removeAttribute ( "alt" ) ;
2022-02-26 08:44:54 +09:00
chat _form _captcha _image . dataset . reloadable = "" ;
2022-07-28 19:48:33 +09:00
chat _form _captcha _image . title = locale . click _for _a _new _captcha || "Click for a new captcha" ;
2022-02-21 11:05:19 +09:00
} ) ;
chat _form _captcha _image . addEventListener ( "error" , ( event ) => {
2022-07-28 19:48:33 +09:00
chat _form _captcha _image . alt = locale . captcha _failed _to _load || "Captcha failed to load" ;
2022-02-26 08:44:54 +09:00
chat _form _captcha _image . dataset . reloadable = "" ;
2022-07-28 19:48:33 +09:00
chat _form _captcha _image . title = locale . click _for _a _new _captcha || "Click for a new captcha" ;
2022-02-26 08:44:54 +09:00
} ) ;
chat _form _captcha _image . addEventListener ( "click" , ( event ) => {
2022-03-05 19:32:21 +09:00
event . preventDefault ( ) ;
if ( chat _form _captcha _image . dataset . reloadable !== undefined ) {
chat _form _submit . disabled = true ;
2022-07-28 19:48:33 +09:00
chat _form _captcha _image . alt = locale . waiting || "Waiting..." ;
2022-03-05 19:32:21 +09:00
chat _form _captcha _image . removeAttribute ( "title" ) ;
chat _form _captcha _image . removeAttribute ( "data-reloadable" ) ;
chat _form _captcha _image . removeAttribute ( "src" ) ;
const payload = { type : "captcha" } ;
ws . send ( JSON . stringify ( payload ) ) ;
2022-02-26 08:44:54 +09:00
}
2022-02-21 11:05:19 +09:00
} ) ;
const enable _captcha = ( digest ) => {
chat _form _captcha _digest . value = digest ;
chat _form _captcha _digest . disabled = false ;
chat _form _captcha _answer . value = "" ;
chat _form _captcha _answer . required = true ;
chat _form _captcha _answer . disabled = false ;
chat _form _comment . required = false ;
chat _form _captcha _image . removeAttribute ( "src" ) ;
2022-02-27 13:24:33 +09:00
chat _form _captcha _image . src = ` /captcha.jpg?token= ${ encodeURIComponent ( TOKEN ) } &digest= ${ encodeURIComponent ( digest ) } ` ;
2022-02-26 08:44:54 +09:00
chat _form _submit . disabled = false ;
2022-02-21 11:05:19 +09:00
chat _form . dataset . captcha = "" ;
}
const disable _captcha = ( ) => {
chat _form . removeAttribute ( "data-captcha" ) ;
chat _form _captcha _digest . disabled = true ;
chat _form _captcha _answer . disabled = true ;
chat _form _comment . required = true ;
chat _form _captcha _digest . value = "" ;
chat _form _captcha _answer . value = "" ;
chat _form _captcha _answer . required = false ;
2022-02-26 08:44:54 +09:00
chat _form _submit . disabled = false ;
2022-02-21 11:05:19 +09:00
chat _form _captcha _image . removeAttribute ( "alt" ) ;
chat _form _captcha _image . removeAttribute ( "src" ) ;
}
2022-02-22 19:43:09 +09:00
const set _title = ( title ) => {
2022-03-04 22:23:28 +09:00
const h1 = document . createElement ( "h1" ) ;
h1 . innerText = title . replaceAll ( /\r?\n/g , " " ) ;
info _title . innerHTML = h1 . outerHTML ;
2022-02-22 19:43:09 +09:00
}
2022-02-22 19:36:59 +09:00
const update _uptime = ( ) => {
2022-02-28 20:01:24 +09:00
if ( stats _received === null ) {
2022-02-22 19:36:59 +09:00
return ;
2022-02-28 20:01:24 +09:00
} else if ( stats === null ) {
2022-02-22 19:36:59 +09:00
info _uptime . innerText = "" ;
} else {
2022-02-28 20:01:24 +09:00
const stats _received _ago = ( new Date ( ) - stats _received ) / 1000 ;
const uptime = Math . round ( stats . uptime + stats _received _ago ) ;
2022-02-22 19:36:59 +09:00
const s = Math . round ( uptime % 60 ) ;
const m = Math . floor ( uptime / 60 ) % 60
const h = Math . floor ( uptime / 3600 ) ;
const ss = s . toString ( ) . padStart ( 2 , "0" ) ;
if ( uptime < 3600 ) {
info _uptime . innerText = ` ${ m } : ${ ss } ` ;
} else {
const mm = m . toString ( ) . padStart ( 2 , "0" ) ;
info _uptime . innerText = ` ${ h } : ${ mm } : ${ ss } ` ;
}
}
}
setInterval ( update _uptime , 1000 ) ; // always update uptime
2022-02-28 20:01:24 +09:00
const update _viewership = ( ) => {
2022-07-28 19:48:33 +09:00
info _viewership . innerText = stats === null ? "" : ( locale . viewers || "{0} viewers" ) . replace ( '{0}' , stats . viewership ) ;
2022-02-28 20:01:24 +09:00
}
const update _stats = ( ) => {
if ( stats === null ) {
update _viewership ( ) ;
update _uptime ( ) ;
} else {
update _uptime ( ) ;
update _viewership ( ) ;
}
2022-02-26 08:06:36 +09:00
}
2022-02-27 13:24:33 +09:00
const update _users _list = ( ) => {
listed _watching = new Set ( ) ;
listed _notwatching = new Set ( ) ;
// remove no-longer-known users
for ( const element of chat _users _watching . querySelectorAll ( ".chat-user" ) ) {
const token _hash = element . dataset . tokenHash ;
if ( ! Object . prototype . hasOwnProperty ( users , token _hash ) ) {
element . remove ( ) ;
} else {
listed _watching . add ( token _hash ) ;
}
}
for ( const element of chat _users _notwatching . querySelectorAll ( ".chat-user" ) ) {
const token _hash = element . dataset . tokenHash ;
if ( ! Object . prototype . hasOwnProperty ( users , token _hash ) ) {
element . remove ( ) ;
} else {
listed _notwatching . add ( token _hash ) ;
}
}
// add remaining watching/non-watching users
const insert = ( user , token _hash , is _you , chat _users _sublist ) => {
2022-02-28 21:40:56 +09:00
const chat _user _components = create _chat _user _components ( user ) ;
2022-02-27 13:24:33 +09:00
const chat _user = document . createElement ( "li" ) ;
chat _user . classList . add ( "chat-user" ) ;
chat _user . dataset . tokenHash = token _hash ;
2022-02-28 21:40:56 +09:00
for ( const chat _user _component of chat _user _components ) {
chat _user . insertAdjacentElement ( "beforeend" , chat _user _component ) ;
}
2022-02-27 13:24:33 +09:00
if ( is _you ) {
const you = document . createElement ( "span" ) ;
2022-07-28 19:48:33 +09:00
you . innerText = locale . you || " (You)" ;
2022-02-27 13:24:33 +09:00
chat _user . insertAdjacentElement ( "beforeend" , you ) ;
}
chat _users _sublist . insertAdjacentElement ( "beforeend" , chat _user ) ;
}
let watching = 0 , notwatching = 0 ;
for ( const token _hash of Object . keys ( users ) ) {
const user = users [ token _hash ] ;
const is _you = token _hash === TOKEN _HASH ;
if ( user . watching === true && ! listed _watching . has ( token _hash ) ) {
insert ( user , token _hash , is _you , chat _users _watching ) ;
watching ++ ;
}
if ( user . watching === false && ! listed _notwatching . has ( token _hash ) ) {
insert ( user , token _hash , is _you , chat _users _notwatching ) ;
notwatching ++ ;
}
}
// show correct numbers
2022-07-28 19:48:33 +09:00
chat _users _watching _header . innerText = ( locale . watching || "Watching ({0})" ) . replace ( "{0}" , watching ) ;
chat _users _notwatching _header . innerText = ( locale . not _watching || "Not watching ({0})" ) . replace ( "{0}" , notwatching ) ;
2022-02-27 13:24:33 +09:00
}
2022-03-30 17:41:42 +09:00
const show _offline _screen = ( ) => {
video . removeAttribute ( "src" ) ;
video . load ( ) ;
stream . dataset . offline = "" ;
}
2022-07-15 01:31:11 +09:00
const on _websocket _message = async ( event ) => {
2022-02-22 14:27:42 +09:00
//console.log("websocket message", event);
2022-02-15 19:16:10 +09:00
const receipt = JSON . parse ( event . data ) ;
switch ( receipt . type ) {
case "error" :
console . log ( "ws error" , receipt ) ;
2022-02-26 08:44:54 +09:00
chat _form _submit . disabled = false ;
2022-03-07 14:39:06 +09:00
chat _appearance _form _submit . disabled = false ;
2022-02-15 19:16:10 +09:00
break ;
case "init" :
console . log ( "ws init" , receipt ) ;
2022-02-16 18:55:30 +09:00
2022-04-02 13:46:24 +09:00
pingpong _period = receipt . pingpong ;
2022-07-28 19:48:33 +09:00
// update locale & put localized strings in js-inserted elements
locale = receipt . locale ;
for ( element of document . querySelectorAll ( '[data-string]' ) ) {
const string = element . dataset . string ;
if ( locale [ string ] !== undefined ) {
const attr = element . dataset . stringAttr ;
if ( attr === undefined )
element . innerText = locale [ string ] ;
else
element [ attr ] = locale [ string ] ;
}
}
// stream title
2022-02-22 19:43:09 +09:00
set _title ( receipt . title ) ;
2022-02-26 08:06:36 +09:00
2022-02-28 20:01:24 +09:00
// update stats (uptime/viewership)
stats = receipt . stats ;
stats _received = new Date ( ) ;
update _stats ( ) ;
2022-02-16 18:55:30 +09:00
2022-03-01 11:06:48 +09:00
// stream reload button
if ( stats === null || stream . networkState === stream . NETWORK _LOADING ) {
info _button . removeAttribute ( "data-visible" ) ;
} else {
info _button . dataset . visible = "" ;
}
2022-02-26 08:06:36 +09:00
// chat form nonce
2022-02-21 11:05:19 +09:00
chat _form _nonce . value = receipt . nonce ;
2022-02-26 08:06:36 +09:00
// chat form captcha digest
2022-02-21 11:05:19 +09:00
receipt . digest === null ? disable _captcha ( ) : enable _captcha ( receipt . digest ) ;
2022-02-26 08:44:54 +09:00
// chat form submit button
chat _form _submit . disabled = false ;
2022-07-15 01:33:51 +09:00
// remove messages the server isn't acknowledging the existence of
2022-02-18 10:32:34 +09:00
const seqs = new Set ( receipt . messages . map ( ( message ) => { return message . seq ; } ) ) ;
2022-02-16 19:07:17 +09:00
const to _delete = [ ] ;
2022-02-16 18:55:30 +09:00
for ( const chat _message of chat _messages . children ) {
2022-02-18 10:32:34 +09:00
const chat _message _seq = parseInt ( chat _message . dataset . seq ) ;
if ( ! seqs . has ( chat _message _seq ) ) {
2022-02-16 19:07:17 +09:00
to _delete . push ( chat _message ) ;
2022-02-16 18:55:30 +09:00
}
}
2022-02-16 19:07:17 +09:00
for ( const chat _message of to _delete ) {
chat _message . remove ( ) ;
}
2022-02-26 08:06:36 +09:00
// settings
2022-02-22 18:30:23 +09:00
default _name = receipt . default ;
max _chat _scrollback = receipt . scrollback ;
2022-02-26 08:06:36 +09:00
2022-06-16 05:39:02 +09:00
// if the chat is scrolled all the way to the bottom, make sure this is
// still the case after updating user names and tripcodes
at _bottom = chat _messages . scrollTop === chat _messages . scrollTopMax ;
2022-02-27 13:24:33 +09:00
// update users
2022-02-22 18:30:23 +09:00
users = receipt . users ;
update _user _names ( ) ;
update _user _colors ( ) ;
update _user _tripcodes ( ) ;
2022-02-27 13:24:33 +09:00
update _users _list ( )
2022-02-22 18:30:23 +09:00
2022-06-16 05:39:02 +09:00
// ensure chat scroll (see above)
if ( at _bottom ) {
chat _messages . scrollTo ( {
left : 0 ,
top : chat _messages . scrollTopMax ,
behavior : "instant" ,
2022-07-20 15:05:32 +09:00
} ) ;
2022-06-16 05:39:02 +09:00
}
2022-03-07 14:39:06 +09:00
// appearance form default values
const user = users [ TOKEN _HASH ] ;
if ( user . name !== null ) {
chat _appearance _form _name . setAttribute ( "value" , user . name ) ;
}
chat _appearance _form _name . setAttribute ( "placeholder" , default _name [ user . broadcaster ] ) ;
chat _appearance _form _color . setAttribute ( "value" , user . color ) ;
2022-02-26 08:06:36 +09:00
// insert new messages
2022-02-18 10:32:34 +09:00
const last = chat _messages . children . length == 0 ? null : chat _messages . children [ chat _messages . children . length - 1 ] ;
const last _seq = last === null ? null : parseInt ( last . dataset . seq ) ;
for ( const message of receipt . messages ) {
if ( message . seq > last _seq ) {
2022-02-18 21:24:19 +09:00
create _and _add _chat _message ( message ) ;
2022-02-16 18:55:30 +09:00
}
}
2022-02-15 19:16:10 +09:00
break ;
2022-02-23 07:51:29 +09:00
case "info" :
console . log ( "ws info" , receipt ) ;
2022-03-01 11:06:48 +09:00
// set title
2022-02-23 07:51:29 +09:00
if ( receipt . title !== undefined ) {
set _title ( receipt . title ) ;
}
2022-03-01 11:06:48 +09:00
// update stats (uptime/viewership)
2022-02-28 20:01:24 +09:00
if ( receipt . stats !== undefined ) {
stats = receipt . stats ;
stats _received = new Date ( ) ;
update _stats ( ) ;
2022-02-26 08:06:36 +09:00
}
2022-03-01 11:06:48 +09:00
// stream reload button
2022-03-30 17:41:42 +09:00
if ( stats === null || video . networkState === video . NETWORK _LOADING ) {
2022-03-01 11:06:48 +09:00
info _button . removeAttribute ( "data-visible" ) ;
} else {
info _button . dataset . visible = "" ;
}
2022-02-15 19:16:10 +09:00
break ;
case "ack" :
console . log ( "ws ack" , receipt ) ;
2022-03-05 19:17:29 +09:00
if ( receipt . notice !== null ) {
show _notice ( receipt . notice ) ;
}
2022-02-21 11:05:19 +09:00
const existing _nonce = chat _form _nonce . value ;
if ( receipt . clear && receipt . nonce === existing _nonce ) {
2022-02-15 19:16:10 +09:00
chat _form _comment . value = "" ;
} else {
2022-02-21 11:05:19 +09:00
console . log ( "nonce does not match ack" , existing _nonce , receipt ) ;
2022-02-15 19:16:10 +09:00
}
chat _form _nonce . value = receipt . next ;
2022-02-21 11:05:19 +09:00
receipt . digest === null ? disable _captcha ( ) : enable _captcha ( receipt . digest ) ;
2022-02-15 19:16:10 +09:00
chat _form _submit . disabled = false ;
2022-03-05 19:17:29 +09:00
2022-02-15 19:16:10 +09:00
break ;
2022-02-22 14:27:42 +09:00
case "message" :
console . log ( "ws message" , receipt ) ;
create _and _add _chat _message ( receipt . message ) ;
2022-03-10 17:21:21 +09:00
if ( chat _messages . dataset . scrollLock === undefined ) {
chat _messages . scrollTo ( {
left : 0 ,
top : chat _messages . scrollTopMax ,
behavior : "smooth" ,
} ) ;
}
2022-02-15 19:16:10 +09:00
break ;
2022-06-14 05:43:51 +09:00
case "delete" :
console . log ( "ws delete" , receipt ) ;
delete _chat _messages ( receipt . seqs ) ;
break ;
2022-02-20 16:20:43 +09:00
case "set-users" :
console . log ( "ws set-users" , receipt ) ;
for ( const token _hash of Object . keys ( receipt . users ) ) {
users [ token _hash ] = receipt . users [ token _hash ] ;
}
2022-06-16 05:39:02 +09:00
// if the chat is scrolled all the way to the bottom, make sure this is
// still the case after updating user names and tripcodes
at _bottom = chat _messages . scrollTop === chat _messages . scrollTopMax ;
// update users
2022-02-20 16:20:43 +09:00
update _user _names ( ) ;
update _user _colors ( ) ;
update _user _tripcodes ( ) ;
2022-02-27 13:24:33 +09:00
update _users _list ( )
2022-06-16 05:39:02 +09:00
// ensure chat scroll (see above)
if ( at _bottom ) {
chat _messages . scrollTo ( {
left : 0 ,
top : chat _messages . scrollTopMax ,
behavior : "instant" ,
} ) ;
}
2022-02-20 16:20:43 +09:00
break ;
2022-02-16 18:55:30 +09:00
case "rem-users" :
2022-02-20 16:20:43 +09:00
console . log ( "ws rem-users" , receipt ) ;
for ( const token _hash of receipt . token _hashes ) {
delete users [ token _hash ] ;
}
update _user _colors ( ) ;
update _user _tripcodes ( ) ;
2022-02-27 13:24:33 +09:00
update _users _list ( )
2022-02-20 16:20:43 +09:00
break ;
2022-02-16 18:55:30 +09:00
2022-02-26 08:44:54 +09:00
case "captcha" :
console . log ( "ws captcha" , receipt ) ;
receipt . digest === null ? disable _captcha ( ) : enable _captcha ( receipt . digest ) ;
break ;
2022-03-07 14:39:06 +09:00
case "appearance" :
console . log ( "ws appearance" , receipt ) ;
if ( receipt . errors === undefined ) {
if ( receipt . name !== null ) {
chat _appearance _form _name . setAttribute ( "value" , receipt . name ) ;
}
chat _appearance _form _color . setAttribute ( "value" , receipt . color ) ;
chat _appearance _form _result . innerHTML = receipt . result ;
} else {
const ul = document . createElement ( "ul" ) ;
for ( const error of receipt . errors ) {
const li = document . createElement ( "li" ) ;
li . innerText = error [ 0 ] ;
for ( const tuple of error . slice ( 1 ) ) {
const mark = document . createElement ( "mark" ) ;
mark . innerText = tuple [ 0 ] ;
li . insertAdjacentText ( "beforeend" , " " ) ;
li . insertAdjacentElement ( "beforeend" , mark ) ;
li . insertAdjacentText ( "beforeend" , tuple [ 1 ] ) ;
}
ul . insertAdjacentElement ( "beforeend" , li ) ;
}
const result = document . createElement ( "div" ) ;
2022-07-28 19:48:33 +09:00
result . innerText = locale . errors || "Errors:" ;
2022-03-07 14:39:06 +09:00
result . insertAdjacentElement ( "beforeend" , ul ) ;
chat _appearance _form _result . innerHTML = result . innerHTML ;
}
chat _appearance _form _submit . disabled = false ;
chat _appearance _form . removeAttribute ( "data-hidden" ) ;
chat _form _settings . style . backgroundColor = "#4f4f53" ;
break ;
2022-04-02 13:46:24 +09:00
case "ping" :
console . log ( "ws ping" ) ;
ping = new Date ( ) ;
const payload = { type : "pong" } ;
ws . send ( JSON . stringify ( payload ) ) ;
break ;
2022-06-22 17:10:42 +09:00
case "kick" :
console . log ( "ws kick" ) ;
window . location . reload ( ) ;
break ;
2022-02-15 19:16:10 +09:00
default :
console . log ( "incomprehensible websocket message" , receipt ) ;
}
2022-02-13 13:00:10 +09:00
} ;
const chat _live _ball = document . getElementById ( "chat-live__ball" ) ;
const chat _live _status = document . getElementById ( "chat-live__status" ) ;
let ws ;
let websocket _backoff = 2000 ; // 2 seconds
const connect _websocket = ( ) => {
2022-02-15 19:16:10 +09:00
if ( ws !== undefined && ( ws . readyState === ws . CONNECTING || ws . readyState === ws . OPEN ) ) {
console . log ( "refusing to open another websocket" ) ;
return ;
}
chat _live _ball . style . borderColor = "gold" ;
2022-07-28 19:48:33 +09:00
chat _live _status . innerHTML = ` <span data-verbose='true'> ${ locale . connecting _to _chat || "Connecting to chat..." } </span><span data-verbose='false'>···</span> ` ;
2022-06-29 11:36:09 +09:00
ws = null ;
2022-07-29 17:04:54 +09:00
ws = new WebSocket ( ` ws:// ${ document . domain } : ${ location . port } /live?token= ${ encodeURIComponent ( TOKEN ) } &lang= ${ encodeURIComponent ( LANG ) } ` ) ;
2022-02-15 19:16:10 +09:00
ws . addEventListener ( "open" , ( event ) => {
2022-02-16 18:55:30 +09:00
console . log ( "websocket open" , event ) ;
2022-02-15 19:16:10 +09:00
chat _form _submit . disabled = false ;
chat _live _ball . style . borderColor = "green" ;
2022-07-28 19:48:33 +09:00
chat _live _status . innerHTML = ` <span><span data-verbose='true'> ${ locale . connected _to _chat || "Connected to chat" } </span><span data-verbose='false'>✓</span></span> ` ;
2022-02-16 18:55:30 +09:00
// When the server is offline, a newly opened websocket can take a second
// to close. This timeout tries to ensure the backoff doesn't instantly
// (erroneously) reset to 2 seconds in that case.
setTimeout ( ( ) => {
if ( event . target === ws ) {
websocket _backoff = 2000 ; // 2 seconds
}
} ,
websocket _backoff + 4000 ,
) ;
2022-02-15 19:16:10 +09:00
} ) ;
ws . addEventListener ( "close" , ( event ) => {
2022-02-16 18:55:30 +09:00
console . log ( "websocket close" , event ) ;
2022-02-15 19:16:10 +09:00
chat _form _submit . disabled = true ;
chat _live _ball . style . borderColor = "maroon" ;
2022-07-28 19:48:33 +09:00
chat _live _status . innerHTML = ` <span data-verbose='true'> ${ locale . disconnected _from _chat || "Disconnected from chat" } </span><span data-verbose='false'>×</span> ` ;
2022-02-15 19:16:10 +09:00
if ( ! ws . successor ) {
ws . successor = true ;
setTimeout ( connect _websocket , websocket _backoff ) ;
websocket _backoff = Math . min ( 32000 , websocket _backoff * 2 ) ;
2022-02-13 13:00:10 +09:00
}
2022-02-15 19:16:10 +09:00
} ) ;
ws . addEventListener ( "error" , ( event ) => {
console . log ( "websocket error" , event ) ;
chat _form _submit . disabled = true ;
chat _live _ball . style . borderColor = "maroon" ;
2022-07-28 19:48:33 +09:00
chat _live _status . innerHTML = ` <span><span data-verbose='true'> ${ locale . error _connecting _to _chat || "Error connecting to chat" } </span><span data-verbose='false'> ${ locale . error _connecting _to _chat _terse || "Error" } </span></span> ` ;
2022-02-15 19:16:10 +09:00
} ) ;
ws . addEventListener ( "message" , on _websocket _message ) ;
2022-02-13 13:00:10 +09:00
}
connect _websocket ( ) ;
2022-03-01 11:06:48 +09:00
/* stream reload button */
2022-03-30 17:41:42 +09:00
const video = document . getElementById ( "stream__video" ) ;
2022-03-01 11:06:48 +09:00
const info _button = document . getElementById ( "info_js__float__button" ) ;
info _button . addEventListener ( "click" , ( event ) => {
2022-03-30 17:41:42 +09:00
stream . removeAttribute ( "data-offline" ) ;
video . src = ` /stream.mp4?token= ${ encodeURIComponent ( TOKEN ) } ` ;
video . load ( ) ;
2022-03-01 11:06:48 +09:00
info _button . removeAttribute ( "data-visible" ) ;
} ) ;
2022-03-30 17:41:42 +09:00
video . addEventListener ( "error" , ( event ) => {
if ( video . error !== null && video . error . message === "404: Not Found" ) {
show _offline _screen ( ) ;
}
2022-03-01 11:06:48 +09:00
if ( stats !== null ) {
info _button . dataset . visible = "" ;
}
} ) ;
2022-03-30 17:41:42 +09:00
/* load stream */
video . src = ` /stream.mp4?token= ${ encodeURIComponent ( TOKEN ) } ` ;
2022-02-13 13:00:10 +09:00
/* override js-only chat form */
2022-02-14 19:16:09 +09:00
const chat _form _nonce = document . getElementById ( "chat-form_js__nonce" ) ;
2022-02-15 19:11:53 +09:00
const chat _form _comment = document . getElementById ( "chat-form_js__comment" ) ;
2022-02-14 19:16:09 +09:00
const chat _form _submit = document . getElementById ( "chat-form_js__submit" ) ;
2022-02-13 13:00:10 +09:00
chat _form . addEventListener ( "submit" , ( event ) => {
2022-02-15 19:16:10 +09:00
event . preventDefault ( ) ;
2022-02-26 08:44:54 +09:00
const form = Object . fromEntries ( new FormData ( chat _form ) ) ;
const payload = { type : "message" , form : form } ;
2022-02-15 19:16:10 +09:00
chat _form _submit . disabled = true ;
ws . send ( JSON . stringify ( payload ) ) ;
2022-02-13 13:00:10 +09:00
} ) ;
2022-02-16 18:55:30 +09:00
2022-03-07 14:39:06 +09:00
/* override js-only appearance form */
const chat _appearance _form _submit = document . getElementById ( "appearance-form_js__row__submit" ) ;
chat _appearance _form . addEventListener ( "submit" , ( event ) => {
event . preventDefault ( ) ;
const form = Object . fromEntries ( new FormData ( chat _appearance _form ) ) ;
const payload = { type : "appearance" , form : form } ;
chat _appearance _form _submit . disabled = true ;
chat _appearance _form _password . value = "" ;
chat _appearance _form _result . innerText = "" ;
ws . send ( JSON . stringify ( payload ) ) ;
} ) ;
2022-02-16 18:55:30 +09:00
/* when chat is being resized, peg its bottom in place (instead of its top) */
const track _scroll = ( element ) => {
chat _messages . dataset . scrollTop = chat _messages . scrollTop ;
chat _messages . dataset . scrollTopMax = chat _messages . scrollTopMax ;
}
const peg _bottom = ( entries ) => {
for ( const entry of entries ) {
const element = entry . target ;
const bottom = chat _messages . dataset . scrollTopMax - chat _messages . dataset . scrollTop ;
element . scrollTop = chat _messages . scrollTopMax - bottom ;
track _scroll ( element ) ;
}
}
const resize = new ResizeObserver ( peg _bottom ) ;
resize . observe ( chat _messages ) ;
2022-03-10 17:21:21 +09:00
track _scroll ( chat _messages ) ;
/* chat scroll lock */
2022-02-16 18:55:30 +09:00
chat _messages . addEventListener ( "scroll" , ( event ) => {
track _scroll ( chat _messages ) ;
2022-03-10 17:21:21 +09:00
const scroll = chat _messages . scrollTopMax - chat _messages . scrollTop ;
const locked = chat _messages . dataset . scrollLock !== undefined
if ( scroll >= 160 && ! locked ) {
chat _messages . dataset . scrollLock = "" ;
} else if ( scroll == 0 && locked ) {
chat _messages . removeAttribute ( "data-scroll-lock" ) ;
}
} ) ;
const chat _messages _unlock = document . getElementById ( "chat-messages-unlock" ) ;
chat _messages _unlock . addEventListener ( "click" , ( event ) => {
chat _messages . scrollTop = chat _messages . scrollTopMax ;
2022-02-16 18:55:30 +09:00
} ) ;
2022-04-02 13:46:24 +09:00
/* close websocket after prolonged absence of pings */
2022-06-14 07:04:51 +09:00
2022-04-02 13:46:24 +09:00
const rotate _websocket = ( ) => {
2022-06-14 07:04:51 +09:00
const timeout _ms = pingpong _timeout _ms ( ) ;
if ( ws . readyState !== ws . CLOSED ) {
if ( ping === null || new Date ( ) - ping > timeout _ms ) {
console . log ( ` no pings heard in ${ timeout _ms / 1000 } seconds, closing websocket... ` ) ;
ws . close ( ) ;
}
2022-04-02 13:46:24 +09:00
}
2022-06-14 07:04:51 +09:00
setTimeout ( rotate _websocket , timeout _ms ) ;
2022-04-02 13:46:24 +09:00
} ;
2022-06-14 07:04:51 +09:00
setTimeout ( rotate _websocket , pingpong _timeout _ms ( ) ) ;