From fde6895720a4f417283b9e375583967b504de2f3 Mon Sep 17 00:00:00 2001 From: marzavec Date: Fri, 9 Mar 2018 23:47:00 -0800 Subject: initial commit --- client/client.js | 534 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 534 insertions(+) create mode 100644 client/client.js (limited to 'client/client.js') diff --git a/client/client.js b/client/client.js new file mode 100644 index 0000000..e8c3b79 --- /dev/null +++ b/client/client.js @@ -0,0 +1,534 @@ +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) +} -- cgit v1.2.1