2018-10-23 23:15:40 +08:00
/ *
*
* NOTE : The client side of hack . chat is currently in development ,
* a new , more modern but still minimal version will be released
* soon . As a result of this , the current code has been deprecated
* and will not actively be updated .
*
* /
2022-10-14 15:47:42 +08:00
//select "chatinput" on "/"
document . addEventListener ( "keydown" , e => {
if ( e . key === '/' && document . getElementById ( "chatinput" ) != document . activeElement ) {
e . preventDefault ( ) ;
document . getElementById ( "chatinput" ) . focus ( ) ;
}
} ) ;
2019-08-18 07:24:29 +08:00
// initialize markdown engine
var markdownOptions = {
html : false ,
xhtmlOut : false ,
breaks : true ,
langPrefix : '' ,
linkify : true ,
linkTarget : '_blank" rel="noreferrer' ,
typographer : true ,
quotes : ` ""'' ` ,
doHighlight : true ,
highlight : function ( str , lang ) {
if ( ! markdownOptions . doHighlight || ! window . hljs ) { return '' ; }
if ( lang && hljs . getLanguage ( lang ) ) {
try {
return hljs . highlight ( lang , str ) . value ;
} catch ( _ _ ) { }
}
try {
return hljs . highlightAuto ( str ) . value ;
} catch ( _ _ ) { }
return '' ;
}
} ;
var md = new Remarkable ( 'full' , markdownOptions ) ;
// image handler
var allowImages = false ;
var imgHostWhitelist = [
'i.imgur.com' ,
'imgur.com' ,
] ;
function getDomain ( link ) {
var a = document . createElement ( 'a' ) ;
a . href = link ;
return a . hostname ;
}
function isWhiteListed ( link ) {
return imgHostWhitelist . indexOf ( getDomain ( link ) ) !== - 1 ;
}
md . renderer . rules . image = function ( tokens , idx , options ) {
var src = Remarkable . utils . escapeHtml ( tokens [ idx ] . src ) ;
if ( isWhiteListed ( src ) && allowImages ) {
var imgSrc = ' src="' + Remarkable . utils . escapeHtml ( tokens [ idx ] . src ) + '"' ;
var title = tokens [ idx ] . title ? ( ' title="' + Remarkable . utils . escapeHtml ( Remarkable . utils . replaceEntities ( tokens [ idx ] . title ) ) + '"' ) : '' ;
var alt = ' alt="' + ( tokens [ idx ] . alt ? Remarkable . utils . escapeHtml ( Remarkable . utils . replaceEntities ( Remarkable . utils . unescapeMd ( tokens [ idx ] . alt ) ) ) : '' ) + '"' ;
var suffix = options . xhtmlOut ? ' /' : '' ;
var scrollOnload = isAtBottom ( ) ? ' onload="window.scrollTo(0, document.body.scrollHeight)"' : '' ;
return '<a href="' + src + '" target="_blank" rel="noreferrer"><img' + scrollOnload + imgSrc + alt + title + suffix + '></a>' ;
}
return '<a href="' + src + '" target="_blank" rel="noreferrer">' + Remarkable . utils . escapeHtml ( Remarkable . utils . replaceEntities ( src ) ) + '</a>' ;
} ;
md . renderer . rules . link _open = function ( tokens , idx , options ) {
var title = tokens [ idx ] . title ? ( ' title="' + Remarkable . utils . escapeHtml ( Remarkable . utils . replaceEntities ( tokens [ idx ] . title ) ) + '"' ) : '' ;
var target = options . linkTarget ? ( ' target="' + options . linkTarget + '"' ) : '' ;
return '<a rel="noreferrer" onclick="return verifyLink(this)" href="' + Remarkable . utils . escapeHtml ( tokens [ idx ] . href ) + '"' + title + target + '>' ;
} ;
md . renderer . rules . text = function ( tokens , idx ) {
tokens [ idx ] . content = Remarkable . utils . escapeHtml ( tokens [ idx ] . content ) ;
if ( tokens [ idx ] . content . indexOf ( '?' ) !== - 1 ) {
tokens [ idx ] . content = tokens [ idx ] . content . replace ( /(^|\s)(\?)\S+?(?=[,.!?:)]?\s|$)/gm , function ( match ) {
var channelLink = Remarkable . utils . escapeHtml ( Remarkable . utils . replaceEntities ( match . trim ( ) ) ) ;
var whiteSpace = '' ;
if ( match [ 0 ] !== '?' ) {
whiteSpace = match [ 0 ] ;
}
return whiteSpace + '<a href="' + channelLink + '" target="_blank">' + channelLink + '</a>' ;
} ) ;
}
return tokens [ idx ] . content ;
} ;
md . use ( remarkableKatex ) ;
function verifyLink ( link ) {
var linkHref = Remarkable . utils . escapeHtml ( Remarkable . utils . replaceEntities ( link . href ) ) ;
if ( linkHref !== link . innerHTML ) {
return confirm ( 'Warning, please verify this is where you want to go: ' + linkHref ) ;
}
return true ;
}
2019-01-14 06:08:16 +08:00
var verifyNickname = function ( nick ) {
return /^[a-zA-Z0-9_]{1,24}$/ . test ( nick ) ;
}
2018-10-23 23:15:40 +08:00
2018-03-10 15:47:00 +08:00
var frontpage = [
" _ _ _ _ " ,
" | |_ ___ ___| |_ ___| |_ ___| |_ " ,
" | |_ || _| '_| | _| |_ || _|" ,
" |_|_|__/|___|_,_|.|___|_|_|__/|_| " ,
"" ,
"" ,
"Welcome to hack.chat, a minimal, distraction-free chat application." ,
2018-10-23 23:15:40 +08:00
"Channels are created, joined and shared with the url, create your own channel by changing the text after the question mark." ,
"If you wanted your channel name to be 'your-channel': https://hack.chat/?your-channel" ,
"There are no channel lists, so a secret channel name can be used for private discussions." ,
2018-03-10 15:47:00 +08:00
"" ,
"Here are some pre-made channels you can join:" ,
"?lounge ?meta" ,
"?math ?physics ?chemistry" ,
"?technology ?programming" ,
"?games ?banana" ,
"And here's a random one generated just for you: ?" + Math . random ( ) . toString ( 36 ) . substr ( 2 , 8 ) ,
"" ,
"Formatting:" ,
"Whitespace is preserved, so source code can be pasted verbatim." ,
"Surround LaTeX with a dollar sign for inline style $\\zeta(2) = \\pi^2/6$, and two dollars for display. $$\\int_0^1 \\int_0^1 \\frac{1}{1-xy} dx dy = \\frac{\\pi^2}{6}$$" ,
2019-08-10 07:26:35 +08:00
"For syntax highlight, wrap the code like: ```<language> <the code>``` where <language> is any known programming language." ,
2018-03-10 15:47:00 +08:00
"" ,
2018-10-23 23:15:40 +08:00
"Current Github: https://github.com/hack-chat" ,
2018-03-26 05:16:02 +08:00
"Legacy GitHub: https://github.com/AndrewBelt/hack.chat" ,
2018-10-23 23:15:40 +08:00
"" ,
"Bots, Android clients, desktop clients, browser extensions, docker images, programming libraries, server modules and more:" ,
"https://github.com/hack-chat/3rd-party-software-list" ,
2018-03-10 15:47:00 +08:00
"" ,
2018-03-26 05:16:02 +08:00
"Server and web client released under the WTFPL and MIT open source license." ,
"No message history is retained on the hack.chat server."
2018-03-29 12:34:27 +08:00
] . join ( "\n" ) ;
2018-03-10 15:47:00 +08:00
2018-03-29 12:34:27 +08:00
function $ ( query ) {
return document . querySelector ( query ) ;
}
2018-03-10 15:47:00 +08:00
function localStorageGet ( key ) {
try {
return window . localStorage [ key ]
2018-05-17 23:42:16 +08:00
} catch ( e ) { }
2018-03-10 15:47:00 +08:00
}
function localStorageSet ( key , val ) {
try {
window . localStorage [ key ] = val
2018-05-17 23:42:16 +08:00
} catch ( e ) { }
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
var ws ;
2018-11-03 22:51:05 +08:00
var myNick = localStorageGet ( 'my-nick' ) || '' ;
2018-03-29 12:34:27 +08:00
var myChannel = window . location . search . replace ( /^\?/ , '' ) ;
var lastSent = [ "" ] ;
var lastSentPos = 0 ;
2018-03-10 15:47:00 +08:00
2019-05-09 14:37:13 +08:00
/** Notification switch and local storage behavior **/
var notifySwitch = document . getElementById ( "notify-switch" )
var notifySetting = localStorageGet ( "notify-api" )
2019-05-12 11:30:01 +08:00
var notifyPermissionExplained = 0 ; // 1 = granted msg shown, -1 = denied message shown
// Inital request for notifications permission
function RequestNotifyPermission ( ) {
try {
var notifyPromise = Notification . requestPermission ( ) ;
if ( notifyPromise ) {
notifyPromise . then ( function ( result ) {
console . log ( "Hack.Chat notification permission: " + result ) ;
if ( result === "granted" ) {
if ( notifyPermissionExplained === 0 ) {
pushMessage ( {
cmd : "chat" ,
nick : "*" ,
text : "Notifications permission granted." ,
time : null
} ) ;
notifyPermissionExplained = 1 ;
}
return false ;
} else {
if ( notifyPermissionExplained === 0 ) {
pushMessage ( {
cmd : "chat" ,
nick : "*" ,
text : "Notifications permission denied, you won't be notified if someone @mentions you." ,
time : null
} ) ;
notifyPermissionExplained = - 1 ;
}
return true ;
}
} ) ;
}
} catch ( error ) {
pushMessage ( {
cmd : "chat" ,
nick : "*" ,
text : "Unable to create a notification." ,
time : null
} ) ;
console . error ( "An error occured trying to request notification permissions. This browser might not support desktop notifications.\nDetails:" )
console . error ( error )
return false ;
}
}
2019-05-09 14:37:13 +08:00
// Update localStorage with value of checkbox
2019-05-12 11:30:01 +08:00
notifySwitch . addEventListener ( 'change' , ( event ) => {
if ( event . target . checked ) {
RequestNotifyPermission ( ) ;
}
2019-05-09 14:37:13 +08:00
localStorageSet ( "notify-api" , notifySwitch . checked )
} )
2019-05-12 11:30:01 +08:00
// Check if localStorage value is set, defaults to OFF
2019-05-09 14:37:13 +08:00
if ( notifySetting === null ) {
2019-05-12 11:30:01 +08:00
localStorageSet ( "notify-api" , "false" )
notifySwitch . checked = false
2019-05-09 14:37:13 +08:00
}
// Configure notifySwitch checkbox element
if ( notifySetting === "true" || notifySetting === true ) {
notifySwitch . checked = true
} else if ( notifySetting === "false" || notifySetting === false ) {
notifySwitch . checked = false
}
/** Sound switch and local storage behavior **/
var soundSwitch = document . getElementById ( "sound-switch" )
var notifySetting = localStorageGet ( "notify-sound" )
// Update localStorage with value of checkbox
2019-05-12 11:30:01 +08:00
soundSwitch . addEventListener ( 'change' , ( event ) => {
2019-05-09 14:37:13 +08:00
localStorageSet ( "notify-sound" , soundSwitch . checked )
} )
// Check if localStorage value is set, defaults to OFF
if ( notifySetting === null ) {
localStorageSet ( "notify-sound" , "false" )
soundSwitch . checked = false
}
// Configure soundSwitch checkbox element
if ( notifySetting === "true" || notifySetting === true ) {
soundSwitch . checked = true
} else if ( notifySetting === "false" || notifySetting === false ) {
soundSwitch . checked = false
}
// Create a new notification after checking if permission has been granted
function spawnNotification ( title , body ) {
// Let's check if the browser supports notifications
if ( ! ( "Notification" in window ) ) {
console . error ( "This browser does not support desktop notification" ) ;
2019-05-12 11:30:01 +08:00
} else if ( Notification . permission === "granted" ) { // Check if notification permissions are already given
2019-05-09 14:37:13 +08:00
// If it's okay let's create a notification
var options = {
body : body ,
icon : "/favicon-96x96.png"
} ;
var n = new Notification ( title , options ) ;
}
// Otherwise, we need to ask the user for permission
else if ( Notification . permission !== "denied" ) {
2019-05-12 11:30:01 +08:00
if ( RequestNotifyPermission ( ) ) {
var options = {
body : body ,
icon : "/favicon-96x96.png"
} ;
var n = new Notification ( title , options ) ;
2019-05-12 10:35:04 +08:00
}
2019-05-12 11:30:01 +08:00
} else if ( Notification . permission == "denied" ) {
2019-08-18 07:24:29 +08:00
// At last, if the user has denied notifications, and you
2019-05-12 11:30:01 +08:00
// want to be respectful, there is no need to bother them any more.
2019-05-09 14:37:13 +08:00
}
}
function notify ( args ) {
// Spawn notification if enabled
if ( notifySwitch . checked ) {
spawnNotification ( "?" + myChannel + " — " + args . nick , args . text )
}
// Play sound if enabled
if ( soundSwitch . checked ) {
2019-05-12 10:35:04 +08:00
var soundPromise = document . getElementById ( "notify-sound" ) . play ( ) ;
if ( soundPromise ) {
soundPromise . catch ( function ( error ) {
console . error ( "Problem playing sound:\n" + error ) ;
} ) ;
}
2019-05-09 14:37:13 +08:00
}
}
2018-03-10 15:47:00 +08:00
function join ( channel ) {
2018-03-26 05:16:02 +08:00
if ( document . domain == 'hack.chat' ) {
// For https://hack.chat/
2018-03-29 12:34:27 +08:00
ws = new WebSocket ( 'wss://hack.chat/chat-ws' ) ;
2018-03-26 05:16:02 +08:00
} else {
// for local installs
2019-05-09 14:37:13 +08:00
var protocol = location . protocol === 'https:' ? 'wss:' : 'ws:'
2019-03-04 05:41:03 +08:00
// if you changed the port during the server config, change 'wsPath'
// to the new port (example: ':8080')
// if you are reverse proxying, change 'wsPath' to the new location
// (example: '/chat-ws')
var wsPath = ':6060' ;
ws = new WebSocket ( protocol + '//' + document . domain + wsPath ) ;
2018-03-26 05:16:02 +08:00
}
2018-03-10 15:47:00 +08:00
2018-03-29 12:34:27 +08:00
var wasConnected = false ;
2018-03-10 15:47:00 +08:00
2018-03-29 12:34:27 +08:00
ws . onopen = function ( ) {
2020-08-05 07:27:35 +08:00
var shouldConnect = true ;
2018-03-10 15:47:00 +08:00
if ( ! wasConnected ) {
if ( location . hash ) {
2018-03-29 12:34:27 +08:00
myNick = location . hash . substr ( 1 ) ;
} else {
2020-07-22 02:59:56 +08:00
var newNick = prompt ( 'Nickname:' , myNick ) ;
if ( newNick !== null ) {
myNick = newNick ;
2020-08-05 07:27:35 +08:00
} else {
// The user cancelled the prompt in some manner
shouldConnect = false ;
2020-07-22 02:59:56 +08:00
}
2018-03-10 15:47:00 +08:00
}
}
2018-03-29 12:34:27 +08:00
2020-08-05 07:27:35 +08:00
if ( myNick && shouldConnect ) {
2018-03-29 12:34:27 +08:00
localStorageSet ( 'my-nick' , myNick ) ;
send ( { cmd : 'join' , channel : channel , nick : myNick } ) ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
wasConnected = true ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
ws . onclose = function ( ) {
2018-03-10 15:47:00 +08:00
if ( wasConnected ) {
2018-03-29 12:34:27 +08:00
pushMessage ( { nick : '!' , text : "Server disconnected. Attempting to reconnect. . ." } ) ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
window . setTimeout ( function ( ) {
join ( channel ) ;
} , 2000 ) ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
ws . onmessage = function ( message ) {
var args = JSON . parse ( message . data ) ;
var cmd = args . cmd ;
var command = COMMANDS [ cmd ] ;
command . call ( null , args ) ;
2018-03-10 15:47:00 +08:00
}
}
var COMMANDS = {
2018-03-29 12:34:27 +08:00
chat : function ( args ) {
2018-03-10 15:47:00 +08:00
if ( ignoredUsers . indexOf ( args . nick ) >= 0 ) {
2018-03-29 12:34:27 +08:00
return ;
2018-03-10 15:47:00 +08:00
}
2019-05-20 05:11:16 +08:00
pushMessage ( args ) ;
2018-03-10 15:47:00 +08:00
} ,
2018-03-29 12:34:27 +08:00
info : function ( args ) {
args . nick = '*' ;
2019-05-20 05:11:16 +08:00
pushMessage ( args ) ;
2018-03-10 15:47:00 +08:00
} ,
2018-03-29 12:34:27 +08:00
warn : function ( args ) {
args . nick = '!' ;
pushMessage ( args ) ;
2018-03-10 15:47:00 +08:00
} ,
2018-03-29 12:34:27 +08:00
onlineSet : function ( args ) {
var nicks = args . nicks ;
usersClear ( ) ;
nicks . forEach ( function ( nick ) {
userAdd ( nick ) ;
} ) ;
pushMessage ( { nick : '*' , text : "Users online: " + nicks . join ( ", " ) } )
2018-03-10 15:47:00 +08:00
} ,
2018-03-29 12:34:27 +08:00
onlineAdd : function ( args ) {
var nick = args . nick ;
userAdd ( nick ) ;
2018-03-10 15:47:00 +08:00
if ( $ ( '#joined-left' ) . checked ) {
2018-03-29 12:34:27 +08:00
pushMessage ( { nick : '*' , text : nick + " joined" } ) ;
2018-03-10 15:47:00 +08:00
}
} ,
2018-03-29 12:34:27 +08:00
onlineRemove : function ( args ) {
var nick = args . nick ;
userRemove ( nick ) ;
2018-03-10 15:47:00 +08:00
if ( $ ( '#joined-left' ) . checked ) {
2018-03-29 12:34:27 +08:00
pushMessage ( { nick : '*' , text : nick + " left" } ) ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
}
2018-03-10 15:47:00 +08:00
}
function pushMessage ( args ) {
// Message container
2018-03-29 12:34:27 +08:00
var messageEl = document . createElement ( 'div' ) ;
2018-10-08 02:45:20 +08:00
2019-05-20 05:11:16 +08:00
if (
typeof ( myNick ) === 'string' && (
2019-06-27 10:04:59 +08:00
args . text . match ( new RegExp ( '@' + myNick . split ( '#' ) [ 0 ] + '\\b' , "gi" ) ) ||
2019-05-20 05:11:16 +08:00
( ( args . type === "whisper" || args . type === "invite" ) && args . from )
)
) {
notify ( args ) ;
2018-10-08 02:45:20 +08:00
}
2018-03-10 15:47:00 +08:00
2019-08-14 07:25:32 +08:00
messageEl . classList . add ( 'message' ) ;
2022-11-05 22:09:51 +08:00
if ( verifyNickname ( myNick . split ( '#' ) [ 0 ] ) && args . nick == myNick . split ( '#' ) [ 0 ] ) {
2018-03-29 12:34:27 +08:00
messageEl . classList . add ( 'me' ) ;
2018-05-17 23:42:16 +08:00
} else if ( args . nick == '!' ) {
2018-03-29 12:34:27 +08:00
messageEl . classList . add ( 'warn' ) ;
2018-05-17 23:42:16 +08:00
} else if ( args . nick == '*' ) {
2018-03-29 12:34:27 +08:00
messageEl . classList . add ( 'info' ) ;
2018-05-17 23:42:16 +08:00
} else if ( args . admin ) {
2018-03-29 12:34:27 +08:00
messageEl . classList . add ( 'admin' ) ;
2018-05-17 23:42:16 +08:00
} else if ( args . mod ) {
2018-03-29 12:34:27 +08:00
messageEl . classList . add ( 'mod' ) ;
2018-03-10 15:47:00 +08:00
}
// Nickname
2018-03-29 12:34:27 +08:00
var nickSpanEl = document . createElement ( 'span' ) ;
nickSpanEl . classList . add ( 'nick' ) ;
messageEl . appendChild ( nickSpanEl ) ;
2018-03-10 15:47:00 +08:00
if ( args . trip ) {
2018-03-29 12:34:27 +08:00
var tripEl = document . createElement ( 'span' ) ;
tripEl . textContent = args . trip + " " ;
tripEl . classList . add ( 'trip' ) ;
nickSpanEl . appendChild ( tripEl ) ;
2018-03-10 15:47:00 +08:00
}
if ( args . nick ) {
2018-03-29 12:34:27 +08:00
var nickLinkEl = document . createElement ( 'a' ) ;
nickLinkEl . textContent = args . nick ;
nickLinkEl . onclick = function ( ) {
insertAtCursor ( "@" + args . nick + " " ) ;
$ ( '#chatinput' ) . focus ( ) ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
var date = new Date ( args . time || Date . now ( ) ) ;
nickLinkEl . title = date . toLocaleString ( ) ;
nickSpanEl . appendChild ( nickLinkEl ) ;
2018-03-10 15:47:00 +08:00
}
// Text
2019-08-18 07:24:29 +08:00
var textEl = document . createElement ( 'p' ) ;
2018-03-29 12:34:27 +08:00
textEl . classList . add ( 'text' ) ;
2019-08-18 07:24:29 +08:00
textEl . innerHTML = md . render ( args . text ) ;
2018-03-10 15:47:00 +08:00
2018-03-29 12:34:27 +08:00
messageEl . appendChild ( textEl ) ;
2018-03-10 15:47:00 +08:00
// Scroll to bottom
2018-03-29 12:34:27 +08:00
var atBottom = isAtBottom ( ) ;
$ ( '#messages' ) . appendChild ( messageEl ) ;
2018-03-10 15:47:00 +08:00
if ( atBottom ) {
2018-03-29 12:34:27 +08:00
window . scrollTo ( 0 , document . body . scrollHeight ) ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
unread += 1 ;
updateTitle ( ) ;
2018-03-10 15:47:00 +08:00
}
function insertAtCursor ( text ) {
2018-03-29 12:34:27 +08:00
var input = $ ( '#chatinput' ) ;
var start = input . selectionStart || 0 ;
var before = input . value . substr ( 0 , start ) ;
var after = input . value . substr ( start ) ;
2018-03-10 15:47:00 +08:00
2018-03-29 12:34:27 +08:00
before += text ;
input . value = before + after ;
input . selectionStart = input . selectionEnd = before . length ;
updateInputSize ( ) ;
}
2018-03-10 15:47:00 +08:00
function send ( data ) {
if ( ws && ws . readyState == ws . OPEN ) {
2018-03-29 12:34:27 +08:00
ws . send ( JSON . stringify ( data ) ) ;
2018-03-10 15:47:00 +08:00
}
}
2018-03-29 12:34:27 +08:00
var windowActive = true ;
var unread = 0 ;
2018-03-10 15:47:00 +08:00
2018-03-29 12:34:27 +08:00
window . onfocus = function ( ) {
windowActive = true ;
2018-03-10 15:47:00 +08:00
2018-03-29 12:34:27 +08:00
updateTitle ( ) ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
window . onblur = function ( ) {
windowActive = false ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
window . onscroll = function ( ) {
2018-03-10 15:47:00 +08:00
if ( isAtBottom ( ) ) {
2018-03-29 12:34:27 +08:00
updateTitle ( ) ;
2018-03-10 15:47:00 +08:00
}
}
function isAtBottom ( ) {
2018-03-29 12:34:27 +08:00
return ( window . innerHeight + window . scrollY ) >= ( document . body . scrollHeight - 1 ) ;
2018-03-10 15:47:00 +08:00
}
function updateTitle ( ) {
if ( windowActive && isAtBottom ( ) ) {
2018-03-29 12:34:27 +08:00
unread = 0 ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
var title ;
2018-03-10 15:47:00 +08:00
if ( myChannel ) {
2018-03-29 12:34:27 +08:00
title = "?" + myChannel ;
2018-05-17 23:42:16 +08:00
} else {
2018-03-29 12:34:27 +08:00
title = "hack.chat" ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
2018-03-10 15:47:00 +08:00
if ( unread > 0 ) {
2018-03-29 12:34:27 +08:00
title = '(' + unread + ') ' + title ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
document . title = title ;
}
2018-03-10 15:47:00 +08:00
2018-03-29 12:34:27 +08:00
$ ( '#footer' ) . onclick = function ( ) {
$ ( '#chatinput' ) . focus ( ) ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
$ ( '#chatinput' ) . onkeydown = function ( e ) {
2018-03-10 15:47:00 +08:00
if ( e . keyCode == 13 /* ENTER */ && ! e . shiftKey ) {
2018-03-29 12:34:27 +08:00
e . preventDefault ( ) ;
2018-03-10 15:47:00 +08:00
// Submit message
if ( e . target . value != '' ) {
2018-03-29 12:34:27 +08:00
var text = e . target . value ;
e . target . value = '' ;
send ( { cmd : 'chat' , text : text } ) ;
lastSent [ 0 ] = text ;
lastSent . unshift ( "" ) ;
lastSentPos = 0 ;
updateInputSize ( ) ;
2018-03-10 15:47:00 +08:00
}
2018-05-17 23:42:16 +08:00
} else if ( e . keyCode == 38 /* UP */ ) {
2018-03-10 15:47:00 +08:00
// Restore previous sent messages
if ( e . target . selectionStart === 0 && lastSentPos < lastSent . length - 1 ) {
2018-03-29 12:34:27 +08:00
e . preventDefault ( ) ;
2018-03-10 15:47:00 +08:00
if ( lastSentPos == 0 ) {
2018-03-29 12:34:27 +08:00
lastSent [ 0 ] = e . target . value ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
lastSentPos += 1 ;
e . target . value = lastSent [ lastSentPos ] ;
e . target . selectionStart = e . target . selectionEnd = e . target . value . length ;
updateInputSize ( ) ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
} else if ( e . keyCode == 40 /* DOWN */ ) {
2018-03-10 15:47:00 +08:00
if ( e . target . selectionStart === e . target . value . length && lastSentPos > 0 ) {
2018-03-29 12:34:27 +08:00
e . preventDefault ( ) ;
lastSentPos -= 1 ;
e . target . value = lastSent [ lastSentPos ] ;
e . target . selectionStart = e . target . selectionEnd = 0 ;
updateInputSize ( ) ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
} else if ( e . keyCode == 27 /* ESC */ ) {
e . preventDefault ( ) ;
2018-03-10 15:47:00 +08:00
// Clear input field
2018-03-29 12:34:27 +08:00
e . target . value = "" ;
lastSentPos = 0 ;
lastSent [ lastSentPos ] = "" ;
updateInputSize ( ) ;
2018-05-17 23:42:16 +08:00
} else if ( e . keyCode == 9 /* TAB */ ) {
2018-03-10 15:47:00 +08:00
// Tab complete nicknames starting with @
2019-05-09 14:37:13 +08:00
2019-04-26 05:07:15 +08:00
if ( e . ctrlKey ) {
// Skip autocompletion and tab insertion if user is pressing ctrl
// ctrl-tab is used by browsers to cycle through tabs
2019-05-09 14:37:13 +08:00
return ;
2019-04-26 05:07:15 +08:00
}
2018-03-29 12:34:27 +08:00
e . preventDefault ( ) ;
var pos = e . target . selectionStart || 0 ;
var text = e . target . value ;
var index = text . lastIndexOf ( '@' , pos ) ;
2019-05-09 14:37:13 +08:00
2019-04-11 06:30:32 +08:00
var autocompletedNick = false ;
2018-03-29 12:34:27 +08:00
2018-03-10 15:47:00 +08:00
if ( index >= 0 ) {
2018-03-29 12:34:27 +08:00
var stub = text . substring ( index + 1 , pos ) . toLowerCase ( ) ;
2018-03-10 15:47:00 +08:00
// Search for nick beginning with stub
2018-03-29 12:34:27 +08:00
var nicks = onlineUsers . filter ( function ( nick ) {
2018-03-10 15:47:00 +08:00
return nick . toLowerCase ( ) . indexOf ( stub ) == 0
2018-03-29 12:34:27 +08:00
} ) ;
2019-05-09 14:37:13 +08:00
2019-04-13 07:55:35 +08:00
if ( nicks . length > 0 ) {
2019-04-11 06:30:32 +08:00
autocompletedNick = true ;
2019-04-13 07:55:35 +08:00
if ( nicks . length == 1 ) {
insertAtCursor ( nicks [ 0 ] . substr ( stub . length ) + " " ) ;
}
2018-03-10 15:47:00 +08:00
}
}
2019-05-09 14:37:13 +08:00
2019-04-11 06:30:32 +08:00
// Since we did not insert a nick, we insert a tab character
if ( ! autocompletedNick ) {
insertAtCursor ( '\t' ) ;
}
2018-03-10 15:47:00 +08:00
}
}
function updateInputSize ( ) {
2018-03-29 12:34:27 +08:00
var atBottom = isAtBottom ( ) ;
2018-03-10 15:47:00 +08:00
2018-03-29 12:34:27 +08:00
var input = $ ( '#chatinput' ) ;
input . style . height = 0 ;
input . style . height = input . scrollHeight + 'px' ;
document . body . style . marginBottom = $ ( '#footer' ) . offsetHeight + 'px' ;
2018-03-10 15:47:00 +08:00
if ( atBottom ) {
2018-03-29 12:34:27 +08:00
window . scrollTo ( 0 , document . body . scrollHeight ) ;
2018-03-10 15:47:00 +08:00
}
}
2018-03-29 12:34:27 +08:00
$ ( '#chatinput' ) . oninput = function ( ) {
updateInputSize ( ) ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
updateInputSize ( ) ;
2018-03-10 15:47:00 +08:00
/* sidebar */
2018-03-29 12:34:27 +08:00
$ ( '#sidebar' ) . onmouseenter = $ ( '#sidebar' ) . ontouchstart = function ( e ) {
$ ( '#sidebar-content' ) . classList . remove ( 'hidden' ) ;
2019-05-09 14:37:13 +08:00
$ ( '#sidebar' ) . classList . add ( 'expand' ) ;
2018-03-29 12:34:27 +08:00
e . stopPropagation ( ) ;
2018-03-10 15:47:00 +08:00
}
2019-08-18 07:24:29 +08:00
$ ( '#sidebar' ) . onmouseleave = document . ontouchstart = function ( event ) {
var e = event . toElement || event . relatedTarget ;
try {
if ( e . parentNode == this || e == this ) {
return ;
}
} catch ( e ) { return ; }
2018-03-10 15:47:00 +08:00
if ( ! $ ( '#pin-sidebar' ) . checked ) {
2018-03-29 12:34:27 +08:00
$ ( '#sidebar-content' ) . classList . add ( 'hidden' ) ;
2019-05-09 14:37:13 +08:00
$ ( '#sidebar' ) . classList . remove ( 'expand' ) ;
2018-03-10 15:47:00 +08:00
}
}
2018-03-29 12:34:27 +08:00
$ ( '#clear-messages' ) . onclick = function ( ) {
2018-03-10 15:47:00 +08:00
// Delete children elements
2018-03-29 12:34:27 +08:00
var messages = $ ( '#messages' ) ;
2019-08-18 07:24:29 +08:00
messages . innerHTML = '' ;
2018-03-10 15:47:00 +08:00
}
// Restore settings from localStorage
if ( localStorageGet ( 'pin-sidebar' ) == 'true' ) {
2018-03-29 12:34:27 +08:00
$ ( '#pin-sidebar' ) . checked = true ;
$ ( '#sidebar-content' ) . classList . remove ( 'hidden' ) ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
2018-03-10 15:47:00 +08:00
if ( localStorageGet ( 'joined-left' ) == 'false' ) {
2018-03-29 12:34:27 +08:00
$ ( '#joined-left' ) . checked = false ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
2018-03-10 15:47:00 +08:00
if ( localStorageGet ( 'parse-latex' ) == 'false' ) {
2018-03-29 12:34:27 +08:00
$ ( '#parse-latex' ) . checked = false ;
2019-08-18 07:24:29 +08:00
md . inline . ruler . disable ( [ 'katex' ] ) ;
md . block . ruler . disable ( [ 'katex' ] ) ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
$ ( '#pin-sidebar' ) . onchange = function ( e ) {
localStorageSet ( 'pin-sidebar' , ! ! e . target . checked ) ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
$ ( '#joined-left' ) . onchange = function ( e ) {
localStorageSet ( 'joined-left' , ! ! e . target . checked ) ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
$ ( '#parse-latex' ) . onchange = function ( e ) {
2019-08-18 07:24:29 +08:00
var enabled = ! ! e . target . checked ;
localStorageSet ( 'parse-latex' , enabled ) ;
if ( enabled ) {
md . inline . ruler . enable ( [ 'katex' ] ) ;
md . block . ruler . enable ( [ 'katex' ] ) ;
} else {
md . inline . ruler . disable ( [ 'katex' ] ) ;
md . block . ruler . disable ( [ 'katex' ] ) ;
}
}
if ( localStorageGet ( 'syntax-highlight' ) == 'false' ) {
$ ( '#syntax-highlight' ) . checked = false ;
markdownOptions . doHighlight = false ;
}
$ ( '#syntax-highlight' ) . onchange = function ( e ) {
var enabled = ! ! e . target . checked ;
localStorageSet ( 'syntax-highlight' , enabled ) ;
markdownOptions . doHighlight = enabled ;
}
if ( localStorageGet ( 'allow-imgur' ) == 'false' ) {
$ ( '#allow-imgur' ) . checked = false ;
allowImages = false ;
}
$ ( '#allow-imgur' ) . onchange = function ( e ) {
var enabled = ! ! e . target . checked ;
localStorageSet ( 'allow-imgur' , enabled ) ;
allowImages = enabled ;
2018-03-10 15:47:00 +08:00
}
// User list
2018-03-29 12:34:27 +08:00
var onlineUsers = [ ] ;
var ignoredUsers = [ ] ;
2018-03-10 15:47:00 +08:00
function userAdd ( nick ) {
2018-03-29 12:34:27 +08:00
var user = document . createElement ( 'a' ) ;
user . textContent = nick ;
user . onclick = function ( e ) {
2018-03-10 15:47:00 +08:00
userInvite ( nick )
}
2018-03-29 12:34:27 +08:00
var userLi = document . createElement ( 'li' ) ;
userLi . appendChild ( user ) ;
$ ( '#users' ) . appendChild ( userLi ) ;
onlineUsers . push ( nick ) ;
2018-03-10 15:47:00 +08:00
}
function userRemove ( nick ) {
2018-03-29 12:34:27 +08:00
var users = $ ( '#users' ) ;
var children = users . children ;
2018-03-10 15:47:00 +08:00
for ( var i = 0 ; i < children . length ; i ++ ) {
2018-03-29 12:34:27 +08:00
var user = children [ i ] ;
2018-03-10 15:47:00 +08:00
if ( user . textContent == nick ) {
2018-03-29 12:34:27 +08:00
users . removeChild ( user ) ;
2018-03-10 15:47:00 +08:00
}
}
2018-03-29 12:34:27 +08:00
var index = onlineUsers . indexOf ( nick ) ;
2018-03-10 15:47:00 +08:00
if ( index >= 0 ) {
2018-03-29 12:34:27 +08:00
onlineUsers . splice ( index , 1 ) ;
2018-03-10 15:47:00 +08:00
}
}
function usersClear ( ) {
2018-03-29 12:34:27 +08:00
var users = $ ( '#users' ) ;
2018-03-10 15:47:00 +08:00
while ( users . firstChild ) {
2018-03-29 12:34:27 +08:00
users . removeChild ( users . firstChild ) ;
2018-03-10 15:47:00 +08:00
}
2018-03-29 12:34:27 +08:00
onlineUsers . length = 0 ;
2018-03-10 15:47:00 +08:00
}
function userInvite ( nick ) {
2018-03-29 12:34:27 +08:00
send ( { cmd : 'invite' , nick : nick } ) ;
2018-03-10 15:47:00 +08:00
}
function userIgnore ( nick ) {
2018-03-29 12:34:27 +08:00
ignoredUsers . push ( nick ) ;
2018-03-10 15:47:00 +08:00
}
/* color scheme switcher */
var schemes = [
'android' ,
2020-03-13 08:23:37 +08:00
'android-white' ,
2018-03-10 15:47:00 +08:00
'atelier-dune' ,
'atelier-forest' ,
'atelier-heath' ,
'atelier-lakeside' ,
'atelier-seaside' ,
2020-08-03 12:41:03 +08:00
'banana' ,
2018-03-10 15:47:00 +08:00
'bright' ,
2020-08-03 12:41:03 +08:00
'bubblegum' ,
2018-03-10 15:47:00 +08:00
'chalk' ,
'default' ,
'eighties' ,
2020-04-03 23:42:46 +08:00
'fresh-green' ,
2019-01-12 00:02:22 +08:00
'greenscreen' ,
2020-08-03 12:41:03 +08:00
'hacker' ,
'maniac' ,
2019-01-11 22:24:10 +08:00
'mariana' ,
2020-08-03 12:41:03 +08:00
'military' ,
2018-03-10 15:47:00 +08:00
'mocha' ,
'monokai' ,
'nese' ,
'ocean' ,
2020-08-03 12:41:03 +08:00
'omega' ,
2018-03-10 15:47:00 +08:00
'pop' ,
'railscasts' ,
'solarized' ,
2021-09-03 03:19:44 +08:00
'tk-night' ,
2021-10-04 09:55:22 +08:00
'tomorrow' ,
2021-10-21 00:50:46 +08:00
'carrot' ,
2021-11-17 18:47:50 +08:00
'lax' ,
2021-12-15 09:34:09 +08:00
'Ubuntu' ,
2022-02-13 12:11:42 +08:00
'gruvbox-light' ,
2022-04-30 14:22:26 +08:00
'fried-egg' ,
2023-04-04 19:54:22 +08:00
'rainbow' ,
'amoled'
2018-03-29 12:34:27 +08:00
] ;
2018-03-10 15:47:00 +08:00
2018-05-17 23:42:16 +08:00
var highlights = [
'agate' ,
'androidstudio' ,
2019-01-12 00:11:34 +08:00
'atom-one-dark' ,
2018-05-17 23:42:16 +08:00
'darcula' ,
'github' ,
'rainbow' ,
2021-09-03 03:19:44 +08:00
'tk-night' ,
2018-05-17 23:42:16 +08:00
'tomorrow' ,
'xcode' ,
'zenburn'
]
2018-03-29 12:34:27 +08:00
var currentScheme = 'atelier-dune' ;
2018-05-17 23:42:16 +08:00
var currentHighlight = 'darcula' ;
2018-03-10 15:47:00 +08:00
function setScheme ( scheme ) {
2018-03-29 12:34:27 +08:00
currentScheme = scheme ;
$ ( '#scheme-link' ) . href = "schemes/" + scheme + ".css" ;
localStorageSet ( 'scheme' , scheme ) ;
2018-03-10 15:47:00 +08:00
}
2018-05-17 23:42:16 +08:00
function setHighlight ( scheme ) {
currentHighlight = scheme ;
2019-08-18 07:24:29 +08:00
$ ( '#highlight-link' ) . href = "vendor/hljs/styles/" + scheme + ".min.css" ;
2018-05-17 23:42:16 +08:00
localStorageSet ( 'highlight' , scheme ) ;
}
2018-03-10 15:47:00 +08:00
// Add scheme options to dropdown selector
2018-03-29 12:34:27 +08:00
schemes . forEach ( function ( scheme ) {
var option = document . createElement ( 'option' ) ;
option . textContent = scheme ;
option . value = scheme ;
$ ( '#scheme-selector' ) . appendChild ( option ) ;
} ) ;
2018-03-10 15:47:00 +08:00
2018-05-17 23:42:16 +08:00
highlights . forEach ( function ( scheme ) {
var option = document . createElement ( 'option' ) ;
option . textContent = scheme ;
option . value = scheme ;
$ ( '#highlight-selector' ) . appendChild ( option ) ;
} ) ;
2018-03-29 12:34:27 +08:00
$ ( '#scheme-selector' ) . onchange = function ( e ) {
setScheme ( e . target . value ) ;
2018-03-10 15:47:00 +08:00
}
2018-05-17 23:42:16 +08:00
$ ( '#highlight-selector' ) . onchange = function ( e ) {
setHighlight ( e . target . value ) ;
}
2018-03-10 15:47:00 +08:00
// Load sidebar configaration values from local storage if available
if ( localStorageGet ( 'scheme' ) ) {
2018-03-29 12:34:27 +08:00
setScheme ( localStorageGet ( 'scheme' ) ) ;
2018-03-10 15:47:00 +08:00
}
2018-05-17 23:42:16 +08:00
if ( localStorageGet ( 'highlight' ) ) {
setHighlight ( localStorageGet ( 'highlight' ) ) ;
}
2018-03-29 12:34:27 +08:00
$ ( '#scheme-selector' ) . value = currentScheme ;
2018-05-17 23:42:16 +08:00
$ ( '#highlight-selector' ) . value = currentHighlight ;
2018-03-10 15:47:00 +08:00
/* main */
if ( myChannel == '' ) {
2018-03-29 12:34:27 +08:00
pushMessage ( { text : frontpage } ) ;
$ ( '#footer' ) . classList . add ( 'hidden' ) ;
$ ( '#sidebar' ) . classList . add ( 'hidden' ) ;
} else {
join ( myChannel ) ;
2018-06-07 19:24:08 +08:00
}