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