]> git.scottworley.com Git - reliable-chat/blame - webclient/rc.html
Remove 'or any later version' license choice
[reliable-chat] / webclient / rc.html
CommitLineData
827f21bb
SW
1<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
2 "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
827f21bb 3<html xmlns="http://www.w3.org/1999/xhtml">
520c21fd
SW
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
777027a8 11 published by the Free Software Foundation, version 3.
520c21fd
SW
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-->
827f21bb
SW
21<head>
22 <title>Reliable Chat</title>
0248a518 23 <style type="text/css"><!--/*--><![CDATA[/*><!--*/
70f46223 24 html, body {
19724ebd 25 width: 100%;
70f46223
JH
26 height: 100%;
27 margin: 0;
28 padding: 0;
b22a2ced
JH
29 background-color: #293134;
30 color: silver;
70f46223
JH
31 font-family: monospace;
32 }
e04a2cfd 33 a {
63b1551b 34 color: #ddd;
e04a2cfd 35 }
70f46223
JH
36 #container {
37 height: 100%;
38 }
39 #status {
40 width: 100%;
638866e9 41 text-align: right;
b22a2ced 42 background-color: #293134;
70f46223
JH
43 padding: 5px 5px 5px 0px;
44 }
45 #client {
19724ebd 46 width: 100%;
70f46223
JH
47 position: fixed;
48 bottom: 0;
72f02cfd 49 display: none;
70f46223
JH
50 }
51 #input {
52 width: 100%;
b22a2ced 53 background-color: #293134;
70f46223 54 }
0248a518 55 #say { width: 100% }
70f46223 56 #history {
19724ebd
SW
57 padding: 0px 5px 30px 5px;
58 vertical-align: bottom;
70f46223 59 }
5afebfc6
SW
60 .banner {
61 font-size: 85%;
62 text-align: right;
63 }
7310ffee
SW
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 }
c0aac85a 76 .servercount {
33fdb071 77 margin-right: -0.5em;
c0aac85a
SW
78 font-size: 70%;
79 }
244a78af
SW
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 }
0248a518
SW
96 img { width: 1px; height: 1px; }
97 iframe { display: none }
70f46223 98 #status span { margin-right: 10px; }
f09c4ede
JH
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 }
72f02cfd
SW
115 #getnick {
116 padding-left: 3em;
117 display: none;
118 }
19724ebd
SW
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;
63b1551b 124 background: silver;
19724ebd
SW
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
0248a518
SW
164 /*]]>*/--></style>
165 <script type="text/javascript"><!--//--><![CDATA[//><!--
b669fb70 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'];
0248a518 167
da9ce2ab 168 var session = Math.random(); // For outgoing message IDs
869430fa
SW
169 var since = {}; // server -> time: For fetch?since=
170 var seen = {}; // seen_key -> message
f9e5ee82 171 var hist = []; // List of messages sorted by Time
5419ea4c 172 // Messages have these fields:
40380bd3 173 // Time: The timestamp. Median of ServerTimes
5419ea4c
SW
174 // ID: Some unique string for deduping
175 // Text: The text of the message
705e26cf 176 // ServerTimes: server -> timestamp
5419ea4c 177 // UI: The DOM node for this message in the UI
0248a518
SW
178
179 function rcnick() {
72f02cfd 180 return localStorage.getItem("nick");
0248a518
SW
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
66b29e97
SW
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
bd1ed9dd
SW
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();
66b29e97 219 return d + " " + rcpad2(h) + ":" + rcpad2(m) + ":" + rcpad2(s);
bd1ed9dd
SW
220 }
221
244a78af
SW
222 function rcaddservertimestamptohover(message, server) {
223 var divs = message.UI.getElementsByTagName("div");
66b29e97 224 var t = message.ServerTimes[server];
244a78af
SW
225 for (var i in divs) {
226 if (divs[i].getAttribute && divs[i].getAttribute("class") == "servertimestamps") {
227 var d = document.createElement("div");
66b29e97
SW
228 var text = t.getFullYear() + "-" +
229 rcpad2(t.getMonth()) + "-" +
230 rcpad2(t.getDay()) + " " +
231 rcformattime(t) + "." +
232 rcpad3(t.getMilliseconds()) + " " +
233 server;
244a78af
SW
234 d.appendChild(document.createTextNode(text));
235 divs[i].appendChild(d);
236 }
237 }
238 }
239
bf8f6a39
SW
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);
33fdb071 248 message.UI.appendChild(document.createTextNode(" "));
bf8f6a39
SW
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);
33fdb071 256 message.UI.appendChild(document.createTextNode(" "));
bf8f6a39 257
244a78af
SW
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
3a89c414
SW
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";
c301e690
SW
273 } else if (/^-!- /.test(message.Text)) {
274 type = "local";
3a89c414
SW
275 } else {
276 type = "text";
277 }
278 if (Object.keys(message.ServerTimes).length == 0) {
279 type += " self";
280 }
281 text_span.setAttribute("class", type);
e04a2cfd
SW
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");
606c01e1 293 anchor.setAttribute("rel", "nofollow");
e04a2cfd
SW
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
3a89c414 303 message.UI.appendChild(text_span);
bf8f6a39
SW
304 }
305
0248a518 306 function rcaddmessagetohistory(message) {
7f54ca0a
SW
307 var message_i;
308 if (message.Time) {
f9e5ee82
SW
309 for (var i = hist.length - 1; ; i--) {
310 if (i < 0 || (hist[i].Time && message.Time >= hist[i].Time)) {
7f54ca0a 311 message_i = i+1;
f9e5ee82 312 hist.splice(message_i, 0, message);
7f54ca0a
SW
313 break;
314 }
315 }
316 } else {
f9e5ee82
SW
317 hist.push(message);
318 message_i = hist.length-1;
7f54ca0a 319 }
f9e5ee82
SW
320 if (message_i + 1 < hist.length) {
321 rcaddmessagetoUI(message, hist[message_i + 1].UI);
16f7340b
SW
322 } else {
323 rcaddmessagetoUI(message, null);
324 }
325 }
7f54ca0a 326
16f7340b 327 function rcaddmessagetoUI(message, before) {
95ba71ee 328 if (!message.UI) {
bf8f6a39 329 rcmakemessageUI(message);
95ba71ee 330 }
0248a518 331 var h = document.getElementById("history");
16f7340b
SW
332 if (before) {
333 h.insertBefore(message.UI, before);
7f54ca0a
SW
334 } else {
335 h.appendChild(message.UI);
336 }
0248a518 337 window.scrollTo(0, document.body.scrollHeight);
0248a518
SW
338 }
339
340 function make_seen_key(id, text) {
341 return id.replace(/@/g, "@@") + "_@_" + text.replace(/@/g, "@@");
342 }
343
40380bd3
SW
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();
40380bd3 351 if (times.length % 2) {
ac4bf273 352 message.Time = times[(times.length-1)/2];
40380bd3 353 } else {
ac4bf273 354 var middle = times.length/2;
40380bd3
SW
355 var difference = times[middle].getTime() - times[middle-1].getTime();
356 message.Time = new Date(times[middle-1].getTime() + difference/2);
357 }
95ba71ee 358
f9e5ee82
SW
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);
95ba71ee
SW
364 rcaddmessagetohistory(message);
365 }
5be4c8ec
SW
366
367 // Update the UI
368 var spans = message.UI.getElementsByTagName("span");
369 for (var i in spans) {
c0aac85a
SW
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 }
5be4c8ec
SW
377 }
378 }
40380bd3
SW
379 }
380
79ced6f1
SW
381 function rcreceivemessages(server, messages) {
382 for (var i in messages) {
ad35b1be 383 var m = messages[i];
bd1ed9dd 384 m.Time = new Date(m.Time);
ad35b1be 385 var seen_key = make_seen_key(m.ID, m.Text);
705e26cf
SW
386 if (seen_key in seen) {
387 seen[seen_key].ServerTimes[server] = m.Time;
40380bd3 388 rcupdatemessagetime(seen[seen_key]);
244a78af 389 rcaddservertimestamptohover(seen[seen_key], server);
705e26cf 390 } else {
16640c5d
SW
391 m.ServerTimes = {};
392 m.ServerTimes[server] = m.Time;
869430fa 393 seen[seen_key] = m;
ad35b1be 394 rcaddmessagetohistory(m);
79ced6f1
SW
395 for (var i in servers) {
396 rcchangeserverstatus(servers[i], "sad");
397 }
0248a518 398 }
79ced6f1 399 rcchangeserverstatus(server, "happy");
0248a518 400 }
0248a518
SW
401 }
402
79ced6f1
SW
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) {
79ced6f1
SW
413 delay = 40;
414 if (messages.length >= 1 && "Time" in messages[messages.length-1]) {
ad35b1be 415 since[server] = messages[messages.length-1].Time;
79ced6f1 416 }
92ca5f8a 417 rcreceivemessages(server, messages);
79ced6f1 418 }
0248a518
SW
419 }
420 }
79ced6f1 421 window.setTimeout(rcfetch, delay, server);
0248a518
SW
422 }
423 }
79ced6f1
SW
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();
0248a518
SW
430 }
431
432 function rcconnect() {
19724ebd 433 makeExpandingArea(document.getElementById("expando"));
0248a518 434 for (var i in servers) {
79ced6f1 435 rcfetch(servers[i]);
0248a518
SW
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 }
0248a518
SW
442 }
443
72f02cfd
SW
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
0248a518 453 function rcsend(d, message) {
7f54ca0a
SW
454 message.ID = new Date().getTime() + "-" + session + "-" + Math.random();
455 seen[make_seen_key(message.ID, message.Text)] = message;
0248a518 456 var path = "/speak" +
7f54ca0a
SW
457 "?id=" + encodeURIComponent(message.ID) +
458 "&text=" + encodeURIComponent(message.Text);
0248a518 459 for (var i in servers) {
9878d03b
SW
460 var xhr = new XMLHttpRequest();
461 xhr.open("POST", rcserverbase(servers[i]) + path);
462 xhr.send();
0248a518
SW
463 }
464 }
465
5a6c082a
SW
466 function rcinput(input) {
467 var message;
219b6a2b 468 var re = /^\/(\S+)(\s(.*))?/;
5a6c082a 469 var match = re.exec(input);
16f7340b 470 if (match) {
112df9d1
SW
471 var command = match[1];
472 var rest = match[3];
473 if (command == 'me') {
474 message = "* " + rcnick() + " " + rest;
475 } else if (command == 'nick') {
219b6a2b
SW
476 if (rcnick() == rest) {
477 rcaddmessagetoUI({'Text': '-!- Your nick is already ' + rcnick(), 'ServerTimes': {}});
478 return;
479 }
a7a2c8b6
SW
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 }
16f7340b 487 } else {
112df9d1 488 rcaddmessagetoUI({'Text': '-!- No such command: ' + command, 'ServerTimes': {}});
16f7340b
SW
489 return;
490 }
5a6c082a
SW
491 } else {
492 message = "<" + rcnick() + "> " + input;
493 }
494
7f54ca0a 495 var m = {'Text': message, 'ServerTimes': {}};
869430fa
SW
496 rcaddmessagetohistory(m);
497 rcsend(m.UI, m);
5a6c082a
SW
498 }
499
0248a518
SW
500 function rckeydown(event) {
501 if (event.keyCode == 13) {
cf44a976
SW
502 if (document.input.say.value) {
503 rcinput(document.input.say.value);
504 }
0248a518 505 document.input.say.value = "";
19724ebd
SW
506 return false;
507 }
508 }
509
72f02cfd
SW
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
19724ebd
SW
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;
0248a518
SW
540 }
541 }
542 //--><!]]></script>
543
827f21bb
SW
544</head>
545
72f02cfd 546<body onload="rcstart()">
70f46223 547 <div id="container">
5afebfc6 548 <div class="banner">(You are using <a href="https://github.com/chkno/reliable-chat">Reliable Chat</a>)</div>
70f46223 549 <div id="history"></div>
19724ebd
SW
550 <div class="expandingArea" style="visibility: hidden">
551 <pre><span id="historypad"></span><br></pre>
552 </div>
72f02cfd
SW
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>
70f46223
JH
560 <div id="client">
561 <div id="input">
562 <form name="input" onsubmit="return false" autocomplete="off">
19724ebd
SW
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>
70f46223 567 </form></div>
19724ebd 568 <div id="status"></div>
70f46223
JH
569 </div>
570 </div>
827f21bb
SW
571</body>
572</html>