Add new message bits to web
[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         l = l.replace(/(\s|^)\[([^\]]+)\]\((https?:\/\/[a-zA-Z0-9.-]*[a-zA-Z0-9](\/[^)"]*?)?)\)/g, '$1<a href="$3">$2</a>');
408
409         // Turn *foo* into italics and **foo** into bold
410         l = l.replace(/([^\w\\]|^)\*\*(\w[^*]*)\*\*(\W|$)/g, '$1<b>$2</b>$3');
411         l = l.replace(/([^\w\\]|^)\*(\w[^*]*)\*(\W|$)/g, '$1<i>$2</i>$3');
412
413         // Turn refs and tags into links
414         l = l.replace(/(\s|^)#([A-Za-z0-9_-]+)/g, '$1<a href="#/tag/$2" class="ref" onclick="return qlink()">#$2</a>');
415         l = l.replace(/(\s|^)@([A-Za-z0-9_-]+)(\/\d+)?/g, '$1<a href="#$2$3" class="ref" onclick="return qlink()">@$2</a>');
416
417         // Create lists when lines begin with *
418         if (l[0] == '*') {
419             if (!listMode) {
420                 var pi = out.lastIndexOf('<p>');
421                 out[pi] = '<ul>';
422                 listMode = true;
423             }
424             l = l.replace(/^\*\s*/, '');
425             out.push('<li>');
426             endpush = '</li>';
427         }
428
429         // Create headers when lines begin with = or #
430         if (l[0] == '=' || l[0] == '#') {
431             var m = l.match(/^([=#]+)/);
432             var depth = m[1].length;
433             if (depth <= 5) {
434                 l = l.replace(/^[=#]+\s*/, '').replace(/\s*[=#]+$/, '');
435                 out.push('<h' + depth + '>');
436                 endpush = '</h' + depth + '>';
437             }
438         }
439
440         // Remove backslashes from escaped metachars
441         l = l.replace(/\\([*\[\]@#])/g, '$1');
442
443         out.push(l);
444         if (endpush) {
445             out.push(endpush);
446             endpush = null;
447         } else {
448             out.push('<br>');
449         }
450     });
451     while (out[out.length - 1] == '<br>' || out[out.length - 1] == '<p>')
452         out.pop();
453     if (listMode)
454         out.push('</ul>');
455
456     record.data = out.join('');
457     record.date = (new Date(record.timestamp * 1000)).toString();
458     record.html = template.evaluate(record);
459 }
460
461 function displayError(msg) {
462     items.innerText = msg;
463 }
464
465
466 // Object for browsing tags
467 function Tag(m) {
468     Pager.call(this);
469     this.type = m[1];
470     this.tag = m[2];
471     this.pageStart = parseInt(m[3]);
472
473     var url = baseURL + "/tag/";
474     switch(this.type) {
475     case 'tag':
476         //url += '%23';
477         url += 'H';  // apache is eating the hash, even encoded.  Probably a security feature.
478         this.baseFrag = '/tag/' + this.tag;
479         break;
480     case 'ref':
481         url += '%40';
482         this.baseFrag = '/ref/' + this.tag;
483         break;
484     default:
485         alert('Invalid tag type: ' + this.type);
486         return;
487     }
488     url += this.tag;
489
490     new Ajax.Request(url, {
491         method: 'get',
492         onSuccess: function(r) {
493             var j = r.responseText.evalJSON();
494             if (j) {
495                 var maxid = j.length - 1;
496                 j.each(function(v, i) {
497                     v.id = maxid - i;
498                     mangleRecord(v, tagRecordTemplate)
499                 });
500                 this.addItems(j);
501                 if (!this.pageStart)
502                     this.pageStart = j.length - 1;
503                 this.itemCount = j.length;
504             }
505             this.displayItems();
506         }.bind(this),
507         onFailure: function(r) {
508             this.displayItems();
509         }.bind(this)
510     });
511
512 }
513 Tag.prototype = new Pager();
514 Tag.prototype.constructor = Tag;
515
516 Tag.prototype.updateState = function(m) {
517     if (this.type != m[1] || this.tag != m[2])
518         return false;
519
520     this.pageStart = parseInt(m[3]) || this.itemCount - 1;
521     this.displayItems();
522
523     return true;
524 }
525
526 Tag.prototype.show = function() {
527     Pager.prototype.show.call(this);
528
529     var ctype = {ref: '@', tag: '#'}[this.type];
530
531     $$('[name=section]').each(function(v) {
532         v.update(' about ' + ctype + this.tag);
533     }.bind(this));
534 }
535
536
537 // Pager for browsing subscription feeds
538 function Feed(m) {
539     Pager.call(this);
540     this.username = loginStatus.username;
541     this.baseFrag = '/feed';
542     this.pageStart = parseInt(m[1]);
543
544     new Ajax.Request(baseURL + '/feed', {
545         method: 'post',
546         parameters: {
547             username: loginStatus.username
548         },
549         onSuccess: function(r) {
550             var response = r.responseText.evalJSON();
551             if (response) {
552                 var maxid = response.length - 1;
553                 response.each(function(v, i) {
554                     v.id = maxid - i;
555                     mangleRecord(v, tagRecordTemplate)
556                 });
557                 this.addItems(response);
558                 if (!this.pageStart)
559                     this.pageStart = response.length - 1;
560                 this.itemCount = response.length;
561                 loginStatus.requestFeedStatus();
562             }
563             this.displayItems();
564         }.bind(this),
565         onFailure: function(r) {
566             this.displayItems();
567         }.bind(this)
568     });
569 }
570 Feed.prototype = new Pager();
571 Feed.prototype.constructor = Feed;
572
573 Feed.prototype.updateState = function(m) {
574     this.pageStart = parseInt(m[1]) || this.itemCount - 1;
575     this.displayItems();
576
577     return true;
578 }
579
580 Feed.prototype.show = function() {
581     Pager.prototype.show.call(this);
582     $$('[name=section]').each(function(v) {
583         v.update(' ' + loginStatus.username + "'s spycam");
584     }.bind(this));
585 }
586
587
588 function postPopup(initial) {
589     if (loginStatus.loggedIn || initial) {
590         var post = $('post');
591         if (post.visible()) {
592             post.hide();
593         } else {
594             post.show();
595             if (initial) {
596                 $('post.content').value = initial;
597             } else if (!$('post.content').value && currentPager.username && currentPager.username != loginStatus.username) {
598                 $('post.content').value = '@' + currentPager.username + ': ';
599             }
600             $('post.content').focus();
601         }
602     }
603 }
604
605 function signup() {
606     var username = $('signup.username').value;
607     var password = $('signup.password').value;
608
609     new Ajax.Request(baseURL + '/create', {
610         parameters: {
611             username: username,
612             password: password
613         },
614         onSuccess: function(r) {
615             $('signup').hide();
616             qlink(username);
617
618             loginStatus.login(username, password);
619         },
620         onFailure: function(r) {
621             alert("Failed to create user");
622         }
623     });
624 }
625
626 function signup_cancel() {
627     $('signup').hide();
628     urlSwitch();
629 }
630
631 function subscribe() {
632     new Ajax.Request(baseURL + '/subscribe/' + currentPager.username, {
633         method: 'post',
634         parameters: {
635             username: loginStatus.username
636         },
637         onSuccess: function(r) {
638             var response = r.responseText.evalJSON();
639             if (response.status == 'success') {
640                 alert("You call " + currentPager.username + " and begin breathing heavily into the handset.");
641                 $$('[name=user.subscribelink]').each(Element.hide);
642                 $$('[name=user.unsubscribelink]').each(Element.show);
643             } else {
644                 alert('Failed to subscribe. This is probably for the best');
645             }
646         },
647         onFailure: function(r) {
648             alert('Failed to subscribe. This is probably for the best');
649         }
650     });
651 }
652
653 function unsubscribe() {
654     new Ajax.Request(baseURL + '/unsubscribe/' + currentPager.username, {
655         method: 'post',
656         parameters: {
657             username: loginStatus.username
658         },
659         onSuccess: function(r) {
660             var response = r.responseText.evalJSON();
661             if (response.status == 'success') {
662                 alert("You come to your senses.");
663                 $$('[name=user.subscribelink]').each(Element.show);
664                 $$('[name=user.unsubscribelink]').each(Element.hide);
665             } else {
666                 alert('You are unable to tear yourself away (because something failed on the server)');
667             }
668         },
669         onFailure: function(r) {
670             alert('You are unable to tear yourself away (because something failed on the server)');
671         }
672     });
673 }
674
675 var resizePostContentTimeout = null;
676 function resizePostContent() {
677     if (resizePostContentTimeout)
678         clearTimeout(resizePostContentTimeout);
679     resizePostContentTimeout = setTimeout(function() {
680         var c = $('post.content');
681         var lines = Math.floor(c.value.length / (100 * (c.clientWidth / 1000))) + 1;
682         var m = c.value.match(/\r?\n/g);
683         if (m)
684             lines += m.length;
685         if (lines <= 3) {
686             c.style.height = "";
687         } else {
688             c.style.height = (lines * 17) + "pt";
689         }
690         resizePostContentTimeout = null;
691     }, 150);
692 }
693
694 var tickerTimer = null;
695 var tickerHead, tickerTail;
696
697 function tickerFader(a, b, p) {
698     var p2 = 1 - p;
699
700     a.style.opacity = p;
701     a.style.lineHeight = (100 * p) + '%';
702
703     b.style.opacity = p2;
704     b.style.lineHeight = (100 * p2) + '%';
705     if (p == 1.0)
706         b.hide();
707 }
708
709 function ticker() {
710     tickerHead.show();
711     Bytex64.FX.run(tickerFader.curry(tickerHead, tickerTail), 0.5);
712     tickerHead = tickerHead.nextSibling;
713     tickerTail = tickerTail.nextSibling;
714     if (tickerHead == null) {
715         stopTicker();
716         loadLatest.delay(10);
717     }
718 }
719
720 function startTicker() {
721     stopTicker();
722     for (var elem = $('latest-posts').firstChild; elem != null; elem = elem.nextSibling) {
723         elem.hide();
724     }
725
726     // Show the first five
727     tickerHead = $('latest-posts').firstChild;
728     for (var i = 0; i < 10 && tickerHead; i++) {
729         tickerHead.show();
730         tickerHead = tickerHead.nextSibling;
731     }
732     tickerTail = $('latest-posts').firstChild;
733     tickerTimer = setInterval(ticker, 5000);
734 }
735
736 function stopTicker() {
737     if (tickerTimer)
738         clearInterval(tickerTimer);
739     tickerTimer = null;
740 }
741
742 function loadLatest() {
743     new Ajax.Request(baseURL + '/latest.json', {
744         method: 'GET',
745         onSuccess: function(r) {
746             var j = r.responseText.evalJSON();
747
748             $('latest-tags').update();
749             j.tags.each(function(v) {
750                 var a = new Element('a', {href: baseURL + '/#/tag/' + v});
751                 a.insert('#' + v);
752                 a.onclick = "return qlink()";
753                 a.className = 'ref';
754                 $('latest-tags').insert(a);
755                 $('latest-tags').appendChild(document.createTextNode(' '));
756             });
757
758             $('latest-posts').update();
759             j.records.each(function(v) {
760                 v.data = v.data.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
761                 v.date = (new Date(v.timestamp * 1000)).toString();
762                 var html = latestRecordsTemplate.evaluate(v);
763                 $('latest-posts').insert(html);
764             });
765             startTicker();
766         }
767     });
768 }
769
770 function qlink(loc) {
771     if (loc) {
772         location.hash = loc;
773     } else if (event && event.target) {
774         location.href = event.target.href;
775     } else {
776         // Bogus qlink
777         return;
778     }
779     lastHash = location.hash;
780     urlSwitch();
781     return false;
782 }
783
784 function Welcome() {
785     loadLatest();
786 }
787
788 Welcome.prototype.show = function() {
789     $$('[name=section]').each(function(v) { v.update('Welcome') });
790     $('welcome').show();
791 }
792
793 Welcome.prototype.hide = function() {
794     stopTicker();
795     $('welcome').hide();
796 }
797
798 function ExternalURLPost(m) {
799     this.title = decodeURIComponent(m[1]).replace(']','').replace('[','');
800     this.url = decodeURIComponent(m[2]);
801 }
802
803 ExternalURLPost.prototype.show = function() {
804     $('post.content').value = '[' + this.title + '](' + this.url + ')';
805     $('post').show();
806 }
807
808 var urlmap = [
809     ['search', /^\?post\/([^/]+)\/(.+)/, ExternalURLPost],
810     ['hash',   /^#\/(ref|tag)\/([A-Za-z0-9_-]+)(?:\/p(\d+))?$/, Tag],
811     ['hash',   /^#\/feed(?:\/p(\d+))?$/, Feed],
812     ['hash',   /^#([A-Za-z0-9_-]+)(?:\/(p)?(\d+))?$/, User]
813 ];
814
815 function urlSwitch() {
816     var m;
817     var pageconstructor;
818
819     for (var i = 0; i < urlmap.length; i++) {
820         if (m = location[urlmap[i][0]].match(urlmap[i][1])) {
821             pageconstructor = urlmap[i][2];
822             break;
823         }
824     }
825     if (i == urlmap.length)
826         pageconstructor = Welcome;
827
828     if (currentPager && currentPager instanceof pageconstructor) {
829         // updateState returns true if the state has been successfully updated.
830         // Otherwise, we continue and create a new instance.
831         if (currentPager.updateState(m))
832             return;
833     }
834
835     if (currentPager && currentPager.hide)
836         currentPager.hide();
837
838     currentPager = new pageconstructor(m);
839     if (currentPager.show)
840         currentPager.show();
841 }
842
843 var lastHash;
844 function hashCheck() {
845     if (location.hash != lastHash) {
846         lastHash = location.hash;
847         urlSwitch();
848     }
849 }
850
851 function init() {
852     items = $('items');
853     loginStatus = new LoginStatus();
854
855     lastHash = location.hash;
856     urlSwitch();
857
858     setInterval(hashCheck, 250);
859
860     document.body.observe('keyup', function(event) {
861         if (event.shiftKey && event.keyCode == 32) {
862             postPopup();
863             event.stop();
864         }
865     });
866     $('post.content').addEventListener('keyup', function(event) {
867         event.stopPropagation();
868     }, true);
869 }