e29c136f9df8f252555f17729158ee11ff1a047a
[Sone.git] / src / main / resources / static / javascript / sone.js
1 /* Sone JavaScript functions. */
2
3 /* jQuery overrides. */
4 oldGetJson = jQuery.prototype.getJSON;
5 jQuery.prototype.getJSON = function(url, data, successCallback, errorCallback) {
6         if (typeof errorCallback == "undefined") {
7                 return oldGetJson(url, data, successCallback);
8         }
9         if (jQuery.isFunction(data)) {
10                 errorCallback = successCallback;
11                 successCallback = data;
12                 data = null;
13         }
14         return jQuery.ajax({
15                 data: data,
16                 error: errorCallback,
17                 success: successCallback,
18                 url: url
19         });
20 }
21
22 function isOnline() {
23         return $("#sone").hasClass("online");
24 }
25
26 function registerInputTextareaSwap(inputSelector, defaultText, inputFieldName, optional, dontUseTextarea) {
27         $(inputSelector).each(function() {
28                 textarea = $(dontUseTextarea ? "<input type=\"text\" name=\"" + inputFieldName + "\">" : "<textarea name=\"" + inputFieldName + "\"></textarea>").blur(function() {
29                         if ($(this).val() == "") {
30                                 $(this).hide();
31                                 inputField = $(this).data("inputField");
32                                 inputField.show().removeAttr("disabled").addClass("default");
33                                 inputField.val(defaultText);
34                         }
35                 }).hide().data("inputField", $(this)).val($(this).val());
36                 $(this).after(textarea);
37                 (function(inputField, textarea) {
38                         inputField.focus(function() {
39                                 $(this).hide().attr("disabled", "disabled");
40                                 textarea.show().focus();
41                         });
42                         if (inputField.val() == "") {
43                                 inputField.addClass("default");
44                                 inputField.val(defaultText);
45                         } else {
46                                 inputField.hide().attr("disabled", "disabled");
47                                 textarea.show();
48                         }
49                         $(inputField.get(0).form).submit(function() {
50                                 if (!optional && (textarea.val() == "")) {
51                                         return false;
52                                 }
53                         });
54                 })($(this), textarea);
55         });
56 }
57
58 /* hide all the “create reply” forms until a link is clicked. */
59 function addCommentLinks() {
60         if (!isOnline()) {
61                 return;
62         }
63         $("#sone .post").each(function() {
64                 postId = $(this).attr("id");
65                 addCommentLink(postId, $(this));
66         });
67 }
68
69 /**
70  * Adds a “comment” link to all status lines contained in the given element.
71  *
72  * @param postId
73  *            The ID of the post
74  * @param element
75  *            The element to add a “comment” link to
76  */
77 function addCommentLink(postId, element) {
78         commentElement = (function(postId) {
79                 var commentElement = $("<div><span>Comment</span></div>").addClass("show-reply-form").click(function() {
80                         replyElement = $("#sone .post#" + postId + " .create-reply");
81                         replyElement.removeClass("hidden");
82                         replyElement.removeClass("light");
83                         (function(replyElement) {
84                                 replyElement.find("input.reply-input").blur(function() {
85                                         if ($(this).hasClass("default")) {
86                                                 replyElement.addClass("light");
87                                         }
88                                 }).focus(function() {
89                                         replyElement.removeClass("light");
90                                 });
91                         })(replyElement);
92                         replyElement.find("input.reply-input").focus();
93                 });
94                 return commentElement;
95         })(postId);
96         element.find(".create-reply").addClass("hidden");
97         element.find(".status-line .time").each(function() {
98                 $(this).after(commentElement.clone(true));
99         });
100 }
101
102 /**
103  * Retrieves the translation for the given key and calls the callback function.
104  * The callback function takes a single parameter, the translated string.
105  *
106  * @param key
107  *            The key of the translation string
108  * @param callback
109  *            The callback function
110  */
111 function getTranslation(key, callback) {
112         $.getJSON("ajax/getTranslation.ajax", {"key": key}, function(data, textStatus) {
113                 if ((data != null) && data.success) {
114                         callback(data.value);
115                 }
116         }, function(xmlHttpRequest, textStatus, error) {
117                 /* ignore error. */
118         });
119 }
120
121 /**
122  * Fires off an AJAX request to retrieve the current status of a Sone.
123  *
124  * @param soneId
125  *            The ID of the Sone
126  * @param local
127  *            <code>true</code> if the Sone is local, <code>false</code>
128  *            otherwise
129  */
130 function getSoneStatus(soneId, local) {
131         $.getJSON("ajax/getSoneStatus.ajax", {"sone": soneId}, function(data, textStatus) {
132                 if ((data != null) && data.success) {
133                         updateSoneStatus(soneId, data.name, data.status, data.modified, data.locked, data.lastUpdated);
134                 }
135                 /* seconds! */
136                 updateInterval = 60;
137                 if (local || (data!= null) && (data.modified || (data.status == "downloading") || (data.status == "inserting"))) {
138                         updateInterval = 5;
139                 }
140                 setTimeout(function() {
141                         getSoneStatus(soneId, local);
142                 }, updateInterval * 1000);
143         }, function(xmlHttpRequest, textStatus, error) {
144                 /* ignore error. */
145         });
146 }
147
148 /**
149  * Filters the given Sone ID, replacing all “~” characters by an underscore.
150  *
151  * @param soneId
152  *            The Sone ID to filter
153  * @returns The filtered Sone ID
154  */
155 function filterSoneId(soneId) {
156         return soneId.replace(/[^a-zA-Z0-9-]/g, "_");
157 }
158
159 /**
160  * Updates the status of the given Sone.
161  *
162  * @param soneId
163  *            The ID of the Sone to update
164  * @param status
165  *            The status of the Sone (“idle”, “unknown”, “inserting”,
166  *            “downloading”)
167  * @param modified
168  *            Whether the Sone is modified
169  * @param locked
170  *            Whether the Sone is locked
171  * @param lastUpdated
172  *            The date and time of the last update (formatted for display)
173  */
174 function updateSoneStatus(soneId, name, status, modified, locked, lastUpdated) {
175         $("#sone .sone." + filterSoneId(soneId)).
176                 toggleClass("unknown", status == "unknown").
177                 toggleClass("idle", status == "idle").
178                 toggleClass("inserting", status == "inserting").
179                 toggleClass("downloading", status == "downloading").
180                 toggleClass("modified", modified);
181         $("#sone .sone." + filterSoneId(soneId) + " .lock").toggleClass("hidden", locked);
182         $("#sone .sone." + filterSoneId(soneId) + " .unlock").toggleClass("hidden", !locked);
183         $("#sone .sone." + filterSoneId(soneId) + " .last-update span.time").text(lastUpdated);
184         $("#sone .sone." + filterSoneId(soneId) + " .profile-link a").text(name);
185 }
186
187 var watchedSones = {};
188
189 /**
190  * Watches this Sone for updates to its status.
191  *
192  * @param soneId
193  *            The ID of the Sone to watch
194  * @param local
195  *            <code>true</code> if the Sone is local, <code>false</code>
196  *            otherwise
197  */
198 function watchSone(soneId, local) {
199         if (watchedSones[soneId]) {
200                 return;
201         }
202         watchedSones[soneId] = true;
203         (function(soneId) {
204                 setTimeout(function() {
205                         getSoneStatus(soneId, local);
206                 }, 5000);
207         })(soneId);
208 }
209
210 /**
211  * Enhances a “delete” button so that the confirmation is done on the same page.
212  *
213  * @param buttonId
214  *            The selector of the button
215  * @param text
216  *            The text to show on the button
217  * @param deleteCallback
218  *            The callback that actually deletes something
219  */
220 function enhanceDeleteButton(buttonId, text, deleteCallback) {
221         button = $(buttonId);
222         (function(button) {
223                 newButton = $("<button></button>").addClass("confirm").hide().text(text).click(function() {
224                         $(this).fadeOut("slow");
225                         deleteCallback();
226                         return false;
227                 }).insertAfter(button);
228                 (function(button, newButton) {
229                         button.click(function() {
230                                 button.fadeOut("slow", function() {
231                                         newButton.fadeIn("slow");
232                                         $(document).one("click", function() {
233                                                 if (this != newButton.get(0)) {
234                                                         newButton.fadeOut(function() {
235                                                                 button.fadeIn();
236                                                         });
237                                                 }
238                                         });
239                                 });
240                                 return false;
241                         });
242                 })(button, newButton);
243         })(button);
244 }
245
246 /**
247  * Enhances a post’s “delete” button.
248  *
249  * @param buttonId
250  *            The selector of the button
251  * @param postId
252  *            The ID of the post to delete
253  * @param text
254  *            The text to replace the button with
255  */
256 function enhanceDeletePostButton(buttonId, postId, text) {
257         enhanceDeleteButton(buttonId, text, function() {
258                 $.getJSON("ajax/deletePost.ajax", { "post": postId, "formPassword": $("#sone #formPassword").text() }, function(data, textStatus) {
259                         if (data == null) {
260                                 return;
261                         }
262                         if (data.success) {
263                                 $("#sone .post#" + postId).slideUp();
264                         } else if (data.error == "invalid-post-id") {
265                                 alert("Invalid post ID given!");
266                         } else if (data.error == "auth-required") {
267                                 alert("You need to be logged in.");
268                         } else if (data.error == "not-authorized") {
269                                 alert("You are not allowed to delete this post.");
270                         }
271                 }, function(xmlHttpRequest, textStatus, error) {
272                         /* ignore error. */
273                 });
274         });
275 }
276
277 /**
278  * Enhances a reply’s “delete” button.
279  *
280  * @param buttonId
281  *            The selector of the button
282  * @param replyId
283  *            The ID of the reply to delete
284  * @param text
285  *            The text to replace the button with
286  */
287 function enhanceDeleteReplyButton(buttonId, replyId, text) {
288         enhanceDeleteButton(buttonId, text, function() {
289                 $.getJSON("ajax/deleteReply.ajax", { "reply": replyId, "formPassword": $("#sone #formPassword").text() }, function(data, textStatus) {
290                         if (data == null) {
291                                 return;
292                         }
293                         if (data.success) {
294                                 $("#sone .reply#" + replyId).slideUp();
295                         } else if (data.error == "invalid-reply-id") {
296                                 alert("Invalid reply ID given!");
297                         } else if (data.error == "auth-required") {
298                                 alert("You need to be logged in.");
299                         } else if (data.error == "not-authorized") {
300                                 alert("You are not allowed to delete this reply.");
301                         }
302                 }, function(xmlHttpRequest, textStatus, error) {
303                         /* ignore error. */
304                 });
305         });
306 }
307
308 function getFormPassword() {
309         return $("#sone #formPassword").text();
310 }
311
312 function getSoneElement(element) {
313         return $(element).parents(".sone");
314 }
315
316 /**
317  * Generates a list of Sones by concatening the names of the given sones with a
318  * new line character (“\n”).
319  *
320  * @param sones
321  *            The sones to format
322  * @returns {String} The created string
323  */
324 function generateSoneList(sones) {
325         var soneList = "";
326         $.each(sones, function() {
327                 if (soneList != "") {
328                         soneList += "\n";
329                 }
330                 soneList += this.name;
331         });
332         return soneList;
333 }
334
335 /**
336  * Returns the ID of the Sone that this element belongs to.
337  *
338  * @param element
339  *            The element to locate the matching Sone ID for
340  * @returns The ID of the Sone, or undefined
341  */
342 function getSoneId(element) {
343         return getSoneElement(element).find(".id").text();
344 }
345
346 function getPostElement(element) {
347         return $(element).parents(".post");
348 }
349
350 function getPostId(element) {
351         return getPostElement(element).attr("id");
352 }
353
354 function getReplyElement(element) {
355         return $(element).parents(".reply");
356 }
357
358 function getReplyId(element) {
359         return getReplyElement(element).attr("id");
360 }
361
362 function likePost(postId) {
363         $.getJSON("ajax/like.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
364                 if ((data == null) || !data.success) {
365                         return;
366                 }
367                 $("#sone .post#" + postId + " > .inner-part > .status-line .like").addClass("hidden");
368                 $("#sone .post#" + postId + " > .inner-part > .status-line .unlike").removeClass("hidden");
369                 updatePostLikes(postId);
370         }, function(xmlHttpRequest, textStatus, error) {
371                 /* ignore error. */
372         });
373 }
374
375 function unlikePost(postId) {
376         $.getJSON("ajax/unlike.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
377                 if ((data == null) || !data.success) {
378                         return;
379                 }
380                 $("#sone .post#" + postId + " > .inner-part > .status-line .unlike").addClass("hidden");
381                 $("#sone .post#" + postId + " > .inner-part > .status-line .like").removeClass("hidden");
382                 updatePostLikes(postId);
383         }, function(xmlHttpRequest, textStatus, error) {
384                 /* ignore error. */
385         });
386 }
387
388 function updatePostLikes(postId) {
389         $.getJSON("ajax/getLikes.ajax", { "type": "post", "post": postId }, function(data, textStatus) {
390                 if ((data != null) && data.success) {
391                         $("#sone .post#" + postId + " > .inner-part > .status-line .likes").toggleClass("hidden", data.likes == 0)
392                         $("#sone .post#" + postId + " > .inner-part > .status-line .likes span.like-count").text(data.likes);
393                         $("#sone .post#" + postId + " > .inner-part > .status-line .likes > span").attr("title", generateSoneList(data.sones));
394                 }
395         }, function(xmlHttpRequest, textStatus, error) {
396                 /* ignore error. */
397         });
398 }
399
400 function likeReply(replyId) {
401         $.getJSON("ajax/like.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
402                 if ((data == null) || !data.success) {
403                         return;
404                 }
405                 $("#sone .reply#" + replyId + " .status-line .like").addClass("hidden");
406                 $("#sone .reply#" + replyId + " .status-line .unlike").removeClass("hidden");
407                 updateReplyLikes(replyId);
408         }, function(xmlHttpRequest, textStatus, error) {
409                 /* ignore error. */
410         });
411 }
412
413 function unlikeReply(replyId) {
414         $.getJSON("ajax/unlike.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
415                 if ((data == null) || !data.success) {
416                         return;
417                 }
418                 $("#sone .reply#" + replyId + " .status-line .unlike").addClass("hidden");
419                 $("#sone .reply#" + replyId + " .status-line .like").removeClass("hidden");
420                 updateReplyLikes(replyId);
421         }, function(xmlHttpRequest, textStatus, error) {
422                 /* ignore error. */
423         });
424 }
425
426 function updateReplyLikes(replyId) {
427         $.getJSON("ajax/getLikes.ajax", { "type": "reply", "reply": replyId }, function(data, textStatus) {
428                 if ((data != null) && data.success) {
429                         $("#sone .reply#" + replyId + " .status-line .likes").toggleClass("hidden", data.likes == 0)
430                         $("#sone .reply#" + replyId + " .status-line .likes span.like-count").text(data.likes);
431                         $("#sone .reply#" + replyId + " .status-line .likes > span").attr("title", generateSoneList(data.sones));
432                 }
433         }, function(xmlHttpRequest, textStatus, error) {
434                 /* ignore error. */
435         });
436 }
437
438 /**
439  * Posts a reply and calls the given callback when the request finishes.
440  *
441  * @param postId
442  *            The ID of the post the reply refers to
443  * @param text
444  *            The text to post
445  * @param callbackFunction
446  *            The callback function to call when the request finishes (takes 3
447  *            parameters: success, error, replyId)
448  */
449 function postReply(postId, text, callbackFunction) {
450         $.getJSON("ajax/createReply.ajax", { "formPassword" : getFormPassword(), "post" : postId, "text": text }, function(data, textStatus) {
451                 if (data == null) {
452                         /* TODO - show error */
453                         return;
454                 }
455                 if (data.success) {
456                         callbackFunction(true, null, data.reply);
457                 } else {
458                         callbackFunction(false, data.error);
459                 }
460         }, function(xmlHttpRequest, textStatus, error) {
461                 /* ignore error. */
462         });
463 }
464
465 /**
466  * Requests information about the reply with the given ID.
467  *
468  * @param replyId
469  *            The ID of the reply
470  * @param callbackFunction
471  *            A callback function (parameters soneId, soneName, replyTime,
472  *            replyDisplayTime, text, html)
473  */
474 function getReply(replyId, callbackFunction) {
475         $.getJSON("ajax/getReply.ajax", { "reply" : replyId }, function(data, textStatus) {
476                 if ((data != null) && data.success) {
477                         callbackFunction(data.soneId, data.soneName, data.time, data.displayTime, data.text, data.html);
478                 }
479         }, function(xmlHttpRequest, textStatus, error) {
480                 /* ignore error. */
481         });
482 }
483
484 /**
485  * Ajaxifies the given notification by replacing the form with AJAX.
486  *
487  * @param notification
488  *            jQuery object representing the notification.
489  */
490 function ajaxifyNotification(notification) {
491         notification.find("form.dismiss").submit(function() {
492                 return false;
493         });
494         notification.find("form.dismiss button").click(function() {
495                 $.getJSON("ajax/dismissNotification.ajax", { "formPassword" : getFormPassword(), "notification" : notification.attr("id") }, function(data, textStatus) {
496                         /* dismiss in case of error, too. */
497                         notification.slideUp();
498                 }, function(xmlHttpRequest, textStatus, error) {
499                         /* ignore error. */
500                 });
501         });
502         return notification;
503 }
504
505 /**
506  * Retrieves all changed notifications.
507  */
508 function getNotifications() {
509         $.getJSON("ajax/getNotifications.ajax", {}, function(data, textStatus) {
510                 if ((data != null) && data.success) {
511                         $.each(data.notifications, function(index, value) {
512                                 oldNotification = $("#sone #notification-area .notification#" + value.id);
513                                 notification = ajaxifyNotification(createNotification(value.id, value.text, value.dismissable)).hide();
514                                 if (oldNotification.length != 0) {
515                                         oldNotification.replaceWith(notification.show());
516                                 } else {
517                                         $("#sone #notification-area").append(notification);
518                                         notification.slideDown();
519                                 }
520                         });
521                         $.each(data.removedNotifications, function(index, value) {
522                                 $("#sone #notification-area .notification#" + value.id).slideUp();
523                         });
524                         setTimeout(getNotifications, 5000);
525                 } else {
526                         setTimeout(getNotifications, 30000);
527                 }
528         }, function(xmlHttpRequest, textStatus, error) {
529                 /* ignore error. */
530         });
531 }
532
533 /**
534  * Creates a new notification.
535  *
536  * @param id
537  *            The ID of the notificaiton
538  * @param text
539  *            The text of the notification
540  * @param dismissable
541  *            <code>true</code> if the notification can be dismissed by the
542  *            user
543  */
544 function createNotification(id, text, dismissable) {
545         notification = $("<div></div>").addClass("notification").attr("id", id);
546         if (dismissable) {
547                 dismissForm = $("#sone #notification-area #notification-dismiss-template").clone().removeClass("hidden").removeAttr("id")
548                 dismissForm.find("input[name=notification]").val(id);
549                 notification.append(dismissForm);
550         }
551         notification.append(text);
552         return notification;
553 }
554
555 //
556 // EVERYTHING BELOW HERE IS EXECUTED AFTER LOADING THE PAGE
557 //
558
559 $(document).ready(function() {
560
561         /* this initializes the status update input field. */
562         getTranslation("WebInterface.DefaultText.StatusUpdate", function(text) {
563                 registerInputTextareaSwap("#sone #update-status .status-input", text, "text", false, false);
564         })
565
566         /* these functions are necessary for updating Sone statuses. */
567         $("#sone .sone").each(function() {
568                 watchSone($(this).find(".id").text(), $(this).hasClass("local"));
569         });
570
571         /* this initializes all reply input fields. */
572         getTranslation("WebInterface.DefaultText.Reply", function(text) {
573                 registerInputTextareaSwap("#sone input.reply-input", text, "text", false, false);
574                 addCommentLinks();
575         })
576
577         /* replaces all “post reply!” forms with AJAX. */
578         $("#sone .create-reply button:submit").click(function() {
579                 $(this.form).submit(function() {
580                         return false;
581                 });
582                 inputField = $(this.form).find(":input:enabled").get(0);
583                 postId = getPostId($(inputField));
584                 text = $(inputField).val();
585                 $(inputField).val("");
586                 postReply(postId, text, function(success, error, replyId) {
587                         if (success) {
588                                 getReply(replyId, function(soneId, soneName, replyTime, replyDisplayTime, text, html) {
589                                         newReply = $(html).insertBefore("#sone .post#" + postId + " .create-reply");
590                                         $("#sone .post#" + postId + " .create-reply").addClass("hidden");
591                                         getTranslation("WebInterface.Confirmation.DeleteReplyButton", function(deleteReplyText) {
592                                                 enhanceDeleteReplyButton("#sone .post#" + postId + " .reply#" + replyId + " .delete button", replyId, deleteReplyText);
593                                         });
594                                         newReply.find(".status-line .like").submit(function() {
595                                                 likeReply(getReplyId(this));
596                                                 return false;
597                                         });
598                                         newReply.find(".status-line .unlike").submit(function() {
599                                                 unlikeReply(getReplyId(this));
600                                                 return false;
601                                         });
602                                         addCommentLink(postId, newReply);
603                                 });
604                         } else {
605                                 alert(error);
606                         }
607                 });
608                 return false;
609         });
610
611         /* replace all “delete” buttons with javascript. */
612         getTranslation("WebInterface.Confirmation.DeletePostButton", function(text) {
613                 deletePostText = text;
614                 getTranslation("WebInterface.Confirmation.DeleteReplyButton", function(text) {
615                         deleteReplyText = text;
616                         $("#sone .post").each(function() {
617                                 postId = $(this).attr("id");
618                                 enhanceDeletePostButton("#sone .post#" + postId + " > .inner-part > .status-line .delete button", postId, deletePostText);
619                                 (function(postId) {
620                                         $("#sone .post#" + postId + " .reply").each(function() {
621                                                 replyId = $(this).attr("id");
622                                                 (function(postId, reply, replyId) {
623                                                         reply.find(".delete button").each(function() {
624                                                                 enhanceDeleteReplyButton("#sone .post#" + postId + " .reply#" + replyId + " .delete button", replyId, deleteReplyText);
625                                                         })
626                                                 })(postId, $(this), replyId);
627                                         });
628                                 })(postId);
629                         });
630                 });
631         });
632
633         /* hides all replies but the latest two. */
634         getTranslation("WebInterface.ClickToShow.Replies", function(text) {
635                 $("#sone .post .replies").each(function() {
636                         allReplies = $(this).find(".reply");
637                         if (allReplies.length > 2) {
638                                 newHidden = false;
639                                 for (replyIndex = 0; replyIndex < (allReplies.length - 2); ++replyIndex) {
640                                         $(allReplies[replyIndex]).addClass("hidden");
641                                         newHidden |= $(allReplies[replyIndex]).hasClass("new");
642                                 }
643                                 clickToShowElement = $("<div></div>").addClass("click-to-show");
644                                 if (newHidden) {
645                                         clickToShowElement.addClass("new");
646                                 }
647                                 (function(clickToShowElement, allReplies, text) {
648                                         clickToShowElement.text(text);
649                                         clickToShowElement.click(function() {
650                                                 allReplies.removeClass("hidden");
651                                                 clickToShowElement.addClass("hidden");
652                                         });
653                                 })(clickToShowElement, allReplies, text);
654                                 $(allReplies[0]).before(clickToShowElement);
655                         }
656                 });
657         });
658
659         /*
660          * convert all “follow”, “unfollow”, “lock”, and “unlock” links to something
661          * nicer.
662          */
663         $("#sone .follow").submit(function() {
664                 var followElement = this;
665                 $.getJSON("ajax/followSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
666                         $(followElement).addClass("hidden");
667                         $(followElement).parent().find(".unfollow").removeClass("hidden");
668                 });
669                 return false;
670         });
671         $("#sone .unfollow").submit(function() {
672                 var unfollowElement = this;
673                 $.getJSON("ajax/unfollowSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
674                         $(unfollowElement).addClass("hidden");
675                         $(unfollowElement).parent().find(".follow").removeClass("hidden");
676                 });
677                 return false;
678         });
679         $("#sone .lock").submit(function() {
680                 var lockElement = this;
681                 $.getJSON("ajax/lockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
682                         $(lockElement).addClass("hidden");
683                         $(lockElement).parent().find(".unlock").removeClass("hidden");
684                 });
685                 return false;
686         });
687         $("#sone .unlock").submit(function() {
688                 var unlockElement = this;
689                 $.getJSON("ajax/unlockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
690                         $(unlockElement).addClass("hidden");
691                         $(unlockElement).parent().find(".lock").removeClass("hidden");
692                 });
693                 return false;
694         });
695
696         /* convert all “like” buttons to javascript functions. */
697         $("#sone .post > .inner-part > .status-line .like").submit(function() {
698                 likePost(getPostId(this));
699                 return false;
700         });
701         $("#sone .post > .inner-part > .status-line .unlike").submit(function() {
702                 unlikePost(getPostId(this));
703                 return false;
704         });
705         $("#sone .post .reply .status-line .like").submit(function() {
706                 likeReply(getReplyId(this));
707                 return false;
708         });
709         $("#sone .post .reply .status-line .unlike").submit(function() {
710                 unlikeReply(getReplyId(this));
711                 return false;
712         });
713
714         /* process all existing notifications, ajaxify dismiss buttons. */
715         $("#sone #notification-area .notification").each(function() {
716                 ajaxifyNotification($(this));
717         });
718
719         /* activate notification polling. */
720         setTimeout(getNotifications, 5000);
721 });