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

611 lines
14 KiB
JavaScript

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 #<format> where <format> 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);
}