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