/* dcwebui.js */ import "./dcwebui.less"; // for webpack import * as io from 'socket.io-client' "use strict"; /** * Display value when loading a saved password */ var SENTINEL_PASSWORD = "************"; /** * Number of lines of chat to keep in the scroll area. * Once there are over 2x $limit lines, the first $limit lines will be trimmed off the list */ var CHAT_SCROLLBACK_LIMIT = 200; /** * Our externally-accessible URL */ var EXTERN_ROOT = window.location.protocol + "//" + window.location.host + "/"; // Help out the braindead minifier, use these functions instead var document_getElementById = function(x) { return document.getElementById(x); } var document_getElementsByClassName = function(x) { return document.getElementsByClassName(x); } var document_createElement = function(x) { return document.createElement(x); } /** * Encode a string for NMDC * * @param str */ var nmdc_escape = function(str: string): string { return ( (''+str).length ? (''+str).replace(/&/g,'&').replace(/\|/g,'|').replace(/\$/g,'$') : ' ' ); }; /** * Encode a string for HTML * * @param s */ var hesc = function(s: string): string { var filter = { '&': '&', '<': '<', '>': '>', '"': '"', '\'': ''' }; return s.toString().replace(/[&<>'"]/g, function(s) { return filter[s]; }); }; /** * Format a number of bytes as a human-readable string * * @param b */ var fmtBytes = function(b: number): string { if (b == 0) { return '(nothing)'; } var k = 1024; var sizes = [' B', ' KiB', ' MiB', ' GiB', ' TiB']; var i = Math.floor(Math.log(b) / Math.log(k)); return parseFloat((b / Math.pow(k, i)).toFixed(3)) + sizes[i]; }; /** * Decode a string that was previously encoded in raw-url format. * * @param s */ var urldesc = function(s: string):string { return decodeURIComponent(s.replace(/\+/g, " ")); } /** * Enhance an HTML string by automatically making links clickable, etc. * * @param str An HTML-safe string */ var linkify = function(str : string):string { // n.b. str is already hesced return (str .replace( /(https?:\/\/[^\s<]+)/g, "$1" ) .replace( /magnet:\?.+dn=([^\< ]+)/g, function(match, m1) { return "[MAGNET] " + urldesc(m1) + ""; } ) ); }; /** * Convert a plain-text string into an enhanced, HTML-safe string. * * @param s */ var sanitise = function(s:string): string { return linkify(hesc(s)); }; /** * Retrieve the plain-text content from an HTML element in a browser-compatible way. * * @param $el */ var textContent = function($el : HTMLElement) : string { if ($el.textContent) { return $el.textContent; } if ($el.innerText) { return $el.innerText; } return ""; }; /** * Calculate the positive modulo of (l % r). * * @param l * @param r */ var negmod = function(l:number, r:number):number { var ret = l % r; if (l < 0) { return ret + r; } else { return ret; } }; /** * Encode a string to base64 in a UTF8-safe way. * * @ref https://developer.mozilla.org/en/docs/Web/API/WindowBase64/Base64_encoding_and_decoding * @param str */ var b64 = function(str : string):string { return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) { return String.fromCharCode(parseInt('0x' + p1)); })).replace(/=/g, ''); } /** * Append content to an HTML element in a browser-compatible way. * * @ref https://gist.github.com/eligrey/1276030 * @param $el * @param html */ var appendInnerHTML = function($el: HTMLElement, html:string) { var child = document_createElement("span"); child.innerHTML = html; var node; while ((node = child.firstChild)) { $el.appendChild(node); } }; /** * Retrieve the left offset of a DOM element relative to the document. * * @ref http://stackoverflow.com/a/5598797 * @param elem */ function getOffsetLeft( elem: HTMLElement ):number { var offsetLeft = 0; do { if (!isNaN(elem.offsetLeft)) { offsetLeft += elem.offsetLeft; } } while (elem = elem.offsetParent); return offsetLeft; } /** * Retrieve the top offset for a DOM element relative to the document. * * @ref http://stackoverflow.com/a/5598797 * @param elem */ function getOffsetTop( elem: HTMLElement ):number { var offsetTop = 0; do { if (!isNaN(elem.offsetTop)) { offsetTop += elem.offsetTop; } } while (elem = elem.offsetParent); return offsetTop; } /* */ /** * Format a string in a date format, analgous to strftime(). * * @param d * @param format Formatting string, supporting HisYmd character specifiers * @return Plain text string */ var date_format = function(d:Date, format:string):string { var pad = function(s) { return (s < 10) ? '0'+s : ''+s ; }; var ret = format; ret = ret.replace(/H/g, pad(d.getHours())); ret = ret.replace(/i/g, pad(d.getMinutes())); ret = ret.replace(/s/g, pad(d.getSeconds())); ret = ret.replace(/Y/g, "" + d.getFullYear()); ret = ret.replace(/m/g, pad(d.getMonth() + 1)); ret = ret.replace(/d/g, pad(d.getDate())); return ret; }; /** * Emit an HTML5 notification. * * @param title * @param body * @param tab */ var notify = function(title:string, body:string, tab:string) { if (!("Notification" in window)) { return; // not supported by browser } switch ( (window as any).Notification.permission) { case "granted": { var n = new Notification(title, { body: body, icon: EXTERN_ROOT + "/favicon.ico" }); n.onclick = function() { parent.focus(); // recent chrome window.focus(); // older browsers tab_set(tab); this.close(); }; } break; case "denied": return; default: { // Clarify permission and retry Notification.requestPermission(function(permission) { notify(title, body, "tab-main"); }); } break; } }; /* Tab writers */ var write = function(tab) { var $tab = document_getElementById("inner-"+tab); return { 'cls': function() { $tab.innerHTML = ''; return this; }, 'scroll': function() { $tab.scrollTop = $tab.scrollHeight; return this; }, 'raw': function(s) { appendInnerHTML($tab, s); return this.scroll(); }, 'c': function(c, s) { return this.raw(''+s+''); }, 'time': function() { return this.raw('['+ date_format(new Date(), timestamp_formats[timestamp_format_index])+"] "); }, 'system': function(s) { return this.time().c('tx-sys', sanitise(s)).raw('
'); }, 'pubnick': function(u) { return this.raw('<'+hesc(u)+'>'); }, 'pub': function(u, s) { return this.time(). pubnick(u).raw(' '). c('tx-chat', sanitise(s)).raw('
'); } }; }; var write_system_message_in_all_pm_tabs = function(system_message) { for (var k in pm_tabs) { writerFor(k).system(system_message); } }; /* Userlist */ var switchToPM = function(u) { writerFor(u); // create tab_set(pm_tabs[u]); // switch writerFor(u).scroll(); // scroll }; var userMenu = function(u, ev) { usermenu.hide(); usermenu = new MenuList(ev.target); usermenu.add("Send private message...", function() { switchToPM(u); }); // Usercommands for (var i = 0; i < user_usercommands.length; i++) (function(i) { var raw = user_usercommands[i].raw; usermenu.add(user_usercommands[i].title, function() { var message = usercommand_process( raw .replace(/%\[nick\]/g, u) ); sock.emit('raw', {message: message}); }); })(i); // Show usermenu.show(); // return noprop(ev); }; var userlist = { 'add': function(u) { if (this.has(u)) return; var userlists = document_getElementsByClassName("userlist"); for (var l = 0, e = userlists.length; l !== e; ++l) { var userlist = userlists[l]; var to_add = document_createElement('li'); to_add.className = "user-" + b64(u); to_add.innerHTML = hesc(u); to_add.onclick = function() { switchToPM(u); }; to_add.oncontextmenu = function(ev) { return userMenu(u, ev); }; var users = userlist.children; var cmp = hesc(u).toUpperCase(); var found = false; for (var i = 0; i < users.length; i++) { if ((''+users[i].innerHTML).toUpperCase() > cmp) { userlist.insertBefore(to_add, users[i]); found = true; break; } } if (! found) { userlist.appendChild(to_add); } } return this; }, 'del': function(u) { var userlists = document_getElementsByClassName("userlist"); for (var l = 0, e = userlists.length; l !== e; ++l) { if (! userlists[l].children) continue; var userlist = userlists[l]; var users = userlist.children; var cmp = hesc(u).toUpperCase(); for (var i = 0; i < users.length; i++) { if ((''+users[i].innerHTML).toUpperCase() === cmp) { userlist.removeChild(users[i]); break; } } } return this; }, 'clear': function() { var userlists = document_getElementsByClassName("userlist"); for (var i in userlists) { if (! userlists[i].children) continue; var userlist = userlists[i]; var users = userlist.children; for (var j = users.length; j --> 0;) { userlist.removeChild(users[j]); } } return this; }, 'names': function() { var userlist = document_getElementsByClassName("userlist")[0].children; var ret = []; for (var i = 0, e = userlist.length; i < e; ++i) { ret.push( textContent((userlist[i])) ); } return ret; }, 'has': function(u) { return document_getElementsByClassName("user-" + b64(u)).length !== 0; /* there are two - large and non-large */ }, 'count': function() { return document_getElementsByClassName("userlist")[0].children.length; }, 'setInfo': function(nick, props) { var baseClass = "user-" + b64(nick); var $el = document_getElementsByClassName("" + baseClass); var prop_str = []; if (props.Description.length > 0) { prop_str.push(props.Description); } if (props.Email.length > 0) { prop_str.push(props.Email); } if (props.ClientTag.length > 0) { prop_str.push(props.ClientTag + " " + props.ClientVersion); } if (props.IPAddress.length > 0) { prop_str.push(props.IPAddress); } prop_str.push("Sharing " + fmtBytes(props.ShareSize)); for (var i = 0; i < $el.length; ++i) { ( $el[i]).title = prop_str.join("\n"); if (props.IsOperator) { $el[i].className = baseClass + " user-is-operator"; } else { $el[i].className = baseClass; // remove op flag } } } }; var submit = function() { var str = (document_getElementById("chatbox")).value; if (! str.length) return; if (hub_state === STATE_READY_FOR_LOGIN) { transition(1); // disables #chatbox hub_last_nick = str.split(":", 2)[0]; var hub_pass = ""; if (str.length > hub_last_nick.length) { hub_pass = str.substr(hub_last_nick.length + 1); } if (hub_pass === SENTINEL_PASSWORD) { // Probably not a real password. Attempt to load a better one from the saved state var cache = persistence_get("login", ""); if (cache.indexOf(":") != -1) { hub_pass = cache.substr(cache.indexOf(":") + 1); } } persistence_set("login", hub_pass.length > 0 ? hub_last_nick+":"+hub_pass : hub_last_nick); sock.emit('hello', {'nick' : hub_last_nick, 'pass' : hub_pass}); write("tab-main").system("Connecting..."); } else if (hub_state === STATE_ACTIVE) { if (pm_target !== PM_TARGET_NONE) { sock.emit('priv', {'user': pm_target, 'message': str}); writerFor(pm_target).pub(hub_last_nick, str ); } else { sock.emit('pub', {'message' : str}); } chat_scrollback.push( str ); chat_scrollback_index = -1; if (chat_scrollback.length > (2*CHAT_SCROLLBACK_LIMIT)) { chat_scrollback = chat_scrollback.slice(CHAT_SCROLLBACK_LIMIT); } } else { write("tab-main").system("Invalid internal state."); } (document_getElementById("chatbox")).value = ''; }; /* page visibility */ var pagevis_currently_visible = true; var pagevis_setup = function(fnActive, fnInactive) { var h, vc; if (typeof document.hidden !== "undefined") { h = "hidden"; vc = "visibilitychange"; } else if (typeof (document as any).msHidden !== "undefined") { h = "msHidden"; vc = "msvisibilitychange"; } else if (typeof (document as any).webkitHidden !== "undefined") { h = "webkitHidden"; vc = "webkitvisibilitychange"; } if (typeof document[h] === "undefined") { // Browser doesn't support Page Visibility API, so behave as if the page is always visible pagevis_currently_visible = true } else { document.addEventListener(vc, function() { if (document[h]) { pagevis_currently_visible = false; fnInactive(); } else { pagevis_currently_visible = true; fnActive(); } }); } } /* tabs */ /** * Switch active tab * * @param {String} tab Full tab ID (e.g. tab-main, tab-users, tab-ext-???) */ var tab_set = function(tab) { var tabs = document_getElementsByClassName("tabpane"); for (var i in tabs) { try { ( tabs[i]).style.display = (tabs[i].id === tab ? 'block' : 'none'); } catch (e) {}; } var tabitems = document_getElementsByClassName("tabitem"); for (var i in tabitems) { try { // Update UNREAD/SELECTED flags for the target var was_unread = (tabitems[i].className.indexOf("unread") !== -1); var is_target = (tabitems[i].getAttribute('data-tab') === tab); var is_still_unread = (was_unread && !is_target); tabitems[i].className = "tabitem" + (is_target ? ' selected' : '') + (is_still_unread ? ' unread' : ''); } catch (e) {}; } pm_target = PM_TARGET_NONE; for (var i in pm_tabs) { if (pm_tabs[i] === tab) { pm_target = i; break; } } if (tab == "tab-main" && pagevis_currently_visible) { mainchat_unread_count = 0; } updateTitle(); write(tab).scroll(); document_getElementById("chatbox").focus(); last_tab = tab; }; var tab_new = function(id, name) { appendInnerHTML(document_getElementById("bar"), '
'+ ''+ hesc(name)+ ' '+ '×'+ '
' ); appendInnerHTML(document_getElementById("extratabs"), ' ' ); tab_addHandlers(); return "tab-ext-"+id; }; var tab_free = function(id) { if (id === "tab-main") return; // remove tab item and body var $el = document_getElementById("tabitem-"+id); $el.parentNode.removeChild($el); $el = document_getElementById(""+id); $el.parentNode.removeChild($el); // clear from PM tabs for (var i in pm_tabs) { if (pm_tabs[i] === id) { // pm_tabs[i] = false; delete(pm_tabs[i]); } } // maybe clear the 'new pm' warning updateTitle(); // don't leave us with no tab displayed if (last_tab === id) { tab_set("tab-main"); } }; var noprop = function(ev) { if (ev.preventDefault) { ev.preventDefault(); } if (ev.stopPropagation) { ev.stopPropagation(); } else { ev.cancelBubble = true; // oldIE } return false; } var tab_addHandlers = function() { var tabitems = document_getElementsByClassName("tabitem"); for (var i = 0; i < tabitems.length; i++) { if (! tabitems[i]) { continue; } ( tabitems[i]).onclick = function(ev) { tab_set( this.getAttribute('data-tab') ); return noprop(ev); }; } var tabclosers = document_getElementsByClassName("tab-closer"); for (var i = 0; i < tabclosers.length; i++) { if (! tabclosers[i]) { continue; } ( tabclosers[i]).onclick = function(ev) { tab_free( this.getAttribute('data-tab') ); return noprop(ev); }; } }; /* */ var maybeWriterFor = function(username: string) { if (! (username in pm_tabs) || ! pm_tabs[username]) { return null; } return write(pm_tabs[username]); }; var writerFor = function(username) { var ret = maybeWriterFor(username); if (ret !== null) { return ret; // found } // create new var tabid = tab_new(next_tabid++, username); pm_tabs[username] = tabid; return write(tabid); }; /* */ var tabcomplete_state = ''; var tabcompletion_start = function(direction) { var cursor = (document_getElementById("chatbox")).value.replace(/^.*\s([^\s]+)$/, '$1'); if (tabcomplete_state === '') { // new tab completion tabcomplete_state = cursor; } // Find all users who start with tabcomplete_state and retrieve the first // one after cursor var users = userlist.names(); var match = []; for (var i = 0, e = users.length; i !== e; ++i) { if (users[i].toUpperCase().indexOf(tabcomplete_state.toUpperCase()) === 0) { match.push(users[i]); } } if (match.length === 0) { // no matches return; } // Is cursor in the list? var cpos = -1; for (var i = 0, e = match.length; i !== e; ++i) { if (match[i] === cursor) { cpos = i; break; } } var targetName = match[negmod(i + direction, match.length)]; // Replace in textbox var $chatbox = (document_getElementById("chatbox")); var chatprefix = $chatbox.value.substr(0, $chatbox.value.length - cursor.length); $chatbox.value = chatprefix + targetName; $chatbox.focus(); }; var tabcompletion_inactive = function() { tabcomplete_state = ''; }; /* */ var MenuList = function(el) { this.el = el; this.div = document_createElement("div"); this.div.classList.add("menu"); this.div.style.position = "absolute"; this.ul = document_createElement("ul"); this.div.appendChild(this.ul); document.body.appendChild(this.div); }; MenuList.prototype.clear = function() { while (this.ul.children.length) { this.ul.children[0].parentNode.removeChild( this.ul.children[0] ); } }; MenuList.prototype.add = function(txt, cb) { var li = document_createElement("li"); li.innerHTML = txt; li.onclick = cb; this.ul.appendChild(li); }; MenuList.prototype.show = function() { this.div.style.display = "block"; this.div.style.top = (getOffsetTop(this.el) + this.el.clientHeight)+"px"; this.div.style.left = (getOffsetLeft(this.el) - 200 + this.el.clientWidth)+"px"; }; MenuList.prototype.hide = function() { this.div.style.display = "none"; }; MenuList.prototype.toggle = function() { // ES5 strict mode sets `this` to undefined (this.div.style.display === "block" ? this.hide : this.show).call(this); }; /* */ var menu = new MenuList(document_getElementById("menubutton")); menu.reset = function() { this.clear(); if (contented_url.length > 0) { menu.add("Upload", function() { contented_load(); }); } this.add(joinparts_getstr(), toggle_joinparts); this.add(desktop_notifications_fmtstr(), desktop_notifications_toggle); this.add(warnonclose_fmtstr(), warnonclose_toggle); this.add(timestamp_display(), timestamp_toggle); }; var usermenu = new MenuList(document.body); /** * Process all substitutions in a usercommand. May prompt the user for input. * * @param {String} str * @returns {String} */ var usercommand_process = function(str) { var message = str .replace(/%\[mynick\]/g, hub_last_nick) ; var match = null; for(;;) { match = message.match(/%\[line:([^\]]*)\]/); if (match === null) break; var res = prompt(match[1]); if (res === null) return; //cancelled message = message.substr(0, match.index) + nmdc_escape(res) + message.substr(match.index + match[0].length) ; } return message; }; var user_usercommands = []; /** * Called when a $UserCommand is recieved from the server. Register it in all * relevant context menus. * * @param {Object} data Has properties type, context, title, raw * @returns {undefined} */ var process_usercommand = function(data) { switch(data.type) { case 0: { /* USERCOMMAND_TYPE_SEPARATOR */ // ignore } break; case 1: /* USERCOMMAND_TYPE_RAW */ case 2:/* USERCOMMAND_TYPE_NICKLIMITED */ { if (data.context & 1) { /* USERCOMMAND_CONTEXT_HUB */ menu.add(data.title, function() { sock.emit('raw', {message: usercommand_process(data.raw)}); }); } if (data.context & 2) { /* USERCOMMAND_CONTEXT_USER */ user_usercommands.push(data); } } break; case 255: { /* USERCOMMAND_TYPE_CLEARALL */ if (data.context & 1) { menu.reset(); } if (data.context & 2) { user_usercommands = []; // clear } } break; } }; var joinparts_getstr = function() { return (show_joins ? "☑" : "☐") + " Show Joins/Parts"; }; var toggle_joinparts = function(ev) { var $el = ev.target || ev.srcElement; show_joins = ! show_joins; persistence_set("show_joins", show_joins); $el.innerHTML = joinparts_getstr(); }; /* */ var updateTitle = function() { var prefix = ""; var unrTabs = document_getElementsByClassName("unread"); if (unrTabs.length === 1 && unrTabs[0].getAttribute('data-tab') == "tab-main") { prefix = "[" + mainchat_unread_count + " NEW] " } else if (unrTabs.length > 0) { prefix = "[NEW PM] " } var suffix = ""; if (userlist.count() > 0) { suffix = " ("+userlist.count()+")"; } document.title = prefix + hub_hubname + suffix; }; var sock:SocketIOClient.Socket = null; var hub_state = 0; // [disconnected, sent-nick, connected] var hub_last_nick = ''; var hub_hubname = "Loading..."; var pm_tabs = {}; // nick => tabid var next_tabid = 1; const PM_TARGET_NONE = ""; var pm_target = PM_TARGET_NONE; var last_tab = "tab-main"; var show_joins = false; var have_cleared_once = false; var STATE_DISCONNECTED = -1; var STATE_READY_FOR_LOGIN = 0; var STATE_CONNECTING = 1; var STATE_ACTIVE = 2; var chat_scrollback = []; var chat_scrollback_index = -1; var timestamp_formats = [ "H:i", "H:i:s", "Y-m-d H:i:s" ]; var timestamp_names = [ "Minutes", "Seconds", "Full" ]; var timestamp_format_index = 0; var mainchat_unread_count = 0; var should_warn_on_close = false; var timestamp_display = function() { return "Timestamp format: " + timestamp_names[timestamp_format_index]; }; var timestamp_toggle = function(ev) { var $el = ev.target || ev.srcElement; timestamp_format_index = (timestamp_format_index + 1) % timestamp_formats.length; persistence_set("timestamps", timestamp_format_index); $el.innerHTML = timestamp_display(); }; var warnonclose_fmtstr = function() { return (should_warn_on_close ? "☑" : "☐") + " Confirm closing window"; }; var warnonclose_toggle = function(ev) { var $el = ev.target || ev.srcElement; should_warn_on_close = !should_warn_on_close; persistence_set("warnonclose", should_warn_on_close); $el.innerHTML = warnonclose_fmtstr(); }; var desktop_notifications_enabled = false; var desktop_notifications_fmtstr = function() { return (desktop_notifications_enabled ? "☑" : "☐") + " Show desktop popups"; }; var desktop_notifications_toggle = function(ev) { var $el = ev.target || ev.srcElement; desktop_notifications_enabled = !desktop_notifications_enabled; persistence_set("notifications", desktop_notifications_enabled); if (desktop_notifications_enabled) { notify(hub_hubname, "Desktop popups enabled", "tab-main"); } persistence_set("popups", desktop_notifications_enabled); $el.innerHTML = desktop_notifications_fmtstr(); }; var scrollback_move = function(delta) { if (chat_scrollback.length === 0) { return; // no effect } if (chat_scrollback_index === -1) { chat_scrollback_index = chat_scrollback.length - 1; // always starts at most recent message } else { chat_scrollback_index += delta; // clamp if (chat_scrollback_index === -1) { chat_scrollback_index = 0; } else if (chat_scrollback_index === chat_scrollback.length) { chat_scrollback_index = chat_scrollback.length - 1; } } (document_getElementById("chatbox")).value = chat_scrollback[chat_scrollback_index]; }; /* */ var persistence_set = function(key, value) { if (window.localStorage) { window.localStorage[key] = JSON.stringify(value); } }; var persistence_get = function(key, fallback) { try { return JSON.parse( window.localStorage[key] ); } catch (ex) { return fallback; } }; var transition = function(new_state) { hub_state = new_state; var $chatbox = (document_getElementById("chatbox")); switch(new_state) { case STATE_DISCONNECTED: { userlist.clear(); $chatbox.disabled = true; $chatbox.value = ''; // clear } break; case STATE_READY_FOR_LOGIN: { userlist.clear(); $chatbox.spellcheck = false; $chatbox.disabled = false; $chatbox.value = ''; // clear } break; case STATE_CONNECTING: { $chatbox.disabled = true; } break; case STATE_ACTIVE: { write("tab-main").system("Now talking on "+hub_hubname); $chatbox.disabled = false; $chatbox.spellcheck = true; } break; } }; var tab_is_visible = function(tabref) { return (last_tab === tabref && pagevis_currently_visible); } var tab_mark_unread = function(tabref) { if (document_getElementById("tabitem-"+tabref).className.indexOf('unread') === -1) { document_getElementById("tabitem-"+tabref).className += " unread"; updateTitle(); } } // declare var contented: any; var contented_url = ""; var contented_loaded_sdk = false; var contented_load = function() { if (contented_url.length === 0) { return; } var onceSDKLoaded = function() { contented.init("#inner-tab-main", function(items) { var val = ( document_getElementById("chatbox")).value; for (var i = 0; i < items.length; ++i) { if (val.length > 0) { val += " "; } val += contented.getPreviewURL(items[i]); } ( document_getElementById("chatbox")).value = val; }); }; if (contented_loaded_sdk) { onceSDKLoaded(); } else { var scriptElement = document_createElement('script'); scriptElement.onload = function() { contented_loaded_sdk = true; onceSDKLoaded(); }; scriptElement.src = contented_url + "sdk.js"; document.body.appendChild(scriptElement); } }; // window.onload = function() { write("tab-main").system("Communicating with server..."); show_joins = persistence_get("show_joins", false); document.title = hub_hubname; // "Loading..."; // HTML event handlers document_getElementById("form-none").onsubmit = function(ev) { submit(); return noprop(ev); // don't submit form }; document_getElementById("chatbox").onkeydown = function(ev) { if (ev.keyCode === 9 /* Tab */) { tabcompletion_start( ev.shiftKey ? -1 : 1 ); return noprop(ev); } else if (ev.keyCode == 38 /* ArrowUp */ && ev.ctrlKey) { scrollback_move(-1); return noprop(ev); } else if (ev.keyCode == 40 /* ArrowDown */ && ev.ctrlKey) { scrollback_move(1); return noprop(ev); } else { tabcompletion_inactive(); chat_scrollback_index = -1; // clear } }; window.onresize = function() { if (last_tab === "tab-users" && document.documentElement.clientWidth >= 590) { tab_set("tab-main"); } menu.hide(); usermenu.hide(); }; document_getElementById("menubutton").onclick = function(ev) { menu.toggle(); return noprop(ev); }; window.onclick = function() { menu.hide(); usermenu.hide(); }; window.onbeforeunload = function (e) { if (should_warn_on_close && hub_state === STATE_ACTIVE) { // n.b. recent Firefox / Chrome don't display the custom message here. var confirmationMessage = "Still connected to the hub \"" + hub_hubname + "\".\nAre you sure you want to close?"; e.returnValue = confirmationMessage; return confirmationMessage; } // else: ignore }; timestamp_format_index = persistence_get("timestamps", 0); should_warn_on_close = persistence_get("warnonclose", false); desktop_notifications_enabled = persistence_get("popups", false); menu.reset(); tab_addHandlers(); pagevis_setup( function() { // We've just become active // If the foreground is a PM window with UNREAD, mark it as read tab_set(last_tab); }, function() { // We've just become inactive // TODO: marker lines } ); // Hacks for WiiU user-agent if (navigator && navigator.userAgent && !!navigator.userAgent.match("Nintendo WiiU")) { document.body.className += " navigator-wiiu"; } // Socket event handlers sock = io.connect(EXTERN_ROOT); sock.on('cls', function() { transition(STATE_READY_FOR_LOGIN); var pre_login = persistence_get("login", ""); if (pre_login.indexOf(":") !== -1) { pre_login = pre_login.substr(0, pre_login.indexOf(":")) + ":" + SENTINEL_PASSWORD; } (document_getElementById("chatbox")).value = pre_login; if (have_cleared_once) { // re-log-in automatically write("tab-main").system("Automatically reconnecting as \"" + pre_login + "\"..."); submit(); } if (!have_cleared_once) { write("tab-main").cls(); userlist.clear(); have_cleared_once = true; // don't re-clear, keep history } }); sock.on('hubname', function(s) { hub_hubname = s; updateTitle(); }); sock.on('sys', function(data) { write("tab-main").system(data); }); sock.on('raw', function(data) { write("tab-main").raw(data); }); sock.on('pub', function(data) { write("tab-main").pub( data.user, data.message); if (! tab_is_visible("tab-main")) { tab_mark_unread("tab-main"); mainchat_unread_count += 1; updateTitle(); } }); sock.on('priv', function(data) { writerFor(data.user).pub(data.user, data.message); if (! tab_is_visible(pm_tabs[data.user])) { // Got PM, but tab isn't focused tab_mark_unread( pm_tabs[data.user] ); if (desktop_notifications_enabled) { notify("Message from " + data.user, data.message, pm_tabs[data.user]); } } }); sock.on('hello', function() { transition(STATE_ACTIVE); write_system_message_in_all_pm_tabs("Reconnected."); }); sock.on('part', function(u) { userlist.del(u.user); if (show_joins) { write("tab-main").system("*** Parts: "+u.user); // sanitised } var targetTab = maybeWriterFor(u.user); if (targetTab !== null) { targetTab.system("*** Parts: "+u.user); } updateTitle(); }); sock.on('join', function(u) { userlist.add(u.user); if (show_joins) { write("tab-main").system("*** Joins: "+u.user); } var targetTab = maybeWriterFor(u.user); if (targetTab !== null) { targetTab.system("*** Joins: "+u.user); } updateTitle(); }); sock.on('info', function(u) { try { var props = JSON.parse(u.message); userlist.setInfo(u.user, props); } catch (ex) {} }); sock.on('close', function() { transition(STATE_DISCONNECTED); write("tab-main").system("Connection closed by remote host."); write_system_message_in_all_pm_tabs("Disconnected."); }); sock.on('disconnect', function() { transition(STATE_DISCONNECTED); write("tab-main").system("Lost connection to the server."); write_system_message_in_all_pm_tabs("Disconnected."); }); sock.on('usercommand', function(data) { process_usercommand(data); }); sock.on('contented', function(url) { contented_url = url; menu.reset(); // sent before login }); };