]>
git.scottworley.com Git - reliable-chat/blob - webclient/rc.html
1 <!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.1//EN"
2 "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
3 <html xmlns=
"http://www.w3.org/1999/xhtml">
5 reliable-chat - multipath chat
6 Copyright (C) 2012 Scott Worley <sworley@chkno.net>
7 Copyright (C) 2012 Jason Hibbs <skitch@gmail.com>
9 This program is free software: you can redistribute it and/or modify
10 it under the terms of the GNU Affero General Public License as
11 published by the Free Software Foundation, version 3.
13 This program is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU Affero General Public License for more details.
18 You should have received a copy of the GNU Affero General Public License
19 along with this program. If not, see <http://www.gnu.org/licenses/>.
22 <title>Reliable Chat
</title>
23 <style type=
"text/css"><!--/*--><![CDATA[/*
><!--*/
29 background-color: #293134;
31 font-family: monospace;
42 background-color: #293134;
43 padding: 5px 5px 5px 0px;
53 background-color: #293134;
57 padding: 0px 5px 30px 5px;
58 vertical-align: bottom;
80 .timestamp:hover, .timestamp:hover .servertimestamps {
81 background-color: #556;
83 .timestamp:hover .servertimestamps {
91 border: 1px solid black;
96 img { width: 1px; height: 1px; }
97 iframe { display: none }
98 #status span { margin-right: 10px; }
100 background-color: #f00;
102 border: 1px solid black;
108 background-color: #0f0;
110 border: 1px solid black;
120 /* BEGIN expando input box trick kindly provided by http://www.alistapart.com/articles/expanding-text-areas-made-elegant/ */
123 border: 1px solid #888;
126 .expandingArea > textarea,
127 .expandingArea > pre {
132 background: transparent;
133 font: 400 13px/16px helvetica, arial, sans-serif;
134 /* Make the text soft-wrap */
135 white-space: pre-wrap;
136 word-wrap: break-word;
138 .expandingArea > textarea {
139 /* The border-box box model is used to allow
140 * padding whilst still keeping the overall width
141 * at exactly that of the containing element.
143 -webkit-box-sizing: border-box;
144 -moz-box-sizing: border-box;
145 -ms-box-sizing: border-box;
146 box-sizing: border-box;
148 /* Hide any scrollbars */
154 /* Remove WebKit user-resize widget */
157 .expandingArea > pre {
159 /* Hide the text; just using it for sizing */
162 /* END expando input box trick kindly provided by http://www.alistapart.com/articles/expanding-text-areas-made-elegant/ */
165 <script type=
"text/javascript"><!--//--><![CDATA[/
/><!--
166 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'];
168 var session = Math.random(); // For outgoing message IDs
169 var since = {}; // server -> time: For fetch?since=
170 var seen = {}; // seen_key -> message
171 var hist = []; // List of messages sorted by Time
172 // Messages have these fields:
173 // Time: The timestamp. Median of ServerTimes
174 // ID: Some unique string for deduping
175 // Text: The text of the message
176 // ServerTimes: server -> timestamp
177 // UI: The DOM node for this message in the UI
180 return localStorage.getItem("nick");
183 function rcsetnick(new_nick) {
184 localStorage.setItem("nick", new_nick);
187 function rcserverbase(server) {
188 // Add the default port if server doesn't contain a port number already
189 if (server.indexOf(":") == -1) {
190 return "http://" + server + ":21059";
192 return "http://" + server;
196 function rcchangeserverstatus(server, new_status) {
197 var statusbar = document.getElementById("status");
198 var spans = statusbar.getElementsByTagName("span");
199 for (var i in spans) {
200 if (spans[i].firstChild && 'data' in spans[i].firstChild && spans[i].firstChild.data == server) {
201 spans[i].setAttribute("class", new_status);
207 return (x < 10 ? "0" : "") + x;
210 return (x < 10 ? "00" : (x < 100 ? "0" : "")) + x;
213 function rcformattime(t) {
215 d = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d];
216 var h = t.getHours();
217 var m = t.getMinutes();
218 var s = t.getSeconds();
219 return d + " " + rcpad2(h) + ":" + rcpad2(m) + ":" + rcpad2(s);
222 function rcaddservertimestamptohover(message, server) {
223 var divs = message.UI.getElementsByTagName("div");
224 var t = message.ServerTimes[server];
225 for (var i in divs) {
226 if (divs[i].getAttribute && divs[i].getAttribute("class") == "servertimestamps") {
227 var d = document.createElement("div");
228 var text = t.getFullYear() + "-" +
229 rcpad2(t.getMonth()) + "-" +
230 rcpad2(t.getDay()) + " " +
231 rcformattime(t) + "." +
232 rcpad3(t.getMilliseconds()) + " " +
234 d.appendChild(document.createTextNode(text));
235 divs[i].appendChild(d);
240 function rcmakemessageUI(message) {
241 message.UI = document.createElement("div");
244 var servercount = document.createElement("span");
245 servercount.setAttribute("class", "servercount");
246 servercount.appendChild(document.createTextNode(Object.keys(message.ServerTimes).length));
247 message.UI.appendChild(servercount);
248 message.UI.appendChild(document.createTextNode(" "));
251 var timestamp_text = message.Time ? rcformattime(message.Time) : "";
252 var timestamp = document.createElement("span");
253 timestamp.setAttribute("class", "timestamp");
254 timestamp.appendChild(document.createTextNode(timestamp_text));
255 message.UI.appendChild(timestamp);
256 message.UI.appendChild(document.createTextNode(" "));
259 var timestamp_hover = document.createElement("div");
260 timestamp_hover.setAttribute("class", "servertimestamps");
261 timestamp.appendChild(timestamp_hover);
262 for (var server in message.ServerTimes) {
263 rcaddservertimestamptohover(message, server);
266 // Classify different message types
267 var text_span = document.createElement("span");
269 if (/^\*\*\* /.test(message.Text)) {
271 } else if (/^\* /.test(message.Text)) {
273 } else if (/^-!- /.test(message.Text)) {
278 if (Object.keys(message.ServerTimes).length == 0) {
281 text_span.setAttribute("class", type);
284 var text = message.Text;
285 var URL_re = /\bhttps?:\/\/\S+/;
286 while (URL_re.test(text)) {
287 var match = URL_re.exec(text);
288 var leading_text = text.substr(0, match.index);
290 text_span.appendChild(document.createTextNode(leading_text));
292 var anchor = document.createElement("a");
293 anchor.setAttribute("rel", "nofollow");
294 anchor.setAttribute("href", encodeURI(match[0]));
295 anchor.appendChild(document.createTextNode(match[0]));
296 text_span.appendChild(anchor);
297 text = text.substr(match.index + match[0].length);
300 text_span.appendChild(document.createTextNode(text));
303 message.UI.appendChild(text_span);
306 function rcaddmessagetohistory(message) {
309 for (var i = hist.length - 1; ; i--) {
310 if (i < 0 || (hist[i].Time && message.Time >= hist[i].Time)) {
312 hist.splice(message_i, 0, message);
318 message_i = hist.length-1;
320 if (message_i + 1 < hist.length) {
321 rcaddmessagetoUI(message, hist[message_i + 1].UI);
323 rcaddmessagetoUI(message, null);
327 function rcaddmessagetoUI(message, before) {
329 rcmakemessageUI(message);
331 var h = document.getElementById("history");
333 h.insertBefore(message.UI, before);
335 h.appendChild(message.UI);
337 window.scrollTo(0, document.body.scrollHeight);
340 function make_seen_key(id, text) {
341 return id.replace(/@/g, "@@") + "_@_" + text.replace(/@/g, "@@");
344 function rcupdatemessagetime(message) {
345 // Set message.Time to be the median of message.ServerTimes
347 for (var i in message.ServerTimes) {
348 times.push(message.ServerTimes[i]);
351 if (times.length % 2) {
352 message.Time = times[(times.length-1)/2];
354 var middle = times.length/2;
355 var difference = times[middle].getTime() - times[middle-1].getTime();
356 message.Time = new Date(times[middle-1].getTime() + difference/2);
359 // This may have broken hist's in-sorted-order invariant
360 var hi = hist.indexOf(message);
361 if ((hist[hi-1] && hist[hi-1].Time > message.Time) ||
362 (hist[hi+1] && hist[hi+1].Time < message.Time)) {
364 rcaddmessagetohistory(message);
368 var spans = message.UI.getElementsByTagName("span");
369 for (var i in spans) {
370 if (spans[i].getAttribute) {
371 var type = spans[i].getAttribute("class");
372 if (type == "servercount") {
373 spans[i].firstChild.data = Object.keys(message.ServerTimes).length;
374 } else if (type == "timestamp") {
375 spans[i].firstChild.data = rcformattime(message.Time);
381 function rcreceivemessages(server, messages) {
382 for (var i in messages) {
384 m.Time = new Date(m.Time);
385 var seen_key = make_seen_key(m.ID, m.Text);
386 if (seen_key in seen) {
387 seen[seen_key].ServerTimes[server] = m.Time;
388 rcupdatemessagetime(seen[seen_key]);
389 rcaddservertimestamptohover(seen[seen_key], server);
392 m.ServerTimes[server] = m.Time;
394 rcaddmessagetohistory(m);
395 for (var i in servers) {
396 rcchangeserverstatus(servers[i], "sad");
399 rcchangeserverstatus(server, "happy");
403 function rcfetch(server) {
404 var delay = 10000; // TODO: Exponential backoff
405 var xhr = new XMLHttpRequest();
406 xhr.onreadystatechange = function() {
407 if (this.readyState == this.DONE) {
408 if (this.status == 200) {
409 var rtxt = this.responseText;
411 var messages = JSON.parse(rtxt);
412 if (messages != null) {
414 if (messages.length >= 1 && "Time" in messages[messages.length-1]) {
415 since[server] = messages[messages.length-1].Time;
417 rcreceivemessages(server, messages);
421 window.setTimeout(rcfetch, delay, server);
424 var uri = rcserverbase(server) + "/fetch";
425 if (server in since) {
426 uri += '?since="' + since[server] + '"';
428 xhr.open("GET", uri);
432 function rcconnect() {
433 makeExpandingArea(document.getElementById("expando"));
434 for (var i in servers) {
437 var status_indicator = document.createElement("span");
438 status_indicator.appendChild(document.createTextNode(servers[i]));
439 status_indicator.setAttribute("class", "sad");
440 document.getElementById("status").appendChild(status_indicator);
446 document.getElementById("client").style.display = 'block';
449 document.getElementById("getnick").style.display = 'block';
453 function rcsend(d, message) {
454 message.ID = new Date().getTime() + "-" + session + "-" + Math.random();
455 seen[make_seen_key(message.ID, message.Text)] = message;
456 var path = "/speak" +
457 "?id=" + encodeURIComponent(message.ID) +
458 "&text=" + encodeURIComponent(message.Text);
459 for (var i in servers) {
460 var xhr = new XMLHttpRequest();
461 xhr.open("POST", rcserverbase(servers[i]) + path);
466 function rcinput(input) {
468 var re = /^\/(\S+)(\s(.*))?/;
469 var match = re.exec(input);
471 var command = match[1];
473 if (command == 'me') {
474 message = "* " + rcnick() + " " + rest;
475 } else if (command == 'nick') {
476 if (rcnick() == rest) {
477 rcaddmessagetoUI({'Text': '-!- Your nick is already ' + rcnick(), 'ServerTimes': {}});
481 message = "*** " + rcnick() + " is now known as " + rest;
484 rcaddmessagetoUI({'Text': '-!- /nick requires an argument', 'ServerTimes': {}});
488 rcaddmessagetoUI({'Text': '-!- No such command: ' + command, 'ServerTimes': {}});
492 message = "<" + rcnick() + "> " + input;
495 var m = {'Text': message, 'ServerTimes': {}};
496 rcaddmessagetohistory(m);
500 function rckeydown(event) {
501 if (event.keyCode == 13) {
502 if (document.input.say.value) {
503 rcinput(document.input.say.value);
505 document.input.say.value = "";
510 function rcsetinitialnick() {
511 if (document.getnickform.initial_nick.value) {
512 rcsetnick(document.getnickform.initial_nick.value);
513 document.getElementById("getnick").style.display = 'none';
514 document.getElementById("client").style.display = 'block';
520 // From http://www.alistapart.com/articles/expanding-text-areas-made-elegant/
521 function makeExpandingArea(container) {
522 var area = container.querySelector('textarea');
523 var span1 = container.querySelector('span');
524 var span2 = document.getElementById('historypad');
525 if (area.addEventListener) {
526 area.addEventListener('input', function() {
527 span1.textContent = area.value;
528 span2.textContent = area.value;
530 span1.textContent = area.value;
531 span2.textContent = area.value;
532 } else if (area.attachEvent) {
534 area.attachEvent('onpropertychange', function() {
535 span1.innerText = area.value;
536 span2.innerText = area.value;
538 span1.innerText = area.value;
539 span2.innerText = area.value;
546 <body onload=
"rcstart()">
548 <div class=
"banner">(You are using
<a href=
"https://github.com/chkno/reliable-chat">Reliable Chat
</a>)
</div>
549 <div id=
"history"></div>
550 <div class=
"expandingArea" style=
"visibility: hidden">
551 <pre><span id=
"historypad"></span><br></pre>
554 <h1>Set your nick
</h1>
555 <form name=
"getnickform" onsubmit=
"return rcsetinitialnick();">
556 <input id=
"initial_nick" type=
"text"></input>
557 <input type=
"submit" value=
"ok"></input>
562 <form name=
"input" onsubmit=
"return false" autocomplete=
"off">
563 <div id=
"expando" class=
"expandingArea">
564 <pre><span></span><br></pre>
565 <textarea id=
"say" onkeydown=
"return rckeydown(event)" autofocus=
"autofocus"></textarea>
568 <div id=
"status"></div>