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() {
650 function subscribe() {
651 new Ajax.Request(baseURL + '/subscribe/' + currentPager.username, {
654 username: loginStatus.username
656 onSuccess: function(r) {
657 var response = r.responseText.evalJSON();
658 if (response.status == 'success') {
659 alert("You call " + currentPager.username + " and begin breathing heavily into the handset.");
660 $$('[name=user.subscribelink]').each(Element.hide);
661 $$('[name=user.unsubscribelink]').each(Element.show);
663 alert('Failed to subscribe. This is probably for the best');
666 onFailure: function(r) {
667 alert('Failed to subscribe. This is probably for the best');
672 function unsubscribe() {
673 new Ajax.Request(baseURL + '/unsubscribe/' + currentPager.username, {
676 username: loginStatus.username
678 onSuccess: function(r) {
679 var response = r.responseText.evalJSON();
680 if (response.status == 'success') {
681 alert("You come to your senses.");
682 $$('[name=user.subscribelink]').each(Element.show);
683 $$('[name=user.unsubscribelink]').each(Element.hide);
685 alert('You are unable to tear yourself away (because something failed on the server)');
688 onFailure: function(r) {
689 alert('You are unable to tear yourself away (because something failed on the server)');
694 var resizePostContentTimeout = null;
695 function resizePostContent() {
696 if (resizePostContentTimeout)
697 clearTimeout(resizePostContentTimeout);
698 resizePostContentTimeout = setTimeout(function() {
699 var c = $('post.content');
700 var lines = Math.floor(c.value.length / (100 * (c.clientWidth / 1000))) + 1;
701 var m = c.value.match(/\r?\n/g);
707 c.style.height = (lines * 17) + "pt";
709 resizePostContentTimeout = null;
713 var tickerTimer = null;
714 var tickerHead, tickerTail;
716 function tickerFader(a, b, p) {
720 a.style.lineHeight = (100 * p) + '%';
722 b.style.opacity = p2;
723 b.style.lineHeight = (100 * p2) + '%';
730 Bytex64.FX.run(tickerFader.curry(tickerHead, tickerTail), 0.5);
731 tickerHead = tickerHead.nextSibling;
732 tickerTail = tickerTail.nextSibling;
733 if (tickerHead == null) {
735 loadLatest.delay(10);
739 function startTicker() {
741 for (var elem = $('latest-posts').firstChild; elem != null; elem = elem.nextSibling) {
745 // Show the first five
746 tickerHead = $('latest-posts').firstChild;
747 for (var i = 0; i < 10 && tickerHead; i++) {
749 tickerHead = tickerHead.nextSibling;
751 tickerTail = $('latest-posts').firstChild;
752 tickerTimer = setInterval(ticker, 5000);
755 function stopTicker() {
757 clearInterval(tickerTimer);
761 function loadLatest() {
762 new Ajax.Request(baseURL + '/latest.json', {
764 onSuccess: function(r) {
765 var j = r.responseText.evalJSON();
767 $('latest-tags').update();
768 j.tags.each(function(v) {
769 var a = new Element('a', {href: baseURL + '/#/tag/' + v});
771 a.onclick = "return qlink()";
773 $('latest-tags').insert(a);
774 $('latest-tags').appendChild(document.createTextNode(' '));
777 $('latest-posts').update();
778 j.records.each(function(v) {
779 v.data = v.data.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
780 v.date = (new Date(v.timestamp * 1000)).toString();
781 var html = latestRecordsTemplate.evaluate(v);
782 $('latest-posts').insert(html);
789 function qlink(loc) {
792 } else if (event && event.target) {
793 location.href = event.target.href;
798 lastHash = location.hash;
807 Welcome.prototype.show = function() {
808 $$('[name=section]').each(function(v) { v.update('Welcome') });
812 Welcome.prototype.hide = function() {
817 function ExternalURLPost(m) {
818 this.title = decodeURIComponent(m[1]).replace(']','').replace('[','');
819 this.url = decodeURIComponent(m[2]);
822 ExternalURLPost.prototype.show = function() {
823 $('post.content').value = '[' + this.title + '](' + this.url + ')';
828 ['search', /^\?post\/([^/]+)\/(.+)/, ExternalURLPost],
829 ['hash', /^#\/(ref|tag)\/([A-Za-z0-9_-]+)(?:\/p(\d+))?$/, Tag],
830 ['hash', /^#\/feed(?:\/p(\d+))?$/, Feed],
831 ['hash', /^#([A-Za-z0-9_-]+)(?:\/(p)?(\d+))?$/, User]
834 function urlSwitch() {
838 for (var i = 0; i < urlmap.length; i++) {
839 if (m = location[urlmap[i][0]].match(urlmap[i][1])) {
840 pageconstructor = urlmap[i][2];
844 if (i == urlmap.length)
845 pageconstructor = Welcome;
847 if (currentPager && currentPager instanceof pageconstructor) {
848 // updateState returns true if the state has been successfully updated.
849 // Otherwise, we continue and create a new instance.
850 if (currentPager.updateState(m))
854 if (currentPager && currentPager.hide)
857 currentPager = new pageconstructor(m);
858 if (currentPager.show)
863 function hashCheck() {
864 if (location.hash != lastHash) {
865 lastHash = location.hash;
872 loginStatus = new LoginStatus();
874 lastHash = location.hash;
877 setInterval(hashCheck, 250);
879 document.body.observe('keyup', function(event) {
880 if (event.shiftKey && event.keyCode == 32) {
885 $('post.content').addEventListener('keyup', function(event) {
886 event.stopPropagation();