const verifyNickname = (nick) => /^[a-zA-Z0-9_]{1,24}$/.test(nick); 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}$$", "For syntax highlight, the first line of the code block must begin with # where can be html, js or any known format", "", "Current Github: https://github.com/hack-chat includes server and client source along with other resources", "", "Legacy GitHub: https://github.com/AndrewBelt/hack.chat", "Android apps: https://goo.gl/UkbKYy https://goo.gl/qasdSu https://goo.gl/fGQFQN", "Other Softwares: https://github.com/hack-chat/3rd-party-software-list", "", "Server and web client released under the WTFPL and MIT open source license.", "No message history is retained on the hack.chat server." ].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; function join(channel) { 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'); } 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 (verifyNickname(myNick) && 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 ($('#syntax-highlight').checked && textEl.textContent.indexOf('#') == 0) { var lang = textEl.textContent.split(/\s+/g)[0].replace('#', ''); var codeEl = document.createElement('code'); codeEl.classList.add(lang); var content = textEl.textContent.replace('#' + lang, ''); codeEl.textContent = content.trim(); hljs.highlightBlock(codeEl); textEl.innerHTML = ''; textEl.appendChild(codeEl); } else 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').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'); $('#sidebar').classList.add('expand'); e.stopPropagation(); } $('#sidebar').onmouseleave = document.ontouchstart = function () { if (!$('#pin-sidebar').checked) { $('#sidebar-content').classList.add('hidden'); $('#sidebar').classList.remove('expand'); } } $('#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 highlights = [ 'agate', 'androidstudio', 'darcula', 'github', 'rainbow', 'tomorrow', 'xcode', 'zenburn' ] var currentScheme = 'atelier-dune'; var currentHighlight = 'darcula'; function setScheme(scheme) { currentScheme = scheme; $('#scheme-link').href = "schemes/" + scheme + ".css"; localStorageSet('scheme', scheme); } function setHighlight(scheme) { currentHighlight = scheme; $('#highlight-link').href = "//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/" + scheme + ".min.css"; localStorageSet('highlight', 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); }); highlights.forEach(function (scheme) { var option = document.createElement('option'); option.textContent = scheme; option.value = scheme; $('#highlight-selector').appendChild(option); }); $('#scheme-selector').onchange = function (e) { setScheme(e.target.value); } $('#highlight-selector').onchange = function (e) { setHighlight(e.target.value); } // Load sidebar configaration values from local storage if available if (localStorageGet('scheme')) { setScheme(localStorageGet('scheme')); } if (localStorageGet('highlight')) { setHighlight(localStorageGet('highlight')); } $('#scheme-selector').value = currentScheme; $('#highlight-selector').value = currentHighlight; /* main */ if (myChannel == '') { pushMessage({ text: frontpage }); $('#footer').classList.add('hidden'); $('#sidebar').classList.add('hidden'); } else { join(myChannel); }