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}$$", "", "GitHub: https://github.com/AndrewBelt/hack.chat", "Android apps: https://goo.gl/UkbKYy https://goo.gl/qasdSu https://goo.gl/fGQFQN", "", "Server and web client released under the MIT open source license.", "No message history is retained on the hack.chat server.", "", "[03/03/2018] Please note that the server is currently undergoing changes, expect random downtime or disconnections!", "[03/03/2018] Hack.chat is now under new management by the core community; @raf924 @bacon @wwandrew @Rut @_0x17 @M4GNV5 @MinusGix @nanotech", ].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) { ws = new WebSocket('ws://127.0.0.1:6060') 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) }