Merge branch 'release-0.6.1' 0.6.1
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sun, 10 Apr 2011 19:06:23 +0000 (21:06 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sun, 10 Apr 2011 19:06:23 +0000 (21:06 +0200)
37 files changed:
pom.xml
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/core/SoneInserter.java
src/main/java/net/pterodactylus/sone/data/Post.java
src/main/java/net/pterodactylus/sone/data/Reply.java
src/main/java/net/pterodactylus/sone/data/Sone.java
src/main/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.java
src/main/java/net/pterodactylus/sone/main/SonePlugin.java
src/main/java/net/pterodactylus/sone/notify/ListNotification.java
src/main/java/net/pterodactylus/sone/notify/ListNotificationFilters.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/template/NotificationManagerAccessor.java
src/main/java/net/pterodactylus/sone/template/ParserFilter.java
src/main/java/net/pterodactylus/sone/template/PostAccessor.java
src/main/java/net/pterodactylus/sone/template/ReplyGroupFilter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/text/FreenetLinkParser.java
src/main/java/net/pterodactylus/sone/text/PartContainer.java
src/main/java/net/pterodactylus/sone/text/TemplatePart.java
src/main/java/net/pterodactylus/sone/web/IndexPage.java
src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java
src/main/java/net/pterodactylus/sone/web/OptionsPage.java
src/main/java/net/pterodactylus/sone/web/SearchPage.java
src/main/java/net/pterodactylus/sone/web/ViewPostPage.java
src/main/java/net/pterodactylus/sone/web/ViewSonePage.java
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetTimesAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java
src/main/java/net/pterodactylus/sone/web/page/RedirectPage.java [new file with mode: 0644]
src/main/resources/i18n/sone.en.properties
src/main/resources/static/css/sone.css
src/main/resources/static/javascript/sone.js
src/main/resources/templates/about.html
src/main/resources/templates/notify/newPostNotification.html
src/main/resources/templates/notify/newReplyNotification.html
src/main/resources/templates/notify/newSoneNotification.html
src/main/resources/templates/options.html
src/test/java/net/pterodactylus/sone/text/FreenetLinkParserTest.java

diff --git a/pom.xml b/pom.xml
index 690bb17..33fd6be 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -2,12 +2,12 @@
        <modelVersion>4.0.0</modelVersion>
        <groupId>net.pterodactylus</groupId>
        <artifactId>sone</artifactId>
-       <version>0.6</version>
+       <version>0.6.1</version>
        <dependencies>
                <dependency>
                        <groupId>net.pterodactylus</groupId>
                        <artifactId>utils</artifactId>
-                       <version>0.9.2</version>
+                       <version>0.9.3</version>
                </dependency>
                <dependency>
                        <groupId>junit</groupId>
index c986d71..1fe843e 100644 (file)
@@ -576,6 +576,27 @@ public class Core implements IdentityListener, UpdateListener {
        }
 
        /**
+        * Returns all posts that have the given Sone as recipient.
+        *
+        * @see Post#getRecipient()
+        * @param recipient
+        *            The recipient of the posts
+        * @return All posts that have the given Sone as recipient
+        */
+       public Set<Post> getDirectedPosts(Sone recipient) {
+               Validation.begin().isNotNull("Recipient", recipient).check();
+               Set<Post> directedPosts = new HashSet<Post>();
+               synchronized (posts) {
+                       for (Post post : posts.values()) {
+                               if (recipient.equals(post.getRecipient())) {
+                                       directedPosts.add(post);
+                               }
+                       }
+               }
+               return directedPosts;
+       }
+
+       /**
         * Returns the reply with the given ID. If there is no reply with the given
         * ID yet, a new one is created.
         *
@@ -884,6 +905,11 @@ public class Core implements IdentityListener, UpdateListener {
                                }
                                if (newSone) {
                                        coreListenerManager.fireNewSoneFound(sone);
+                                       for (Sone localSone : getLocalSones()) {
+                                               if (localSone.getOptions().getBooleanOption("AutoFollow").get()) {
+                                                       localSone.addFriend(sone.getId());
+                                               }
+                                       }
                                }
                        }
                        remoteSones.put(identity.getId(), sone);
@@ -1242,6 +1268,10 @@ public class Core implements IdentityListener, UpdateListener {
                        friends.add(friendId);
                }
 
+               /* load options. */
+               sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
+               sone.getOptions().getBooleanOption("AutoFollow").set(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(null));
+
                /* if we’re still here, Sone was loaded successfully. */
                synchronized (sone) {
                        sone.setTime(soneTime);
@@ -1357,6 +1387,9 @@ public class Core implements IdentityListener, UpdateListener {
                        }
                        configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter + "/ID").setValue(null);
 
+                       /* save options. */
+                       configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").setValue(sone.getOptions().getBooleanOption("AutoFollow").getReal());
+
                        configuration.save();
                        logger.log(Level.INFO, "Sone %s saved.", sone);
                } catch (ConfigurationException ce1) {
@@ -1437,7 +1470,8 @@ public class Core implements IdentityListener, UpdateListener {
                        posts.put(post.getId(), post);
                }
                synchronized (newPosts) {
-                       knownPosts.add(post.getId());
+                       newPosts.add(post.getId());
+                       coreListenerManager.fireNewPostFound(post);
                }
                sone.addPost(post);
                saveSone(sone);
@@ -1459,6 +1493,10 @@ public class Core implements IdentityListener, UpdateListener {
                synchronized (posts) {
                        posts.remove(post.getId());
                }
+               synchronized (newPosts) {
+                       markPostKnown(post);
+                       knownPosts.remove(post.getId());
+               }
                saveSone(post.getSone());
        }
 
@@ -1561,7 +1599,8 @@ public class Core implements IdentityListener, UpdateListener {
                        replies.put(reply.getId(), reply);
                }
                synchronized (newReplies) {
-                       knownReplies.add(reply.getId());
+                       newReplies.add(reply.getId());
+                       coreListenerManager.fireNewReplyFound(reply);
                }
                sone.addReply(reply);
                saveSone(sone);
@@ -1583,6 +1622,10 @@ public class Core implements IdentityListener, UpdateListener {
                synchronized (replies) {
                        replies.remove(reply.getId());
                }
+               synchronized (newReplies) {
+                       markReplyKnown(reply);
+                       knownReplies.remove(reply.getId());
+               }
                sone.removeReply(reply);
                saveSone(sone);
        }
@@ -1645,6 +1688,7 @@ public class Core implements IdentityListener, UpdateListener {
                try {
                        configuration.getIntValue("Option/ConfigurationVersion").setValue(0);
                        configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal());
+                       configuration.getIntValue("Option/PostsPerPage").setValue(options.getIntegerOption("PostsPerPage").getReal());
                        configuration.getIntValue("Option/PositiveTrust").setValue(options.getIntegerOption("PositiveTrust").getReal());
                        configuration.getIntValue("Option/NegativeTrust").setValue(options.getIntegerOption("NegativeTrust").getReal());
                        configuration.getStringValue("Option/TrustComment").setValue(options.getStringOption("TrustComment").getReal());
@@ -1718,8 +1762,9 @@ public class Core implements IdentityListener, UpdateListener {
                        }
 
                }));
+               options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(10));
                options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75));
-               options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-100));
+               options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-25));
                options.addStringOption("TrustComment", new DefaultOption<String>("Set from Sone Web Interface"));
                options.addBooleanOption("SoneRescueMode", new DefaultOption<Boolean>(false));
                options.addBooleanOption("ClearOnNextRestart", new DefaultOption<Boolean>(false));
@@ -1737,6 +1782,7 @@ public class Core implements IdentityListener, UpdateListener {
                }
 
                options.getIntegerOption("InsertionDelay").set(configuration.getIntValue("Option/InsertionDelay").getValue(null));
+               options.getIntegerOption("PostsPerPage").set(configuration.getIntValue("Option/PostsPerPage").getValue(null));
                options.getIntegerOption("PositiveTrust").set(configuration.getIntValue("Option/PositiveTrust").getValue(null));
                options.getIntegerOption("NegativeTrust").set(configuration.getIntValue("Option/NegativeTrust").getValue(null));
                options.getStringOption("TrustComment").set(configuration.getStringValue("Option/TrustComment").getValue(null));
@@ -1926,6 +1972,27 @@ public class Core implements IdentityListener, UpdateListener {
                }
 
                /**
+                * Returns the number of posts to show per page.
+                *
+                * @return The number of posts to show per page
+                */
+               public int getPostsPerPage() {
+                       return options.getIntegerOption("PostsPerPage").get();
+               }
+
+               /**
+                * Sets the number of posts to show per page.
+                *
+                * @param postsPerPage
+                *            The number of posts to show per page
+                * @return This preferences object
+                */
+               public Preferences setPostsPerPage(Integer postsPerPage) {
+                       options.getIntegerOption("PostsPerPage").set(postsPerPage);
+                       return this;
+               }
+
+               /**
                 * Returns the positive trust.
                 *
                 * @return The positive trust
index 05fa4b4..15e054f 100644 (file)
@@ -160,7 +160,7 @@ public class SoneInserter extends AbstractService {
        protected void serviceRun() {
                long lastModificationTime = 0;
                String lastFingerprint = "";
-               while (!shouldStop()) {
+               while (!shouldStop()) { try {
                        /* check every seconds. */
                        sleep(1000);
 
@@ -236,7 +236,9 @@ public class SoneInserter extends AbstractService {
                                        }
                                }
                        }
-               }
+               } catch (Throwable t1) {
+                       logger.log(Level.SEVERE, "SoneInserter threw an Exception!", t1);
+               }}
        }
 
        /**
index 964a054..bb431d0 100644 (file)
@@ -20,6 +20,8 @@ package net.pterodactylus.sone.data;
 import java.util.Comparator;
 import java.util.UUID;
 
+import net.pterodactylus.util.filter.Filter;
+
 /**
  * A post is a short message that a user writes in his Sone to let other users
  * know what is going on.
@@ -38,6 +40,16 @@ public class Post {
 
        };
 
+       /** Filter for posts with timestamps from the future. */
+       public static final Filter<Post> FUTURE_POSTS_FILTER = new Filter<Post>() {
+
+               @Override
+               public boolean filterObject(Post post) {
+                       return post.getTime() <= System.currentTimeMillis();
+               }
+
+       };
+
        /** The GUID of the post. */
        private final UUID id;
 
index a106391..2dacfbe 100644 (file)
@@ -20,6 +20,8 @@ package net.pterodactylus.sone.data;
 import java.util.Comparator;
 import java.util.UUID;
 
+import net.pterodactylus.util.filter.Filter;
+
 /**
  * A reply is like a {@link Post} but can never be posted on its own, it always
  * refers to another {@link Post}.
@@ -38,6 +40,16 @@ public class Reply {
 
        };
 
+       /** Filter for replies with timestamps from the future. */
+       public static final Filter<Reply> FUTURE_REPLIES_FILTER = new Filter<Reply>() {
+
+               @Override
+               public boolean filterObject(Reply reply) {
+                       return reply.getTime() <= System.currentTimeMillis();
+               }
+
+       };
+
        /** The ID of the reply. */
        private final UUID id;
 
index e5695fa..2dd4bc1 100644 (file)
@@ -27,6 +27,7 @@ import java.util.Set;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import net.pterodactylus.sone.core.Options;
 import net.pterodactylus.sone.freenet.wot.Identity;
 import net.pterodactylus.sone.template.SoneAccessor;
 import net.pterodactylus.util.filter.Filter;
@@ -109,6 +110,9 @@ public class Sone implements Fingerprintable, Comparable<Sone> {
        /** The IDs of all liked replies. */
        private final Set<String> likedReplyIds = Collections.synchronizedSet(new HashSet<String>());
 
+       /** Sone-specific options. */
+       private final Options options = new Options();
+
        /**
         * Creates a new Sone.
         *
@@ -592,6 +596,15 @@ public class Sone implements Fingerprintable, Comparable<Sone> {
                return this;
        }
 
+       /**
+        * Returns Sone-specific options.
+        *
+        * @return The options of this Sone
+        */
+       public Options getOptions() {
+               return options;
+       }
+
        //
        // FINGERPRINTABLE METHODS
        //
index eade03e..ab46b43 100644 (file)
@@ -171,12 +171,18 @@ public class DefaultOwnIdentity extends DefaultIdentity implements OwnIdentity {
        // OBJECT METHODS
        //
 
+       /**
+        * {@inheritDoc}
+        */
        @Override
        public int hashCode() {
                /* The hash of DefaultIdentity is fine. */
                return super.hashCode();
        }
 
+       /**
+        * {@inheritDoc}
+        */
        @Override
        public boolean equals(Object object) {
                /* The ID of the superclass is still enough. */
index 3b6824b..de4fee8 100644 (file)
@@ -78,7 +78,7 @@ public class SonePlugin implements FredPlugin, FredPluginL10n, FredPluginBaseL10
        }
 
        /** The version. */
-       public static final Version VERSION = new Version(0, 6);
+       public static final Version VERSION = new Version(0, 6, 1);
 
        /** The logger. */
        private static final Logger logger = Logging.getLogger(SonePlugin.class);
index 2f43c58..c66d376 100644 (file)
@@ -18,6 +18,7 @@
 package net.pterodactylus.sone.notify;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
 
@@ -33,6 +34,9 @@ import net.pterodactylus.util.template.Template;
  */
 public class ListNotification<T> extends TemplateNotification {
 
+       /** The key under which to store the elements in the template. */
+       private final String key;
+
        /** The list of new elements. */
        private final List<T> elements = new CopyOnWriteArrayList<T>();
 
@@ -47,10 +51,42 @@ public class ListNotification<T> extends TemplateNotification {
         *            The template to render
         */
        public ListNotification(String id, String key, Template template) {
-               super(id, template);
+               this(id, key, template, true);
+       }
+
+       /**
+        * Creates a new list notification.
+        *
+        * @param id
+        *            The ID of the notification
+        * @param key
+        *            The key under which to store the elements in the template
+        * @param template
+        *            The template to render
+        * @param dismissable
+        *            {@code true} if this notification should be dismissable by the
+        *            user, {@code false} otherwise
+        */
+       public ListNotification(String id, String key, Template template, boolean dismissable) {
+               super(id, System.currentTimeMillis(), System.currentTimeMillis(), dismissable, template);
+               this.key = key;
                template.getInitialContext().set(key, elements);
        }
 
+       /**
+        * Creates a new list notification that copies its ID and the template from
+        * the given list notification.
+        *
+        * @param listNotification
+        *            The list notification to copy
+        */
+       public ListNotification(ListNotification<T> listNotification) {
+               super(listNotification.getId(), listNotification.getCreatedTime(), listNotification.getLastUpdatedTime(), listNotification.isDismissable(), new Template());
+               this.key = listNotification.key;
+               getTemplate().add(listNotification.getTemplate());
+               getTemplate().getInitialContext().set(key, elements);
+       }
+
        //
        // ACTIONS
        //
@@ -65,6 +101,18 @@ public class ListNotification<T> extends TemplateNotification {
        }
 
        /**
+        * Sets the elements to show in this notification.
+        *
+        * @param elements
+        *            The elements to show
+        */
+       public void setElements(Collection<? extends T> elements) {
+               this.elements.clear();
+               this.elements.addAll(elements);
+               touch();
+       }
+
+       /**
         * Returns whether there are any new elements.
         *
         * @return {@code true} if there are no new elements, {@code false} if there
diff --git a/src/main/java/net/pterodactylus/sone/notify/ListNotificationFilters.java b/src/main/java/net/pterodactylus/sone/notify/ListNotificationFilters.java
new file mode 100644 (file)
index 0000000..2a98ec8
--- /dev/null
@@ -0,0 +1,169 @@
+/*
+ * Sone - ListNotificationFilters.java - Copyright © 2010 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.notify;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.notify.Notification;
+
+/**
+ * Filter for {@link ListNotification}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ListNotificationFilters {
+
+       /**
+        * Filters new-post and new-reply notifications in the given list of
+        * notifications. If {@code currentSone} is <code>null</code>, new-post and
+        * new-reply notifications are removed completely. If {@code currentSone} is
+        * not {@code null}, only posts that are posted by a friend Sone or the Sone
+        * itself, and replies that are replies to posts of friend Sones or the Sone
+        * itself will be retained in the notifications.
+        *
+        * @param notifications
+        *            The notifications to filter
+        * @param currentSone
+        *            The current Sone, or {@code null} if not logged in
+        * @return The filtered notifications
+        */
+       public static List<Notification> filterNotifications(List<Notification> notifications, Sone currentSone) {
+               ListNotification<Post> newPostNotification = getNotification(notifications, "new-post-notification", Post.class);
+               if (newPostNotification != null) {
+                       ListNotification<Post> filteredNotification = filterNewPostNotification(newPostNotification, currentSone);
+                       int notificationIndex = notifications.indexOf(newPostNotification);
+                       if (filteredNotification == null) {
+                               notifications.remove(notificationIndex);
+                       } else {
+                               notifications.set(notificationIndex, filteredNotification);
+                       }
+               }
+               ListNotification<Reply> newReplyNotification = getNotification(notifications, "new-replies-notification", Reply.class);
+               if (newReplyNotification != null) {
+                       ListNotification<Reply> filteredNotification = filterNewReplyNotification(newReplyNotification, currentSone);
+                       int notificationIndex = notifications.indexOf(newReplyNotification);
+                       if (filteredNotification == null) {
+                               notifications.remove(notificationIndex);
+                       } else {
+                               notifications.set(notificationIndex, filteredNotification);
+                       }
+               }
+               return notifications;
+       }
+
+       /**
+        * Filters the new posts of the given notification. If {@code currentSone}
+        * is {@code null}, {@code null} is returned and the notification is
+        * subsequently removed. Otherwise only posts that are posted by friend
+        * Sones of the given Sone are retained; all other posts are removed.
+        *
+        * @param newPostNotification
+        *            The new-post notification
+        * @param currentSone
+        *            The current Sone, or {@code null} if not logged in
+        * @return The filtered new-post notification, or {@code null} if the
+        *         notification should be removed
+        */
+       private static ListNotification<Post> filterNewPostNotification(ListNotification<Post> newPostNotification, Sone currentSone) {
+               if (currentSone == null) {
+                       return null;
+               }
+               List<Post> newPosts = new ArrayList<Post>();
+               for (Post post : newPostNotification.getElements()) {
+                       if (currentSone.hasFriend(post.getSone().getId()) || currentSone.equals(post.getSone()) || currentSone.equals(post.getRecipient())) {
+                               newPosts.add(post);
+                       }
+               }
+               if (newPosts.isEmpty()) {
+                       return null;
+               }
+               if (newPosts.size() == newPostNotification.getElements().size()) {
+                       return newPostNotification;
+               }
+               ListNotification<Post> filteredNotification = new ListNotification<Post>(newPostNotification);
+               filteredNotification.setElements(newPosts);
+               return filteredNotification;
+       }
+
+       /**
+        * Filters the new replies of the given notification. If {@code currentSone}
+        * is {@code null}, {@code null} is returned and the notification is
+        * subsequently removed. Otherwise only replies that are replies to posts
+        * that are posted by friend Sones of the given Sone are retained; all other
+        * replies are removed.
+        *
+        * @param newReplyNotification
+        *            The new-reply notification
+        * @param currentSone
+        *            The current Sone, or {@code null} if not logged in
+        * @return The filtered new-reply notification, or {@code null} if the
+        *         notification should be removed
+        */
+       private static ListNotification<Reply> filterNewReplyNotification(ListNotification<Reply> newReplyNotification, Sone currentSone) {
+               if (currentSone == null) {
+                       return null;
+               }
+               List<Reply> newReplies = new ArrayList<Reply>();
+               for (Reply reply : newReplyNotification.getElements()) {
+                       if (currentSone.hasFriend(reply.getPost().getSone().getId()) || currentSone.equals(reply.getPost().getSone()) || currentSone.equals(reply.getPost().getRecipient())) {
+                               newReplies.add(reply);
+                       }
+               }
+               if (newReplies.isEmpty()) {
+                       return null;
+               }
+               if (newReplies.size() == newReplyNotification.getElements().size()) {
+                       return newReplyNotification;
+               }
+               ListNotification<Reply> filteredNotification = new ListNotification<Reply>(newReplyNotification);
+               filteredNotification.setElements(newReplies);
+               return filteredNotification;
+       }
+
+       /**
+        * Finds the notification with the given ID in the list of notifications and
+        * returns it.
+        *
+        * @param <T>
+        *            The type of the item in the notification
+        * @param notifications
+        *            The notification to search
+        * @param notificationId
+        *            The ID of the requested notification
+        * @param notificationElementClass
+        *            The class of the notification item
+        * @return The requested notification, or {@code null} if no notification
+        *         with the given ID could be found
+        */
+       @SuppressWarnings("unchecked")
+       private static <T> ListNotification<T> getNotification(Collection<? extends Notification> notifications, String notificationId, Class<T> notificationElementClass) {
+               for (Notification notification : notifications) {
+                       if (!notificationId.equals(notification.getId())) {
+                               continue;
+                       }
+                       return (ListNotification<T>) notification;
+               }
+               return null;
+       }
+
+}
index a8f34a8..c4468ea 100644 (file)
@@ -21,6 +21,8 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.notify.ListNotificationFilters;
 import net.pterodactylus.util.notify.Notification;
 import net.pterodactylus.util.notify.NotificationManager;
 import net.pterodactylus.util.template.ReflectionAccessor;
@@ -47,13 +49,9 @@ public class NotificationManagerAccessor extends ReflectionAccessor {
        public Object get(TemplateContext templateContext, Object object, String member) {
                NotificationManager notificationManager = (NotificationManager) object;
                if ("all".equals(member)) {
-                       List<Notification> notifications = new ArrayList<Notification>(notificationManager.getNotifications());
+                       List<Notification> notifications = ListNotificationFilters.filterNotifications(new ArrayList<Notification>(notificationManager.getNotifications()), (Sone) templateContext.get("currentSone"));
                        Collections.sort(notifications, Notification.CREATED_TIME_SORTER);
                        return notifications;
-               } else if ("new".equals(member)) {
-                       List<Notification> notifications = new ArrayList<Notification>(notificationManager.getChangedNotifications());
-                       Collections.sort(notifications, Notification.LAST_UPDATED_TIME_SORTER);
-                       return notifications;
                }
                return super.get(templateContext, object, member);
        }
index 1e274cc..477e972 100644 (file)
@@ -53,7 +53,7 @@ public class ParserFilter implements Filter {
         */
        public ParserFilter(Core core, TemplateContextFactory templateContextFactory) {
                this.core = core;
-               linkParser = new FreenetLinkParser(templateContextFactory);
+               linkParser = new FreenetLinkParser(core, templateContextFactory);
        }
 
        /**
index ca0c84a..99d6845 100644 (file)
@@ -19,7 +19,9 @@ package net.pterodactylus.sone.template;
 
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.filter.Filters;
 import net.pterodactylus.util.template.ReflectionAccessor;
 import net.pterodactylus.util.template.TemplateContext;
 
@@ -54,7 +56,7 @@ public class PostAccessor extends ReflectionAccessor {
        public Object get(TemplateContext templateContext, Object object, String member) {
                Post post = (Post) object;
                if ("replies".equals(member)) {
-                       return core.getReplies(post);
+                       return Filters.filteredList(core.getReplies(post), Reply.FUTURE_REPLIES_FILTER);
                } else if (member.equals("likes")) {
                        return core.getLikes(post);
                } else if (member.equals("liked")) {
diff --git a/src/main/java/net/pterodactylus/sone/template/ReplyGroupFilter.java b/src/main/java/net/pterodactylus/sone/template/ReplyGroupFilter.java
new file mode 100644 (file)
index 0000000..2567284
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Sone - ReplyGroupFilter.java - Copyright © 2010 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.template;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.template.Filter;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * {@link Filter} implementation that groups replies by the post the are in
+ * reply to, returning a map with the post as key and the list of replies as
+ * values.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ReplyGroupFilter implements Filter {
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Object format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
+               @SuppressWarnings("unchecked")
+               List<Reply> allReplies = (List<Reply>) data;
+               Map<Post, Set<Sone>> postSones = new HashMap<Post, Set<Sone>>();
+               Map<Post, Set<Reply>> postReplies = new HashMap<Post, Set<Reply>>();
+               for (Reply reply : allReplies) {
+                       Post post = reply.getPost();
+                       Set<Sone> sones = postSones.get(post);
+                       if (sones == null) {
+                               sones = new HashSet<Sone>();
+                               postSones.put(post, sones);
+                       }
+                       sones.add(reply.getSone());
+                       Set<Reply> replies = postReplies.get(post);
+                       if (replies == null) {
+                               replies = new HashSet<Reply>();
+                               postReplies.put(post, replies);
+                       }
+                       replies.add(reply);
+               }
+               Map<Post, Map<String, Set<?>>> result = new HashMap<Post, Map<String, Set<?>>>();
+               for (Post post : postSones.keySet()) {
+                       if (result.containsKey(post)) {
+                               continue;
+                       }
+                       Map<String, Set<?>> postResult = new HashMap<String, Set<?>>();
+                       postResult.put("sones", postSones.get(post));
+                       postResult.put("replies", postReplies.get(post));
+                       result.put(post, postResult);
+               }
+               return result;
+       }
+
+}
index 67cd7e3..cea4452 100644 (file)
@@ -27,6 +27,10 @@ import java.util.logging.Logger;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.template.SoneAccessor;
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.template.TemplateContextFactory;
 import net.pterodactylus.util.template.TemplateParser;
@@ -68,20 +72,32 @@ public class FreenetLinkParser implements Parser<FreenetLinkParserContext> {
                HTTP,
 
                /** Link is HTTPS. */
-               HTTPS;
+               HTTPS,
+
+               /** Link is a Sone. */
+               SONE,
+
+               /** Link is a post. */
+               POST,
 
        }
 
+       /** The core. */
+       private final Core core;
+
        /** The template factory. */
        private final TemplateContextFactory templateContextFactory;
 
        /**
         * Creates a new freenet link parser.
         *
+        * @param core
+        *            The core
         * @param templateContextFactory
         *            The template context factory
         */
-       public FreenetLinkParser(TemplateContextFactory templateContextFactory) {
+       public FreenetLinkParser(Core core, TemplateContextFactory templateContextFactory) {
+               this.core = core;
                this.templateContextFactory = templateContextFactory;
        }
 
@@ -118,7 +134,9 @@ public class FreenetLinkParser implements Parser<FreenetLinkParserContext> {
                                int nextUsk = line.indexOf("USK@");
                                int nextHttp = line.indexOf("http://");
                                int nextHttps = line.indexOf("https://");
-                               if ((nextKsk == -1) && (nextChk == -1) && (nextSsk == -1) && (nextUsk == -1) && (nextHttp == -1) && (nextHttps == -1)) {
+                               int nextSone = line.indexOf("sone://");
+                               int nextPost = line.indexOf("post://");
+                               if ((nextKsk == -1) && (nextChk == -1) && (nextSsk == -1) && (nextUsk == -1) && (nextHttp == -1) && (nextHttps == -1) && (nextSone == -1) && (nextPost == -1)) {
                                        if (lineComplete && !lastLineEmpty) {
                                                parts.add(createPlainTextPart("\n" + line));
                                        } else {
@@ -152,6 +170,14 @@ public class FreenetLinkParser implements Parser<FreenetLinkParserContext> {
                                        next = nextHttps;
                                        linkType = LinkType.HTTPS;
                                }
+                               if ((nextSone > -1) && (nextSone < next)) {
+                                       next = nextSone;
+                                       linkType = LinkType.SONE;
+                               }
+                               if ((nextPost > -1) && (nextPost < next)) {
+                                       next = nextPost;
+                                       linkType = LinkType.POST;
+                               }
                                if ((next >= 8) && (line.substring(next - 8, next).equals("freenet:"))) {
                                        next -= 8;
                                        line = line.substring(0, next) + line.substring(next + 8);
@@ -216,6 +242,25 @@ public class FreenetLinkParser implements Parser<FreenetLinkParserContext> {
                                                }
                                                link = "?_CHECKED_HTTP_=" + link;
                                                parts.add(createInternetLinkPart(link, name));
+                                       } else if (linkType == LinkType.SONE) {
+                                               String soneId = link.substring(7);
+                                               Sone sone = core.getSone(soneId, false);
+                                               if (sone != null) {
+                                                       parts.add(createInSoneLinkPart("viewSone.html?sone=" + soneId, SoneAccessor.getNiceName(sone)));
+                                               } else {
+                                                       parts.add(createPlainTextPart(link));
+                                               }
+                                       } else if (linkType == LinkType.POST) {
+                                               String postId = link.substring(7);
+                                               Post post = core.getPost(postId, false);
+                                               if (post != null) {
+                                                       String postText = post.getText();
+                                                       postText = postText.substring(0, Math.min(postText.length(), 20)) + "…";
+                                                       Sone postSone = post.getSone();
+                                                       parts.add(createInSoneLinkPart("viewPost.html?post=" + postId, postText, (postSone == null) ? postText : SoneAccessor.getNiceName(post.getSone())));
+                                               } else {
+                                                       parts.add(createPlainTextPart(link));
+                                               }
                                        }
                                        line = line.substring(nextSpace);
                                } else {
@@ -296,4 +341,32 @@ public class FreenetLinkParser implements Parser<FreenetLinkParserContext> {
                return new TemplatePart(templateContextFactory, TemplateParser.parse(new StringReader("<a class=\"freenet-trusted\" href=\"/<% link|html>\" title=\"<% link|html>\"><% name|html></a>"))).set("link", link).set("name", name);
        }
 
+       /**
+        * Creates a new part based on a template that links to a page in Sone.
+        *
+        * @param link
+        *            The target of the link
+        * @param name
+        *            The name of the link
+        * @return The part that displays the link
+        */
+       private Part createInSoneLinkPart(String link, String name) {
+               return createInSoneLinkPart(link, name, name);
+       }
+
+       /**
+        * Creates a new part based on a template that links to a page in Sone.
+        *
+        * @param link
+        *            The target of the link
+        * @param name
+        *            The name of the link
+        * @param title
+        *            The title attribute of the link
+        * @return The part that displays the link
+        */
+       private Part createInSoneLinkPart(String link, String name, String title) {
+               return new TemplatePart(templateContextFactory, TemplateParser.parse(new StringReader("<a class=\"in-sone\" href=\"<%link|html>\" title=\"<%title|html>\"><%name|html></a>"))).set("link", link).set("name", name).set("title", title);
+       }
+
 }
index 4993324..d52658e 100644 (file)
@@ -97,6 +97,9 @@ public class PartContainer implements Part {
        // OBJECT METHODS
        //
 
+       /**
+        * {@inheritDoc}
+        */
        @Override
        public String toString() {
                StringWriter stringWriter = new StringWriter();
index b948f10..ac5694c 100644 (file)
@@ -94,6 +94,9 @@ public class TemplatePart implements Part, net.pterodactylus.util.template.Part
        // OBJECT METHODS
        //
 
+       /**
+        * {@inheritDoc}
+        */
        @Override
        public String toString() {
                StringWriter stringWriter = new StringWriter();
index 2d85e02..e675311 100644 (file)
@@ -22,9 +22,9 @@ import java.util.Collections;
 import java.util.List;
 
 import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.util.collection.Pagination;
+import net.pterodactylus.util.filter.Filters;
 import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
@@ -73,25 +73,11 @@ public class IndexPage extends SoneTemplatePage {
                                }
                        }
                }
+               allPosts = Filters.filteredList(allPosts, Post.FUTURE_POSTS_FILTER);
                Collections.sort(allPosts, Post.TIME_COMPARATOR);
-               Pagination<Post> pagination = new Pagination<Post>(allPosts, 25).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
+               Pagination<Post> pagination = new Pagination<Post>(allPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
                templateContext.set("pagination", pagination);
                templateContext.set("posts", pagination.getItems());
        }
 
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void postProcess(Request request, TemplateContext templateContext) {
-               @SuppressWarnings("unchecked")
-               List<Post> posts = (List<Post>) templateContext.get("posts");
-               for (Post post : posts) {
-                       webInterface.getCore().markPostKnown(post);
-                       for (Reply reply : webInterface.getCore().getReplies(post)) {
-                               webInterface.getCore().markReplyKnown(reply);
-                       }
-               }
-       }
-
 }
index c0e6364..7c8f9ad 100644 (file)
@@ -64,17 +64,4 @@ public class KnownSonesPage extends SoneTemplatePage {
                templateContext.set("knownSones", sonePagination.getItems());
        }
 
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void postProcess(Request request, TemplateContext templateContext) {
-               super.postProcess(request, templateContext);
-               @SuppressWarnings("unchecked")
-               List<Sone> sones = (List<Sone>) templateContext.get("knownSones");
-               for (Sone sone : sones) {
-                       webInterface.getCore().markSoneKnown(sone);
-               }
-       }
-
 }
index 84d7c79..39cacc6 100644 (file)
@@ -18,6 +18,7 @@
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.core.Core.Preferences;
+import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
 import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.template.Template;
@@ -53,9 +54,17 @@ public class OptionsPage extends SoneTemplatePage {
        protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                Preferences preferences = webInterface.getCore().getPreferences();
+               Sone currentSone = webInterface.getCurrentSone(request.getToadletContext(), false);
                if (request.getMethod() == Method.POST) {
+                       if (currentSone != null) {
+                               boolean autoFollow = request.getHttpRequest().isPartSet("auto-follow");
+                               currentSone.getOptions().getBooleanOption("AutoFollow").set(autoFollow);
+                               webInterface.getCore().saveSone(currentSone);
+                       }
                        Integer insertionDelay = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("insertion-delay", 16));
                        preferences.setInsertionDelay(insertionDelay);
+                       Integer postsPerPage = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("posts-per-page", 4), null);
+                       preferences.setPostsPerPage(postsPerPage);
                        Integer positiveTrust = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("positive-trust", 3));
                        preferences.setPositiveTrust(positiveTrust);
                        Integer negativeTrust = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("negative-trust", 4));
@@ -74,7 +83,11 @@ public class OptionsPage extends SoneTemplatePage {
                        webInterface.getCore().saveConfiguration();
                        throw new RedirectException(getPath());
                }
+               if (currentSone != null) {
+                       templateContext.set("auto-follow", currentSone.getOptions().getBooleanOption("AutoFollow").get());
+               }
                templateContext.set("insertion-delay", preferences.getInsertionDelay());
+               templateContext.set("posts-per-page", preferences.getPostsPerPage());
                templateContext.set("positive-trust", preferences.getPositiveTrust());
                templateContext.set("negative-trust", preferences.getNegativeTrust());
                templateContext.set("trust-comment", preferences.getTrustComment());
index 2b854f3..1d1aa36 100644 (file)
@@ -86,7 +86,7 @@ public class SearchPage extends SoneTemplatePage {
                        posts.addAll(sone.getPosts());
                }
                @SuppressWarnings("synthetic-access")
-               Set<Hit<Post>> postHits = getHits(posts, phrases, new PostStringGenerator());
+               Set<Hit<Post>> postHits = getHits(Filters.filteredSet(posts, Post.FUTURE_POSTS_FILTER), phrases, new PostStringGenerator());
 
                /* now filter. */
                soneHits = Filters.filteredSet(soneHits, Hit.POSITIVE_FILTER);
@@ -103,8 +103,8 @@ public class SearchPage extends SoneTemplatePage {
                List<Post> resultPosts = Converters.convertList(sortedPostHits, new HitConverter<Post>());
 
                /* pagination. */
-               Pagination<Sone> sonePagination = new Pagination<Sone>(resultSones, 10).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("sonePage"), 0));
-               Pagination<Post> postPagination = new Pagination<Post>(resultPosts, 10).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("postPage"), 0));
+               Pagination<Sone> sonePagination = new Pagination<Sone>(resultSones, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("sonePage"), 0));
+               Pagination<Post> postPagination = new Pagination<Post>(resultPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("postPage"), 0));
 
                templateContext.set("sonePagination", sonePagination);
                templateContext.set("soneHits", sonePagination.getItems());
@@ -318,7 +318,7 @@ public class SearchPage extends SoneTemplatePage {
                        if (post.getRecipient() != null) {
                                postString.append(' ').append(SoneStringGenerator.NAME_GENERATOR.generateString(post.getRecipient()));
                        }
-                       for (Reply reply : webInterface.getCore().getReplies(post)) {
+                       for (Reply reply : Filters.filteredList(webInterface.getCore().getReplies(post), Reply.FUTURE_REPLIES_FILTER)) {
                                postString.append(' ').append(SoneStringGenerator.NAME_GENERATOR.generateString(reply.getSone()));
                                postString.append(' ').append(reply.getText());
                        }
index e528709..b62e19c 100644 (file)
@@ -18,7 +18,6 @@
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.template.SoneAccessor;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
@@ -75,19 +74,4 @@ public class ViewPostPage extends SoneTemplatePage {
                templateContext.set("raw", raw);
        }
 
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       protected void postProcess(Request request, TemplateContext templateContext) {
-               Post post = (Post) templateContext.get("post");
-               if (post == null) {
-                       return;
-               }
-               webInterface.getCore().markPostKnown(post);
-               for (Reply reply : webInterface.getCore().getReplies(post)) {
-                       webInterface.getCore().markReplyKnown(reply);
-               }
-       }
-
 }
index 758b93d..14c4f94 100644 (file)
@@ -81,14 +81,16 @@ public class ViewSonePage extends SoneTemplatePage {
                Sone sone = webInterface.getCore().getSone(soneId, false);
                templateContext.set("sone", sone);
                List<Post> sonePosts = sone.getPosts();
-               Pagination<Post> postPagination = new Pagination<Post>(sonePosts, 10).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("postPage"), 0));
+               sonePosts.addAll(webInterface.getCore().getDirectedPosts(sone));
+               Collections.sort(sonePosts, Post.TIME_COMPARATOR);
+               Pagination<Post> postPagination = new Pagination<Post>(sonePosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("postPage"), 0));
                templateContext.set("postPagination", postPagination);
                templateContext.set("posts", postPagination.getItems());
                Set<Reply> replies = sone.getReplies();
                final Map<Post, List<Reply>> repliedPosts = new HashMap<Post, List<Reply>>();
                for (Reply reply : replies) {
                        Post post = reply.getPost();
-                       if (repliedPosts.containsKey(post) || sone.equals(post.getSone())) {
+                       if (repliedPosts.containsKey(post) || sone.equals(post.getSone()) || (sone.equals(post.getRecipient()))) {
                                continue;
                        }
                        repliedPosts.put(post, webInterface.getCore().getReplies(post));
@@ -103,32 +105,9 @@ public class ViewSonePage extends SoneTemplatePage {
 
                });
 
-               Pagination<Post> repliedPostPagination = new Pagination<Post>(posts, 10).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("repliedPostPage"), 0));
+               Pagination<Post> repliedPostPagination = new Pagination<Post>(posts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("repliedPostPage"), 0));
                templateContext.set("repliedPostPagination", repliedPostPagination);
                templateContext.set("repliedPosts", repliedPostPagination.getItems());
        }
 
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       @SuppressWarnings("unchecked")
-       protected void postProcess(Request request, TemplateContext templateContext) {
-               Sone sone = (Sone) templateContext.get("sone");
-               if (sone == null) {
-                       return;
-               }
-               webInterface.getCore().markSoneKnown(sone);
-               List<Post> posts = (List<Post>) templateContext.get("posts");
-               posts.addAll((List<Post>) templateContext.get("repliedPosts"));
-               for (Post post : posts) {
-                       if (post.getSone() != null) {
-                               webInterface.getCore().markPostKnown(post);
-                       }
-                       for (Reply reply : webInterface.getCore().getReplies(post)) {
-                               webInterface.getCore().markReplyKnown(reply);
-                       }
-               }
-       }
-
 }
index acfdc7f..b3772f9 100644 (file)
@@ -53,6 +53,7 @@ import net.pterodactylus.sone.template.NotificationManagerAccessor;
 import net.pterodactylus.sone.template.ParserFilter;
 import net.pterodactylus.sone.template.PostAccessor;
 import net.pterodactylus.sone.template.ReplyAccessor;
+import net.pterodactylus.sone.template.ReplyGroupFilter;
 import net.pterodactylus.sone.template.RequestChangeFilter;
 import net.pterodactylus.sone.template.SoneAccessor;
 import net.pterodactylus.sone.template.SubstringFilter;
@@ -72,6 +73,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;
@@ -85,6 +87,7 @@ import net.pterodactylus.sone.web.ajax.UnlockSoneAjaxPage;
 import net.pterodactylus.sone.web.ajax.UntrustAjaxPage;
 import net.pterodactylus.sone.web.page.PageToadlet;
 import net.pterodactylus.sone.web.page.PageToadletFactory;
+import net.pterodactylus.sone.web.page.RedirectPage;
 import net.pterodactylus.sone.web.page.StaticPage;
 import net.pterodactylus.sone.web.page.TemplatePage;
 import net.pterodactylus.util.cache.Cache;
@@ -207,19 +210,20 @@ public class WebInterface implements CoreListener {
                templateContextFactory.addFilter("unknown", new UnknownDateFilter(getL10n(), "View.Sone.Text.UnknownDate"));
                templateContextFactory.addFilter("format", new FormatFilter());
                templateContextFactory.addFilter("sort", new CollectionSortFilter());
+               templateContextFactory.addFilter("replyGroup", new ReplyGroupFilter());
                templateContextFactory.addProvider(Provider.TEMPLATE_CONTEXT_PROVIDER);
                templateContextFactory.addProvider(new ClassPathTemplateProvider());
                templateContextFactory.addTemplateObject("formPassword", formPassword);
 
                /* create notifications. */
                Template newSoneNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newSoneNotification.html"));
-               newSoneNotification = new ListNotification<Sone>("new-sone-notification", "sones", newSoneNotificationTemplate);
+               newSoneNotification = new ListNotification<Sone>("new-sone-notification", "sones", newSoneNotificationTemplate, false);
 
                Template newPostNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newPostNotification.html"));
-               newPostNotification = new ListNotification<Post>("new-post-notification", "posts", newPostNotificationTemplate);
+               newPostNotification = new ListNotification<Post>("new-post-notification", "posts", newPostNotificationTemplate, false);
 
                Template newReplyNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newReplyNotification.html"));
-               newReplyNotification = new ListNotification<Reply>("new-replies-notification", "replies", newReplyNotificationTemplate);
+               newReplyNotification = new ListNotification<Reply>("new-replies-notification", "replies", newReplyNotificationTemplate, false);
 
                Template rescuingSonesTemplate = TemplateParser.parse(createReader("/templates/notify/rescuingSonesNotification.html"));
                rescuingSonesNotification = new ListNotification<Sone>("sones-being-rescued-notification", "sones", rescuingSonesTemplate);
@@ -541,6 +545,7 @@ public class WebInterface implements CoreListener {
                Template openSearchTemplate = TemplateParser.parse(createReader("/templates/xml/OpenSearch.xml"));
 
                PageToadletFactory pageToadletFactory = new PageToadletFactory(sonePlugin.pluginRespirator().getHLSimpleClient(), "/Sone/");
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new RedirectPage("", "index.html")));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new IndexPage(indexTemplate, this), "Index"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateSonePage(createSoneTemplate, this), "CreateSone"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new KnownSonesPage(knownSonesTemplate, this), "KnownSones"));
@@ -586,6 +591,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)));
index fd9ae76..426c08f 100644 (file)
@@ -31,8 +31,11 @@ import java.util.Set;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.notify.ListNotificationFilters;
 import net.pterodactylus.sone.template.SoneAccessor;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.filter.Filter;
+import net.pterodactylus.util.filter.Filters;
 import net.pterodactylus.util.json.JsonArray;
 import net.pterodactylus.util.json.JsonObject;
 import net.pterodactylus.util.notify.Notification;
@@ -65,6 +68,7 @@ public class GetStatusAjaxPage extends JsonPage {
         */
        @Override
        protected JsonObject createJsonObject(Request request) {
+               final Sone currentSone = getCurrentSone(request.getToadletContext(), false);
                /* load Sones. */
                boolean loadAllSones = Boolean.parseBoolean(request.getHttpRequest().getParam("loadAllSones", "true"));
                Set<Sone> sones = new HashSet<Sone>(Collections.singleton(getCurrentSone(request.getToadletContext(), false)));
@@ -80,19 +84,24 @@ public class GetStatusAjaxPage extends JsonPage {
                        jsonSones.add(jsonSone);
                }
                /* load notifications. */
-               List<Notification> notifications = new ArrayList<Notification>(webInterface.getNotifications().getChangedNotifications());
-               Set<Notification> removedNotifications = webInterface.getNotifications().getRemovedNotifications();
+               List<Notification> notifications = ListNotificationFilters.filterNotifications(new ArrayList<Notification>(webInterface.getNotifications().getNotifications()), currentSone);
                Collections.sort(notifications, Notification.LAST_UPDATED_TIME_SORTER);
                JsonArray jsonNotifications = new JsonArray();
                for (Notification notification : notifications) {
                        jsonNotifications.add(createJsonNotification(notification));
                }
-               JsonArray jsonRemovedNotifications = new JsonArray();
-               for (Notification notification : removedNotifications) {
-                       jsonRemovedNotifications.add(createJsonNotification(notification));
-               }
                /* load new posts. */
                Set<Post> newPosts = webInterface.getNewPosts();
+               if (currentSone != null) {
+                       newPosts = Filters.filteredSet(newPosts, new Filter<Post>() {
+
+                               @Override
+                               public boolean filterObject(Post post) {
+                                       return currentSone.hasFriend(post.getSone().getId()) || currentSone.equals(post.getSone()) || currentSone.equals(post.getRecipient());
+                               }
+
+                       });
+               }
                JsonArray jsonPosts = new JsonArray();
                for (Post post : newPosts) {
                        JsonObject jsonPost = new JsonObject();
@@ -104,6 +113,16 @@ public class GetStatusAjaxPage extends JsonPage {
                }
                /* load new replies. */
                Set<Reply> newReplies = webInterface.getNewReplies();
+               if (currentSone != null) {
+                       newReplies = Filters.filteredSet(newReplies, new Filter<Reply>() {
+
+                               @Override
+                               public boolean filterObject(Reply reply) {
+                                       return currentSone.hasFriend(reply.getPost().getSone().getId()) || currentSone.equals(reply.getPost().getSone()) || currentSone.equals(reply.getPost().getRecipient());
+                               }
+
+                       });
+               }
                JsonArray jsonReplies = new JsonArray();
                for (Reply reply : newReplies) {
                        JsonObject jsonReply = new JsonObject();
@@ -113,7 +132,7 @@ public class GetStatusAjaxPage extends JsonPage {
                        jsonReply.put("postSone", reply.getPost().getSone().getId());
                        jsonReplies.add(jsonReply);
                }
-               return createSuccessJsonObject().put("sones", jsonSones).put("notifications", jsonNotifications).put("removedNotifications", jsonRemovedNotifications).put("newPosts", jsonPosts).put("newReplies", jsonReplies);
+               return createSuccessJsonObject().put("sones", jsonSones).put("notifications", jsonNotifications).put("newPosts", jsonPosts).put("newReplies", jsonReplies);
        }
 
        /**
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..b17e4a2
--- /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 < 20 * Time.SECOND) {
+                       text = webInterface.getL10n().getDefaultString("View.Time.AFewSecondsAgo");
+                       refresh = 10 * Time.SECOND;
+               } else if (age < 45 * Time.SECOND) {
+                       text = webInterface.getL10n().getString("View.Time.HalfAMinuteAgo");
+                       refresh = 20 * Time.SECOND;
+               } else if (age < 90 * Time.SECOND) {
+                       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 = 1 * Time.MINUTE;
+               } else if (age < 45 * Time.MINUTE) {
+                       text = webInterface.getL10n().getString("View.Time.HalfAnHourAgo");
+                       refresh = 10 * Time.MINUTE;
+               } else if (age < 90 * Time.MINUTE) {
+                       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 4334a7f..f7f59a6 100644 (file)
@@ -53,6 +53,7 @@ public class PageToadlet extends Toadlet implements LinkEnabledCallback {
         * Creates a new toadlet that hands off processing to a {@link Page}.
         *
         * @param highLevelSimpleClient
+        *            The high-level simple client
         * @param menuName
         *            The name of the menu item
         * @param page
diff --git a/src/main/java/net/pterodactylus/sone/web/page/RedirectPage.java b/src/main/java/net/pterodactylus/sone/web/page/RedirectPage.java
new file mode 100644 (file)
index 0000000..2ce34f9
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * Sone - RedirectPage.java - Copyright © 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.page;
+
+/**
+ * Page implementation that redirects the user to another URL.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class RedirectPage implements Page {
+
+       /** The original path. */
+       private String originalPath;
+
+       /** The path to redirect the browser to. */
+       private String newPath;
+
+       /**
+        * Creates a new redirect page.
+        *
+        * @param originalPath
+        *            The original path
+        * @param newPath
+        *            The path to redirect the browser to
+        */
+       public RedirectPage(String originalPath, String newPath) {
+               this.originalPath = originalPath;
+               this.newPath = newPath;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String getPath() {
+               return originalPath;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response handleRequest(Request request) {
+               return new RedirectResponse(newPath);
+       }
+
+}
index 104d3c7..a73432d 100644 (file)
@@ -23,12 +23,21 @@ Navigation.Menu.Item.About.Tooltip=Information about Sone
 
 Page.About.Title=About - Sone
 Page.About.Page.Title=About
+Page.About.Flattr.Description=If you like Sone and you would like to reward me, you can use the Flattr button at the bottom of each page. Flattr is a non-anonymous micro payment that acts like an internet tip jar where the amount each user spends is limited (lowest being 2 € per month). More information can be found on {link}flattr.com{/link}.
+Page.About.Homepage.Title=Homepage
+Page.About.Homepage.Description=You can find more information and the source code of Sone on the {link}homepage{/link}.
+Page.About.License.Title=License
 
 Page.Options.Title=Options - Sone
 Page.Options.Page.Title=Options
 Page.Options.Page.Description=These options influence the runtime behaviour of the Sone plugin.
+Page.Options.Section.SoneSpecificOptions.Title=Sone-specific Options
+Page.Options.Section.SoneSpecificOptions.NotLoggedIn=These options are only available if you are {link}logged in{/link}.
+Page.Options.Section.SoneSpecificOptions.LoggedIn=These options are only available while you are logged in and they are only valid for the Sone you are logged in as.
+Page.Options.Option.AutoFollow.Description=If a new Sone is discovered, follow it automatically.
 Page.Options.Section.RuntimeOptions.Title=Runtime Behaviour
 Page.Options.Option.InsertionDelay.Description=The number of seconds the Sone inserter waits after a modification of a Sone before it is being inserted.
+Page.Options.Option.PostsPerPage.Description=The number of posts to display on a page before pagination controls are being shown.
 Page.Options.Section.TrustOptions.Title=Trust Settings
 Page.Options.Option.PositiveTrust.Description=The amount of positive trust you want to assign to other Sones by clicking the checkmark below a post or reply.
 Page.Options.Option.NegativeTrust.Description=The amount of trust you want to assign to other Sones by clicking the red X below a post or reply. This value should be negative.
@@ -196,7 +205,7 @@ View.Search.Button.Search=Search
 View.CreateSone.Text.WotIdentityRequired=To create a Sone you need an identity from the {link}Web of Trust plugin{/link}.
 View.CreateSone.Select.Default=Select an identity
 View.CreateSone.Text.NoIdentities=You do not have any Web of Trust identities. Please head over to the {link}Web of Trust plugin{/link} and create an identity.
-View.CreateSone.Text.NoNonSoneIdentities=You do not have any Web of Trust identities that are not already a Sone. Please head over to the {link}Web of Trust plugin{/link} and create an identity.
+View.CreateSone.Text.NoNonSoneIdentities=You do not have any Web of Trust identities that are not already a Sone. Use one of the remaining Web of Trust identities to create a new Sone or head over to the {link}Web of Trust plugin{/link} to create a new identity.
 View.CreateSone.Button.Create=Create Sone
 View.CreateSone.Text.Error.NoIdentity=You have not selected an identity.
 
@@ -231,6 +240,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.HalfAMinuteAgo=about half a minute 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…
@@ -242,6 +268,10 @@ WebInterface.DefaultText.BirthMonth=Month
 WebInterface.DefaultText.BirthYear=Year
 WebInterface.DefaultText.FieldName=Field name
 WebInterface.DefaultText.Option.InsertionDelay=Time to wait after a Sone is modified before insert (in seconds)
+WebInterface.DefaultText.Option.PostsPerPage=Number of posts to show on a page
+WebInterface.DefaultText.Option.PositiveTrust=The positive trust to assign
+WebInterface.DefaultText.Option.NegativeTrust=The negative trust to assign
+WebInterface.DefaultText.Option.TrustComment=The comment to set in the web of trust
 WebInterface.DefaultText.Search=What are you looking for?
 WebInterface.Confirmation.DeletePostButton=Yes, delete!
 WebInterface.Confirmation.DeleteReplyButton=Yes, delete!
@@ -263,7 +293,7 @@ Notification.NewPost.ShortText=New posts have been discovered.
 Notification.NewPost.Text=New posts have been discovered by the following Sones:
 Notification.NewPost.Button.MarkRead=Mark as read
 Notification.NewReply.ShortText=New replies have been discovered.
-Notification.NewReply.Text=New replies have been discovered by the following Sones:
+Notification.NewReply.Text=New replies have been discovered for posts by the following Sones:
 Notification.SoneIsBeingRescued.Text=The following Sones are currently being rescued:
 Notification.SoneRescued.Text=The following Sones have been rescued:
 Notification.SoneRescued.Text.RememberToUnlock=Please remember to control the posts and replies you have given and don’t forget to unlock your Sones!
index 38fcd19..f6bb62d 100644 (file)
@@ -121,6 +121,10 @@ textarea {
        float: right;
 }
 
+#sone #notification-area .notification .hidden {
+       display: none;
+}
+
 #sone #plugin-warning {
        border: solid 0.5em red;
        padding: 0.5em;
index c0fbb6d..d60ee23 100644 (file)
@@ -37,7 +37,8 @@ function registerInputTextareaSwap(inputElement, defaultText, inputFieldName, op
                (function(inputField, textarea) {
                        inputField.focus(function() {
                                $(this).hide().attr("disabled", "disabled");
-                               textarea.show().focus();
+                               /* no, show(), “display: block” is not what I need. */
+                               textarea.attr("style", "display: inline").focus();
                        });
                        if (inputField.val() == "") {
                                inputField.addClass("default");
@@ -217,7 +218,8 @@ function enhanceDeletePostButton(button, postId, text) {
                        if (data.success) {
                                $("#sone .post#" + postId).slideUp();
                        } else if (data.error == "invalid-post-id") {
-                               alert("Invalid post ID given!");
+                               /* pretend the post is already gone. */
+                               getPost(postId).slideUp();
                        } else if (data.error == "auth-required") {
                                alert("You need to be logged in.");
                        } else if (data.error == "not-authorized") {
@@ -248,7 +250,8 @@ function enhanceDeleteReplyButton(button, replyId, text) {
                        if (data.success) {
                                $("#sone .reply#" + replyId).slideUp();
                        } else if (data.error == "invalid-reply-id") {
-                               alert("Invalid reply ID given!");
+                               /* pretend the reply is already gone. */
+                               getReply(replyId).slideUp();
                        } else if (data.error == "auth-required") {
                                alert("You need to be logged in.");
                        } else if (data.error == "not-authorized") {
@@ -264,6 +267,19 @@ function getFormPassword() {
        return $("#sone #formPassword").text();
 }
 
+/**
+ * Returns the element of the Sone with the given ID.
+ *
+ * @param soneId
+ *            The ID of the Sone
+ * @returns All Sone elements with the given ID
+ */
+function getSone(soneId) {
+       return $("#sone .sone").filter(function(index) {
+               return $(".id").text() == soneId;
+       });
+}
+
 function getSoneElement(element) {
        return $(element).closest(".sone");
 }
@@ -332,6 +348,17 @@ function getPostAuthor(element) {
        return getPostElement(element).find(".post-author").text();
 }
 
+/**
+ * Returns the element of the reply with the given ID.
+ *
+ * @param replyId
+ *            The ID of the reply
+ * @returns The element of the reply
+ */
+function getReply(replyId) {
+       return $("#sone .reply#" + replyId);
+}
+
 function getReplyElement(element) {
        return $(element).closest(".reply");
 }
@@ -355,6 +382,39 @@ function getReplyAuthor(element) {
        return getReplyElement(element).find(".reply-author").text();
 }
 
+/**
+ * Returns the notification with the given ID.
+ *
+ * @param notificationId
+ *            The ID of the notification
+ * @returns The notification element
+ */
+function getNotification(notificationId) {
+       return $("#sone #notification-area .notification#" + notificationId);
+}
+
+/**
+ * Returns the notification element closest to the given element.
+ *
+ * @param element
+ *            The element to get the closest notification of
+ * @return The closest notification element
+ */
+function getNotificationElement(element) {
+       return $(element).closest(".notification");
+}
+
+/**
+ * Returns the ID of the notification element.
+ *
+ * @param notificationElement
+ *            The notification element
+ * @returns The ID of the notification
+ */
+function getNotificationId(notificationElement) {
+       return $(notificationElement).attr("id");
+}
+
 function likePost(postId) {
        $.getJSON("like.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
                if ((data == null) || !data.success) {
@@ -561,25 +621,6 @@ function postReply(sender, postId, text, callbackFunction) {
 }
 
 /**
- * Requests information about the reply with the given ID.
- *
- * @param replyId
- *            The ID of the reply
- * @param callbackFunction
- *            A callback function (parameters soneId, soneName, replyTime,
- *            replyDisplayTime, text, html)
- */
-function getReply(replyId, callbackFunction) {
-       $.getJSON("getReply.ajax", { "reply" : replyId }, function(data, textStatus) {
-               if ((data != null) && data.success) {
-                       callbackFunction(data.soneId, data.soneName, data.time, data.displayTime, data.text, data.html);
-               }
-       }, function(xmlHttpRequest, textStatus, error) {
-               /* ignore error. */
-       });
-}
-
-/**
  * Ajaxifies the given Sone by enhancing all eligible elements with AJAX.
  *
  * @param soneElement
@@ -719,9 +760,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) {
@@ -837,6 +881,90 @@ function ajaxifyNotification(notification) {
        return notification;
 }
 
+/**
+ * Retrieves element IDs from notification elements.
+ *
+ * @param notification
+ *            The notification element
+ * @param selector
+ *            The selector of the element containing the ID as text
+ * @returns All extracted IDs
+ */
+function getElementIds(notification, selector) {
+       elementIds = [];
+       $(selector, notification).each(function() {
+               elementIds.push($(this).text());
+       });
+       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) {
@@ -844,9 +972,45 @@ function getStatus() {
                        $.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();
+                                               /* remove activity when no notifications are visible. */
+                                               if ($("#sone #notification-area .notification").length == 0) {
+                                                       resetActivity();
+                                               }
+                                       });
+                               }
+                       });
                        /* process notifications. */
                        $.each(data.notifications, function(index, value) {
-                               oldNotification = $("#sone #notification-area .notification#" + value.id);
+                               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)) {
@@ -854,15 +1018,15 @@ function getStatus() {
                                                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();
                                }
-                               setActivity();
-                       });
-                       $.each(data.removedNotifications, function(index, value) {
-                               $("#sone #notification-area .notification#" + value.id).slideUp();
                        });
                        /* process new posts. */
                        $.each(data.newPosts, function(index, value) {
@@ -1021,6 +1185,7 @@ function loadNewPost(postId, soneId, recipientId, time) {
                                newPost.insertBefore(firstOlderPost);
                        }
                        ajaxifyPost(newPost);
+                       updatePostTimes(data.post.id);
                        newPost.slideDown();
                        setActivity();
                }
@@ -1059,6 +1224,7 @@ function loadNewReply(replyId, soneId, postId, postSoneId) {
                                        }
                                }
                                ajaxifyReply(newReply);
+                               updateReplyTimes(data.reply.id);
                                newReply.slideDown();
                                setActivity();
                                return false;
@@ -1072,46 +1238,133 @@ function loadNewReply(replyId, soneId, postId, postSoneId) {
  *
  * @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) {
+function markSoneAsKnown(soneElement, skipRequest) {
        if ($(".new", soneElement).length > 0) {
-               $.getJSON("maskAsKnown.ajax", {"formPassword": getFormPassword(), "type": "sone", "id": getSoneId(soneElement)}, function(data, textStatus) {
-                       $(soneElement).removeClass("new");
-               });
+               if ((typeof skipRequest != "undefined") && !skipRequest) {
+                       $.getJSON("maskAsKnown.ajax", {"formPassword": getFormPassword(), "type": "sone", "id": getSoneId(soneElement)}, function(data, textStatus) {
+                               $(soneElement).removeClass("new");
+                       });
+               }
        }
 }
 
-function markPostAsKnown(postElements) {
+function markPostAsKnown(postElements, skipRequest) {
        $(postElements).each(function() {
                postElement = this;
                if ($(postElement).hasClass("new")) {
                        (function(postElement) {
                                $(postElement).removeClass("new");
                                $(".click-to-show", postElement).removeClass("new");
-                               $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "post", "id": getPostId(postElement)});
+                               if ((typeof skipRequest == "undefined") || !skipRequest) {
+                                       $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "post", "id": getPostId(postElement)});
+                               }
                        })(postElement);
                }
        });
        markReplyAsKnown($(postElements).find(".reply"));
 }
 
-function markReplyAsKnown(replyElements) {
+function markReplyAsKnown(replyElements, skipRequest) {
        $(replyElements).each(function() {
                replyElement = this;
                if ($(replyElement).hasClass("new")) {
                        (function(replyElement) {
                                $(replyElement).removeClass("new");
-                               $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "reply", "id": getReplyId(replyElement)});
+                               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) {
+       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));
        }
+       iconBlinking = false;
 }
 
 function setActivity() {
@@ -1150,7 +1403,7 @@ var iconBlinking = false;
  * showing the activity state, it is returned to normal.
  */
 function toggleIcon() {
-       if (focus) {
+       if (focus || !iconBlinking) {
                if (iconActive) {
                        changeIcon("images/icon.png");
                        iconActive = false;
@@ -1309,9 +1562,6 @@ $(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) {
-                               if ((data != null) && data.success) {
-                                       loadNewPost(data.postId, data.sone, data.recipient);
-                               }
                                button.removeAttr("disabled");
                        });
                        $(this).find(":input[name=sender]").val(getCurrentSoneId());
@@ -1340,11 +1590,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 }, function(data, textStatus) {
-                               if ((data != null) && data.success) {
-                                       loadNewPost(data.postId, getCurrentSoneId());
-                               }
-                       });
+                       $.getJSON("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();
@@ -1365,6 +1611,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) {
index 96f4c8e..e04f4f2 100644 (file)
@@ -2,17 +2,20 @@
 
        <h1><%= Page.About.Page.Title|l10n|html></h1>
 
-       <p>Sone – The Freenet Social Network Plugin, Version <% version|html>, © 2010 by David ‘Bombe’ Roden.</p>
+       <p>Sone – The Freenet Social Network Plugin, Version <% version|html>, © 2010–2011 by David ‘Bombe’ Roden.</p>
 
        <p>
-               If you like Sone and you would like to reward me, you can use the
-               Flattr button at the bottom of each page. Flattr is a non-anonymous
-               micro payment that acts like an internet tip jar where the amount
-               each user spends is limited (lowest being 2 € per month). More
-               information can be found on <a href="/?_CHECKED_HTTP_=https://www.flattr.com/"
-               title="Flattr Homepage" target="_blank">flattr.com</a>.
+               <%= Page.About.Flattr.Description|l10n|html|replace needle="{link}" replacement='<a href="/?_CHECKED_HTTP_=https://www.flattr.com/" title="Flattr Homepage" target="_blank">'|replace needle="{/link}" replacement='</a>'>
        </p>
 
+       <h2><%= Page.About.Homepage.Title|l10n|html></h2>
+
+       <p>
+               <%= Page.About.Homepage.Description|l10n|html|replace needle="{link}" replacement='<a href="/USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/33/">'|replace needle="{/link}" replacement='</a>'>
+       </p>
+
+       <h2><%= Page.About.License.Title|l10n|html></h2>
+
        <p>
                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
index 177b470..54008dc 100644 (file)
@@ -12,6 +12,7 @@
        </form>
        <%= Notification.NewPost.Text|l10n|html>
        <%foreach posts post>
+               <div class="hidden post-id"><%post.id|html></div>
                <a class="link-<% post.id|html>" href="viewPost.html?post=<% post.id|html>"><% post.sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
        <%/foreach>
 </div>
index 21a5ca0..ee8c410 100644 (file)
@@ -10,8 +10,9 @@
                <input type="hidden" name="id" value="<%foreach replies reply><% reply.id|html><%notlast> <%/notlast><%/foreach>" />
                <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
        </form>
+       <%foreach replies reply><div class="hidden reply-id"><%reply.id|html></div><%/foreach>
        <%= Notification.NewReply.Text|l10n|html>
-       <%foreach replies reply>
-               <a class="link-<% reply.post.id|html>" href="viewPost.html?post=<% reply.post.id|html>"><% reply.sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
+       <%foreach replies postGroup|replyGroup>
+               <a class="link-<% postGroup.key.id|html>" href="viewPost.html?post=<% postGroup.key.id|html>"><% postGroup.key.sone.niceName|html></a> (<%foreach postGroup.value.sones sone><%sone.niceName|html><%notlast>, <%/notlast><%/foreach>)<%notlast>, <%/notlast><%last>.<%/last>
        <%/foreach>
 </div>
index 45357b1..aaa0d14 100644 (file)
@@ -12,6 +12,7 @@
        </form>
        <%= Notification.NewSone.Text|l10n|html>
        <%foreach sones sone>
+               <div class="hidden sone-id"><% sone.id|html></div>
                <a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
        <%/foreach>
 </div>
index 5c47764..db5eb80 100644 (file)
@@ -5,6 +5,18 @@
                        getTranslation("WebInterface.DefaultText.Option.InsertionDelay", function(insertionDelayDefaultText) {
                                registerInputTextareaSwap("#sone #options input[name=insertion-delay]", insertionDelayDefaultText, "insertion-delay", true, true);
                        });
+                       getTranslation("WebInterface.DefaultText.Option.PostsPerPage", function(postsPerPageText) {
+                               registerInputTextareaSwap("#sone #options input[name=posts-per-page]", postsPerPageText, "posts-per-page", true, true);
+                       });
+                       getTranslation("WebInterface.DefaultText.Option.PositiveTrust", function(positiveTrustText) {
+                               registerInputTextareaSwap("#sone #options input[name=positive-trust]", positiveTrustText, "positive-trust", true, true);
+                       });
+                       getTranslation("WebInterface.DefaultText.Option.NegativeTrust", function(negativeTrustText) {
+                               registerInputTextareaSwap("#sone #options input[name=negative-trust]", negativeTrustText, "negative-trust", true, true);
+                       });
+                       getTranslation("WebInterface.DefaultText.Option.TrustComment", function(trustCommentText) {
+                               registerInputTextareaSwap("#sone #options input[name=trust-comment]", trustCommentText, "trust-comment", true, true);
+                       });
                });
        </script>
 
        <form id="options" method="post">
                <input type="hidden" name="formPassword" value="<% formPassword|html>" />
 
+               <h2><%= Page.Options.Section.SoneSpecificOptions.Title|l10n|html></h2>
+
+               <%ifnull currentSone>
+                       <p><%= Page.Options.Section.SoneSpecificOptions.NotLoggedIn|l10n|html|replace needle="{link}" replacement='<a href="login.html">'|replace needle="{/link}" replacement='</a>'></p>
+               <%else>
+                       <p><%= Page.Options.Section.SoneSpecificOptions.LoggedIn|l10n|html></p>
+               <%/if>
+
+               <p>
+                       <input type="checkbox" name="auto-follow"<%ifnull currentSone> disabled="disabled"<%/if><%if auto-follow> checked="checked"<%/if> />
+                       <%= Page.Options.Option.AutoFollow.Description|l10n|html>
+               </p>
+
                <h2><%= Page.Options.Section.RuntimeOptions.Title|l10n|html></h2>
 
                <p><%= Page.Options.Option.InsertionDelay.Description|l10n|html></p>
                <p><input type="text" name="insertion-delay" value="<% insertion-delay|html>" /></p>
 
+               <p><%= Page.Options.Option.PostsPerPage.Description|l10n|html></p>
+               <p><input type="text" name="posts-per-page" value="<% posts-per-page|html>" /></p>
+
                <h2><%= Page.Options.Section.TrustOptions.Title|l10n|html></h2>
 
                <p><%= Page.Options.Option.PositiveTrust.Description|l10n|html></p>
index e350961..7e05fd5 100644 (file)
@@ -40,7 +40,7 @@ public class FreenetLinkParserTest extends TestCase {
        public void testParser() throws IOException {
                TemplateContextFactory templateContextFactory = new TemplateContextFactory();
                templateContextFactory.addFilter("html", new HtmlFilter());
-               FreenetLinkParser parser = new FreenetLinkParser(templateContextFactory);
+               FreenetLinkParser parser = new FreenetLinkParser(null, templateContextFactory);
                FreenetLinkParserContext context = new FreenetLinkParserContext(null);
                Part part;