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