+ return elementIds;
+}
+
+/**
+ * Compares the given notification elements and calls {@link #markSoneAsKnown()}
+ * for every ID that is contained in the old notification but not in the new.
+ *
+ * @param oldNotification
+ * The old notification element
+ * @param newNotification
+ * The new notification element
+ */
+function checkForRemovedSones(oldNotification, newNotification) {
+ if (getNotificationId(oldNotification) != "new-sone-notification") {
+ return;
+ }
+ oldIds = getElementIds(oldNotification, ".sone-id");
+ newIds = getElementIds(newNotification, ".sone-id");
+ $.each(oldIds, function(index, value) {
+ if ($.inArray(value, newIds) == -1) {
+ markSoneAsKnown(getSone(value), true);
+ }
+ });
+}
+
+/**
+ * Compares the given notification elements and calls {@link #markPostAsKnown()}
+ * for every ID that is contained in the old notification but not in the new.
+ *
+ * @param oldNotification
+ * The old notification element
+ * @param newNotification
+ * The new notification element
+ */
+function checkForRemovedPosts(oldNotification, newNotification) {
+ if (getNotificationId(oldNotification) != "new-post-notification") {
+ return;
+ }
+ oldIds = getElementIds(oldNotification, ".post-id");
+ newIds = getElementIds(newNotification, ".post-id");
+ $.each(oldIds, function(index, value) {
+ if ($.inArray(value, newIds) == -1) {
+ markPostAsKnown(getPost(value), true);
+ }
+ });
+}
+
+/**
+ * Compares the given notification elements and calls
+ * {@link #markReplyAsKnown()} for every ID that is contained in the old
+ * notification but not in the new.
+ *
+ * @param oldNotification
+ * The old notification element
+ * @param newNotification
+ * The new notification element
+ */
+function checkForRemovedReplies(oldNotification, newNotification) {
+ if (getNotificationId(oldNotification) != "new-replies-notification") {
+ return;
+ }
+ oldIds = getElementIds(oldNotification, ".reply-id");
+ newIds = getElementIds(newNotification, ".reply-id");
+ $.each(oldIds, function(index, value) {
+ if ($.inArray(value, newIds) == -1) {
+ markReplyAsKnown(getReply(value), true);
+ }
+ });
+}
+
+function getStatus() {
+ $.getJSON("getStatus.ajax", {"loadAllSones": isKnownSonesPage()}, 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);
+ });
+ /* search for removed notifications. */
+ $("#sone #notification-area .notification").each(function() {
+ notificationId = $(this).attr("id");
+ foundNotification = false;
+ $.each(data.notifications, function(index, value) {
+ if (value.id == notificationId) {
+ foundNotification = true;
+ return false;
+ }
+ });
+ if (!foundNotification) {
+ if (notificationId == "new-sone-notification") {
+ $(".sone-id", this).each(function(index, element) {
+ soneId = $(this).text();
+ markSoneAsKnown(getSone(soneId), true);
+ });
+ } else if (notificationId == "new-post-notification") {
+ $(".post-id", this).each(function(index, element) {
+ postId = $(this).text();
+ markPostAsKnown(getPost(postId), true);
+ });
+ } else if (notificationId == "new-replies-notification") {
+ $(".reply-id", this).each(function(index, element) {
+ replyId = $(this).text();
+ markReplyAsKnown(getReply(replyId), true);
+ });
+ }
+ $(this).slideUp("normal", function() {
+ $(this).remove();
+ });
+ }
+ });
+ /* process notifications. */
+ $.each(data.notifications, function(index, value) {
+ oldNotification = getNotification(value.id);
+ notification = ajaxifyNotification(createNotification(value.id, 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();
+ }
+ });
+ /* 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);
+ });
+ /* remove activity when no notifications are visible. */
+ if ($("#sone #notification-area .notification").length == 0) {
+ resetActivity();
+ }
+ /* do it again in 5 seconds. */
+ setTimeout(getStatus, 5000);
+ } else {
+ /* data.success was false, wait 30 seconds. */
+ setTimeout(getStatus, 30000);
+ }
+ }, function(xmlHttpRequest, textStatus, error) {
+ /* something really bad happend, wait a minute. */
+ setTimeout(getStatus, 60000);
+ })
+}
+
+/**
+ * Returns the ID of the currently logged in Sone.
+ *
+ * @return The ID of the current Sone, or an empty string if no Sone is logged
+ * in
+ */
+function getCurrentSoneId() {
+ return $("#currentSoneId").text();
+}
+
+/**
+ * Returns the content of the page-id attribute.
+ *
+ * @returns The page ID
+ */
+function getPageId() {
+ return $("#sone .page-id").text();
+}
+
+/**
+ * Returns whether the current page is the index page.
+ *
+ * @returns {Boolean} <code>true</code> if the current page is the index page,
+ * <code>false</code> otherwise
+ */
+function isIndexPage() {
+ return getPageId() == "index";
+}
+
+/**
+ * Returns whether the current page is a “view Sone” page.
+ *
+ * @returns {Boolean} <code>true</code> if the current page is a “view Sone”
+ * page, <code>false</code> otherwise
+ */
+function isViewSonePage() {
+ return getPageId() == "view-sone";
+}
+
+/**
+ * Returns the ID of the currently shown Sone. This will only return a sensible
+ * value if isViewSonePage() returns <code>true</code>.
+ *
+ * @returns The ID of the currently shown Sone
+ */
+function getShownSoneId() {
+ return $("#sone .sone-id").text();
+}
+
+/**
+ * Returns whether the current page is a “view post” page.
+ *
+ * @returns {Boolean} <code>true</code> if the current page is a “view post”
+ * page, <code>false</code> otherwise
+ */
+function isViewPostPage() {
+ return getPageId() == "view-post";
+}
+
+/**
+ * Returns the ID of the currently shown post. This will only return a sensible
+ * value if isViewPostPage() returns <code>true</code>.
+ *
+ * @returns The ID of the currently shown post
+ */
+function getShownPostId() {
+ return $("#sone .post-id").text();
+}
+
+/**
+ * Returns whether the current page is the “known Sones” page.
+ *
+ * @returns {Boolean} <code>true</code> if the current page is the “known
+ * Sones” page, <code>false</code> otherwise
+ */
+function isKnownSonesPage() {
+ return getPageId() == "known-sones";
+}
+
+/**
+ * Returns whether a post with the given ID exists on the current page.
+ *
+ * @param postId
+ * The post ID to check for
+ * @returns {Boolean} <code>true</code> if a post with the given ID already
+ * exists on the page, <code>false</code> otherwise
+ */
+function hasPost(postId) {
+ return $(".post#" + postId).length > 0;
+}
+
+/**
+ * Returns whether a reply with the given ID exists on the current page.
+ *
+ * @param replyId
+ * The reply ID to check for
+ * @returns {Boolean} <code>true</code> if a reply with the given ID already
+ * exists on the page, <code>false</code> otherwise
+ */
+function hasReply(replyId) {
+ return $("#sone .reply#" + replyId).length > 0;
+}
+
+function loadNewPost(postId, soneId, recipientId, time) {
+ if (hasPost(postId)) {
+ return;
+ }
+ if (!isIndexPage()) {
+ if (!isViewPostPage() || (getShownPostId() != postId)) {
+ if (!isViewSonePage() || ((getShownSoneId() != soneId) && (getShownSoneId() != recipientId))) {
+ return;
+ }
+ }
+ }
+ if (getPostTime($("#sone .post").last()) > time) {
+ return;
+ }
+ $.getJSON("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)))) {
+ return;
+ }
+ var firstOlderPost = null;
+ $("#sone .post").each(function() {
+ if (getPostTime(this) < data.post.time) {
+ firstOlderPost = $(this);
+ return false;
+ }
+ });
+ newPost = $(data.post.html).addClass("hidden");
+ if (firstOlderPost != null) {
+ newPost.insertBefore(firstOlderPost);
+ }
+ ajaxifyPost(newPost);
+ updatePostTimes(data.post.id);
+ newPost.slideDown();
+ setActivity();
+ }
+ });
+}
+
+function loadNewReply(replyId, soneId, postId, postSoneId) {
+ if (hasReply(replyId)) {
+ return;
+ }
+ if (!hasPost(postId)) {
+ return;
+ }
+ $.getJSON("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() {
+ var firstNewerReply = null;
+ $(this).find(".replies .reply").each(function() {
+ if (getReplyTime(this) > data.reply.time) {
+ firstNewerReply = $(this);
+ return false;
+ }
+ });
+ newReply = $(data.reply.html).addClass("hidden");
+ if (firstNewerReply != null) {
+ newReply.insertBefore(firstNewerReply);
+ } else {
+ if ($(this).find(".replies .create-reply")) {
+ $(this).find(".replies .create-reply").before(newReply);
+ } else {
+ $(this).find(".replies").append(newReply);
+ }
+ }
+ ajaxifyReply(newReply);
+ updateReplyTimes(data.reply.id);
+ newReply.slideDown();
+ setActivity();
+ return false;
+ });
+ }
+ });
+}
+
+/**
+ * Marks the given Sone as known if it is still new.
+ *
+ * @param soneElement
+ * The Sone to mark as known
+ * @param skipRequest
+ * true to skip the JSON request, false or omit to perform the JSON
+ * 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");
+ });
+ }
+ }
+}
+
+function markPostAsKnown(postElements, skipRequest) {
+ $(postElements).each(function() {
+ postElement = this;
+ if ($(postElement).hasClass("new")) {
+ (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)});
+ }
+ })(postElement);
+ }
+ });
+ markReplyAsKnown($(postElements).find(".reply"));
+}
+
+function markReplyAsKnown(replyElements, skipRequest) {
+ $(replyElements).each(function() {
+ replyElement = this;
+ if ($(replyElement).hasClass("new")) {
+ (function(replyElement) {
+ $(replyElement).removeClass("new");
+ if ((typeof skipRequest == "undefined") || !skipRequest) {
+ $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "reply", "id": getReplyId(replyElement)});
+ }
+ })(replyElement);
+ }
+ });
+}
+
+/**
+ * Updates the time of the post with the given ID.
+ *
+ * @param postId
+ * The ID of the post to update
+ * @param timeText
+ * The text of the time to show
+ * @param refreshTime
+ * The refresh time after which to request a new time (in seconds)
+ * @param tooltip
+ * The tooltip to show
+ */
+function updatePostTime(postId, timeText, refreshTime, tooltip) {
+ if (!getPost(postId).is(":visible")) {
+ return;
+ }
+ getPost(postId).find(".post-status-line > .time a").html(timeText).attr("title", tooltip);
+ (function(postId, refreshTime) {
+ setTimeout(function() {
+ updatePostTimes(postId);
+ }, refreshTime * 1000);
+ })(postId, refreshTime);
+}
+
+/**
+ * Requests new rendered times for the posts with the given IDs.
+ *
+ * @param postIds
+ * Comma-separated post IDs
+ */
+function updatePostTimes(postIds) {
+ $.getJSON("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);
+ });
+ }
+ });
+}
+
+/**
+ * Updates the time of the reply with the given ID.
+ *
+ * @param postId
+ * The ID of the reply to update
+ * @param timeText
+ * The text of the time to show
+ * @param refreshTime
+ * The refresh time after which to request a new time (in seconds)
+ * @param tooltip
+ * The tooltip to show
+ */
+function updateReplyTime(replyId, timeText, refreshTime, tooltip) {
+ if (!getReply(replyId).is(":visible")) {
+ return;
+ }
+ getReply(replyId).find(".reply-status-line > .time").html(timeText).attr("title", tooltip);
+ (function(replyId, refreshTime) {
+ setTimeout(function() {
+ updateReplyTimes(replyId);
+ }, refreshTime * 1000);
+ })(replyId, refreshTime);
+}
+
+/**
+ * Requests new rendered times for the posts with the given IDs.
+ *
+ * @param postIds
+ * Comma-separated post IDs
+ */
+function updateReplyTimes(replyIds) {
+ $.getJSON("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);
+ });
+ }
+ });
+}
+
+function resetActivity() {
+ title = document.title;
+ if (title.indexOf('(') == 0) {
+ setTitle(title.substr(title.indexOf(' ') + 1));
+ }
+}
+
+function setActivity() {
+ if (!focus) {
+ title = document.title;
+ if (title.indexOf('(') != 0) {
+ setTitle("(!) " + title);
+ }
+ if (!iconBlinking) {
+ setTimeout(toggleIcon, 1500);
+ iconBlinking = true;
+ }
+ }
+}
+
+/**
+ * Sets the window title after a small delay to prevent race-condition issues.
+ *
+ * @param title
+ * The title to set
+ */
+function setTitle(title) {
+ setTimeout(function() {
+ document.title = title;
+ }, 50);
+}
+
+/** Whether the icon is currently showing activity. */
+var iconActive = false;
+
+/** Whether the icon is currently supposed to blink. */
+var iconBlinking = false;
+
+/**
+ * Toggles the icon. If the window has gained focus and the icon is still
+ * showing the activity state, it is returned to normal.
+ */
+function toggleIcon() {
+ if (focus) {
+ if (iconActive) {
+ changeIcon("images/icon.png");
+ iconActive = false;
+ }
+ iconBlinking = false;
+ } else {
+ iconActive = !iconActive;
+ changeIcon(iconActive ? "images/icon-activity.png" : "images/icon.png");
+ setTimeout(toggleIcon, 1500);
+ }
+}
+
+/**
+ * Changes the icon of the page.
+ *
+ * @param iconUrl
+ * The new URL of the icon
+ */
+function changeIcon(iconUrl) {
+ $("link[rel=icon]").remove();
+ $("head").append($("<link>").attr("rel", "icon").attr("type", "image/png").attr("href", iconUrl));
+ $("iframe[id=icon-update]")[0].src += "";