1
0
mirror of https://github.com/hack-chat/main.git synced 2024-03-22 13:20:33 +08:00
hack-chat-main/client/client.js

535 lines
12 KiB
JavaScript
Raw Normal View History

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}$$",
"",
"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)
}