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