Add front-end password changing bits
[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 passwd() {
651     var old_password = $('passwd.old_password').value;
652     var new_password = $('passwd.new_password').value;
653
654     new Ajax.Request(baseURL + '/passwd', {
655         parameters: {
656             username: loginStatus.username,
657             password: old_password,
658             new_password: new_password
659         },
660         onSuccess: function(r) {
661             if (r.responseJSON.status == 'success') {
662                 alert('Password changed');
663                 passwd_cancel();
664             } else {
665                 alert('Password change failed.  Your password has NOT been changed.');
666             }
667         },
668         onFailure: function(r) {
669             alert('Password change error');
670         }
671     });
672 }
673
674 function passwd_cancel() {
675     $('passwd').hide();
676     $('navigation').show();
677     urlSwitch();
678 }
679
680 function subscribe() {
681     new Ajax.Request(baseURL + '/subscribe/' + currentPager.username, {
682         method: 'post',
683         parameters: {
684             username: loginStatus.username
685         },
686         onSuccess: function(r) {
687             var response = r.responseText.evalJSON();
688             if (response.status == 'success') {
689                 alert("You call " + currentPager.username + " and begin breathing heavily into the handset.");
690                 $$('[name=user.subscribelink]').each(Element.hide);
691                 $$('[name=user.unsubscribelink]').each(Element.show);
692             } else {
693                 alert('Failed to subscribe. This is probably for the best');
694             }
695         },
696         onFailure: function(r) {
697             alert('Failed to subscribe. This is probably for the best');
698         }
699     });
700 }
701
702 function unsubscribe() {
703     new Ajax.Request(baseURL + '/unsubscribe/' + currentPager.username, {
704         method: 'post',
705         parameters: {
706             username: loginStatus.username
707         },
708         onSuccess: function(r) {
709             var response = r.responseText.evalJSON();
710             if (response.status == 'success') {
711                 alert("You come to your senses.");
712                 $$('[name=user.subscribelink]').each(Element.show);
713                 $$('[name=user.unsubscribelink]').each(Element.hide);
714             } else {
715                 alert('You are unable to tear yourself away (because something failed on the server)');
716             }
717         },
718         onFailure: function(r) {
719             alert('You are unable to tear yourself away (because something failed on the server)');
720         }
721     });
722 }
723
724 var resizePostContentTimeout = null;
725 function resizePostContent() {
726     if (resizePostContentTimeout)
727         clearTimeout(resizePostContentTimeout);
728     resizePostContentTimeout = setTimeout(function() {
729         var c = $('post.content');
730         var lines = Math.floor(c.value.length / (100 * (c.clientWidth / 1000))) + 1;
731         var m = c.value.match(/\r?\n/g);
732         if (m)
733             lines += m.length;
734         if (lines <= 3) {
735             c.style.height = "";
736         } else {
737             c.style.height = (lines * 17) + "pt";
738         }
739         resizePostContentTimeout = null;
740     }, 150);
741 }
742
743 var tickerTimer = null;
744 var tickerHead, tickerTail;
745
746 function tickerFader(a, b, p) {
747     var p2 = 1 - p;
748
749     a.style.opacity = p;
750     a.style.lineHeight = (100 * p) + '%';
751
752     b.style.opacity = p2;
753     b.style.lineHeight = (100 * p2) + '%';
754     if (p == 1.0)
755         b.hide();
756 }
757
758 function ticker() {
759     tickerHead.show();
760     Bytex64.FX.run(tickerFader.curry(tickerHead, tickerTail), 0.5);
761     tickerHead = tickerHead.nextSibling;
762     tickerTail = tickerTail.nextSibling;
763     if (tickerHead == null) {
764         stopTicker();
765         loadLatest.delay(10);
766     }
767 }
768
769 function startTicker() {
770     stopTicker();
771     for (var elem = $('latest-posts').firstChild; elem != null; elem = elem.nextSibling) {
772         elem.hide();
773     }
774
775     // Show the first five
776     tickerHead = $('latest-posts').firstChild;
777     for (var i = 0; i < 10 && tickerHead; i++) {
778         tickerHead.show();
779         tickerHead = tickerHead.nextSibling;
780     }
781     tickerTail = $('latest-posts').firstChild;
782     tickerTimer = setInterval(ticker, 5000);
783 }
784
785 function stopTicker() {
786     if (tickerTimer)
787         clearInterval(tickerTimer);
788     tickerTimer = null;
789 }
790
791 function loadLatest() {
792     new Ajax.Request(baseURL + '/latest.json', {
793         method: 'GET',
794         onSuccess: function(r) {
795             var j = r.responseText.evalJSON();
796
797             $('latest-tags').update();
798             j.tags.each(function(v) {
799                 var a = new Element('a', {href: baseURL + '/#/tag/' + v});
800                 a.insert('#' + v);
801                 a.onclick = "return qlink()";
802                 a.className = 'ref';
803                 $('latest-tags').insert(a);
804                 $('latest-tags').appendChild(document.createTextNode(' '));
805             });
806
807             $('latest-posts').update();
808             j.records.each(function(v) {
809                 v.data = v.data.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
810                 v.date = (new Date(v.timestamp * 1000)).toString();
811                 var html = latestRecordsTemplate.evaluate(v);
812                 $('latest-posts').insert(html);
813             });
814             startTicker();
815         }
816     });
817 }
818
819 function qlink(loc) {
820     if (loc) {
821         location.hash = loc;
822     } else if (event && event.target) {
823         location.href = event.target.href;
824     } else {
825         // Bogus qlink
826         return;
827     }
828     lastHash = location.hash;
829     urlSwitch();
830     return false;
831 }
832
833 function Welcome() {
834     loadLatest();
835 }
836
837 Welcome.prototype.show = function() {
838     $$('[name=section]').each(function(v) { v.update('Welcome') });
839     $('welcome').show();
840 }
841
842 Welcome.prototype.hide = function() {
843     stopTicker();
844     $('welcome').hide();
845 }
846
847 Welcome.prototype.updateState = function() {
848     this.show();
849 }
850
851 function ExternalURLPost(m) {
852     this.title = decodeURIComponent(m[1]).replace(']','').replace('[','');
853     this.url = decodeURIComponent(m[2]);
854 }
855
856 ExternalURLPost.prototype.show = function() {
857     $('post.content').value = '[' + this.title + '](' + this.url + ')';
858     $('post').show();
859 }
860
861 var urlmap = [
862     ['search', /^\?post\/([^/]+)\/(.+)/, ExternalURLPost],
863     ['hash',   /^#\/(ref|tag)\/([A-Za-z0-9_-]+)(?:\/p(\d+))?$/, Tag],
864     ['hash',   /^#\/feed(?:\/p(\d+))?$/, Feed],
865     ['hash',   /^#([A-Za-z0-9_-]+)(?:\/(p)?(\d+))?$/, User]
866 ];
867
868 function urlSwitch() {
869     var m;
870     var pageconstructor;
871
872     for (var i = 0; i < urlmap.length; i++) {
873         if (m = location[urlmap[i][0]].match(urlmap[i][1])) {
874             pageconstructor = urlmap[i][2];
875             break;
876         }
877     }
878     if (i == urlmap.length)
879         pageconstructor = Welcome;
880
881     if (currentPager && currentPager instanceof pageconstructor) {
882         // updateState returns true if the state has been successfully updated.
883         // Otherwise, we continue and create a new instance.
884         if (currentPager.updateState(m))
885             return;
886     }
887
888     if (currentPager && currentPager.hide)
889         currentPager.hide();
890
891     currentPager = new pageconstructor(m);
892     if (currentPager.show)
893         currentPager.show();
894 }
895
896 var lastHash;
897 function hashCheck() {
898     if (location.hash != lastHash) {
899         lastHash = location.hash;
900         urlSwitch();
901     }
902 }
903
904 function init() {
905     items = $('items');
906     loginStatus = new LoginStatus();
907
908     lastHash = location.hash;
909     urlSwitch();
910
911     setInterval(hashCheck, 250);
912
913     document.body.observe('keyup', function(event) {
914         if (event.shiftKey && event.keyCode == 32) {
915             postPopup();
916             event.stop();
917         }
918     });
919     $('post.content').addEventListener('keyup', function(event) {
920         event.stopPropagation();
921     }, true);
922 }