]> git.scottworley.com Git - reliable-chat/blob - webclient/rc.html
41c1e54e3164a01f684e507b45a07fecbfef8d31
[reliable-chat] / 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">
4 <!--
5 reliable-chat - multipath chat
6 Copyright (C) 2012 Scott Worley <sworley@chkno.net>
7 Copyright (C) 2012 Jason Hibbs <skitch@gmail.com>
8
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.
12
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.
17
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/>.
20 -->
21 <head>
22 <title>Reliable Chat</title>
23 <style type="text/css"><!--/*--><![CDATA[/*><!--*/
24 html, body {
25 width: 100%;
26 height: 100%;
27 margin: 0;
28 padding: 0;
29 background-color: #293134;
30 color: silver;
31 font-family: monospace;
32 }
33 a {
34 color: #ddd;
35 }
36 #container {
37 height: 100%;
38 }
39 #status {
40 width: 100%;
41 text-align: right;
42 background-color: #293134;
43 padding: 5px 5px 5px 0px;
44 }
45 #client {
46 width: 100%;
47 position: fixed;
48 bottom: 0;
49 display: none;
50 }
51 #input {
52 width: 100%;
53 background-color: #293134;
54 }
55 #say { width: 100% }
56 #history {
57 padding: 0px 5px 30px 5px;
58 vertical-align: bottom;
59 }
60 .banner {
61 font-size: 85%;
62 text-align: right;
63 }
64 .status {
65 color: #dd8;
66 }
67 .local.self {
68 color: #d8d;
69 }
70 .self {
71 color: #8d8;
72 }
73 .me {
74 color: #bbd;
75 }
76 .servercount {
77 margin-right: -0.5em;
78 font-size: 70%;
79 }
80 .timestamp:hover, .timestamp:hover .servertimestamps {
81 background-color: #556;
82 }
83 .timestamp:hover .servertimestamps {
84 display: block;
85 }
86 .servertimestamps {
87 display: none;
88 position: absolute;
89 left: 3em;
90 z-index: 1;
91 border: 1px solid black;
92 border-radius: 5px;
93 padding-left: 5px;
94 padding-right: 5px;
95 }
96 img { width: 1px; height: 1px; }
97 iframe { display: none }
98 #status span { margin-right: 10px; }
99 #status span.sad {
100 background-color: #f00;
101 color: #fff;
102 border: 1px solid black;
103 border-radius: 5px;
104 padding-left: 5px;
105 padding-right: 5px;
106 }
107 #status span.happy {
108 background-color: #0f0;
109 color: #000;
110 border: 1px solid black;
111 border-radius: 5px;
112 padding-left: 5px;
113 padding-right: 5px;
114 }
115 #getnick {
116 padding-left: 3em;
117 display: none;
118 }
119
120 /* BEGIN expando input box trick kindly provided by http://www.alistapart.com/articles/expanding-text-areas-made-elegant/ */
121 .expandingArea {
122 position: relative;
123 border: 1px solid #888;
124 background: silver;
125 }
126 .expandingArea > textarea,
127 .expandingArea > pre {
128 margin: 0;
129 outline: 0;
130 border: 0;
131 padding: 5px;
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;
137 }
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.
142 */
143 -webkit-box-sizing: border-box;
144 -moz-box-sizing: border-box;
145 -ms-box-sizing: border-box;
146 box-sizing: border-box;
147 width: 100%;
148 /* Hide any scrollbars */
149 overflow: hidden;
150 position: absolute;
151 top: 0;
152 left: 0;
153 height: 100%;
154 /* Remove WebKit user-resize widget */
155 resize: none;
156 }
157 .expandingArea > pre {
158 display: block;
159 /* Hide the text; just using it for sizing */
160 visibility: hidden;
161 }
162 /* END expando input box trick kindly provided by http://www.alistapart.com/articles/expanding-text-areas-made-elegant/ */
163
164 /*]]>*/--></style>
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'];
167
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
178
179 function rcnick() {
180 return localStorage.getItem("nick");
181 }
182
183 function rcsetnick(new_nick) {
184 localStorage.setItem("nick", new_nick);
185 }
186
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";
191 } else {
192 return "http://" + server;
193 }
194 }
195
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);
202 }
203 }
204 }
205
206 function rcpad2(x) {
207 return (x < 10 ? "0" : "") + x;
208 }
209 function rcpad3(x) {
210 return (x < 10 ? "00" : (x < 100 ? "0" : "")) + x;
211 }
212
213 function rcformattime(t) {
214 var d = t.getDay();
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);
220 }
221
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()) + " " +
233 server;
234 d.appendChild(document.createTextNode(text));
235 divs[i].appendChild(d);
236 }
237 }
238 }
239
240 function rcmakemessageUI(message) {
241 message.UI = document.createElement("div");
242
243 // Server count
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(" "));
249
250 // Timestamp
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(" "));
257
258 // Timestamp hover
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);
264 }
265
266 // Classify different message types
267 var text_span = document.createElement("span");
268 var type;
269 if (/^\*\*\* /.test(message.Text)) {
270 type = "status";
271 } else if (/^\* /.test(message.Text)) {
272 type = "me";
273 } else if (/^-!- /.test(message.Text)) {
274 type = "local";
275 } else {
276 type = "text";
277 }
278 if (Object.keys(message.ServerTimes).length == 0) {
279 type += " self";
280 }
281 text_span.setAttribute("class", type);
282
283 // URL detection
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);
289 if (leading_text) {
290 text_span.appendChild(document.createTextNode(leading_text));
291 }
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);
298 }
299 if (text) {
300 text_span.appendChild(document.createTextNode(text));
301 }
302
303 message.UI.appendChild(text_span);
304 }
305
306 function rcaddmessagetohistory(message) {
307 var message_i;
308 if (message.Time) {
309 for (var i = hist.length - 1; ; i--) {
310 if (i < 0 || (hist[i].Time && message.Time >= hist[i].Time)) {
311 message_i = i+1;
312 hist.splice(message_i, 0, message);
313 break;
314 }
315 }
316 } else {
317 hist.push(message);
318 message_i = hist.length-1;
319 }
320 if (message_i + 1 < hist.length) {
321 rcaddmessagetoUI(message, hist[message_i + 1].UI);
322 } else {
323 rcaddmessagetoUI(message, null);
324 }
325 }
326
327 function rcaddmessagetoUI(message, before) {
328 if (!message.UI) {
329 rcmakemessageUI(message);
330 }
331 var h = document.getElementById("history");
332 if (before) {
333 h.insertBefore(message.UI, before);
334 } else {
335 h.appendChild(message.UI);
336 }
337 window.scrollTo(0, document.body.scrollHeight);
338 }
339
340 function make_seen_key(id, text) {
341 return id.replace(/@/g, "@@") + "_@_" + text.replace(/@/g, "@@");
342 }
343
344 function rcupdatemessagetime(message) {
345 // Set message.Time to be the median of message.ServerTimes
346 var times = [];
347 for (var i in message.ServerTimes) {
348 times.push(message.ServerTimes[i]);
349 }
350 times.sort();
351 if (times.length % 2) {
352 message.Time = times[(times.length-1)/2];
353 } else {
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);
357 }
358
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)) {
363 hist.splice(hi,1);
364 rcaddmessagetohistory(message);
365 }
366
367 // Update the UI
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);
376 }
377 }
378 }
379 }
380
381 function rcreceivemessages(server, messages) {
382 for (var i in messages) {
383 var m = messages[i];
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);
390 } else {
391 m.ServerTimes = {};
392 m.ServerTimes[server] = m.Time;
393 seen[seen_key] = m;
394 rcaddmessagetohistory(m);
395 for (var i in servers) {
396 rcchangeserverstatus(servers[i], "sad");
397 }
398 }
399 rcchangeserverstatus(server, "happy");
400 }
401 }
402
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;
410 if (rtxt != null) {
411 var messages = JSON.parse(rtxt);
412 if (messages != null) {
413 delay = 40;
414 if (messages.length >= 1 && "Time" in messages[messages.length-1]) {
415 since[server] = messages[messages.length-1].Time;
416 }
417 rcreceivemessages(server, messages);
418 }
419 }
420 }
421 window.setTimeout(rcfetch, delay, server);
422 }
423 }
424 var uri = rcserverbase(server) + "/fetch";
425 if (server in since) {
426 uri += '?since="' + since[server] + '"';
427 }
428 xhr.open("GET", uri);
429 xhr.send();
430 }
431
432 function rcconnect() {
433 makeExpandingArea(document.getElementById("expando"));
434 for (var i in servers) {
435 rcfetch(servers[i]);
436 // Status bar entry
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);
441 }
442 }
443
444 function rcstart() {
445 if (rcnick()) {
446 document.getElementById("client").style.display = 'block';
447 rcconnect();
448 } else {
449 document.getElementById("getnick").style.display = 'block';
450 }
451 }
452
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);
462 xhr.send();
463 }
464 }
465
466 function rcinput(input) {
467 var message;
468 var re = /^\/(\S+)(\s(.*))?/;
469 var match = re.exec(input);
470 if (match) {
471 var command = match[1];
472 var rest = match[3];
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': {}});
478 return;
479 }
480 if (rest) {
481 message = "*** " + rcnick() + " is now known as " + rest;
482 rcsetnick(rest);
483 } else {
484 rcaddmessagetoUI({'Text': '-!- /nick requires an argument', 'ServerTimes': {}});
485 return;
486 }
487 } else {
488 rcaddmessagetoUI({'Text': '-!- No such command: ' + command, 'ServerTimes': {}});
489 return;
490 }
491 } else {
492 message = "<" + rcnick() + "> " + input;
493 }
494
495 var m = {'Text': message, 'ServerTimes': {}};
496 rcaddmessagetohistory(m);
497 rcsend(m.UI, m);
498 }
499
500 function rckeydown(event) {
501 if (event.keyCode == 13) {
502 if (document.input.say.value) {
503 rcinput(document.input.say.value);
504 }
505 document.input.say.value = "";
506 return false;
507 }
508 }
509
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';
515 rcconnect();
516 }
517 return false;
518 }
519
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;
529 }, false);
530 span1.textContent = area.value;
531 span2.textContent = area.value;
532 } else if (area.attachEvent) {
533 // IE8 compatibility
534 area.attachEvent('onpropertychange', function() {
535 span1.innerText = area.value;
536 span2.innerText = area.value;
537 });
538 span1.innerText = area.value;
539 span2.innerText = area.value;
540 }
541 }
542 //--><!]]></script>
543
544 </head>
545
546 <body onload="rcstart()">
547 <div id="container">
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>
552 </div>
553 <div id="getnick">
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>
558 </form>
559 </div>
560 <div id="client">
561 <div id="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>
566 </div>
567 </form></div>
568 <div id="status"></div>
569 </div>
570 </div>
571 </body>
572 </html>