Add relative and dynamic timestamps to the web interface.
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Wed, 6 Apr 2011 05:28:51 +0000 (07:28 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Wed, 6 Apr 2011 05:28:51 +0000 (07:28 +0200)
This fixes #2.

src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/java/net/pterodactylus/sone/web/ajax/GetTimesAjaxPage.java [new file with mode: 0644]
src/main/resources/i18n/sone.en.properties
src/main/resources/static/javascript/sone.js

index acfdc7f..7c33803 100644 (file)
@@ -72,6 +72,7 @@ import net.pterodactylus.sone.web.ajax.GetLikesAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetPostAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetReplyAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetStatusAjaxPage;
+import net.pterodactylus.sone.web.ajax.GetTimesAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetTranslationPage;
 import net.pterodactylus.sone.web.ajax.LikeAjaxPage;
 import net.pterodactylus.sone.web.ajax.LockSoneAjaxPage;
@@ -586,6 +587,7 @@ public class WebInterface implements CoreListener {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateReplyAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetReplyAjaxPage(this, replyTemplate)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetPostAjaxPage(this, postTemplate)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new GetTimesAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new MarkAsKnownAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DeletePostAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteReplyAjaxPage(this)));
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/GetTimesAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/GetTimesAjaxPage.java
new file mode 100644 (file)
index 0000000..5711257
--- /dev/null
@@ -0,0 +1,244 @@
+/*
+ * Sone - GetTimesAjaxPage.java - Copyright © 2010–2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later
+ * version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.ajax;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+import net.pterodactylus.util.number.Digits;
+
+/**
+ * Ajax page that returns a formatted, relative timestamp for replies or posts.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class GetTimesAjaxPage extends JsonPage {
+
+       /** Formatter for tooltips. */
+       private static final DateFormat dateFormat = new SimpleDateFormat("MMM d, yyyy, HH:mm:ss");
+
+       /**
+        * Creates a new get times AJAX page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public GetTimesAjaxPage(WebInterface webInterface) {
+               super("getTimes.ajax", webInterface);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected JsonObject createJsonObject(Request request) {
+               long now = System.currentTimeMillis();
+               String allIds = request.getHttpRequest().getParam("posts");
+               JsonObject postTimes = new JsonObject();
+               if (allIds.length() > 0) {
+                       String[] ids = allIds.split(",");
+                       for (String id : ids) {
+                               Post post = webInterface.getCore().getPost(id, false);
+                               if (post == null) {
+                                       continue;
+                               }
+                               long age = now - post.getTime();
+                               JsonObject postTime = new JsonObject();
+                               Time time = getTime(age);
+                               postTime.put("timeText", time.getText());
+                               postTime.put("refreshTime", time.getRefresh() / Time.SECOND);
+                               postTime.put("tooltip", dateFormat.format(new Date(post.getTime())));
+                               postTimes.put(id, postTime);
+                       }
+               }
+               JsonObject replyTimes = new JsonObject();
+               allIds = request.getHttpRequest().getParam("replies");
+               if (allIds.length() > 0) {
+                       String[] ids = allIds.split(",");
+                       for (String id : ids) {
+                               Reply reply = webInterface.getCore().getReply(id, false);
+                               if (reply == null) {
+                                       continue;
+                               }
+                               long age = now - reply.getTime();
+                               JsonObject replyTime = new JsonObject();
+                               Time time = getTime(age);
+                               replyTime.put("timeText", time.getText());
+                               replyTime.put("refreshTime", time.getRefresh() / Time.SECOND);
+                               replyTime.put("tooltip", dateFormat.format(new Date(reply.getTime())));
+                               replyTimes.put(id, replyTime);
+                       }
+               }
+               return createSuccessJsonObject().put("postTimes", postTimes).put("replyTimes", replyTimes);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean needsFormPassword() {
+               return false;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Returns the formatted relative time for a given age.
+        *
+        * @param age
+        *            The age to format (in milliseconds)
+        * @return The formatted age
+        */
+       private Time getTime(long age) {
+               String text;
+               long refresh;
+               if (age < 0) {
+                       text = webInterface.getL10n().getDefaultString("View.Time.InTheFuture");
+                       refresh = 5 * Time.MINUTE;
+               } else if (age < 30 * Time.SECOND) {
+                       text = webInterface.getL10n().getDefaultString("View.Time.AFewSecondsAgo");
+                       refresh = 5 * Time.SECOND;
+               } else if (age < 1 * Time.MINUTE) {
+                       text = webInterface.getL10n().getString("View.Time.XSecondsAgo", "sec", String.valueOf((int) Digits.round(age / Time.SECOND, 10)));
+                       refresh = 10 * Time.SECOND;
+               } else if (age < 2 * Time.MINUTE) {
+                       text = webInterface.getL10n().getString("View.Time.AMinuteAgo");
+                       refresh = Time.MINUTE;
+               } else if (age < 30 * Time.MINUTE) {
+                       text = webInterface.getL10n().getString("View.Time.XMinutesAgo", "min", String.valueOf((int) Digits.round(age / Time.MINUTE, 1)));
+                       refresh = Time.MINUTE;
+               } else if (age < 45 * Time.MINUTE) {
+                       text = webInterface.getL10n().getString("View.Time.HalfAnHourAgo");
+                       refresh = 10 * Time.MINUTE;
+               } else if (age < 2 * Time.HOUR) {
+                       text = webInterface.getL10n().getString("View.Time.AnHourAgo");
+                       refresh = Time.HOUR;
+               } else if (age < 21 * Time.HOUR) {
+                       text = webInterface.getL10n().getString("View.Time.XHoursAgo", "hour", String.valueOf((int) Digits.round(age / Time.HOUR, 1)));
+                       refresh = Time.HOUR;
+               } else if (age < 42 * Time.HOUR) {
+                       text = webInterface.getL10n().getString("View.Time.ADayAgo");
+                       refresh = Time.DAY;
+               } else if (age < 6 * Time.DAY) {
+                       text = webInterface.getL10n().getString("View.Time.XDaysAgo", "day", String.valueOf((int) Digits.round(age / Time.DAY, 1)));
+                       refresh = Time.DAY;
+               } else if (age < 11 * Time.DAY) {
+                       text = webInterface.getL10n().getString("View.Time.AWeekAgo");
+                       refresh = Time.DAY;
+               } else if (age < 4 * Time.WEEK) {
+                       text = webInterface.getL10n().getString("View.Time.XWeeksAgo", "week", String.valueOf((int) Digits.round(age / Time.WEEK, 1)));
+                       refresh = Time.DAY;
+               } else if (age < 6 * Time.WEEK) {
+                       text = webInterface.getL10n().getString("View.Time.AMonthAgo");
+                       refresh = Time.DAY;
+               } else if (age < 11 * Time.MONTH) {
+                       text = webInterface.getL10n().getString("View.Time.XMonthsAgo", "month", String.valueOf((int) Digits.round(age / Time.MONTH, 1)));
+                       refresh = Time.DAY;
+               } else if (age < 18 * Time.MONTH) {
+                       text = webInterface.getL10n().getString("View.Time.AYearAgo");
+                       refresh = Time.WEEK;
+               } else {
+                       text = webInterface.getL10n().getString("View.Time.XYearsAgo", "year", String.valueOf((int) Digits.round(age / Time.YEAR, 1)));
+                       refresh = Time.WEEK;
+               }
+               return new Time(text, refresh);
+       }
+
+       /**
+        * Container for a formatted time.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       private static class Time {
+
+               /** Number of milliseconds in a second. */
+               private static final long SECOND = 1000;
+
+               /** Number of milliseconds in a minute. */
+               private static final long MINUTE = 60 * SECOND;
+
+               /** Number of milliseconds in an hour. */
+               private static final long HOUR = 60 * MINUTE;
+
+               /** Number of milliseconds in a day. */
+               private static final long DAY = 24 * HOUR;
+
+               /** Number of milliseconds in a week. */
+               private static final long WEEK = 7 * DAY;
+
+               /** Number of milliseconds in a 30-day month. */
+               private static final long MONTH = 30 * DAY;
+
+               /** Number of milliseconds in a year. */
+               private static final long YEAR = 365 * DAY;
+
+               /** The formatted time. */
+               private final String text;
+
+               /** The time after which to refresh the time. */
+               private final long refresh;
+
+               /**
+                * Creates a new formatted time container.
+                *
+                * @param text
+                *            The formatted time
+                * @param refresh
+                *            The time after which to refresh the time (in milliseconds)
+                */
+               public Time(String text, long refresh) {
+                       this.text = text;
+                       this.refresh = refresh;
+               }
+
+               /**
+                * Returns the formatted time.
+                *
+                * @return The formatted time
+                */
+               public String getText() {
+                       return text;
+               }
+
+               /**
+                * Returns the time after which to refresh the time.
+                *
+                * @return The time after which to refresh the time (in milliseconds)
+                */
+               public long getRefresh() {
+                       return refresh;
+               }
+
+       }
+
+}
index e8d36a0..b8a8570 100644 (file)
@@ -238,6 +238,23 @@ View.Trust.Tooltip.Trust=Trust this person
 View.Trust.Tooltip.Distrust=Assign negative trust to this person
 View.Trust.Tooltip.Untrust=Remove your trust assignment for this person
 
+View.Time.InTheFuture=in the future
+View.Time.AFewSecondsAgo=a few seconds ago
+View.Time.XSecondsAgo=${sec} seconds ago
+View.Time.AMinuteAgo=about a minute ago
+View.Time.XMinutesAgo=${min} minutes ago
+View.Time.HalfAnHourAgo=half an hour ago
+View.Time.AnHourAgo=about an hour ago
+View.Time.XHoursAgo=${hour} hours ago
+View.Time.ADayAgo=about a day ago
+View.Time.XDaysAgo=${day} days ago
+View.Time.AWeekAgo=about a week ago
+View.Time.XWeeksAgo=${week} week ago
+View.Time.AMonthAgo=about a month ago
+View.Time.XMonthsAgo=${month} months ago
+View.Time.AYearAgo=about a year ago
+View.Time.XYearsAgo=${year} years ago
+
 WebInterface.DefaultText.StatusUpdate=What’s on your mind?
 WebInterface.DefaultText.Message=Write a Message…
 WebInterface.DefaultText.Reply=Write a Reply…
index 232d9fc..86475e5 100644 (file)
@@ -712,9 +712,12 @@ function ajaxifyPost(postElement) {
        addCommentLink(getPostId(postElement), postElement, $(postElement).find(".post-status-line .time"));
 
        /* process all replies. */
+       replyIds = [];
        $(postElement).find(".reply").each(function() {
+               replyIds.push(getReplyId(this));
                ajaxifyReply(this);
        });
+       updateReplyTimes(replyIds.join(","));
 
        /* process reply input fields. */
        getTranslation("WebInterface.DefaultText.Reply", function(text) {
@@ -1027,6 +1030,7 @@ function loadNewPost(postId, soneId, recipientId, time) {
                                newPost.insertBefore(firstOlderPost);
                        }
                        ajaxifyPost(newPost);
+                       updatePostTimes(data.post.id);
                        newPost.slideDown();
                        setActivity();
                }
@@ -1065,6 +1069,7 @@ function loadNewReply(replyId, soneId, postId, postSoneId) {
                                        }
                                }
                                ajaxifyReply(newReply);
+                               updateReplyTimes(data.reply.id);
                                newReply.slideDown();
                                setActivity();
                                return false;
@@ -1113,6 +1118,86 @@ function markReplyAsKnown(replyElements) {
        });
 }
 
+/**
+ * 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) {
@@ -1371,6 +1456,13 @@ $(document).ready(function() {
                });
        });
 
+       /* update post times. */
+       postIds = [];
+       $("#sone .post").each(function() {
+               postIds.push(getPostId(this));
+       });
+       updatePostTimes(postIds.join(","));
+
        /* hides all replies but the latest two. */
        if (!isViewPostPage()) {
                getTranslation("WebInterface.ClickToShow.Replies", function(text) {