<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <!-- reliable-chat - multipath chat Copyright (C) 2012 Scott Worley <sworley@chkno.net> Copyright (C) 2012 Jason Hibbs <skitch@gmail.com> This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. --> <head> <title>Reliable Chat</title> <style type="text/css"><!--/*--><![CDATA[/*><!--*/ html, body { width: 100%; height: 100%; margin: 0; padding: 0; background-color: #293134; color: silver; font-family: monospace; } a { color: #ddd; } #container { height: 100%; } #status { width: 100%; text-align: right; background-color: #293134; padding: 5px 5px 5px 0px; } #client { width: 100%; position: fixed; bottom: 0; display: none; } #input { width: 100%; background-color: #293134; } #say { width: 100% } #history { padding: 0px 5px 30px 5px; vertical-align: bottom; } .banner { font-size: 85%; text-align: right; } .status { color: #dd8; } .local.self { color: #d8d; } .self { color: #8d8; } .me { color: #bbd; } .servercount { margin-right: -0.5em; font-size: 70%; } .timestamp:hover, .timestamp:hover .servertimestamps { background-color: #556; } .timestamp:hover .servertimestamps { display: block; } .servertimestamps { display: none; position: absolute; left: 3em; z-index: 1; border: 1px solid black; border-radius: 5px; padding-left: 5px; padding-right: 5px; } img { width: 1px; height: 1px; } iframe { display: none } #status span { margin-right: 10px; } #status span.sad { background-color: #f00; color: #fff; border: 1px solid black; border-radius: 5px; padding-left: 5px; padding-right: 5px; } #status span.happy { background-color: #0f0; color: #000; border: 1px solid black; border-radius: 5px; padding-left: 5px; padding-right: 5px; } #getnick { padding-left: 3em; display: none; } /* BEGIN expando input box trick kindly provided by http://www.alistapart.com/articles/expanding-text-areas-made-elegant/ */ .expandingArea { position: relative; border: 1px solid #888; background: silver; } .expandingArea > textarea, .expandingArea > pre { margin: 0; outline: 0; border: 0; padding: 5px; background: transparent; font: 400 13px/16px helvetica, arial, sans-serif; /* Make the text soft-wrap */ white-space: pre-wrap; word-wrap: break-word; } .expandingArea > textarea { /* The border-box box model is used to allow * padding whilst still keeping the overall width * at exactly that of the containing element. */ -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box; width: 100%; /* Hide any scrollbars */ overflow: hidden; position: absolute; top: 0; left: 0; height: 100%; /* Remove WebKit user-resize widget */ resize: none; } .expandingArea > pre { display: block; /* Hide the text; just using it for sizing */ visibility: hidden; } /* END expando input box trick kindly provided by http://www.alistapart.com/articles/expanding-text-areas-made-elegant/ */ /*]]>*/--></style> <script type="text/javascript"><!--//--><![CDATA[//><!-- var servers = ['chkno.net', 'rc2.chkno.net', 'reliablechat-chk.rhcloud.com:80', 'intense-basin-3395.herokuapp.com:80', 'echto.net', 'the-wes.com', 'vibrantlogic.com']; var session = Math.random(); // For outgoing message IDs var since = {}; // server -> time: For fetch?since= var seen = {}; // seen_key -> message var hist = []; // List of messages sorted by Time // Messages have these fields: // Time: The timestamp. Median of ServerTimes // ID: Some unique string for deduping // Text: The text of the message // ServerTimes: server -> timestamp // UI: The DOM node for this message in the UI function rcnick() { return localStorage.getItem("nick"); } function rcsetnick(new_nick) { localStorage.setItem("nick", new_nick); } function rcserverbase(server) { // Add the default port if server doesn't contain a port number already if (server.indexOf(":") == -1) { return "http://" + server + ":21059"; } else { return "http://" + server; } } function rcchangeserverstatus(server, new_status) { var statusbar = document.getElementById("status"); var spans = statusbar.getElementsByTagName("span"); for (var i in spans) { if (spans[i].firstChild && 'data' in spans[i].firstChild && spans[i].firstChild.data == server) { spans[i].setAttribute("class", new_status); } } } function rcpad2(x) { return (x < 10 ? "0" : "") + x; } function rcpad3(x) { return (x < 10 ? "00" : (x < 100 ? "0" : "")) + x; } function rcformattime(t) { var d = t.getDay(); d = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d]; var h = t.getHours(); var m = t.getMinutes(); var s = t.getSeconds(); return d + " " + rcpad2(h) + ":" + rcpad2(m) + ":" + rcpad2(s); } function rcaddservertimestamptohover(message, server) { var divs = message.UI.getElementsByTagName("div"); var t = message.ServerTimes[server]; for (var i in divs) { if (divs[i].getAttribute && divs[i].getAttribute("class") == "servertimestamps") { var d = document.createElement("div"); var text = t.getFullYear() + "-" + rcpad2(t.getMonth()) + "-" + rcpad2(t.getDay()) + " " + rcformattime(t) + "." + rcpad3(t.getMilliseconds()) + " " + server; d.appendChild(document.createTextNode(text)); divs[i].appendChild(d); } } } function rcmakemessageUI(message) { message.UI = document.createElement("div"); // Server count var servercount = document.createElement("span"); servercount.setAttribute("class", "servercount"); servercount.appendChild(document.createTextNode(Object.keys(message.ServerTimes).length)); message.UI.appendChild(servercount); message.UI.appendChild(document.createTextNode(" ")); // Timestamp var timestamp_text = message.Time ? rcformattime(message.Time) : ""; var timestamp = document.createElement("span"); timestamp.setAttribute("class", "timestamp"); timestamp.appendChild(document.createTextNode(timestamp_text)); message.UI.appendChild(timestamp); message.UI.appendChild(document.createTextNode(" ")); // Timestamp hover var timestamp_hover = document.createElement("div"); timestamp_hover.setAttribute("class", "servertimestamps"); timestamp.appendChild(timestamp_hover); for (var server in message.ServerTimes) { rcaddservertimestamptohover(message, server); } // Classify different message types var text_span = document.createElement("span"); var type; if (/^\*\*\* /.test(message.Text)) { type = "status"; } else if (/^\* /.test(message.Text)) { type = "me"; } else if (/^-!- /.test(message.Text)) { type = "local"; } else { type = "text"; } if (Object.keys(message.ServerTimes).length == 0) { type += " self"; } text_span.setAttribute("class", type); // URL detection var text = message.Text; var URL_re = /\bhttps?:\/\/\S+/; while (URL_re.test(text)) { var match = URL_re.exec(text); var leading_text = text.substr(0, match.index); if (leading_text) { text_span.appendChild(document.createTextNode(leading_text)); } var anchor = document.createElement("a"); anchor.setAttribute("rel", "nofollow"); anchor.setAttribute("href", encodeURI(match[0])); anchor.appendChild(document.createTextNode(match[0])); text_span.appendChild(anchor); text = text.substr(match.index + match[0].length); } if (text) { text_span.appendChild(document.createTextNode(text)); } message.UI.appendChild(text_span); } function rcaddmessagetohistory(message) { var message_i; if (message.Time) { for (var i = hist.length - 1; ; i--) { if (i < 0 || (hist[i].Time && message.Time >= hist[i].Time)) { message_i = i+1; hist.splice(message_i, 0, message); break; } } } else { hist.push(message); message_i = hist.length-1; } if (message_i + 1 < hist.length) { rcaddmessagetoUI(message, hist[message_i + 1].UI); } else { rcaddmessagetoUI(message, null); } } function rcaddmessagetoUI(message, before) { if (!message.UI) { rcmakemessageUI(message); } var h = document.getElementById("history"); if (before) { h.insertBefore(message.UI, before); } else { h.appendChild(message.UI); } window.scrollTo(0, document.body.scrollHeight); } function make_seen_key(id, text) { return id.replace(/@/g, "@@") + "_@_" + text.replace(/@/g, "@@"); } function rcupdatemessagetime(message) { // Set message.Time to be the median of message.ServerTimes var times = []; for (var i in message.ServerTimes) { times.push(message.ServerTimes[i]); } times.sort(); if (times.length % 2) { message.Time = times[(times.length-1)/2]; } else { var middle = times.length/2; var difference = times[middle].getTime() - times[middle-1].getTime(); message.Time = new Date(times[middle-1].getTime() + difference/2); } // This may have broken hist's in-sorted-order invariant var hi = hist.indexOf(message); if ((hist[hi-1] && hist[hi-1].Time > message.Time) || (hist[hi+1] && hist[hi+1].Time < message.Time)) { hist.splice(hi,1); rcaddmessagetohistory(message); } // Update the UI var spans = message.UI.getElementsByTagName("span"); for (var i in spans) { if (spans[i].getAttribute) { var type = spans[i].getAttribute("class"); if (type == "servercount") { spans[i].firstChild.data = Object.keys(message.ServerTimes).length; } else if (type == "timestamp") { spans[i].firstChild.data = rcformattime(message.Time); } } } } function rcreceivemessages(server, messages) { for (var i in messages) { var m = messages[i]; m.Time = new Date(m.Time); var seen_key = make_seen_key(m.ID, m.Text); if (seen_key in seen) { seen[seen_key].ServerTimes[server] = m.Time; rcupdatemessagetime(seen[seen_key]); rcaddservertimestamptohover(seen[seen_key], server); } else { m.ServerTimes = {}; m.ServerTimes[server] = m.Time; seen[seen_key] = m; rcaddmessagetohistory(m); for (var i in servers) { rcchangeserverstatus(servers[i], "sad"); } } rcchangeserverstatus(server, "happy"); } } function rcfetch(server) { var delay = 10000; // TODO: Exponential backoff var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (this.readyState == this.DONE) { if (this.status == 200) { var rtxt = this.responseText; if (rtxt != null) { var messages = JSON.parse(rtxt); if (messages != null) { delay = 40; if (messages.length >= 1 && "Time" in messages[messages.length-1]) { since[server] = messages[messages.length-1].Time; } rcreceivemessages(server, messages); } } } window.setTimeout(rcfetch, delay, server); } } var uri = rcserverbase(server) + "/fetch"; if (server in since) { uri += '?since="' + since[server] + '"'; } xhr.open("GET", uri); xhr.send(); } function rcconnect() { makeExpandingArea(document.getElementById("expando")); for (var i in servers) { rcfetch(servers[i]); // Status bar entry var status_indicator = document.createElement("span"); status_indicator.appendChild(document.createTextNode(servers[i])); status_indicator.setAttribute("class", "sad"); document.getElementById("status").appendChild(status_indicator); } } function rcstart() { if (rcnick()) { document.getElementById("client").style.display = 'block'; rcconnect(); } else { document.getElementById("getnick").style.display = 'block'; } } function rcsend(d, message) { message.ID = new Date().getTime() + "-" + session + "-" + Math.random(); seen[make_seen_key(message.ID, message.Text)] = message; var path = "/speak" + "?id=" + encodeURIComponent(message.ID) + "&text=" + encodeURIComponent(message.Text); for (var i in servers) { var xhr = new XMLHttpRequest(); xhr.open("POST", rcserverbase(servers[i]) + path); xhr.send(); } } function rcinput(input) { var message; var re = /^\/(\S+)(\s(.*))?/; var match = re.exec(input); if (match) { var command = match[1]; var rest = match[3]; if (command == 'me') { message = "* " + rcnick() + " " + rest; } else if (command == 'nick') { if (rcnick() == rest) { rcaddmessagetoUI({'Text': '-!- Your nick is already ' + rcnick(), 'ServerTimes': {}}); return; } if (rest) { message = "*** " + rcnick() + " is now known as " + rest; rcsetnick(rest); } else { rcaddmessagetoUI({'Text': '-!- /nick requires an argument', 'ServerTimes': {}}); return; } } else { rcaddmessagetoUI({'Text': '-!- No such command: ' + command, 'ServerTimes': {}}); return; } } else { message = "<" + rcnick() + "> " + input; } var m = {'Text': message, 'ServerTimes': {}}; rcaddmessagetohistory(m); rcsend(m.UI, m); } function rckeydown(event) { if (event.keyCode == 13) { if (document.input.say.value) { rcinput(document.input.say.value); } document.input.say.value = ""; return false; } } function rcsetinitialnick() { if (document.getnickform.initial_nick.value) { rcsetnick(document.getnickform.initial_nick.value); document.getElementById("getnick").style.display = 'none'; document.getElementById("client").style.display = 'block'; rcconnect(); } return false; } // From http://www.alistapart.com/articles/expanding-text-areas-made-elegant/ function makeExpandingArea(container) { var area = container.querySelector('textarea'); var span1 = container.querySelector('span'); var span2 = document.getElementById('historypad'); if (area.addEventListener) { area.addEventListener('input', function() { span1.textContent = area.value; span2.textContent = area.value; }, false); span1.textContent = area.value; span2.textContent = area.value; } else if (area.attachEvent) { // IE8 compatibility area.attachEvent('onpropertychange', function() { span1.innerText = area.value; span2.innerText = area.value; }); span1.innerText = area.value; span2.innerText = area.value; } } //--><!]]></script> </head> <body onload="rcstart()"> <div id="container"> <div class="banner">(You are using <a href="https://github.com/chkno/reliable-chat">Reliable Chat</a>)</div> <div id="history"></div> <div class="expandingArea" style="visibility: hidden"> <pre><span id="historypad"></span><br></pre> </div> <div id="getnick"> <h1>Set your nick</h1> <form name="getnickform" onsubmit="return rcsetinitialnick();"> <input id="initial_nick" type="text"></input> <input type="submit" value="ok"></input> </form> </div> <div id="client"> <div id="input"> <form name="input" onsubmit="return false" autocomplete="off"> <div id="expando" class="expandingArea"> <pre><span></span><br></pre> <textarea id="say" onkeydown="return rckeydown(event)" autofocus="autofocus"></textarea> </div> </form></div> <div id="status"></div> </div> </div> </body> </html>