2018-03-10 15:47:00 +08:00
var frontpage = [
" _ _ _ _ " ,
" | |_ ___ ___| |_ ___| |_ ___| |_ " ,
" | |_ || _| '_| | _| |_ || _|" ,
" |_|_|__/|___|_,_|.|___|_|_|__/|_| " ,
"" ,
"" ,
"Welcome to hack.chat, a minimal, distraction-free chat application." ,
"Channels are created and joined by going to https://hack.chat/?your-channel. There are no channel lists, so a secret channel name can be used for private discussions." ,
"" ,
"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}$$" ,
"" ,
2018-03-26 05:16:02 +08:00
"Current Github: https://github.com/hack-chat includes server and client source along with other resources" ,
2018-03-10 15:47:00 +08:00
"" ,
2018-03-26 05:16:02 +08:00
"Legacy GitHub: https://github.com/AndrewBelt/hack.chat" ,
"Android apps: https://goo.gl/UkbKYy https://goo.gl/qasdSu https://goo.gl/fGQFQN" ,
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-10 15:47:00 +08:00
] . join ( "\n" )
function $ ( query ) { return document . querySelector ( query ) }
function localStorageGet ( key ) {
try {
return window . localStorage [ key ]
}
catch ( e ) { }
}
function localStorageSet ( key , val ) {
try {
window . localStorage [ key ] = val
}
catch ( e ) { }
}
var ws
var myNick = localStorageGet ( 'my-nick' )
var myChannel = window . location . search . replace ( /^\?/ , '' )
var lastSent = [ "" ]
var lastSentPos = 0
// Ping server every 50 seconds to retain WebSocket connection
window . setInterval ( function ( ) {
send ( { cmd : 'ping' } )
} , 50000 )
function join ( channel ) {
2018-03-26 05:16:02 +08:00
if ( document . domain == 'hack.chat' ) {
// For https://hack.chat/
ws = new WebSocket ( 'wss://hack.chat/chat-ws' )
} else {
// for local installs
ws = new WebSocket ( 'ws://' + document . domain + ':6060' )
}
2018-03-10 15:47:00 +08:00
var wasConnected = false
ws . onopen = function ( ) {
if ( ! wasConnected ) {
if ( location . hash ) {
myNick = location . hash . substr ( 1 )
}
else {
myNick = prompt ( 'Nickname:' , myNick )
}
}
if ( myNick ) {
localStorageSet ( 'my-nick' , myNick )
send ( { cmd : 'join' , channel : channel , nick : myNick } )
}
wasConnected = true
}
ws . onclose = function ( ) {
if ( wasConnected ) {
pushMessage ( { nick : '!' , text : "Server disconnected. Attempting to reconnect..." } )
}
window . setTimeout ( function ( ) {
join ( channel )
} , 2000 )
}
ws . onmessage = function ( message ) {
var args = JSON . parse ( message . data )
var cmd = args . cmd
var command = COMMANDS [ cmd ]
command . call ( null , args )
}
}
var COMMANDS = {
chat : function ( args ) {
if ( ignoredUsers . indexOf ( args . nick ) >= 0 ) {
return
}
pushMessage ( args )
} ,
info : function ( args ) {
args . nick = '*'
pushMessage ( args )
} ,
warn : function ( args ) {
args . nick = '!'
pushMessage ( args )
} ,
onlineSet : function ( args ) {
var nicks = args . nicks
usersClear ( )
nicks . forEach ( function ( nick ) {
userAdd ( nick )
} )
pushMessage ( { nick : '*' , text : "Users online: " + nicks . join ( ", " ) } )
} ,
onlineAdd : function ( args ) {
var nick = args . nick
userAdd ( nick )
if ( $ ( '#joined-left' ) . checked ) {
pushMessage ( { nick : '*' , text : nick + " joined" } )
}
} ,
onlineRemove : function ( args ) {
var nick = args . nick
userRemove ( nick )
if ( $ ( '#joined-left' ) . checked ) {
pushMessage ( { nick : '*' , text : nick + " left" } )
}
} ,
}
function pushMessage ( args ) {
// Message container
var messageEl = document . createElement ( 'div' )
messageEl . classList . add ( 'message' )
if ( args . nick == myNick ) {
messageEl . classList . add ( 'me' )
}
else if ( args . nick == '!' ) {
messageEl . classList . add ( 'warn' )
}
else if ( args . nick == '*' ) {
messageEl . classList . add ( 'info' )
}
else if ( args . admin ) {
messageEl . classList . add ( 'admin' )
}
else if ( args . mod ) {
messageEl . classList . add ( 'mod' )
}
// Nickname
var nickSpanEl = document . createElement ( 'span' )
nickSpanEl . classList . add ( 'nick' )
messageEl . appendChild ( nickSpanEl )
if ( args . trip ) {
var tripEl = document . createElement ( 'span' )
tripEl . textContent = args . trip + " "
tripEl . classList . add ( 'trip' )
nickSpanEl . appendChild ( tripEl )
}
if ( args . nick ) {
var nickLinkEl = document . createElement ( 'a' )
nickLinkEl . textContent = args . nick
nickLinkEl . onclick = function ( ) {
insertAtCursor ( "@" + args . nick + " " )
$ ( '#chatinput' ) . focus ( )
}
var date = new Date ( args . time || Date . now ( ) )
nickLinkEl . title = date . toLocaleString ( )
nickSpanEl . appendChild ( nickLinkEl )
}
// Text
var textEl = document . createElement ( 'pre' )
textEl . classList . add ( 'text' )
textEl . textContent = args . text || ''
textEl . innerHTML = textEl . innerHTML . replace ( /(\?|https?:\/\/)\S+?(?=[,.!?:)]?\s|$)/g , parseLinks )
if ( $ ( '#parse-latex' ) . checked ) {
// Temporary hotfix for \rule spamming, see https://github.com/Khan/KaTeX/issues/109
textEl . innerHTML = textEl . innerHTML . replace ( /\\rule|\\\\\s*\[.*?\]/g , '' )
try {
renderMathInElement ( textEl , { delimiters : [
{ left : "$$" , right : "$$" , display : true } ,
{ left : "$" , right : "$" , display : false } ,
] } )
}
catch ( e ) {
console . warn ( e )
}
}
messageEl . appendChild ( textEl )
// Scroll to bottom
var atBottom = isAtBottom ( )
$ ( '#messages' ) . appendChild ( messageEl )
if ( atBottom ) {
window . scrollTo ( 0 , document . body . scrollHeight )
}
unread += 1
updateTitle ( )
}
function insertAtCursor ( text ) {
var input = $ ( '#chatinput' )
var start = input . selectionStart || 0
var before = input . value . substr ( 0 , start )
var after = input . value . substr ( start )
before += text
input . value = before + after
input . selectionStart = input . selectionEnd = before . length
updateInputSize ( )
}
function send ( data ) {
if ( ws && ws . readyState == ws . OPEN ) {
ws . send ( JSON . stringify ( data ) )
}
}
function parseLinks ( g0 ) {
var a = document . createElement ( 'a' )
a . innerHTML = g0
var url = a . textContent
a . href = url
a . target = '_blank'
return a . outerHTML
}
var windowActive = true
var unread = 0
window . onfocus = function ( ) {
windowActive = true
updateTitle ( )
}
window . onblur = function ( ) {
windowActive = false
}
window . onscroll = function ( ) {
if ( isAtBottom ( ) ) {
updateTitle ( )
}
}
function isAtBottom ( ) {
return ( window . innerHeight + window . scrollY ) >= ( document . body . scrollHeight - 1 )
}
function updateTitle ( ) {
if ( windowActive && isAtBottom ( ) ) {
unread = 0
}
var title
if ( myChannel ) {
title = "?" + myChannel
}
else {
title = "hack.chat"
}
if ( unread > 0 ) {
title = '(' + unread + ') ' + title
}
document . title = title
}
/* footer */
$ ( '#footer' ) . onclick = function ( ) {
$ ( '#chatinput' ) . focus ( )
}
$ ( '#chatinput' ) . onkeydown = function ( e ) {
if ( e . keyCode == 13 /* ENTER */ && ! e . shiftKey ) {
e . preventDefault ( )
// Submit message
if ( e . target . value != '' ) {
var text = e . target . value
e . target . value = ''
send ( { cmd : 'chat' , text : text } )
lastSent [ 0 ] = text
lastSent . unshift ( "" )
lastSentPos = 0
updateInputSize ( )
}
}
else if ( e . keyCode == 38 /* UP */ ) {
// Restore previous sent messages
if ( e . target . selectionStart === 0 && lastSentPos < lastSent . length - 1 ) {
e . preventDefault ( )
if ( lastSentPos == 0 ) {
lastSent [ 0 ] = e . target . value
}
lastSentPos += 1
e . target . value = lastSent [ lastSentPos ]
e . target . selectionStart = e . target . selectionEnd = e . target . value . length
updateInputSize ( )
}
}
else if ( e . keyCode == 40 /* DOWN */ ) {
if ( e . target . selectionStart === e . target . value . length && lastSentPos > 0 ) {
e . preventDefault ( )
lastSentPos -= 1
e . target . value = lastSent [ lastSentPos ]
e . target . selectionStart = e . target . selectionEnd = 0
updateInputSize ( )
}
}
else if ( e . keyCode == 27 /* ESC */ ) {
e . preventDefault ( )
// Clear input field
e . target . value = ""
lastSentPos = 0
lastSent [ lastSentPos ] = ""
updateInputSize ( )
}
else if ( e . keyCode == 9 /* TAB */ ) {
// Tab complete nicknames starting with @
e . preventDefault ( )
var pos = e . target . selectionStart || 0
var text = e . target . value
var index = text . lastIndexOf ( '@' , pos )
if ( index >= 0 ) {
var stub = text . substring ( index + 1 , pos ) . toLowerCase ( )
// Search for nick beginning with stub
var nicks = onlineUsers . filter ( function ( nick ) {
return nick . toLowerCase ( ) . indexOf ( stub ) == 0
} )
if ( nicks . length == 1 ) {
insertAtCursor ( nicks [ 0 ] . substr ( stub . length ) + " " )
}
}
}
}
function updateInputSize ( ) {
var atBottom = isAtBottom ( )
var input = $ ( '#chatinput' )
input . style . height = 0
input . style . height = input . scrollHeight + 'px'
document . body . style . marginBottom = $ ( '#footer' ) . offsetHeight + 'px'
if ( atBottom ) {
window . scrollTo ( 0 , document . body . scrollHeight )
}
}
$ ( '#chatinput' ) . oninput = function ( ) {
updateInputSize ( )
}
updateInputSize ( )
/* sidebar */
$ ( '#sidebar' ) . onmouseenter = $ ( '#sidebar' ) . ontouchstart = function ( e ) {
$ ( '#sidebar-content' ) . classList . remove ( 'hidden' )
e . stopPropagation ( )
}
$ ( '#sidebar' ) . onmouseleave = document . ontouchstart = function ( ) {
if ( ! $ ( '#pin-sidebar' ) . checked ) {
$ ( '#sidebar-content' ) . classList . add ( 'hidden' )
}
}
$ ( '#clear-messages' ) . onclick = function ( ) {
// Delete children elements
var messages = $ ( '#messages' )
while ( messages . firstChild ) {
messages . removeChild ( messages . firstChild )
}
}
// Restore settings from localStorage
if ( localStorageGet ( 'pin-sidebar' ) == 'true' ) {
$ ( '#pin-sidebar' ) . checked = true
$ ( '#sidebar-content' ) . classList . remove ( 'hidden' )
}
if ( localStorageGet ( 'joined-left' ) == 'false' ) {
$ ( '#joined-left' ) . checked = false
}
if ( localStorageGet ( 'parse-latex' ) == 'false' ) {
$ ( '#parse-latex' ) . checked = false
}
$ ( '#pin-sidebar' ) . onchange = function ( e ) {
localStorageSet ( 'pin-sidebar' , ! ! e . target . checked )
}
$ ( '#joined-left' ) . onchange = function ( e ) {
localStorageSet ( 'joined-left' , ! ! e . target . checked )
}
$ ( '#parse-latex' ) . onchange = function ( e ) {
localStorageSet ( 'parse-latex' , ! ! e . target . checked )
}
// User list
var onlineUsers = [ ]
var ignoredUsers = [ ]
function userAdd ( nick ) {
var user = document . createElement ( 'a' )
user . textContent = nick
user . onclick = function ( e ) {
userInvite ( nick )
}
var userLi = document . createElement ( 'li' )
userLi . appendChild ( user )
$ ( '#users' ) . appendChild ( userLi )
onlineUsers . push ( nick )
}
function userRemove ( nick ) {
var users = $ ( '#users' )
var children = users . children
for ( var i = 0 ; i < children . length ; i ++ ) {
var user = children [ i ]
if ( user . textContent == nick ) {
users . removeChild ( user )
}
}
var index = onlineUsers . indexOf ( nick )
if ( index >= 0 ) {
onlineUsers . splice ( index , 1 )
}
}
function usersClear ( ) {
var users = $ ( '#users' )
while ( users . firstChild ) {
users . removeChild ( users . firstChild )
}
onlineUsers . length = 0
}
function userInvite ( nick ) {
send ( { cmd : 'invite' , nick : nick } )
}
function userIgnore ( nick ) {
ignoredUsers . push ( nick )
}
/* color scheme switcher */
var schemes = [
'android' ,
'atelier-dune' ,
'atelier-forest' ,
'atelier-heath' ,
'atelier-lakeside' ,
'atelier-seaside' ,
'bright' ,
'chalk' ,
'default' ,
'eighties' ,
'greenscreen' ,
'mocha' ,
'monokai' ,
'nese' ,
'ocean' ,
'pop' ,
'railscasts' ,
'solarized' ,
'tomorrow' ,
]
var currentScheme = 'atelier-dune'
function setScheme ( scheme ) {
currentScheme = scheme
$ ( '#scheme-link' ) . href = "/schemes/" + scheme + ".css"
localStorageSet ( 'scheme' , scheme )
}
// Add scheme options to dropdown selector
schemes . forEach ( function ( scheme ) {
var option = document . createElement ( 'option' )
option . textContent = scheme
option . value = scheme
$ ( '#scheme-selector' ) . appendChild ( option )
} )
$ ( '#scheme-selector' ) . onchange = function ( e ) {
setScheme ( e . target . value )
}
// Load sidebar configaration values from local storage if available
if ( localStorageGet ( 'scheme' ) ) {
setScheme ( localStorageGet ( 'scheme' ) )
}
$ ( '#scheme-selector' ) . value = currentScheme
/* main */
if ( myChannel == '' ) {
pushMessage ( { text : frontpage } )
$ ( '#footer' ) . classList . add ( 'hidden' )
$ ( '#sidebar' ) . classList . add ( 'hidden' )
}
else {
join ( myChannel )
}