Fix comparisons that were assignments.
[Sone.git] / src / main / resources / static / javascript / sone.js
index 5aaa6a5..accfabc 100644 (file)
@@ -1,26 +1,23 @@
 /* Sone JavaScript functions. */
 
-/* jQuery overrides. */
-oldGetJson = jQuery.prototype.getJSON;
-jQuery.prototype.getJSON = function(url, data, successCallback, errorCallback) {
-       if (typeof errorCallback == "undefined") {
-               return oldGetJson(url, data, successCallback);
-       }
-       if (jQuery.isFunction(data)) {
-               errorCallback = successCallback;
-               successCallback = data;
-               data = null;
-       }
-       return jQuery.ajax({
-               data: data,
-               error: errorCallback,
-               success: successCallback,
-               url: url
-       });
-}
-
-function isOnline() {
-       return $("#sone").hasClass("online");
+function ajaxGet(url, data, successCallback, errorCallback) {
+       (function(url, data, successCallback, errorCallback) {
+               $.ajax({"type": "GET", "url": url, "data": data, "dataType": "json", "success": function(data, textStatus, xmlHttpRequest) {
+                       ajaxSuccess();
+                       if (typeof successCallback != "undefined") {
+                               successCallback(data, textStatus);
+                       }
+               }, "error": function(xmlHttpRequest, textStatus, errorThrown) {
+                       if (xmlHttpRequest.status == 403) {
+                               notLoggedIn = true;
+                       }
+                       if (typeof errorCallback != "undefined") {
+                               errorCallback();
+                       } else {
+                               ajaxError();
+                       }
+               }});
+       })(url, data, successCallback, errorCallback);
 }
 
 function registerInputTextareaSwap(inputElement, defaultText, inputFieldName, optional, dontUseTextarea) {
@@ -70,28 +67,31 @@ function addCommentLink(postId, author, element, insertAfterThisElement) {
        if (($(element).find(".show-reply-form").length > 0) || (getPostElement(element).find(".create-reply").length == 0)) {
                return;
        }
-       commentElement = (function(postId, author) {
+       (function(postId, author, insertAfterThisElement) {
                separator = $("<span> · </span>").addClass("separator");
-               var commentElement = $("<div><span>Comment</span></div>").addClass("show-reply-form").click(function() {
-                       replyElement = $("#sone .post#" + postId + " .create-reply");
-                       replyElement.removeClass("hidden");
-                       replyElement.removeClass("light");
-                       (function(replyElement) {
-                               replyElement.find("input.reply-input").blur(function() {
-                                       if ($(this).hasClass("default")) {
-                                               replyElement.addClass("light");
-                                       }
-                               }).focus(function() {
-                                       replyElement.removeClass("light");
-                               });
-                       })(replyElement);
-                       textArea = replyElement.find("input.reply-input").focus().data("textarea");
-                       textArea.val(textArea.val() + "@sone://" + author + " ");
+               getTranslation("WebInterface.Button.Comment", function(text) {
+                       commentElement = $("<div><span>" + text + "</span></div>").addClass("show-reply-form").click(function() {
+                               replyElement = $("#sone .post#post-" + postId + " .create-reply");
+                               replyElement.removeClass("hidden");
+                               replyElement.removeClass("light");
+                               (function(replyElement) {
+                                       replyElement.find("input.reply-input").blur(function() {
+                                               if ($(this).hasClass("default")) {
+                                                       replyElement.addClass("light");
+                                               }
+                                       }).focus(function() {
+                                               replyElement.removeClass("light");
+                                       });
+                               })(replyElement);
+                               textArea = replyElement.find("input.reply-input").focus().data("textarea");
+                               if (author != getCurrentSoneId()) {
+                                       textArea.val(textArea.val() + "@sone://" + author + " ");
+                               }
+                       });
+                       $(insertAfterThisElement).after(commentElement.clone(true));
+                       $(insertAfterThisElement).after(separator);
                });
-               return commentElement;
-       })(postId, author);
-       $(insertAfterThisElement).after(commentElement.clone(true));
-       $(insertAfterThisElement).after(separator);
+       })(postId, author, insertAfterThisElement);
 }
 
 var translations = {};
@@ -110,13 +110,11 @@ function getTranslation(key, callback) {
                callback(translations[key]);
                return;
        }
-       $.getJSON("getTranslation.ajax", {"key": key}, function(data, textStatus) {
+       ajaxGet("getTranslation.ajax", {"key": key}, function(data, textStatus) {
                if ((data != null) && data.success) {
                        translations[key] = data.value;
                        callback(data.value);
                }
-       }, function(xmlHttpRequest, textStatus, error) {
-               /* ignore error. */
        });
 }
 
@@ -146,7 +144,7 @@ function filterSoneId(soneId) {
  * @param lastUpdated
  *            The date and time of the last update (formatted for display)
  */
-function updateSoneStatus(soneId, name, status, modified, locked, lastUpdated) {
+function updateSoneStatus(soneId, name, status, modified, locked, lastUpdated, lastUpdatedText) {
        $("#sone .sone." + filterSoneId(soneId)).
                toggleClass("unknown", status == "unknown").
                toggleClass("idle", status == "idle").
@@ -156,7 +154,7 @@ function updateSoneStatus(soneId, name, status, modified, locked, lastUpdated) {
        $("#sone .sone." + filterSoneId(soneId) + " .lock").toggleClass("hidden", locked);
        $("#sone .sone." + filterSoneId(soneId) + " .unlock").toggleClass("hidden", !locked);
        if (lastUpdated != null) {
-               $("#sone .sone." + filterSoneId(soneId) + " .last-update span.time").text(lastUpdated);
+               $("#sone .sone." + filterSoneId(soneId) + " .last-update span.time").attr("title", lastUpdated).text(lastUpdatedText);
        } else {
                getTranslation("View.Sone.Text.UnknownDate", function(unknown) {
                        $("#sone .sone." + filterSoneId(soneId) + " .last-update span.time").text(unknown);
@@ -212,12 +210,12 @@ function enhanceDeleteButton(button, text, deleteCallback) {
  */
 function enhanceDeletePostButton(button, postId, text) {
        enhanceDeleteButton(button, text, function() {
-               $.getJSON("deletePost.ajax", { "post": postId, "formPassword": getFormPassword() }, function(data, textStatus) {
+               ajaxGet("deletePost.ajax", { "post": postId, "formPassword": getFormPassword() }, function(data, textStatus) {
                        if (data == null) {
                                return;
                        }
                        if (data.success) {
-                               $("#sone .post#" + postId).slideUp();
+                               $("#sone .post#post-" + postId).slideUp();
                        } else if (data.error == "invalid-post-id") {
                                /* pretend the post is already gone. */
                                getPost(postId).slideUp();
@@ -244,12 +242,12 @@ function enhanceDeletePostButton(button, postId, text) {
  */
 function enhanceDeleteReplyButton(button, replyId, text) {
        enhanceDeleteButton(button, text, function() {
-               $.getJSON("deleteReply.ajax", { "reply": replyId, "formPassword": $("#sone #formPassword").text() }, function(data, textStatus) {
+               ajaxGet("deleteReply.ajax", { "reply": replyId, "formPassword": $("#sone #formPassword").text() }, function(data, textStatus) {
                        if (data == null) {
                                return;
                        }
                        if (data.success) {
-                               $("#sone .reply#" + replyId).slideUp();
+                               $("#sone .reply#reply-" + replyId).slideUp();
                        } else if (data.error == "invalid-reply-id") {
                                /* pretend the reply is already gone. */
                                getReply(replyId).slideUp();
@@ -277,7 +275,7 @@ function getFormPassword() {
  */
 function getSone(soneId) {
        return $("#sone .sone").filter(function(index) {
-               return $(".id").text() == soneId;
+               return $(".id", this).text() == soneId;
        });
 }
 
@@ -286,6 +284,18 @@ function getSoneElement(element) {
 }
 
 /**
+ * Returns the ID of the sone of the context menu that contains the given
+ * element.
+ *
+ * @param element
+ *            The element within a context menu to get the Sone ID for
+ * @return The Sone ID
+ */
+function getMenuSone(element) {
+       return $(element).closest(".sone-menu").find(".sone-menu-id").text();
+}
+
+/**
  * Generates a list of Sones by concatening the names of the given sones with a
  * new line character (“\n”).
  *
@@ -323,7 +333,7 @@ function getSoneId(element) {
  * @returns The element of the post
  */
 function getPost(postId) {
-       return $("#sone .post#" + postId);
+       return $("#sone .post#post-" + postId);
 }
 
 function getPostElement(element) {
@@ -331,7 +341,7 @@ function getPostElement(element) {
 }
 
 function getPostId(element) {
-       return getPostElement(element).attr("id");
+       return getPostElement(element).attr("id").substr(5);
 }
 
 function getPostTime(element) {
@@ -357,7 +367,7 @@ function getPostAuthor(element) {
  * @returns The element of the reply
  */
 function getReply(replyId) {
-       return $("#sone .reply#" + replyId);
+       return $("#sone .reply#reply-" + replyId);
 }
 
 function getReplyElement(element) {
@@ -365,7 +375,7 @@ function getReplyElement(element) {
 }
 
 function getReplyId(element) {
-       return getReplyElement(element).attr("id");
+       return getReplyElement(element).attr("id").substr(6);
 }
 
 function getReplyTime(element) {
@@ -428,12 +438,12 @@ function getNotificationLastUpdatedTime(notificationElement) {
 }
 
 function likePost(postId) {
-       $.getJSON("like.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
+       ajaxGet("like.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
                if ((data == null) || !data.success) {
                        return;
                }
-               $("#sone .post#" + postId + " > .inner-part > .status-line .like").addClass("hidden");
-               $("#sone .post#" + postId + " > .inner-part > .status-line .unlike").removeClass("hidden");
+               $("#sone .post#post-" + postId + " > .inner-part > .status-line .like").addClass("hidden");
+               $("#sone .post#post-" + postId + " > .inner-part > .status-line .unlike").removeClass("hidden");
                updatePostLikes(postId);
        }, function(xmlHttpRequest, textStatus, error) {
                /* ignore error. */
@@ -441,12 +451,12 @@ function likePost(postId) {
 }
 
 function unlikePost(postId) {
-       $.getJSON("unlike.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
+       ajaxGet("unlike.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
                if ((data == null) || !data.success) {
                        return;
                }
-               $("#sone .post#" + postId + " > .inner-part > .status-line .unlike").addClass("hidden");
-               $("#sone .post#" + postId + " > .inner-part > .status-line .like").removeClass("hidden");
+               $("#sone .post#post-" + postId + " > .inner-part > .status-line .unlike").addClass("hidden");
+               $("#sone .post#post-" + postId + " > .inner-part > .status-line .like").removeClass("hidden");
                updatePostLikes(postId);
        }, function(xmlHttpRequest, textStatus, error) {
                /* ignore error. */
@@ -454,11 +464,11 @@ function unlikePost(postId) {
 }
 
 function updatePostLikes(postId) {
-       $.getJSON("getLikes.ajax", { "type": "post", "post": postId }, function(data, textStatus) {
+       ajaxGet("getLikes.ajax", { "type": "post", "post": postId }, function(data, textStatus) {
                if ((data != null) && data.success) {
-                       $("#sone .post#" + postId + " > .inner-part > .status-line .likes").toggleClass("hidden", data.likes == 0)
-                       $("#sone .post#" + postId + " > .inner-part > .status-line .likes span.like-count").text(data.likes);
-                       $("#sone .post#" + postId + " > .inner-part > .status-line .likes > span").attr("title", generateSoneList(data.sones));
+                       $("#sone .post#post-" + postId + " > .inner-part > .status-line .likes").toggleClass("hidden", data.likes == 0);
+                       $("#sone .post#post-" + postId + " > .inner-part > .status-line .likes span.like-count").text(data.likes);
+                       $("#sone .post#post-" + postId + " > .inner-part > .status-line .likes > span").attr("title", generateSoneList(data.sones));
                }
        }, function(xmlHttpRequest, textStatus, error) {
                /* ignore error. */
@@ -466,12 +476,12 @@ function updatePostLikes(postId) {
 }
 
 function likeReply(replyId) {
-       $.getJSON("like.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
+       ajaxGet("like.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
                if ((data == null) || !data.success) {
                        return;
                }
-               $("#sone .reply#" + replyId + " .status-line .like").addClass("hidden");
-               $("#sone .reply#" + replyId + " .status-line .unlike").removeClass("hidden");
+               $("#sone .reply#reply-" + replyId + " .status-line .like").addClass("hidden");
+               $("#sone .reply#reply-" + replyId + " .status-line .unlike").removeClass("hidden");
                updateReplyLikes(replyId);
        }, function(xmlHttpRequest, textStatus, error) {
                /* ignore error. */
@@ -479,12 +489,12 @@ function likeReply(replyId) {
 }
 
 function unlikeReply(replyId) {
-       $.getJSON("unlike.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
+       ajaxGet("unlike.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
                if ((data == null) || !data.success) {
                        return;
                }
-               $("#sone .reply#" + replyId + " .status-line .unlike").addClass("hidden");
-               $("#sone .reply#" + replyId + " .status-line .like").removeClass("hidden");
+               $("#sone .reply#reply-" + replyId + " .status-line .unlike").addClass("hidden");
+               $("#sone .reply#reply-" + replyId + " .status-line .like").removeClass("hidden");
                updateReplyLikes(replyId);
        }, function(xmlHttpRequest, textStatus, error) {
                /* ignore error. */
@@ -498,7 +508,7 @@ function unlikeReply(replyId) {
  *            The ID of the Sone to trust
  */
 function trustSone(soneId) {
-       $.getJSON("trustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
+       ajaxGet("trustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
                if ((data != null) && data.success) {
                        updateTrustControls(soneId, data.trustValue);
                }
@@ -512,7 +522,7 @@ function trustSone(soneId) {
  *            The ID of the Sone to distrust
  */
 function distrustSone(soneId) {
-       $.getJSON("distrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
+       ajaxGet("distrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
                if ((data != null) && data.success) {
                        updateTrustControls(soneId, data.trustValue);
                }
@@ -526,7 +536,7 @@ function distrustSone(soneId) {
  *            The ID of the Sone to untrust
  */
 function untrustSone(soneId) {
-       $.getJSON("untrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
+       ajaxGet("untrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
                if ((data != null) && data.success) {
                        updateTrustControls(soneId, data.trustValue);
                }
@@ -567,7 +577,7 @@ function updateTrustControls(soneId, trustValue) {
  */
 function bookmarkPost(postId) {
        (function(postId) {
-               $.getJSON("bookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
+               ajaxGet("bookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
                        if ((data != null) && data.success) {
                                getPost(postId).find(".bookmark").toggleClass("hidden", true);
                                getPost(postId).find(".unbookmark").toggleClass("hidden", false);
@@ -583,7 +593,7 @@ function bookmarkPost(postId) {
  *            The ID of the post to unbookmark
  */
 function unbookmarkPost(postId) {
-       $.getJSON("unbookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
+       ajaxGet("unbookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
                if ((data != null) && data.success) {
                        getPost(postId).find(".bookmark").toggleClass("hidden", false);
                        getPost(postId).find(".unbookmark").toggleClass("hidden", true);
@@ -592,11 +602,11 @@ function unbookmarkPost(postId) {
 }
 
 function updateReplyLikes(replyId) {
-       $.getJSON("getLikes.ajax", { "type": "reply", "reply": replyId }, function(data, textStatus) {
+       ajaxGet("getLikes.ajax", { "type": "reply", "reply": replyId }, function(data, textStatus) {
                if ((data != null) && data.success) {
-                       $("#sone .reply#" + replyId + " .status-line .likes").toggleClass("hidden", data.likes == 0)
-                       $("#sone .reply#" + replyId + " .status-line .likes span.like-count").text(data.likes);
-                       $("#sone .reply#" + replyId + " .status-line .likes > span").attr("title", generateSoneList(data.sones));
+                       $("#sone .reply#reply-" + replyId + " .status-line .likes").toggleClass("hidden", data.likes == 0);
+                       $("#sone .reply#reply-" + replyId + " .status-line .likes span.like-count").text(data.likes);
+                       $("#sone .reply#reply-" + replyId + " .status-line .likes > span").attr("title", generateSoneList(data.sones));
                }
        }, function(xmlHttpRequest, textStatus, error) {
                /* ignore error. */
@@ -617,7 +627,7 @@ function updateReplyLikes(replyId) {
  *            parameters: success, error, replyId)
  */
 function postReply(sender, postId, text, callbackFunction) {
-       $.getJSON("createReply.ajax", { "formPassword" : getFormPassword(), "sender": sender, "post" : postId, "text": text }, function(data, textStatus) {
+       ajaxGet("createReply.ajax", { "formPassword" : getFormPassword(), "sender": sender, "post" : postId, "text": text }, function(data, textStatus) {
                if (data == null) {
                        /* TODO - show error */
                        return;
@@ -645,7 +655,7 @@ function ajaxifySone(soneElement) {
         */
        $(".follow", soneElement).submit(function() {
                var followElement = this;
-               $.getJSON("followSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
+               ajaxGet("followSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
                        $(followElement).addClass("hidden");
                        $(followElement).parent().find(".unfollow").removeClass("hidden");
                });
@@ -653,7 +663,7 @@ function ajaxifySone(soneElement) {
        });
        $(".unfollow", soneElement).submit(function() {
                var unfollowElement = this;
-               $.getJSON("unfollowSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
+               ajaxGet("unfollowSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
                        $(unfollowElement).addClass("hidden");
                        $(unfollowElement).parent().find(".follow").removeClass("hidden");
                });
@@ -661,7 +671,7 @@ function ajaxifySone(soneElement) {
        });
        $(".lock", soneElement).submit(function() {
                var lockElement = this;
-               $.getJSON("lockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
+               ajaxGet("lockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
                        $(lockElement).addClass("hidden");
                        $(lockElement).parent().find(".unlock").removeClass("hidden");
                });
@@ -669,7 +679,7 @@ function ajaxifySone(soneElement) {
        });
        $(".unlock", soneElement).submit(function() {
                var unlockElement = this;
-               $.getJSON("unlockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
+               ajaxGet("unlockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
                        $(unlockElement).addClass("hidden");
                        $(unlockElement).parent().find(".lock").removeClass("hidden");
                });
@@ -704,10 +714,10 @@ function ajaxifyPost(postElement) {
                                if (success) {
                                        $(inputField).val("");
                                        loadNewReply(replyId, soneId, postId);
-                                       $("#sone .post#" + postId + " .create-reply").addClass("hidden");
-                                       $("#sone .post#" + postId + " .create-reply .sender").hide();
-                                       $("#sone .post#" + postId + " .create-reply .select-sender").show();
-                                       $("#sone .post#" + postId + " .create-reply :input[name=sender]").val(getCurrentSoneId());
+                                       $("#sone .post#post-" + postId + " .create-reply").addClass("hidden");
+                                       $("#sone .post#post-" + postId + " .create-reply .sender").hide();
+                                       $("#sone .post#post-" + postId + " .create-reply .select-sender").show();
+                                       $("#sone .post#post-" + postId + " .create-reply :input[name=sender]").val(getCurrentSoneId());
                                } else {
                                        alert(error);
                                }
@@ -762,14 +772,55 @@ function ajaxifyPost(postElement) {
        /* convert “show source” link into javascript function. */
        $(postElement).find(".show-source").each(function() {
                $("a", this).click(function() {
+                       post = getPostElement(this);
+                       rawPostText = $(".post-text.raw-text", post);
+                       rawPostText.toggleClass("hidden");
+                       if (rawPostText.hasClass("hidden")) {
+                               $(".post-text.short-text", post).removeClass("hidden");
+                               $(".post-text.text", post).addClass("hidden");
+                               $(".expand-post-text", post).removeClass("hidden");
+                               $(".shrink-post-text", post).addClass("hidden");
+                       } else {
+                               $(".post-text.short-text", post).addClass("hidden");
+                               $(".post-text.text", post).addClass("hidden");
+                               $(".expand-post-text", post).addClass("hidden");
+                               $(".shrink-post-text", post).addClass("hidden");
+                       }
+                       return false;
+               });
+       });
+
+       /* convert “show more” link into javascript function. */
+       $(postElement).find(".expand-post-text").each(function() {
+               $(this).click(function() {
+                       $(".post-text.text", getPostElement(this)).toggleClass("hidden");
+                       $(".post-text.short-text", getPostElement(this)).toggleClass("hidden");
+                       $(".expand-post-text", getPostElement(this)).toggleClass("hidden");
+                       $(".shrink-post-text", getPostElement(this)).toggleClass("hidden");
+                       return false;
+               });
+       });
+       $(postElement).find(".shrink-post-text").each(function() {
+               $(this).click(function() {
                        $(".post-text.text", getPostElement(this)).toggleClass("hidden");
-                       $(".post-text.raw-text", getPostElement(this)).toggleClass("hidden");
+                       $(".post-text.short-text", getPostElement(this)).toggleClass("hidden");
+                       $(".expand-post-text", getPostElement(this)).toggleClass("hidden");
+                       $(".shrink-post-text", getPostElement(this)).toggleClass("hidden");
                        return false;
                });
        });
 
+       /* ajaxify author/post links */
+       $(".post-status-line .permalink a", postElement).click(function() {
+               if (!$(".create-reply", postElement).hasClass("hidden")) {
+                       textArea = $("input.reply-input", postElement).focus().data("textarea");
+                       $(textArea).replaceSelection($(this).attr("href"));
+               }
+               return false;
+       });
+
        /* add “comment” link. */
-       addCommentLink(getPostId(postElement), getPostAuthor(postElement), postElement, $(postElement).find(".post-status-line .time"));
+       addCommentLink(getPostId(postElement), getPostAuthor(postElement), postElement, $(postElement).find(".post-status-line .permalink-author"));
 
        /* process all replies. */
        replyIds = [];
@@ -796,15 +847,66 @@ function ajaxifyPost(postElement) {
        });
 
        /* mark everything as known on click. */
-       $(postElement).click(function(event) {
-               if ($(event.target).hasClass("click-to-show")) {
-                       return false;
-               }
-               markPostAsKnown(this);
-       });
+       (function(postElement) {
+               $(postElement).click(function(event) {
+                       if ($(event.target).hasClass("click-to-show")) {
+                               return false;
+                       }
+                       markPostAsKnown(postElement, false);
+               });
+       })(postElement);
 
        /* hide reply input field. */
        $(postElement).find(".create-reply").addClass("hidden");
+
+       /* show Sone menu when hovering over the avatar. */
+       $(postElement).find(".post-avatar").mouseover(function() {
+               if (typeof currentSoneMenuTimeoutHandler != undefined) {
+                       clearTimeout(currentSoneMenuTimeoutHandler);
+               }
+               currentSoneMenuId = getPostId(this);
+               currentSoneMenuTimeoutHandler = setTimeout(function() {
+                       $(".sone-menu:visible").fadeOut();
+                       $(".sone-post-menu", postElement).mouseleave(function() {
+                               $(this).fadeOut();
+                       }).fadeIn();
+               }, 1000);
+       }).mouseleave(function() {
+               if (currentSoneMenuId == getPostId(this)) {
+                       clearTimeout(currentSoneMenuTimeoutHandler);
+               }
+       });
+       (function(postElement) {
+               var soneId = $(".sone-menu-id:first", postElement).text();
+               $(".sone-post-menu .follow", postElement).click(function() {
+                       var followElement = this;
+                       ajaxGet("followSone.ajax", { "sone": soneId, "formPassword": getFormPassword() }, function() {
+                               $(followElement).addClass("hidden");
+                               $(followElement).parent().find(".unfollow").removeClass("hidden");
+                               $("#sone .sone-menu").each(function() {
+                                       if (getMenuSone(this) == soneId) {
+                                               $(".follow", this).toggleClass("hidden", true);
+                                               $(".unfollow", this).toggleClass("hidden", false);
+                                       }
+                               });
+                       });
+                       return false;
+               });
+               $(".sone-post-menu .unfollow", postElement).click(function() {
+                       var unfollowElement = this;
+                       ajaxGet("unfollowSone.ajax", { "sone": soneId, "formPassword": getFormPassword() }, function() {
+                               $(unfollowElement).addClass("hidden");
+                               $(unfollowElement).parent().find(".follow").removeClass("hidden");
+                               $("#sone .sone-menu").each(function() {
+                                       if (getMenuSone(this) == soneId) {
+                                               $(".follow", this).toggleClass("hidden", false);
+                                               $(".unfollow", this).toggleClass("hidden", true);
+                                       }
+                               });
+                       });
+                       return false;
+               });
+       })(postElement);
 }
 
 /**
@@ -829,13 +931,55 @@ function ajaxifyReply(replyElement) {
                        });
                });
        })(replyElement);
-       addCommentLink(getPostId(replyElement), getReplyAuthor(replyElement), replyElement, $(replyElement).find(".reply-status-line .time"));
+
+       /* ajaxify author links */
+       $(".reply-status-line .permalink a", replyElement).click(function() {
+               if (!$(".create-reply", getPostElement(replyElement)).hasClass("hidden")) {
+                       textArea = $("input.reply-input", getPostElement(replyElement)).focus().data("textarea");
+                       $(textArea).replaceSelection($(this).attr("href"));
+               }
+               return false;
+       });
+
+       addCommentLink(getPostId(replyElement), getReplyAuthor(replyElement), replyElement, $(replyElement).find(".reply-status-line .permalink-author"));
 
        /* convert “show source” link into javascript function. */
        $(replyElement).find(".show-reply-source").each(function() {
                $("a", this).click(function() {
+                       reply = getReplyElement(this);
+                       rawReplyText = $(".reply-text.raw-text", reply);
+                       rawReplyText.toggleClass("hidden");
+                       if (rawReplyText.hasClass("hidden")) {
+                               $(".reply-text.short-text", reply).removeClass("hidden");
+                               $(".reply-text.text", reply).addClass("hidden");
+                               $(".expand-reply-text", reply).removeClass("hidden");
+                               $(".shrink-reply-text", reply).addClass("hidden");
+                       } else {
+                               $(".reply-text.short-text", reply).addClass("hidden");
+                               $(".reply-text.text", reply).addClass("hidden");
+                               $(".expand-reply-text", reply).addClass("hidden");
+                               $(".shrink-reply-text", reply).addClass("hidden");
+                       }
+                       return false;
+               });
+       });
+
+       /* convert “show more” link into javascript function. */
+       $(replyElement).find(".expand-reply-text").each(function() {
+               $(this).click(function() {
                        $(".reply-text.text", getReplyElement(this)).toggleClass("hidden");
-                       $(".reply-text.raw-text", getReplyElement(this)).toggleClass("hidden");
+                       $(".reply-text.short-text", getReplyElement(this)).toggleClass("hidden");
+                       $(".expand-reply-text", getReplyElement(this)).toggleClass("hidden");
+                       $(".shrink-reply-text", getReplyElement(this)).toggleClass("hidden");
+                       return false;
+               });
+       });
+       $(replyElement).find(".shrink-reply-text").each(function() {
+               $(this).click(function() {
+                       $(".reply-text.text", getReplyElement(this)).toggleClass("hidden");
+                       $(".reply-text.short-text", getReplyElement(this)).toggleClass("hidden");
+                       $(".expand-reply-text", getReplyElement(this)).toggleClass("hidden");
+                       $(".shrink-reply-text", getReplyElement(this)).toggleClass("hidden");
                        return false;
                });
        });
@@ -853,6 +997,55 @@ function ajaxifyReply(replyElement) {
                untrustSone(getReplyAuthor(this));
                return false;
        });
+
+       /* show Sone menu when hovering over the avatar. */
+       $(replyElement).find(".reply-avatar").mouseover(function() {
+               if (typeof currentSoneMenuTimeoutHandler != undefined) {
+                       clearTimeout(currentSoneMenuTimeoutHandler);
+               }
+               currentSoneMenuId = getPostId(this) + "-" + getReplyId(this);
+               currentSoneMenuTimeoutHandler = setTimeout(function() {
+                       $(".sone-menu:visible").fadeOut();
+                       $(".sone-reply-menu", replyElement).mouseleave(function() {
+                               $(this).fadeOut();
+                       }).fadeIn();
+               }, 1000);
+       }).mouseleave(function() {
+               if (currentSoneMenuId == getPostId(this) + "-" + getReplyId(this)) {
+                       clearTimeout(currentSoneMenuTimeoutHandler);
+               }
+       });
+       (function(replyElement) {
+               var soneId = $(".sone-menu-id", replyElement).text();
+               $(".sone-menu .follow", replyElement).click(function() {
+                       var followElement = this;
+                       ajaxGet("followSone.ajax", { "sone": soneId, "formPassword": getFormPassword() }, function() {
+                               $(followElement).addClass("hidden");
+                               $(followElement).parent().find(".unfollow").removeClass("hidden");
+                               $("#sone .sone-menu").each(function() {
+                                       if (getMenuSone(this) == soneId) {
+                                               $(".follow", this).toggleClass("hidden", true);
+                                               $(".unfollow", this).toggleClass("hidden", false);
+                                       }
+                               });
+                       });
+                       return false;
+               });
+               $(".sone-menu .unfollow", replyElement).click(function() {
+                       var unfollowElement = this;
+                       ajaxGet("unfollowSone.ajax", { "sone": soneId, "formPassword": getFormPassword() }, function() {
+                               $(unfollowElement).addClass("hidden");
+                               $(unfollowElement).parent().find(".follow").removeClass("hidden");
+                               $("#sone .sone-menu").each(function() {
+                                       if (getMenuSone(this) == soneId) {
+                                               $(".follow", this).toggleClass("hidden", false);
+                                               $(".unfollow", this).toggleClass("hidden", true);
+                                       }
+                               });
+                       });
+                       return false;
+               });
+       })(replyElement);
 }
 
 /**
@@ -871,19 +1064,23 @@ function ajaxifyNotification(notification) {
                notification.find(".text").addClass("hidden");
        }
        notification.find("form.mark-as-read button").click(function() {
-               $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": $(":input[name=type]", this.form).val(), "id": $(":input[name=id]", this.form).val()});
+               allIds = $(":input[name=id]", this.form).val().split(" ");
+               for (var index = 0; index < allIds.length; index += 16) {
+                       ids = allIds.slice(index, index + 16).join(" ");
+                       ajaxGet("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": $(":input[name=type]", this.form).val(), "id": ids});
+               }
        });
        notification.find("a[class^='link-']").each(function() {
                linkElement = $(this);
                if (linkElement.is("[href^='viewPost']")) {
                        id = linkElement.attr("class").substr(5);
                        if (hasPost(id)) {
-                               linkElement.attr("href", "#post-" + id);
+                               linkElement.attr("href", "#post-" + id).addClass("in-page-link");
                        }
                }
        });
        notification.find("form.dismiss button").click(function() {
-               $.getJSON("dismissNotification.ajax", { "formPassword" : getFormPassword(), "notification" : notification.attr("id") }, function(data, textStatus) {
+               ajaxGet("dismissNotification.ajax", { "formPassword" : getFormPassword(), "notification" : notification.attr("id") }, function(data, textStatus) {
                        /* dismiss in case of error, too. */
                        notification.slideUp();
                }, function(xmlHttpRequest, textStatus, error) {
@@ -894,6 +1091,24 @@ function ajaxifyNotification(notification) {
 }
 
 /**
+ * Returns the notification hash. This hash is used in {@link #getStatus()} to
+ * determine whether the notifications changed and need to be reloaded.
+ */
+function getNotificationHash() {
+       return $("#sone #notification-area #notification-hash").text();
+}
+
+/**
+ * Sets the notification hash.
+ *
+ * @param notificationHash
+ *            The new notification hash
+ */
+function setNotificationHash(notificationHash) {
+       $("#sone #notification-area #notification-hash").text(notificationHash);
+}
+
+/**
  * Retrieves element IDs from notification elements.
  *
  * @param notification
@@ -923,8 +1138,8 @@ function checkForRemovedSones(oldNotification, newNotification) {
        if (getNotificationId(oldNotification) != "new-sone-notification") {
                return;
        }
-       oldIds = getElementIds(oldNotification, ".sone-id");
-       newIds = getElementIds(newNotification, ".sone-id");
+       oldIds = getElementIds(oldNotification, ".new-sone-id");
+       newIds = getElementIds(newNotification, ".new-sone-id");
        $.each(oldIds, function(index, value) {
                if ($.inArray(value, newIds) == -1) {
                        markSoneAsKnown(getSone(value), true);
@@ -965,7 +1180,7 @@ function checkForRemovedPosts(oldNotification, newNotification) {
  *            The new notification element
  */
 function checkForRemovedReplies(oldNotification, newNotification) {
-       if (getNotificationId(oldNotification) != "new-replies-notification") {
+       if (getNotificationId(oldNotification) != "new-reply-notification") {
                return;
        }
        oldIds = getElementIds(oldNotification, ".reply-id");
@@ -978,12 +1193,43 @@ function checkForRemovedReplies(oldNotification, newNotification) {
 }
 
 function getStatus() {
-       $.getJSON("getStatus.ajax", {"loadAllSones": isKnownSonesPage()}, function(data, textStatus) {
+       ajaxGet("getStatus.ajax", isViewSonePage() ? {"soneIds": getShownSoneId() } : isKnownSonesPage() ? {"soneIds": getShownSoneIds() } : {}, function(data, textStatus) {
                if ((data != null) && data.success) {
                        /* process Sone information. */
                        $.each(data.sones, function(index, value) {
-                               updateSoneStatus(value.id, value.name, value.status, value.modified, value.locked, value.lastUpdatedUnknown ? null : value.lastUpdated);
+                               updateSoneStatus(value.id, value.name, value.status, value.modified, value.locked, value.lastUpdatedUnknown ? null : value.lastUpdated, value.lastUpdatedText);
                        });
+                       notLoggedIn = !data.loggedIn;
+                       if (!notLoggedIn) {
+                               showOfflineMarker(!online);
+                       }
+                       if (data.notificationHash != getNotificationHash()) {
+                               console.log("Old hash: ", getNotificationHash(), ", new hash: ", data.notificationHash);
+                               requestNotifications();
+                               /* process new posts. */
+                               $.each(data.newPosts, function(index, value) {
+                                       loadNewPost(value.id, value.sone, value.recipient, value.time);
+                               });
+                               /* process new replies. */
+                               $.each(data.newReplies, function(index, value) {
+                                       loadNewReply(value.id, value.sone, value.post, value.postSone);
+                               });
+                       }
+                       /* do it again in 5 seconds. */
+                       setTimeout(getStatus, 5000);
+               } else {
+                       /* data.success was false, wait 30 seconds. */
+                       setTimeout(getStatus, 30000);
+               }
+       }, function() {
+               statusRequestQueued = false;
+               ajaxError();
+       });
+}
+
+function requestNotifications() {
+       ajaxGet("getNotifications.ajax", {}, function(data, textStatus) {
+               if (data && data.success) {
                        /* search for removed notifications. */
                        $("#sone #notification-area .notification").each(function() {
                                notificationId = $(this).attr("id");
@@ -995,17 +1241,17 @@ function getStatus() {
                                        }
                                });
                                if (!foundNotification) {
-                                       if (notificationId == "new-sone-notification") {
-                                               $(".sone-id", this).each(function(index, element) {
+                                       if (notificationId == "new-sone-notification" && (data.options["ShowNotification/NewSones"] == true)) {
+                                               $(".new-sone-id", this).each(function(index, element) {
                                                        soneId = $(this).text();
                                                        markSoneAsKnown(getSone(soneId), true);
                                                });
-                                       } else if (notificationId == "new-post-notification") {
+                                       } else if (notificationId == "new-post-notification" && (data.options["ShowNotification/NewPosts"] == true)) {
                                                $(".post-id", this).each(function(index, element) {
                                                        postId = $(this).text();
                                                        markPostAsKnown(getPost(postId), true);
                                                });
-                                       } else if (notificationId == "new-replies-notification") {
+                                       } else if (notificationId == "new-reply-notification" && (data.options["ShowNotification/NewReplies"] == true)) {
                                                $(".reply-id", this).each(function(index, element) {
                                                        replyId = $(this).text();
                                                        markReplyAsKnown(getReply(replyId), true);
@@ -1021,67 +1267,29 @@ function getStatus() {
                                }
                        });
                        /* process notifications. */
-                       notificationIds = [];
                        $.each(data.notifications, function(index, value) {
                                oldNotification = getNotification(value.id);
-                               if ((oldNotification.length == 0) || (value.lastUpdatedTime > getNotificationLastUpdatedTime(oldNotification))) {
-                                       notificationIds.push(value.id);
+                               notification = ajaxifyNotification(createNotification(value.id, value.lastUpdatedTime, value.text, value.dismissable)).hide();
+                               if (oldNotification.length != 0) {
+                                       if ((oldNotification.find(".short-text").length > 0) && (notification.find(".short-text").length > 0)) {
+                                               opened = oldNotification.is(":visible") && oldNotification.find(".short-text").hasClass("hidden");
+                                               notification.find(".short-text").toggleClass("hidden", opened);
+                                               notification.find(".text").toggleClass("hidden", !opened);
+                                       }
+                                       checkForRemovedSones(oldNotification, notification);
+                                       checkForRemovedPosts(oldNotification, notification);
+                                       checkForRemovedReplies(oldNotification, notification);
+                                       oldNotification.replaceWith(notification.show());
+                               } else {
+                                       $("#sone #notification-area").append(notification);
+                                       if (value.id.substring(0, 5) != "local") {
+                                               notification.slideDown();
+                                               setActivity();
+                                       }
                                }
                        });
-                       if (notificationIds.length > 0) {
-                               loadNotifications(notificationIds);
-                       }
-                       /* process new posts. */
-                       $.each(data.newPosts, function(index, value) {
-                               loadNewPost(value.id, value.sone, value.recipient, value.time);
-                       });
-                       /* process new replies. */
-                       $.each(data.newReplies, function(index, value) {
-                               loadNewReply(value.id, value.sone, value.post, value.postSone);
-                       });
-                       /* do it again in 5 seconds. */
-                       setTimeout(getStatus, 5000);
-               } else {
-                       /* data.success was false, wait 30 seconds. */
-                       setTimeout(getStatus, 30000);
+                       setNotificationHash(data.notificationHash);
                }
-       }, function(xmlHttpRequest, textStatus, error) {
-               /* something really bad happend, wait a minute. */
-               setTimeout(getStatus, 60000);
-       })
-}
-
-/**
- * Requests multiple notifications from Sone and displays them.
- *
- * @param notificationIds
- *            Array of IDs of the notifications to load
- */
-function loadNotifications(notificationIds) {
-       $.getJSON("getNotification.ajax", {"notifications": notificationIds.join(",")}, function(data, textStatus) {
-               if (!data || !data.success) {
-                       // TODO - show error
-                       return;
-               }
-               $.each(data.notifications, function(index, value) {
-                       oldNotification = getNotification(value.id);
-                       notification = ajaxifyNotification(createNotification(value.id, value.lastUpdatedTime, value.text, value.dismissable)).hide();
-                       if (oldNotification.length != 0) {
-                               if ((oldNotification.find(".short-text").length > 0) && (notification.find(".short-text").length > 0)) {
-                                       opened = oldNotification.is(":visible") && oldNotification.find(".short-text").hasClass("hidden");
-                                       notification.find(".short-text").toggleClass("hidden", opened);
-                                       notification.find(".text").toggleClass("hidden", !opened);
-                               }
-                               checkForRemovedSones(oldNotification, notification);
-                               checkForRemovedPosts(oldNotification, notification);
-                               checkForRemovedReplies(oldNotification, notification);
-                               oldNotification.replaceWith(notification.show());
-                       } else {
-                               $("#sone #notification-area").append(notification);
-                               notification.slideDown();
-                               setActivity();
-                       }
-               })
        });
 }
 
@@ -1115,6 +1323,22 @@ function isIndexPage() {
 }
 
 /**
+ * Returns the current page of the selected pagination. If no pagination can be
+ * found with the given selector, {@code 1} is returned.
+ *
+ * @param paginationSelector
+ *            The pagination selector
+ * @returns The current page of the pagination
+ */
+function getPage(paginationSelector) {
+       pagination = $(paginationSelector);
+       if (pagination.length > 0) {
+               return $(".current-page", paginationSelector).text();
+       }
+       return 1;
+}
+
+/**
  * Returns whether the current page is a “view Sone” page.
  *
  * @returns {Boolean} <code>true</code> if the current page is a “view Sone”
@@ -1131,7 +1355,21 @@ function isViewSonePage() {
  * @returns The ID of the currently shown Sone
  */
 function getShownSoneId() {
-       return $("#sone .sone-id").text();
+       return $("#sone .sone-id").first().text();
+}
+
+/**
+ * Returns the ID of all currently visible Sones. This is mainly used on the
+ * “Known Sones” page.
+ *
+ * @returns The ID of the currently shown Sones
+ */
+function getShownSoneIds() {
+       var soneIds = new Array();
+       $("#sone #known-sones .sone .id").each(function() {
+               soneIds.push($(this).text());
+       });
+       return soneIds.join(",");
 }
 
 /**
@@ -1173,7 +1411,7 @@ function isKnownSonesPage() {
  *          exists on the page, <code>false</code> otherwise
  */
 function hasPost(postId) {
-       return $(".post#" + postId).length > 0;
+       return $(".post#post-" + postId).length > 0;
 }
 
 /**
@@ -1185,16 +1423,16 @@ function hasPost(postId) {
  *          exists on the page, <code>false</code> otherwise
  */
 function hasReply(replyId) {
-       return $("#sone .reply#" + replyId).length > 0;
+       return $("#sone .reply#reply-" + replyId).length > 0;
 }
 
 function loadNewPost(postId, soneId, recipientId, time) {
        if (hasPost(postId)) {
                return;
        }
-       if (!isIndexPage()) {
+       if (!isIndexPage() || (getPage(".pagination-index") > 1)) {
                if (!isViewPostPage() || (getShownPostId() != postId)) {
-                       if (!isViewSonePage() || ((getShownSoneId() != soneId) && (getShownSoneId() != recipientId))) {
+                       if (!isViewSonePage() || ((getShownSoneId() != soneId) && (getShownSoneId() != recipientId)) || (getPage(".post-navigation") > 1)) {
                                return;
                        }
                }
@@ -1202,12 +1440,12 @@ function loadNewPost(postId, soneId, recipientId, time) {
        if (getPostTime($("#sone .post").last()) > time) {
                return;
        }
-       $.getJSON("getPost.ajax", { "post" : postId }, function(data, textStatus) {
+       ajaxGet("getPost.ajax", { "post" : postId }, function(data, textStatus) {
                if ((data != null) && data.success) {
                        if (hasPost(data.post.id)) {
                                return;
                        }
-                       if (!isIndexPage() && !(isViewSonePage() && ((getShownSoneId() == data.post.sone) || (getShownSoneId() == data.post.recipient)))) {
+                       if ((!isIndexPage() || (getPage(".pagination-index") > 1)) && !(isViewSonePage() && ((getShownSoneId() == data.post.sone) || (getShownSoneId() == data.post.recipient) || (getPage(".post-navigation") > 1)))) {
                                return;
                        }
                        var firstOlderPost = null;
@@ -1218,6 +1456,9 @@ function loadNewPost(postId, soneId, recipientId, time) {
                                }
                        });
                        newPost = $(data.post.html).addClass("hidden");
+                       if ($(".post-author-local", newPost).text() == "true") {
+                               newPost.removeClass("new");
+                       }
                        if (firstOlderPost != null) {
                                newPost.insertBefore(firstOlderPost);
                        }
@@ -1236,13 +1477,13 @@ function loadNewReply(replyId, soneId, postId, postSoneId) {
        if (!hasPost(postId)) {
                return;
        }
-       $.getJSON("getReply.ajax", { "reply": replyId }, function(data, textStatus) {
+       ajaxGet("getReply.ajax", { "reply": replyId }, function(data, textStatus) {
                /* find post. */
                if ((data != null) && data.success) {
                        if (hasReply(data.reply.id)) {
                                return;
                        }
-                       $("#sone .post#" + data.reply.postId).each(function() {
+                       $("#sone .post#post-" + data.reply.postId).each(function() {
                                var firstNewerReply = null;
                                $(this).find(".replies .reply").each(function() {
                                        if (getReplyTime(this) > data.reply.time) {
@@ -1251,6 +1492,14 @@ function loadNewReply(replyId, soneId, postId, postSoneId) {
                                        }
                                });
                                newReply = $(data.reply.html).addClass("hidden");
+                               if ($(".reply-author-local", newReply).text() == "true") {
+                                       newReply.removeClass("new");
+                                       (function(newReply) {
+                                               setTimeout(function() {
+                                                       markReplyAsKnown(newReply, false);
+                                               }, 5000);
+                                       })(newReply);
+                               }
                                if (firstNewerReply != null) {
                                        newReply.insertBefore(firstNewerReply);
                                } else {
@@ -1280,11 +1529,11 @@ function loadNewReply(replyId, soneId, postId, postSoneId) {
  *            request
  */
 function markSoneAsKnown(soneElement, skipRequest) {
-       if ($(".new", soneElement).length > 0) {
-               if ((typeof skipRequest != "undefined") && !skipRequest) {
-                       $.getJSON("maskAsKnown.ajax", {"formPassword": getFormPassword(), "type": "sone", "id": getSoneId(soneElement)}, function(data, textStatus) {
-                               $(soneElement).removeClass("new");
-                       });
+       if ($(soneElement).hasClass("new")) {
+               $(soneElement).removeClass("new");
+               if ((typeof skipRequest == "undefined") || !skipRequest) {
+                       ajaxGet("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "sone", "id": getSoneId(soneElement)});
+                       requestNotifications();
                }
        }
 }
@@ -1292,27 +1541,29 @@ function markSoneAsKnown(soneElement, skipRequest) {
 function markPostAsKnown(postElements, skipRequest) {
        $(postElements).each(function() {
                postElement = this;
-               if ($(postElement).hasClass("new")) {
+               if ($(postElement).hasClass("new") || ((typeof skipRequest != "undefined"))) {
                        (function(postElement) {
                                $(postElement).removeClass("new");
-                               $(".click-to-show", postElement).removeClass("new");
                                if ((typeof skipRequest == "undefined") || !skipRequest) {
-                                       $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "post", "id": getPostId(postElement)});
+                                       ajaxGet("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "post", "id": getPostId(postElement)});
+                                       requestNotifications();
                                }
                        })(postElement);
                }
+               $(".click-to-show", postElement).removeClass("new");
        });
-       markReplyAsKnown($(postElements).find(".reply"));
+       markReplyAsKnown($(postElements).find(".reply"), true);
 }
 
 function markReplyAsKnown(replyElements, skipRequest) {
        $(replyElements).each(function() {
                replyElement = this;
-               if ($(replyElement).hasClass("new")) {
+               if ($(replyElement).hasClass("new") || ((typeof skipRequest != "undefined"))) {
                        (function(replyElement) {
                                $(replyElement).removeClass("new");
                                if ((typeof skipRequest == "undefined") || !skipRequest) {
-                                       $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "reply", "id": getReplyId(replyElement)});
+                                       ajaxGet("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "reply", "id": getReplyId(replyElement)});
+                                       requestNotifications();
                                }
                        })(replyElement);
                }
@@ -1350,7 +1601,7 @@ function updatePostTime(postId, timeText, refreshTime, tooltip) {
  *            Comma-separated post IDs
  */
 function updatePostTimes(postIds) {
-       $.getJSON("getTimes.ajax", { "posts" : postIds }, function(data, textStatus) {
+       ajaxGet("getTimes.ajax", { "posts" : postIds }, function(data, textStatus) {
                if ((data != null) && data.success) {
                        $.each(data.postTimes, function(index, value) {
                                updatePostTime(index, value.timeText, value.refreshTime, value.tooltip);
@@ -1387,7 +1638,7 @@ function updateReplyTime(replyId, timeText, refreshTime, tooltip) {
  *            Comma-separated post IDs
  */
 function updateReplyTimes(replyIds) {
-       $.getJSON("getTimes.ajax", { "replies" : replyIds }, function(data, textStatus) {
+       ajaxGet("getTimes.ajax", { "replies" : replyIds }, function(data, textStatus) {
                if ((data != null) && data.success) {
                        $.each(data.replyTimes, function(index, value) {
                                updateReplyTime(index, value.timeText, value.refreshTime, value.tooltip);
@@ -1479,7 +1730,7 @@ function changeIcon(iconUrl) {
 function createNotification(id, lastUpdatedTime, text, dismissable) {
        notification = $("<div></div>").addClass("notification").attr("id", id).attr("lastUpdatedTime", lastUpdatedTime);
        if (dismissable) {
-               dismissForm = $("#sone #notification-area #notification-dismiss-template").clone().removeClass("hidden").removeAttr("id")
+               dismissForm = $("#sone #notification-area #notification-dismiss-template").clone().removeClass("hidden").removeAttr("id");
                dismissForm.find("input[name=notification]").val(id);
                notification.append(dismissForm);
        }
@@ -1505,7 +1756,7 @@ function showNotificationDetails(notificationId) {
  *            The ID of the field to delete
  */
 function deleteProfileField(fieldId) {
-       $.getJSON("deleteProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId}, function(data, textStatus) {
+       ajaxGet("deleteProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId}, function(data, textStatus) {
                if (data && data.success) {
                        $("#sone .profile-field#" + data.field.id).slideUp();
                }
@@ -1523,7 +1774,7 @@ function deleteProfileField(fieldId) {
  *            Called when the renaming was successful
  */
 function editProfileField(fieldId, newName, successFunction) {
-       $.getJSON("editProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "name": newName}, function(data, textStatus) {
+       ajaxGet("editProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "name": newName}, function(data, textStatus) {
                if (data && data.success) {
                        successFunction();
                }
@@ -1541,7 +1792,7 @@ function editProfileField(fieldId, newName, successFunction) {
  *            Function to call on success
  */
 function moveProfileField(fieldId, direction, successFunction) {
-       $.getJSON("moveProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "direction": direction}, function(data, textStatus) {
+       ajaxGet("moveProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "direction": direction}, function(data, textStatus) {
                if (data && data.success) {
                        successFunction();
                }
@@ -1572,14 +1823,70 @@ function moveProfileFieldDown(fieldId, successFunction) {
        moveProfileField(fieldId, "down", successFunction);
 }
 
+var statusRequestQueued = true;
+
+/**
+ * Sets the status of the web interface as offline.
+ */
+function ajaxError() {
+       online = false;
+       showOfflineMarker(true);
+       if (!statusRequestQueued) {
+               setTimeout(getStatus, 5000);
+               statusRequestQueued = true;
+       }
+}
+
+/**
+ * Sets the status of the web interface as online.
+ */
+function ajaxSuccess() {
+       online = true;
+       showOfflineMarker(!online || (initiallyLoggedIn && notLoggedIn));
+}
+
+/**
+ * Shows or hides the offline marker.
+ *
+ * @param visible
+ *            {@code true} to display the offline marker, {@code false} to hide
+ *            it
+ */
+function showOfflineMarker(visible) {
+       /* jQuery documentation says toggle() works the other way around?! */
+       $("#sone #offline-marker").toggle(visible);
+       if (visible) {
+               $("#sone #main").addClass("offline");
+       } else {
+               $("#sone #main").removeClass("offline");
+       }
+}
+
 //
 // EVERYTHING BELOW HERE IS EXECUTED AFTER LOADING THE PAGE
 //
 
 var focus = true;
+var online = true;
+var initiallyLoggedIn = $("#sone #loggedIn").text() == "true";
+var notLoggedIn = !initiallyLoggedIn;
+
+/** ID of the next-to-show Sone context menu. */
+var currentSoneMenuId;
+
+/** Timeout handler for the next-to-show Sone context menu. */
+var currentSoneMenuTimeoutHandler;
 
 $(document).ready(function() {
 
+       /* rip out the status update textarea. */
+       $("#sone .rip-out").each(function() {
+               var oldElement = $(this);
+               newElement = $("<input type='text'/>");
+               newElement.attr("class", oldElement.attr("class")).attr("name", oldElement.attr("name"));
+               oldElement.before(newElement).remove();
+       });
+
        /* this initializes the status update input field. */
        getTranslation("WebInterface.DefaultText.StatusUpdate", function(defaultText) {
                registerInputTextareaSwap("#sone #update-status .status-input", defaultText, "text", false, false);
@@ -1598,7 +1905,7 @@ $(document).ready(function() {
                        }
                        sender = $(this).find(":input[name=sender]").val();
                        text = $(this).find(":input[name=text]:enabled").val();
-                       $.getJSON("createPost.ajax", { "formPassword": getFormPassword(), "sender": sender, "text": text }, function(data, textStatus) {
+                       ajaxGet("createPost.ajax", { "formPassword": getFormPassword(), "sender": sender, "text": text }, function(data, textStatus) {
                                button.removeAttr("disabled");
                        });
                        $(this).find(":input[name=sender]").val(getCurrentSoneId());
@@ -1627,7 +1934,7 @@ $(document).ready(function() {
                $("#sone #post-message").submit(function() {
                        sender = $(this).find(":input[name=sender]").val();
                        text = $(this).find(":input[name=text]:enabled").val();
-                       $.getJSON("createPost.ajax", { "formPassword": getFormPassword(), "recipient": getShownSoneId(), "sender": sender, "text": text });
+                       ajaxGet("createPost.ajax", { "formPassword": getFormPassword(), "recipient": getShownSoneId(), "sender": sender, "text": text });
                        $(this).find(":input[name=sender]").val(getCurrentSoneId());
                        $(this).find(":input[name=text]:enabled").val("").blur();
                        $(this).find(".sender").hide();
@@ -1641,8 +1948,10 @@ $(document).ready(function() {
        getTranslation("WebInterface.Confirmation.DeletePostButton", function(text) {
                getTranslation("WebInterface.Confirmation.DeleteReplyButton", function(text) {
                        getTranslation("WebInterface.DefaultText.Reply", function(text) {
-                               $("#sone .post").each(function() {
-                                       ajaxifyPost(this);
+                               getTranslation("WebInterface.Button.Comment", function(text) {
+                                       $("#sone .post").each(function() {
+                                               ajaxifyPost(this);
+                                       });
                                });
                        });
                });
@@ -1701,6 +2010,6 @@ $(document).ready(function() {
                resetActivity();
        }).blur(function() {
                focus = false;
-       })
+       });
 
 });