83483fa4b0e4ec671fa2ea6afb5030642cfe3a49
[blerg.git] / www / js / blerg.js
1 /* Blerg is (C) 2011 The Dominion of Awesome, and is distributed under a
2  * BSD-style license.  Please see the COPYING file for details.
3  */
4
5 // Config
6 var baseURL = '';
7 var recordTemplate = new Template(
8     '<div class="record">#{data}<div class="info">Posted #{date}. <a href="' + baseURL + '/\##{author}/#{record}" onclick="return qlink()">[permalink]</a> <a href="#" onclick="postPopup(\'@#{author}/#{record}: \'); return false">[reply]</a></div></div>'
9 );
10 var tagRecordTemplate = new Template(
11     '<div class="record">#{data}<div class="info">Posted by <a class="author ref" href="/\##{author}" onclick="return qlink()">@#{author}</a> on #{date}. <a href="' + baseURL + '/\##{author}/#{record}" onclick="return qlink()">[permalink]</a> <a href="#" onclick="postPopup(\'@#{author}/#{record}: \'); return false">[reply]</a></div></div>'
12 );
13 var latestRecordsTemplate = new Template(
14     '<div class="record"><a class="author ref" href="' + baseURL + '/\##{author}" onclick="return qlink()">@#{author}</a> #{data}</div>'
15 );
16
17 // Page elements
18 var items;
19
20 // Other globals
21 var currentPager;
22 var loginStatus;
23
24 // Object to keep track of login status
25 function LoginStatus() {
26     var cookies = {};
27     document.cookie.split(/;\s+/).each(function(v) {
28         kv = v.split('=');
29         cookies[kv[0]] = kv[1];
30     });
31     if (cookies.auth && cookies.username) {
32         this.loggedIn = true;
33         this.username = cookies.username;
34         this.requestFeedStatus();
35         this.feedStatusUpdateInterval = setInterval(this.requestFeedStatus.bind(this), 900000);
36     } else {
37         this.loggedIn = false;
38         this.username = null;
39     }
40     this.update();
41 }
42
43 LoginStatus.prototype.login = function(username, password) {
44     new Ajax.Request(baseURL + '/login', {
45         parameters: {
46             username: username,
47             password: password
48         },
49         onSuccess: function(r) {
50             var j = r.responseText.evalJSON();
51             if (j && j.status == 'success') {
52                 this.loggedIn = true;
53                 this.username = username;
54                 document.cookie = "username=" + username;
55                 $('login.password').value = '';
56                 this.requestFeedStatus();
57                 this.feedStatusUpdateInterval = setInterval(this.requestFeedStatus.bind(this), 900000);
58                 this.update();
59             } else {
60                 alert("Could not log in");
61                 $('login.username').focus();
62             }
63         }.bind(this),
64         onFailure: function(r) {
65             alert("Could not log in");
66             $('login.username').focus();
67         }
68     });
69 }
70
71 LoginStatus.prototype.logout = function() {
72     new Ajax.Request(baseURL + '/logout', {
73         parameters: {
74             username: this.username
75         },
76         onComplete: function(r) {
77             this.loggedIn = false;
78             document.cookie = "auth=; expires=1-Jan-1970 00:00:00 GMT";
79             this.update();
80             clearInterval(this.feedStatusUpdateInterval);
81         }.bind(this)
82     });
83     document.cookie = "username=; expires=1-Jan-1970 00:00:00 GMT";
84 }
85
86 LoginStatus.prototype.update = function() {
87     if (this.loggedIn) {
88         $('userlink').href = baseURL + '/#' + this.username;
89         $('userlink').update('@' + this.username);
90         $('reflink').href = baseURL + '/#/ref/' + this.username;
91         $('login').hide();
92         $('logout').show();
93     } else {
94         $('post').hide();
95         $('login').show();
96         $('logout').hide();
97     }
98 }
99
100 LoginStatus.prototype.post = function(msg) {
101     if (!this.loggedIn) {
102         alert("You are not logged in!");
103         return;
104     }
105
106     new Ajax.Request(baseURL + '/put', {
107         parameters: {
108             username: this.username,
109             data: msg
110         },
111         onSuccess: function(r) {
112             var j = r.responseText.evalJSON();
113             if (j && j.status == 'success') {
114                 $('post.content').value = '';
115                 if (location.hash != '#' + this.username) {
116                     qlink(this.username);
117                 } else {
118                     currentPager.itemCount++;
119                     currentPager.reload();
120                 }
121             } else {
122                 alert('Post failed!');
123             }
124         }.bind(this),
125         onFailure: function(r) {
126             alert('Post failed!');
127         }
128     });
129 }
130
131 LoginStatus.prototype.requestFeedStatus = function() {
132     new Ajax.Request('/feedinfo', {
133         parameters: { username: this.username },
134         onSuccess: function(r) {
135             var j = r.responseText.evalJSON();
136             if (j['new'] > 0) {
137                 $('newFeedMessages').update('(' + j['new'] + ' new)');
138             } else {
139                 $('newFeedMessages').update('');
140             }
141         }
142     });
143 }
144
145 // Base object for paged data
146 function Pager() {
147     this.itemsPerPage = 10;
148     this.itemCache = new Hash();
149     this.pageStart = null;
150 }
151
152 Pager.prototype.updateState = function(m) {
153     return false;
154 }
155
156 Pager.prototype.show = function() {
157     items.show();
158 }
159
160 Pager.prototype.hide = function() {
161     items.hide();
162     items.update();
163     $('newer_link').hide();
164     $('older_link').hide();
165 }
166
167 Pager.prototype.olderPage = function() {
168     if (this.pageStart >= this.itemsPerPage) {
169         qlink(this.baseFrag + '/p' + (this.pageStart - this.itemsPerPage));
170     }
171 }
172
173 Pager.prototype.newerPage = function() {
174     if (this.pageStart + this.itemsPerPage < this.itemCount) {
175         qlink(this.baseFrag + '/p' + (this.pageStart + this.itemsPerPage));
176     }
177 }
178
179 Pager.prototype.addItems = function(items) {
180     items.each(function(v) {
181         if (!this.itemCache[v.id])
182             this.itemCache[v.id] = v;
183     }.bind(this));
184 }
185
186 Pager.prototype.displayItems = function() {
187     if (this.pageStart == undefined)
188         this.pageStart == this.itemCount - 1;
189     items.update();
190
191     if (this.pageStart != undefined && this.itemCache[this.pageStart]) {
192         var end = (this.pageStart >= this.itemsPerPage ? this.pageStart - this.itemsPerPage + 1 : 0);
193         for (var i = this.pageStart; i >= end; i--) {
194             items.insert(this.itemCache[i].html);
195         }
196     } else {
197         items.insert("There doesn't seem to be anything here!");
198     }
199
200     if (this.pageStart < this.itemCount - 1) {
201         $('newer_link').href = baseURL + '/#' + this.baseFrag + '/p' + (this.pageStart + this.itemsPerPage);
202         $('newer_link').show();
203     } else {
204         $('newer_link').hide();
205     }
206
207     if (this.pageStart >= 10) {
208         $('older_link').href = baseURL + '/#' + this.baseFrag + '/p' + (this.pageStart - this.itemsPerPage);
209         $('older_link').show();
210     } else {
211         $('older_link').hide();
212     }
213
214     document.body.scrollTo();
215 }
216
217 Pager.prototype.reload = function() {
218     this.pageStart = null;
219     this.loadItems(null, null, Pager.prototype.showPageAt.bind(this, this.itemCount - 1));
220 }
221
222 Pager.prototype.showPageAt = function(r) {
223     var end = (r - 9 > 0 ? r - 9 : 0);
224     if (this.itemCache[r] && this.itemCache[end]) {
225         this.pageStart = r;
226         this.displayItems();
227     } else {
228         this.loadItems((r >= 49 ? r - 49 : 0), r, Pager.prototype.showPageAt.bind(this, r));
229     }
230 }
231
232 Pager.prototype.showRecord = function(r) {
233     if (this.itemCache[r]) {
234         $('older_link').hide();
235         $('newer_link').hide();
236         items.update(this.itemCache[r].html);
237     } else {
238         this.loadItems(r, r, Pager.prototype.showRecord.bind(this, r));
239     }
240 }
241
242 Pager.prototype.loadItems = function(from, to, continuation) { }
243
244
245 // Object to render user pages
246 function User(m) {
247     Pager.call(this);
248     this.username = m[1];
249     this.baseFrag = m[1];
250     this.permalink = (m[2] != 'p');
251     this.pageStart = parseInt(m[3]);
252 }
253 User.prototype = new Pager();
254 User.prototype.constructor = User;
255
256 User.prototype.updateState = function(m) {
257     if (m[1] != this.username)
258         return false;
259     
260     this.permalink = (m[2] != 'p');
261     this.pageStart = parseInt(m[3]);
262     this.show();
263
264     return true;
265 }
266
267 User.prototype.show = function() {
268     Pager.prototype.show.call(this);
269
270     $$('[name=section]').each(function(v) { v.update(' @' + this.username) }.bind(this));
271     $('rss').show();
272     $('rsslink').href = '/rss/' + this.username;
273     $$('[name=user.reflink]').each(function(e) {
274         e.href = baseURL + '/#/ref/' + this.username;
275     }.bind(this));
276     $('usercontrols').show();
277
278     if (this.permalink && this.pageStart >= 0) {
279         this.showRecord(this.pageStart);
280     } else if (this.pageStart >= 0) {
281         this.showPageAt(this.pageStart);
282     } else {
283         this.reload();
284     }
285 }
286
287 User.prototype.hide = function() {
288     Pager.prototype.hide.call(this);
289     $('signup').hide();
290     $('rss').hide();
291     $('usercontrols').hide();
292 }
293
294 User.prototype.reload = function() {
295     this.pageStart = null;
296
297     $$('[name=user.subscribelink]').each(Element.hide);
298     $$('[name=user.unsubscribelink]').each(Element.hide);
299
300     if (loginStatus.loggedIn) {
301         new Ajax.Request(baseURL + '/feedinfo/' + this.username, {
302             method: 'post',
303             parameters: {
304                 username: loginStatus.username
305             },
306             onSuccess: function(r) {
307                 var json = r.responseText.evalJSON();
308                 if (json.subscribed) {
309                     $$('[name=user.subscribelink]').each(Element.hide);
310                     $$('[name=user.unsubscribelink]').each(Element.show);
311                 } else {
312                     $$('[name=user.subscribelink]').each(Element.show);
313                     $$('[name=user.unsubscribelink]').each(Element.hide);
314                 }
315             }
316         });
317     }
318
319     new Ajax.Request(baseURL + '/info/' + this.username, {
320         method: 'get',
321         onSuccess: function(r) {
322             var response = r.responseText.evalJSON();
323             if (response) {
324                 this.itemCount = parseInt(response.record_count);
325                 this.showPageAt(this.itemCount - 1);
326             }
327         }.bind(this)
328     });
329 }
330
331 User.prototype.loadItems = function(from, to, continuation) {
332     if (to < 0)
333         return;
334
335     var url;
336     if (from != undefined && to != undefined) {
337         url = baseURL + '/get/' + this.username + '/' + from + '-' + to;
338         this.pageStart = to;
339     } else {
340         url = baseURL + '/get/' + this.username;
341     }
342
343     new Ajax.Request(url, {
344         method: 'get',
345         onSuccess: function(r) {
346             var records = r.responseText.evalJSON();
347             if (records && records.length > 0) {
348                 records.each(function(v) {
349                     v.id = v.record;
350                     v.author = this.username;
351                     mangleRecord(v, recordTemplate);
352                 }.bind(this));
353                 this.addItems(records);
354                 if (!this.pageStart)
355                     this.pageStart = records[0].recInt;
356             }
357             continuation();
358         }.bind(this),
359         onFailure: function(r) {
360             this.displayItems();
361         }.bind(this),
362         on404: function(r) {
363             displayError('User not found');
364         }
365     });
366 }
367
368 function mangleRecord(record, template) {
369     record.recInt = parseInt(record.record);
370
371     var lines = record.data.split(/\r?\n/);
372     if (lines[lines.length - 1] == '')
373         lines.pop();
374
375     var out = ['<p>'];
376     var endpush = null;
377     var listMode = false;
378     lines.each(function(l) {
379         if (l == '') {
380             if (out[out.length - 1] == '<br>') {
381                 out[out.length - 1] = '<p>';
382             }
383             if (out[out.length - 1] == '</li>') {
384                 out.push('</ul>');
385                 out.push('<p>');
386                 listMode = false;
387             }
388             return;
389         }
390
391         // Put quoted material into a special paragraph
392         if (l[0] == '>') {
393             var pi = out.lastIndexOf('<p>');
394             if (pi != -1) {
395                 out[pi] = '<p class="quote">';
396                 l = l.replace(/^>\s*/, '');
397             }
398         }
399
400         // Sanitize HTML input
401         l = l.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
402
403         // Turn HTTP URLs into links
404         l = l.replace(/(\s|^)(https?:\/\/[a-zA-Z0-9.-]*[a-zA-Z0-9](\/([^\s"]*[^.!,;?()\s])?)?)/g, '$1<a href="$2">$2</a>');
405
406         // Turn markdown links into links
407         var re;
408
409         // Craft a regex that finds URLs that end in the extensions specified by BlergMedia.audioExtensions.
410         re = new RegExp('(\\s|^)\\[([^\\]]+)\\]\\((https?:\\/\\/[a-zA-Z0-9.-]*[a-zA-Z0-9]\\/[^)"]*?\\.(' + BlergMedia.audioExtensions.join('|') + '))\\)', 'g');
411         l = l.replace(re, '$1<a href="$3">$2</a> <a href="$3" onclick="play_audio(); return false"><img src="/images/play.png"></a>');
412
413         // Ditto, but use the extended markdown link syntax to specify the format
414         re = new RegExp('(\\s|^)\\[([^\\]]+)\\]\\((https?:\\/\\/[a-zA-Z0-9.-]*[a-zA-Z0-9](\\/[^)"]*?)?)\\s+audio:(' + BlergMedia.audioExtensions.join('|') + ')\\)', 'g');
415         l = l.replace(re, '$1<a href="$3">$2</a> <a href="$3" onclick="play_audio(); return false"><img src="/images/play.png"></a>');
416
417         // Craft a regex that finds URLs that end in the extensions specified by BlergMedia.videoExtensions.
418         re = new RegExp('(\\s|^)\\[([^\\]]+)\\]\\((https?:\\/\\/[a-zA-Z0-9.-]*[a-zA-Z0-9]\\/[^)"]*?\\.(' + BlergMedia.videoExtensions.join('|') + '))\\)', 'g');
419         l = l.replace(re, '$1<a href="$3">$2</a> <a href="$3" onclick="play_video(); return false"><img src="/images/play.png"></a>');
420
421         // Ditto, but use the extended markdown link syntax to specify the format
422         re = new RegExp('(\\s|^)\\[([^\\]]+)\\]\\((https?:\\/\\/[a-zA-Z0-9.-]*[a-zA-Z0-9](\\/[^)"]*?)?)\\s+video:(' + BlergMedia.videoExtensions.join('|') + ')\\)', 'g');
423         l = l.replace(re, '$1<a href="$3">$2</a> <a href="$3" onclick="play_video(); return false"><img src="/images/play.png"></a>');
424
425         // Regular markdown links
426         l = l.replace(/(\s|^)\[([^\]]+)\]\((https?:\/\/[a-zA-Z0-9.-]*[a-zA-Z0-9](\/[^)"]*?)?)\)/g, '$1<a href="$3">$2</a>');
427
428         // Turn *foo* into italics and **foo** into bold
429         l = l.replace(/([^\w\\]|^)\*\*(\w[^*]*)\*\*(\W|$)/g, '$1<b>$2</b>$3');
430         l = l.replace(/([^\w\\]|^)\*(\w[^*]*)\*(\W|$)/g, '$1<i>$2</i>$3');
431
432         // Turn refs and tags into links
433         l = l.replace(/(\s|^)#([A-Za-z0-9_-]+)/g, '$1<a href="#/tag/$2" class="ref" onclick="return qlink()">#$2</a>');
434         l = l.replace(/(\s|^)@([A-Za-z0-9_-]+)(\/\d+)?/g, '$1<a href="#$2$3" class="ref" onclick="return qlink()">@$2</a>');
435
436         // Create lists when lines begin with *
437         if (l[0] == '*') {
438             if (!listMode) {
439                 var pi = out.lastIndexOf('<p>');
440                 out[pi] = '<ul>';
441                 listMode = true;
442             }
443             l = l.replace(/^\*\s*/, '');
444             out.push('<li>');
445             endpush = '</li>';
446         }
447
448         // Create headers when lines begin with = or #
449         if (l[0] == '=' || l[0] == '#') {
450             var m = l.match(/^([=#]+)/);
451             var depth = m[1].length;
452             if (depth <= 5) {
453                 l = l.replace(/^[=#]+\s*/, '').replace(/\s*[=#]+$/, '');
454                 out.push('<h' + depth + '>');
455                 endpush = '</h' + depth + '>';
456             }
457         }
458
459         // Remove backslashes from escaped metachars
460         l = l.replace(/\\([*\[\]@#])/g, '$1');
461
462         out.push(l);
463         if (endpush) {
464             out.push(endpush);
465             endpush = null;
466         } else {
467             out.push('<br>');
468         }
469     });
470     while (out[out.length - 1] == '<br>' || out[out.length - 1] == '<p>')
471         out.pop();
472     if (listMode)
473         out.push('</ul>');
474
475     record.data = out.join('');
476     record.date = (new Date(record.timestamp * 1000)).toString();
477     record.html = template.evaluate(record);
478 }
479
480 function displayError(msg) {
481     items.innerText = msg;
482 }
483
484
485 // Object for browsing tags
486 function Tag(m) {
487     Pager.call(this);
488     this.type = m[1];
489     this.tag = m[2];
490     this.pageStart = parseInt(m[3]);
491
492     var url = baseURL + "/tag/";
493     switch(this.type) {
494     case 'tag':
495         //url += '%23';
496         url += 'H';  // apache is eating the hash, even encoded.  Probably a security feature.
497         this.baseFrag = '/tag/' + this.tag;
498         break;
499     case 'ref':
500         url += '%40';
501         this.baseFrag = '/ref/' + this.tag;
502         break;
503     default:
504         alert('Invalid tag type: ' + this.type);
505         return;
506     }
507     url += this.tag;
508
509     new Ajax.Request(url, {
510         method: 'get',
511         onSuccess: function(r) {
512             var j = r.responseText.evalJSON();
513             if (j) {
514                 var maxid = j.length - 1;
515                 j.each(function(v, i) {
516                     v.id = maxid - i;
517                     mangleRecord(v, tagRecordTemplate)
518                 });
519                 this.addItems(j);
520                 if (!this.pageStart)
521                     this.pageStart = j.length - 1;
522                 this.itemCount = j.length;
523             }
524             this.displayItems();
525         }.bind(this),
526         onFailure: function(r) {
527             this.displayItems();
528         }.bind(this)
529     });
530
531 }
532 Tag.prototype = new Pager();
533 Tag.prototype.constructor = Tag;
534
535 Tag.prototype.updateState = function(m) {
536     if (this.type != m[1] || this.tag != m[2])
537         return false;
538
539     this.pageStart = parseInt(m[3]) || this.itemCount - 1;
540     this.displayItems();
541
542     return true;
543 }
544
545 Tag.prototype.show = function() {
546     Pager.prototype.show.call(this);
547
548     var ctype = {ref: '@', tag: '#'}[this.type];
549
550     $$('[name=section]').each(function(v) {
551         v.update(' about ' + ctype + this.tag);
552     }.bind(this));
553 }
554
555
556 // Pager for browsing subscription feeds
557 function Feed(m) {
558     Pager.call(this);
559     this.username = loginStatus.username;
560     this.baseFrag = '/feed';
561     this.pageStart = parseInt(m[1]);
562
563     new Ajax.Request(baseURL + '/feed', {
564         method: 'post',
565         parameters: {
566             username: loginStatus.username
567         },
568         onSuccess: function(r) {
569             var response = r.responseText.evalJSON();
570             if (response) {
571                 var maxid = response.length - 1;
572                 response.each(function(v, i) {
573                     v.id = maxid - i;
574                     mangleRecord(v, tagRecordTemplate)
575                 });
576                 this.addItems(response);
577                 if (!this.pageStart)
578                     this.pageStart = response.length - 1;
579                 this.itemCount = response.length;
580                 loginStatus.requestFeedStatus();
581             }
582             this.displayItems();
583         }.bind(this),
584         onFailure: function(r) {
585             this.displayItems();
586         }.bind(this)
587     });
588 }
589 Feed.prototype = new Pager();
590 Feed.prototype.constructor = Feed;
591
592 Feed.prototype.updateState = function(m) {
593     this.pageStart = parseInt(m[1]) || this.itemCount - 1;
594     this.displayItems();
595
596     return true;
597 }
598
599 Feed.prototype.show = function() {
600     Pager.prototype.show.call(this);
601     $$('[name=section]').each(function(v) {
602         v.update(' ' + loginStatus.username + "'s spycam");
603     }.bind(this));
604 }
605
606
607 function postPopup(initial) {
608     if (loginStatus.loggedIn || initial) {
609         var post = $('post');
610         if (post.visible()) {
611             post.hide();
612         } else {
613             post.show();
614             if (initial) {
615                 $('post.content').value = initial;
616             } else if (!$('post.content').value && currentPager.username && currentPager.username != loginStatus.username) {
617                 $('post.content').value = '@' + currentPager.username + ': ';
618             }
619             $('post.content').focus();
620         }
621     }
622 }
623
624 function signup() {
625     var username = $('signup.username').value;
626     var password = $('signup.password').value;
627
628     new Ajax.Request(baseURL + '/create', {
629         parameters: {
630             username: username,
631             password: password
632         },
633         onSuccess: function(r) {
634             $('signup').hide();
635             qlink(username);
636
637             loginStatus.login(username, password);
638         },
639         onFailure: function(r) {
640             alert("Failed to create user");
641         }
642     });
643 }
644
645 function signup_cancel() {
646     $('signup').hide();
647     urlSwitch();
648 }
649
650 function subscribe() {
651     new Ajax.Request(baseURL + '/subscribe/' + currentPager.username, {
652         method: 'post',
653         parameters: {
654             username: loginStatus.username
655         },
656         onSuccess: function(r) {
657             var response = r.responseText.evalJSON();
658             if (response.status == 'success') {
659                 alert("You call " + currentPager.username + " and begin breathing heavily into the handset.");
660                 $$('[name=user.subscribelink]').each(Element.hide);
661                 $$('[name=user.unsubscribelink]').each(Element.show);
662             } else {
663                 alert('Failed to subscribe. This is probably for the best');
664             }
665         },
666         onFailure: function(r) {
667             alert('Failed to subscribe. This is probably for the best');
668         }
669     });
670 }
671
672 function unsubscribe() {
673     new Ajax.Request(baseURL + '/unsubscribe/' + currentPager.username, {
674         method: 'post',
675         parameters: {
676             username: loginStatus.username
677         },
678         onSuccess: function(r) {
679             var response = r.responseText.evalJSON();
680             if (response.status == 'success') {
681                 alert("You come to your senses.");
682                 $$('[name=user.subscribelink]').each(Element.show);
683                 $$('[name=user.unsubscribelink]').each(Element.hide);
684             } else {
685                 alert('You are unable to tear yourself away (because something failed on the server)');
686             }
687         },
688         onFailure: function(r) {
689             alert('You are unable to tear yourself away (because something failed on the server)');
690         }
691     });
692 }
693
694 var resizePostContentTimeout = null;
695 function resizePostContent() {
696     if (resizePostContentTimeout)
697         clearTimeout(resizePostContentTimeout);
698     resizePostContentTimeout = setTimeout(function() {
699         var c = $('post.content');
700         var lines = Math.floor(c.value.length / (100 * (c.clientWidth / 1000))) + 1;
701         var m = c.value.match(/\r?\n/g);
702         if (m)
703             lines += m.length;
704         if (lines <= 3) {
705             c.style.height = "";
706         } else {
707             c.style.height = (lines * 17) + "pt";
708         }
709         resizePostContentTimeout = null;
710     }, 150);
711 }
712
713 var tickerTimer = null;
714 var tickerHead, tickerTail;
715
716 function tickerFader(a, b, p) {
717     var p2 = 1 - p;
718
719     a.style.opacity = p;
720     a.style.lineHeight = (100 * p) + '%';
721
722     b.style.opacity = p2;
723     b.style.lineHeight = (100 * p2) + '%';
724     if (p == 1.0)
725         b.hide();
726 }
727
728 function ticker() {
729     tickerHead.show();
730     Bytex64.FX.run(tickerFader.curry(tickerHead, tickerTail), 0.5);
731     tickerHead = tickerHead.nextSibling;
732     tickerTail = tickerTail.nextSibling;
733     if (tickerHead == null) {
734         stopTicker();
735         loadLatest.delay(10);
736     }
737 }
738
739 function startTicker() {
740     stopTicker();
741     for (var elem = $('latest-posts').firstChild; elem != null; elem = elem.nextSibling) {
742         elem.hide();
743     }
744
745     // Show the first five
746     tickerHead = $('latest-posts').firstChild;
747     for (var i = 0; i < 10 && tickerHead; i++) {
748         tickerHead.show();
749         tickerHead = tickerHead.nextSibling;
750     }
751     tickerTail = $('latest-posts').firstChild;
752     tickerTimer = setInterval(ticker, 5000);
753 }
754
755 function stopTicker() {
756     if (tickerTimer)
757         clearInterval(tickerTimer);
758     tickerTimer = null;
759 }
760
761 function loadLatest() {
762     new Ajax.Request(baseURL + '/latest.json', {
763         method: 'GET',
764         onSuccess: function(r) {
765             var j = r.responseText.evalJSON();
766
767             $('latest-tags').update();
768             j.tags.each(function(v) {
769                 var a = new Element('a', {href: baseURL + '/#/tag/' + v});
770                 a.insert('#' + v);
771                 a.onclick = "return qlink()";
772                 a.className = 'ref';
773                 $('latest-tags').insert(a);
774                 $('latest-tags').appendChild(document.createTextNode(' '));
775             });
776
777             $('latest-posts').update();
778             j.records.each(function(v) {
779                 v.data = v.data.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
780                 v.date = (new Date(v.timestamp * 1000)).toString();
781                 var html = latestRecordsTemplate.evaluate(v);
782                 $('latest-posts').insert(html);
783             });
784             startTicker();
785         }
786     });
787 }
788
789 function qlink(loc) {
790     if (loc) {
791         location.hash = loc;
792     } else if (event && event.target) {
793         location.href = event.target.href;
794     } else {
795         // Bogus qlink
796         return;
797     }
798     lastHash = location.hash;
799     urlSwitch();
800     return false;
801 }
802
803 function Welcome() {
804     loadLatest();
805 }
806
807 Welcome.prototype.show = function() {
808     $$('[name=section]').each(function(v) { v.update('Welcome') });
809     $('welcome').show();
810 }
811
812 Welcome.prototype.hide = function() {
813     stopTicker();
814     $('welcome').hide();
815 }
816
817 function ExternalURLPost(m) {
818     this.title = decodeURIComponent(m[1]).replace(']','').replace('[','');
819     this.url = decodeURIComponent(m[2]);
820 }
821
822 ExternalURLPost.prototype.show = function() {
823     $('post.content').value = '[' + this.title + '](' + this.url + ')';
824     $('post').show();
825 }
826
827 var urlmap = [
828     ['search', /^\?post\/([^/]+)\/(.+)/, ExternalURLPost],
829     ['hash',   /^#\/(ref|tag)\/([A-Za-z0-9_-]+)(?:\/p(\d+))?$/, Tag],
830     ['hash',   /^#\/feed(?:\/p(\d+))?$/, Feed],
831     ['hash',   /^#([A-Za-z0-9_-]+)(?:\/(p)?(\d+))?$/, User]
832 ];
833
834 function urlSwitch() {
835     var m;
836     var pageconstructor;
837
838     for (var i = 0; i < urlmap.length; i++) {
839         if (m = location[urlmap[i][0]].match(urlmap[i][1])) {
840             pageconstructor = urlmap[i][2];
841             break;
842         }
843     }
844     if (i == urlmap.length)
845         pageconstructor = Welcome;
846
847     if (currentPager && currentPager instanceof pageconstructor) {
848         // updateState returns true if the state has been successfully updated.
849         // Otherwise, we continue and create a new instance.
850         if (currentPager.updateState(m))
851             return;
852     }
853
854     if (currentPager && currentPager.hide)
855         currentPager.hide();
856
857     currentPager = new pageconstructor(m);
858     if (currentPager.show)
859         currentPager.show();
860 }
861
862 var lastHash;
863 function hashCheck() {
864     if (location.hash != lastHash) {
865         lastHash = location.hash;
866         urlSwitch();
867     }
868 }
869
870 function init() {
871     items = $('items');
872     loginStatus = new LoginStatus();
873
874     lastHash = location.hash;
875     urlSwitch();
876
877     setInterval(hashCheck, 250);
878
879     document.body.observe('keyup', function(event) {
880         if (event.shiftKey && event.keyCode == 32) {
881             postPopup();
882             event.stop();
883         }
884     });
885     $('post.content').addEventListener('keyup', function(event) {
886         event.stopPropagation();
887     }, true);
888 }