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.
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>'
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>'
13 var latestRecordsTemplate = new Template(
14 '<div class="record"><a class="author ref" href="' + baseURL + '/\##{author}" onclick="return qlink()">@#{author}</a> #{data}</div>'
24 // Object to keep track of login status
25 function LoginStatus() {
27 document.cookie.split(/;\s+/).each(function(v) {
29 cookies[kv[0]] = kv[1];
31 if (cookies.auth && cookies.username) {
33 this.username = cookies.username;
34 this.requestFeedStatus();
35 this.feedStatusUpdateInterval = setInterval(this.requestFeedStatus.bind(this), 900000);
37 this.loggedIn = false;
43 LoginStatus.prototype.login = function(username, password) {
44 new Ajax.Request(baseURL + '/login', {
49 onSuccess: function(r) {
50 var j = r.responseText.evalJSON();
51 if (j && j.status == 'success') {
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);
60 alert("Could not log in");
61 $('login.username').focus();
64 onFailure: function(r) {
65 alert("Could not log in");
66 $('login.username').focus();
71 LoginStatus.prototype.logout = function() {
72 new Ajax.Request(baseURL + '/logout', {
74 username: this.username
76 onComplete: function(r) {
77 this.loggedIn = false;
78 document.cookie = "auth=; expires=1-Jan-1970 00:00:00 GMT";
80 clearInterval(this.feedStatusUpdateInterval);
83 document.cookie = "username=; expires=1-Jan-1970 00:00:00 GMT";
86 LoginStatus.prototype.update = function() {
88 $('userlink').href = baseURL + '/#' + this.username;
89 $('userlink').update('@' + this.username);
90 $('reflink').href = baseURL + '/#/ref/' + this.username;
100 LoginStatus.prototype.post = function(msg) {
101 if (!this.loggedIn) {
102 alert("You are not logged in!");
106 new Ajax.Request(baseURL + '/put', {
108 username: this.username,
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);
118 currentPager.itemCount++;
119 currentPager.reload();
122 alert('Post failed!');
125 onFailure: function(r) {
126 alert('Post failed!');
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();
137 $('newFeedMessages').update('(' + j['new'] + ' new)');
139 $('newFeedMessages').update('');
145 // Base object for paged data
147 this.itemsPerPage = 10;
148 this.itemCache = new Hash();
149 this.pageStart = null;
152 Pager.prototype.updateState = function(m) {
156 Pager.prototype.show = function() {
160 Pager.prototype.hide = function() {
163 $('newer_link').hide();
164 $('older_link').hide();
167 Pager.prototype.olderPage = function() {
168 if (this.pageStart >= this.itemsPerPage) {
169 qlink(this.baseFrag + '/p' + (this.pageStart - this.itemsPerPage));
173 Pager.prototype.newerPage = function() {
174 if (this.pageStart + this.itemsPerPage < this.itemCount) {
175 qlink(this.baseFrag + '/p' + (this.pageStart + this.itemsPerPage));
179 Pager.prototype.addItems = function(items) {
180 items.each(function(v) {
181 if (!this.itemCache[v.id])
182 this.itemCache[v.id] = v;
186 Pager.prototype.displayItems = function() {
187 if (this.pageStart == undefined)
188 this.pageStart == this.itemCount - 1;
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);
197 items.insert("There doesn't seem to be anything here!");
200 if (this.pageStart < this.itemCount - 1) {
201 $('newer_link').href = baseURL + '/#' + this.baseFrag + '/p' + (this.pageStart + this.itemsPerPage);
202 $('newer_link').show();
204 $('newer_link').hide();
207 if (this.pageStart >= 10) {
208 $('older_link').href = baseURL + '/#' + this.baseFrag + '/p' + (this.pageStart - this.itemsPerPage);
209 $('older_link').show();
211 $('older_link').hide();
214 document.body.scrollTo();
217 Pager.prototype.reload = function() {
218 this.pageStart = null;
219 this.loadItems(null, null, Pager.prototype.showPageAt.bind(this, this.itemCount - 1));
222 Pager.prototype.showPageAt = function(r) {
223 var end = (r - 9 > 0 ? r - 9 : 0);
224 if (this.itemCache[r] && this.itemCache[end]) {
228 this.loadItems((r >= 49 ? r - 49 : 0), r, Pager.prototype.showPageAt.bind(this, r));
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);
238 this.loadItems(r, r, Pager.prototype.showRecord.bind(this, r));
242 Pager.prototype.loadItems = function(from, to, continuation) { }
245 // Object to render user pages
248 this.username = m[1];
249 this.baseFrag = m[1];
250 this.permalink = (m[2] != 'p');
251 this.pageStart = parseInt(m[3]);
253 User.prototype = new Pager();
254 User.prototype.constructor = User;
256 User.prototype.updateState = function(m) {
257 if (m[1] != this.username)
260 this.permalink = (m[2] != 'p');
261 this.pageStart = parseInt(m[3]);
267 User.prototype.show = function() {
268 Pager.prototype.show.call(this);
270 $$('[name=section]').each(function(v) { v.update(' @' + this.username) }.bind(this));
272 $('rsslink').href = '/rss/' + this.username;
273 $$('[name=user.reflink]').each(function(e) {
274 e.href = baseURL + '/#/ref/' + this.username;
276 $('usercontrols').show();
278 if (this.permalink && this.pageStart >= 0) {
279 this.showRecord(this.pageStart);
280 } else if (this.pageStart >= 0) {
281 this.showPageAt(this.pageStart);
287 User.prototype.hide = function() {
288 Pager.prototype.hide.call(this);
291 $('usercontrols').hide();
294 User.prototype.reload = function() {
295 this.pageStart = null;
297 $$('[name=user.subscribelink]').each(Element.hide);
298 $$('[name=user.unsubscribelink]').each(Element.hide);
300 if (loginStatus.loggedIn) {
301 new Ajax.Request(baseURL + '/feedinfo/' + this.username, {
304 username: loginStatus.username
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);
312 $$('[name=user.subscribelink]').each(Element.show);
313 $$('[name=user.unsubscribelink]').each(Element.hide);
319 new Ajax.Request(baseURL + '/info/' + this.username, {
321 onSuccess: function(r) {
322 var response = r.responseText.evalJSON();
324 this.itemCount = parseInt(response.record_count);
325 this.showPageAt(this.itemCount - 1);
331 User.prototype.loadItems = function(from, to, continuation) {
336 if (from != undefined && to != undefined) {
337 url = baseURL + '/get/' + this.username + '/' + from + '-' + to;
340 url = baseURL + '/get/' + this.username;
343 new Ajax.Request(url, {
345 onSuccess: function(r) {
346 var records = r.responseText.evalJSON();
347 if (records && records.length > 0) {
348 records.each(function(v) {
350 v.author = this.username;
351 mangleRecord(v, recordTemplate);
353 this.addItems(records);
355 this.pageStart = records[0].recInt;
359 onFailure: function(r) {
363 displayError('User not found');
368 function mangleRecord(record, template) {
369 record.recInt = parseInt(record.record);
371 var lines = record.data.split(/\r?\n/);
372 if (lines[lines.length - 1] == '')
377 var listMode = false;
378 lines.each(function(l) {
380 if (out[out.length - 1] == '<br>') {
381 out[out.length - 1] = '<p>';
383 if (out[out.length - 1] == '</li>') {
391 // Put quoted material into a special paragraph
393 var pi = out.lastIndexOf('<p>');
395 out[pi] = '<p class="quote">';
396 l = l.replace(/^>\s*/, '');
400 // Sanitize HTML input
401 l = l.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
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>');
406 // Turn markdown links into links
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>');
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>');
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>');
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>');
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>');
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');
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>');
436 // Create lists when lines begin with *
439 var pi = out.lastIndexOf('<p>');
443 l = l.replace(/^\*\s*/, '');
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;
453 l = l.replace(/^[=#]+\s*/, '').replace(/\s*[=#]+$/, '');
454 out.push('<h' + depth + '>');
455 endpush = '</h' + depth + '>';
459 // Remove backslashes from escaped metachars
460 l = l.replace(/\\([*\[\]@#])/g, '$1');
470 while (out[out.length - 1] == '<br>' || out[out.length - 1] == '<p>')
475 record.data = out.join('');
476 record.date = (new Date(record.timestamp * 1000)).toString();
477 record.html = template.evaluate(record);
480 function displayError(msg) {
481 items.innerText = msg;
485 // Object for browsing tags
490 this.pageStart = parseInt(m[3]);
492 var url = baseURL + "/tag/";
496 url += 'H'; // apache is eating the hash, even encoded. Probably a security feature.
497 this.baseFrag = '/tag/' + this.tag;
501 this.baseFrag = '/ref/' + this.tag;
504 alert('Invalid tag type: ' + this.type);
509 new Ajax.Request(url, {
511 onSuccess: function(r) {
512 var j = r.responseText.evalJSON();
514 var maxid = j.length - 1;
515 j.each(function(v, i) {
517 mangleRecord(v, tagRecordTemplate)
521 this.pageStart = j.length - 1;
522 this.itemCount = j.length;
526 onFailure: function(r) {
532 Tag.prototype = new Pager();
533 Tag.prototype.constructor = Tag;
535 Tag.prototype.updateState = function(m) {
536 if (this.type != m[1] || this.tag != m[2])
539 this.pageStart = parseInt(m[3]) || this.itemCount - 1;
545 Tag.prototype.show = function() {
546 Pager.prototype.show.call(this);
548 var ctype = {ref: '@', tag: '#'}[this.type];
550 $$('[name=section]').each(function(v) {
551 v.update(' about ' + ctype + this.tag);
556 // Pager for browsing subscription feeds
559 this.username = loginStatus.username;
560 this.baseFrag = '/feed';
561 this.pageStart = parseInt(m[1]);
563 new Ajax.Request(baseURL + '/feed', {
566 username: loginStatus.username
568 onSuccess: function(r) {
569 var response = r.responseText.evalJSON();
571 var maxid = response.length - 1;
572 response.each(function(v, i) {
574 mangleRecord(v, tagRecordTemplate)
576 this.addItems(response);
578 this.pageStart = response.length - 1;
579 this.itemCount = response.length;
580 loginStatus.requestFeedStatus();
584 onFailure: function(r) {
589 Feed.prototype = new Pager();
590 Feed.prototype.constructor = Feed;
592 Feed.prototype.updateState = function(m) {
593 this.pageStart = parseInt(m[1]) || this.itemCount - 1;
599 Feed.prototype.show = function() {
600 Pager.prototype.show.call(this);
601 $$('[name=section]').each(function(v) {
602 v.update(' ' + loginStatus.username + "'s spycam");
607 function postPopup(initial) {
608 if (loginStatus.loggedIn || initial) {
609 var post = $('post');
610 if (post.visible()) {
615 $('post.content').value = initial;
616 } else if (!$('post.content').value && currentPager.username && currentPager.username != loginStatus.username) {
617 $('post.content').value = '@' + currentPager.username + ': ';
619 $('post.content').focus();
625 var username = $('signup.username').value;
626 var password = $('signup.password').value;
628 new Ajax.Request(baseURL + '/create', {
633 onSuccess: function(r) {
637 loginStatus.login(username, password);
639 onFailure: function(r) {
640 alert("Failed to create user");
645 function signup_cancel() {
651 var old_password = $('passwd.old_password').value;
652 var new_password = $('passwd.new_password').value;
654 new Ajax.Request(baseURL + '/passwd', {
656 username: loginStatus.username,
657 password: old_password,
658 new_password: new_password
660 onSuccess: function(r) {
661 if (r.responseJSON.status == 'success') {
662 alert('Password changed');
665 alert('Password change failed. Your password has NOT been changed.');
668 onFailure: function(r) {
669 alert('Password change error');
674 function passwd_cancel() {
676 $('navigation').show();
680 function subscribe() {
681 new Ajax.Request(baseURL + '/subscribe/' + currentPager.username, {
684 username: loginStatus.username
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);
693 alert('Failed to subscribe. This is probably for the best');
696 onFailure: function(r) {
697 alert('Failed to subscribe. This is probably for the best');
702 function unsubscribe() {
703 new Ajax.Request(baseURL + '/unsubscribe/' + currentPager.username, {
706 username: loginStatus.username
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);
715 alert('You are unable to tear yourself away (because something failed on the server)');
718 onFailure: function(r) {
719 alert('You are unable to tear yourself away (because something failed on the server)');
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);
737 c.style.height = (lines * 17) + "pt";
739 resizePostContentTimeout = null;
743 var tickerTimer = null;
744 var tickerHead, tickerTail;
746 function tickerFader(a, b, p) {
750 a.style.lineHeight = (100 * p) + '%';
752 b.style.opacity = p2;
753 b.style.lineHeight = (100 * p2) + '%';
760 Bytex64.FX.run(tickerFader.curry(tickerHead, tickerTail), 0.5);
761 tickerHead = tickerHead.nextSibling;
762 tickerTail = tickerTail.nextSibling;
763 if (tickerHead == null) {
765 loadLatest.delay(10);
769 function startTicker() {
771 for (var elem = $('latest-posts').firstChild; elem != null; elem = elem.nextSibling) {
775 // Show the first five
776 tickerHead = $('latest-posts').firstChild;
777 for (var i = 0; i < 10 && tickerHead; i++) {
779 tickerHead = tickerHead.nextSibling;
781 tickerTail = $('latest-posts').firstChild;
782 tickerTimer = setInterval(ticker, 5000);
785 function stopTicker() {
787 clearInterval(tickerTimer);
791 function loadLatest() {
792 new Ajax.Request(baseURL + '/latest.json', {
794 onSuccess: function(r) {
795 var j = r.responseText.evalJSON();
797 $('latest-tags').update();
798 j.tags.each(function(v) {
799 var a = new Element('a', {href: baseURL + '/#/tag/' + v});
801 a.onclick = "return qlink()";
803 $('latest-tags').insert(a);
804 $('latest-tags').appendChild(document.createTextNode(' '));
807 $('latest-posts').update();
808 j.records.each(function(v) {
809 v.data = v.data.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
810 v.date = (new Date(v.timestamp * 1000)).toString();
811 var html = latestRecordsTemplate.evaluate(v);
812 $('latest-posts').insert(html);
819 function qlink(loc) {
822 } else if (event && event.target) {
823 location.href = event.target.href;
828 lastHash = location.hash;
837 Welcome.prototype.show = function() {
838 $$('[name=section]').each(function(v) { v.update('Welcome') });
842 Welcome.prototype.hide = function() {
847 Welcome.prototype.updateState = function() {
851 function ExternalURLPost(m) {
852 this.title = decodeURIComponent(m[1]).replace(']','').replace('[','');
853 this.url = decodeURIComponent(m[2]);
856 ExternalURLPost.prototype.show = function() {
857 $('post.content').value = '[' + this.title + '](' + this.url + ')';
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]
868 function urlSwitch() {
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];
878 if (i == urlmap.length)
879 pageconstructor = Welcome;
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))
888 if (currentPager && currentPager.hide)
891 currentPager = new pageconstructor(m);
892 if (currentPager.show)
897 function hashCheck() {
898 if (location.hash != lastHash) {
899 lastHash = location.hash;
906 loginStatus = new LoginStatus();
908 lastHash = location.hash;
911 setInterval(hashCheck, 250);
913 document.body.observe('keyup', function(event) {
914 if (event.shiftKey && event.keyCode == 32) {
919 $('post.content').addEventListener('keyup', function(event) {
920 event.stopPropagation();