Refactor notification filtering
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Wed, 8 Jun 2016 19:38:35 +0000 (21:38 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Wed, 8 Jun 2016 19:41:49 +0000 (21:41 +0200)
14 files changed:
src/main/java/net/pterodactylus/sone/notify/ListNotificationFilters.java
src/main/java/net/pterodactylus/sone/notify/PostVisibilityFilter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/notify/ReplyVisibilityFilter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/DismissNotificationPage.java
src/main/java/net/pterodactylus/sone/web/IndexPage.java
src/main/java/net/pterodactylus/sone/web/NewPage.java
src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/java/net/pterodactylus/sone/web/ajax/DismissNotificationAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationsAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java
src/test/java/net/pterodactylus/sone/notify/ListNotificationFiltersTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/notify/PostVisibilityFilterTest.java [new file with mode: 0644]
src/test/java/net/pterodactylus/sone/notify/ReplyVisibilityFilterTest.java [new file with mode: 0644]

index 8a423c2..0a2a518 100644 (file)
 
 package net.pterodactylus.sone.notify;
 
-import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.FluentIterable.from;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import javax.annotation.Nonnull;
+import javax.inject.Inject;
+import javax.inject.Singleton;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.PostReply;
-import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-import net.pterodactylus.sone.freenet.wot.Trust;
 import net.pterodactylus.util.notify.Notification;
 
 import com.google.common.base.Optional;
@@ -38,8 +38,18 @@ import com.google.common.base.Optional;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
+@Singleton
 public class ListNotificationFilters {
 
+       private final PostVisibilityFilter postVisibilityFilter;
+       private final ReplyVisibilityFilter replyVisibilityFilter;
+
+       @Inject
+       public ListNotificationFilters(@Nonnull PostVisibilityFilter postVisibilityFilter, @Nonnull ReplyVisibilityFilter replyVisibilityFilter) {
+               this.postVisibilityFilter = postVisibilityFilter;
+               this.replyVisibilityFilter = replyVisibilityFilter;
+       }
+
        /**
         * Filters new-post and new-reply notifications in the given list of
         * notifications. If {@code currentSone} is <code>null</code>, new-post and
@@ -49,13 +59,13 @@ public class ListNotificationFilters {
         * itself will be retained in the notifications.
         *
         * @param notifications
-        *            The notifications to filter
+        *              The notifications to filter
         * @param currentSone
-        *            The current Sone, or {@code null} if not logged in
+        *              The current Sone, or {@code null} if not logged in
         * @return The filtered notifications
         */
        @SuppressWarnings("unchecked")
-       public static List<Notification> filterNotifications(Collection<? extends Notification> notifications, Sone currentSone) {
+       public List<Notification> filterNotifications(Collection<? extends Notification> notifications, Sone currentSone) {
                List<Notification> filteredNotifications = new ArrayList<Notification>();
                for (Notification notification : notifications) {
                        if (notification.getId().equals("new-sone-notification")) {
@@ -64,23 +74,30 @@ public class ListNotificationFilters {
                                }
                                filteredNotifications.add(notification);
                        } else if (notification.getId().equals("new-post-notification")) {
-                               if ((currentSone != null) && !currentSone.getOptions().isShowNewPostNotifications()) {
+                               if (currentSone == null) {
                                        continue;
                                }
-                               Optional<ListNotification<Post>> filteredNotification = filterNewPostNotification((ListNotification<Post>) notification, currentSone, true);
+                               if (!currentSone.getOptions().isShowNewPostNotifications()) {
+                                       continue;
+                               }
+                               Optional<ListNotification<Post>> filteredNotification = filterNewPostNotification((ListNotification<Post>) notification, currentSone);
                                if (filteredNotification.isPresent()) {
                                        filteredNotifications.add(filteredNotification.get());
                                }
                        } else if (notification.getId().equals("new-reply-notification")) {
-                               if ((currentSone != null) && !currentSone.getOptions().isShowNewReplyNotifications()) {
+                               if (currentSone == null) {
                                        continue;
                                }
-                               Optional<ListNotification<PostReply>> filteredNotification = filterNewReplyNotification((ListNotification<PostReply>) notification, currentSone);
+                               if (!currentSone.getOptions().isShowNewReplyNotifications()) {
+                                       continue;
+                               }
+                               Optional<ListNotification<PostReply>> filteredNotification =
+                                               filterNewReplyNotification((ListNotification<PostReply>) notification, currentSone);
                                if (filteredNotification.isPresent()) {
                                        filteredNotifications.add(filteredNotification.get());
                                }
                        } else if (notification.getId().equals("mention-notification")) {
-                               Optional<ListNotification<Post>> filteredNotification = filterNewPostNotification((ListNotification<Post>) notification, null, false);
+                               Optional<ListNotification<Post>> filteredNotification = filterNewPostNotification((ListNotification<Post>) notification, null);
                                if (filteredNotification.isPresent()) {
                                        filteredNotifications.add(filteredNotification.get());
                                }
@@ -99,25 +116,16 @@ public class ListNotificationFilters {
         * other posts are removed.
         *
         * @param newPostNotification
-        *            The new-post notification
+        *              The new-post notification
         * @param currentSone
-        *            The current Sone, or {@code null} if not logged in
-        * @param soneRequired
-        *            Whether a non-{@code null} Sone in {@code currentSone} is
-        *            required
+        *              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
+        * notification should be removed
         */
-       private static Optional<ListNotification<Post>> filterNewPostNotification(ListNotification<Post> newPostNotification, Sone currentSone, boolean soneRequired) {
-               if (soneRequired && (currentSone == null)) {
-                       return Optional.absent();
-               }
-               List<Post> newPosts = new ArrayList<Post>();
-               for (Post post : newPostNotification.getElements()) {
-                       if (isPostVisible(currentSone, post)) {
-                               newPosts.add(post);
-                       }
-               }
+       @Nonnull
+       private Optional<ListNotification<Post>> filterNewPostNotification(@Nonnull ListNotification<Post> newPostNotification,
+                       @Nonnull Sone currentSone) {
+               List<Post> newPosts = from(newPostNotification.getElements()).filter(postVisibilityFilter.isVisible(currentSone)).toList();
                if (newPosts.isEmpty()) {
                        return Optional.absent();
                }
@@ -138,22 +146,15 @@ public class ListNotificationFilters {
         * replies are removed.
         *
         * @param newReplyNotification
-        *            The new-reply notification
+        *              The new-reply notification
         * @param currentSone
-        *            The current Sone, or {@code null} if not logged in
+        *              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
+        * notification should be removed
         */
-       private static Optional<ListNotification<PostReply>> filterNewReplyNotification(ListNotification<PostReply> newReplyNotification, Sone currentSone) {
-               if (currentSone == null) {
-                       return Optional.absent();
-               }
-               List<PostReply> newReplies = new ArrayList<PostReply>();
-               for (PostReply reply : newReplyNotification.getElements()) {
-                       if (isReplyVisible(currentSone, reply)) {
-                               newReplies.add(reply);
-                       }
-               }
+       private Optional<ListNotification<PostReply>> filterNewReplyNotification(ListNotification<PostReply> newReplyNotification,
+                       @Nonnull Sone currentSone) {
+               List<PostReply> newReplies = from(newReplyNotification.getElements()).filter(replyVisibilityFilter.isVisible(currentSone)).toList();
                if (newReplies.isEmpty()) {
                        return Optional.absent();
                }
@@ -166,128 +167,4 @@ public class ListNotificationFilters {
                return Optional.of(filteredNotification);
        }
 
-       /**
-        * Filters the given posts, using {@link #isPostVisible(Sone, Post)} to
-        * decide whether a post should be contained in the returned list. If
-        * {@code currentSone} is not {@code null} it is used to filter out posts
-        * that are from Sones that are not followed or not trusted by the given
-        * Sone.
-        *
-        * @param posts
-        *            The posts to filter
-        * @param currentSone
-        *            The current Sone (may be {@code null})
-        * @return The filtered posts
-        */
-       public static List<Post> filterPosts(Collection<Post> posts, Sone currentSone) {
-               List<Post> filteredPosts = new ArrayList<Post>();
-               for (Post post : posts) {
-                       if (isPostVisible(currentSone, post)) {
-                               filteredPosts.add(post);
-                       }
-               }
-               return filteredPosts;
-       }
-
-       /**
-        * Checks whether a post is visible to the given Sone. A post is not
-        * considered visible if one of the following statements is true:
-        * <ul>
-        * <li>The post does not have a Sone.</li>
-        * <li>The post’s {@link Post#getTime() time} is in the future.</li>
-        * </ul>
-        * <p>
-        * If {@code post} is not {@code null} more checks are performed, and the
-        * post will be invisible if:
-        * </p>
-        * <ul>
-        * <li>The Sone of the post is not the given Sone, the given Sone does not
-        * follow the post’s Sone, and the given Sone is not the recipient of the
-        * post.</li>
-        * <li>The trust relationship between the two Sones can not be retrieved.</li>
-        * <li>The given Sone has explicitely assigned negative trust to the post’s
-        * Sone.</li>
-        * <li>The given Sone has not explicitely assigned negative trust to the
-        * post’s Sone but the implicit trust is negative.</li>
-        * </ul>
-        * If none of these statements is true the post is considered visible.
-        *
-        * @param sone
-        *            The Sone that checks for a post’s visibility (may be
-        *            {@code null} to skip Sone-specific checks, such as trust)
-        * @param post
-        *            The post to check for visibility
-        * @return {@code true} if the post is considered visible, {@code false}
-        *         otherwise
-        */
-       public static boolean isPostVisible(Sone sone, Post post) {
-               checkNotNull(post, "post must not be null");
-               if (!post.isLoaded()) {
-                       return false;
-               }
-               Sone postSone = post.getSone();
-               if (sone != null) {
-                       Trust trust = postSone.getIdentity().getTrust((OwnIdentity) sone.getIdentity());
-                       if (trust != null) {
-                               if ((trust.getExplicit() != null) && (trust.getExplicit() < 0)) {
-                                       return false;
-                               }
-                               if ((trust.getExplicit() == null) && (trust.getImplicit() != null) && (trust.getImplicit() < 0)) {
-                                       return false;
-                               }
-                       } else {
-                               /*
-                                * a null trust means that the trust updater has not yet
-                                * received a trust value for this relation. if we return false,
-                                * the post feed will stay empty until the trust updater has
-                                * received trust values. to prevent this we simply assume that
-                                * posts are visible if there is no trust.
-                                */
-                       }
-                       if ((!postSone.equals(sone)) && !sone.hasFriend(postSone.getId()) && !sone.getId().equals(post.getRecipientId().orNull())) {
-                               return false;
-                       }
-               }
-               return post.getTime() <= System.currentTimeMillis();
-       }
-
-       /**
-        * Checks whether a reply is visible to the given Sone. A reply is not
-        * considered visible if one of the following statements is true:
-        * <ul>
-        * <li>The reply does not have a post.</li>
-        * <li>The reply’s post does not have a Sone.</li>
-        * <li>The Sone of the reply’s post is not the given Sone, the given Sone
-        * does not follow the reply’s post’s Sone, and the given Sone is not the
-        * recipient of the reply’s post.</li>
-        * <li>The trust relationship between the two Sones can not be retrieved.</li>
-        * <li>The given Sone has explicitely assigned negative trust to the post’s
-        * Sone.</li>
-        * <li>The given Sone has not explicitely assigned negative trust to the
-        * reply’s post’s Sone but the implicit trust is negative.</li>
-        * <li>The reply’s post’s {@link Post#getTime() time} is in the future.</li>
-        * <li>The reply’s {@link Reply#getTime() time} is in the future.</li>
-        * </ul>
-        * If none of these statements is true the reply is considered visible.
-        *
-        * @param sone
-        *            The Sone that checks for a post’s visibility (may be
-        *            {@code null} to skip Sone-specific checks, such as trust)
-        * @param reply
-        *            The reply to check for visibility
-        * @return {@code true} if the reply is considered visible, {@code false}
-        *         otherwise
-        */
-       public static boolean isReplyVisible(Sone sone, PostReply reply) {
-               checkNotNull(reply, "reply must not be null");
-               Optional<Post> post = reply.getPost();
-               if (!post.isPresent()) {
-                       return false;
-               }
-               if (!isPostVisible(sone, post.get())) {
-                       return false;
-               }
-               return reply.getTime() <= System.currentTimeMillis();
-       }
-
 }
diff --git a/src/main/java/net/pterodactylus/sone/notify/PostVisibilityFilter.java b/src/main/java/net/pterodactylus/sone/notify/PostVisibilityFilter.java
new file mode 100644 (file)
index 0000000..edcd148
--- /dev/null
@@ -0,0 +1,98 @@
+package net.pterodactylus.sone.notify;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.inject.Singleton;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.wot.OwnIdentity;
+import net.pterodactylus.sone.freenet.wot.Trust;
+import net.pterodactylus.util.notify.Notification;
+
+import com.google.common.base.Predicate;
+
+/**
+ * Filters {@link Notification}s involving {@link Post}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+@Singleton
+public class PostVisibilityFilter {
+
+       /**
+        * Checks whether a post is visible to the given Sone. A post is not
+        * considered visible if one of the following statements is true:
+        * <ul>
+        * <li>The post does not have a Sone.</li>
+        * <li>The post’s {@link Post#getTime() time} is in the future.</li>
+        * </ul>
+        * <p>
+        * If {@code post} is not {@code null} more checks are performed, and the
+        * post will be invisible if:
+        * </p>
+        * <ul>
+        * <li>The Sone of the post is not the given Sone, the given Sone does not
+        * follow the post’s Sone, and the given Sone is not the recipient of the
+        * post.</li>
+        * <li>The trust relationship between the two Sones can not be retrieved.</li>
+        * <li>The given Sone has explicitely assigned negative trust to the post’s
+        * Sone.</li>
+        * <li>The given Sone has not explicitely assigned negative trust to the
+        * post’s Sone but the implicit trust is negative.</li>
+        * </ul>
+        * If none of these statements is true the post is considered visible.
+        *
+        * @param sone
+        *              The Sone that checks for a post’s visibility (may be
+        *              {@code null} to skip Sone-specific checks, such as trust)
+        * @param post
+        *              The post to check for visibility
+        * @return {@code true} if the post is considered visible, {@code false}
+        * otherwise
+        */
+       boolean isPostVisible(@Nullable Sone sone, @Nonnull Post post) {
+               checkNotNull(post, "post must not be null");
+               if (!post.isLoaded()) {
+                       return false;
+               }
+               Sone postSone = post.getSone();
+               if (sone != null) {
+                       Trust trust = postSone.getIdentity().getTrust((OwnIdentity) sone.getIdentity());
+                       if (trust != null) {
+                               if ((trust.getExplicit() != null) && (trust.getExplicit() < 0)) {
+                                       return false;
+                               }
+                               if ((trust.getExplicit() == null) && (trust.getImplicit() != null) && (trust.getImplicit() < 0)) {
+                                       return false;
+                               }
+                       } else {
+                               /*
+                                * a null trust means that the trust updater has not yet
+                                * received a trust value for this relation. if we return false,
+                                * the post feed will stay empty until the trust updater has
+                                * received trust values. to prevent this we simply assume that
+                                * posts are visible if there is no trust.
+                                */
+                       }
+                       if ((!postSone.equals(sone)) && !sone.hasFriend(postSone.getId()) && !sone.getId().equals(post.getRecipientId().orNull())) {
+                               return false;
+                       }
+               }
+               return post.getTime() <= System.currentTimeMillis();
+       }
+
+       @Nonnull
+       public Predicate<Post> isVisible(@Nullable final Sone currentSone) {
+               return new Predicate<Post>() {
+                       @Nonnull
+                       @Override
+                       public boolean apply(@Nullable Post post) {
+                               return isPostVisible(currentSone, post);
+                       }
+               };
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/notify/ReplyVisibilityFilter.java b/src/main/java/net/pterodactylus/sone/notify/ReplyVisibilityFilter.java
new file mode 100644 (file)
index 0000000..a76ae25
--- /dev/null
@@ -0,0 +1,73 @@
+package net.pterodactylus.sone.notify;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.PostReply;
+import net.pterodactylus.sone.data.Sone;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+
+/**
+ * Filter that checks a {@link PostReply} for visibility.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+@Singleton
+public class ReplyVisibilityFilter {
+
+       private final PostVisibilityFilter postVisibilityFilter;
+
+       @Inject
+       public ReplyVisibilityFilter(@Nonnull PostVisibilityFilter postVisibilityFilter) {
+               this.postVisibilityFilter = postVisibilityFilter;
+       }
+
+       /**
+        * Checks whether a reply is visible to the given Sone. A reply is not
+        * considered visible if one of the following statements is true:
+        * <ul>
+        * <li>The reply does not have a post.</li>
+        * <li>The reply’s post {@link PostVisibilityFilter#isPostVisible(Sone, Post) is not visible}.</li>
+        * <li>The reply’s {@link PostReply#getTime() time} is in the future.</li>
+        * </ul>
+        * If none of these statements is true the reply is considered visible.
+        *
+        * @param sone
+        *              The Sone that checks for a post’s visibility (may be
+        *              {@code null} to skip Sone-specific checks, such as trust)
+        * @param reply
+        *              The reply to check for visibility
+        * @return {@code true} if the reply is considered visible, {@code false}
+        * otherwise
+        */
+       boolean isReplyVisible(@Nullable Sone sone, @Nonnull PostReply reply) {
+               checkNotNull(reply, "reply must not be null");
+               Optional<Post> post = reply.getPost();
+               if (!post.isPresent()) {
+                       return false;
+               }
+               if (!postVisibilityFilter.isPostVisible(sone, post.get())) {
+                       return false;
+               }
+               return reply.getTime() <= System.currentTimeMillis();
+       }
+
+       @Nonnull
+       public Predicate<PostReply> isVisible(@Nullable final Sone currentSone) {
+               return new Predicate<PostReply>() {
+                       @Nonnull
+                       @Override
+                       public boolean apply(@Nullable PostReply postReply) {
+                               return isReplyVisible(currentSone, postReply);
+                       }
+               };
+       }
+
+}
index d5bb6ea..24b6aaa 100644 (file)
@@ -22,6 +22,8 @@ import net.pterodactylus.util.notify.Notification;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
 
+import com.google.common.base.Optional;
+
 /**
  * Page that lets the user dismiss a notification.
  *
@@ -52,9 +54,9 @@ public class DismissNotificationPage extends SoneTemplatePage {
        protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                String notificationId = request.getHttpRequest().getPartAsStringFailsafe("notification", 36);
-               Notification notification = webInterface.getNotifications().getNotification(notificationId);
-               if ((notification != null) && notification.isDismissable()) {
-                       notification.dismiss();
+               Optional<Notification> notification = webInterface.getNotification(notificationId);
+               if (notification.isPresent() && notification.get().isDismissable()) {
+                       notification.get().dismiss();
                }
                String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
                throw new RedirectException(returnPage);
index c5e80c4..12d88db 100644 (file)
@@ -26,14 +26,13 @@ import java.util.List;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.notify.ListNotificationFilters;
+import net.pterodactylus.sone.notify.PostVisibilityFilter;
 import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.collection.Pagination;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
 
 import com.google.common.base.Optional;
-import com.google.common.base.Predicate;
 import com.google.common.collect.Collections2;
 
 /**
@@ -44,14 +43,11 @@ import com.google.common.collect.Collections2;
  */
 public class IndexPage extends SoneTemplatePage {
 
-       /**
-        * @param template
-        *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public IndexPage(Template template, WebInterface webInterface) {
+       private final PostVisibilityFilter postVisibilityFilter;
+
+       public IndexPage(Template template, WebInterface webInterface, PostVisibilityFilter postVisibilityFilter) {
                super("index.html", template, "Page.Index.Title", webInterface, true);
+               this.postVisibilityFilter = postVisibilityFilter;
        }
 
        //
@@ -81,14 +77,7 @@ public class IndexPage extends SoneTemplatePage {
                                }
                        }
                }
-               allPosts = Collections2.filter(allPosts, new Predicate<Post>() {
-
-                       @Override
-                       public boolean apply(Post post) {
-                               return ListNotificationFilters.isPostVisible(currentSone, post);
-                       }
-               });
-               allPosts = Collections2.filter(allPosts, Post.FUTURE_POSTS_FILTER);
+               allPosts = Collections2.filter(allPosts, postVisibilityFilter.isVisible(currentSone));
                List<Post> sortedPosts = new ArrayList<Post>(allPosts);
                Collections.sort(sortedPosts, Post.TIME_COMPARATOR);
                Pagination<Post> pagination = new Pagination<Post>(sortedPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("page"), 0));
index ee28a58..6aa07d9 100644 (file)
@@ -21,15 +21,12 @@ import static net.pterodactylus.sone.utils.NumberParsers.parseInt;
 
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
-
-import com.google.common.collect.Collections2;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.PostReply;
-import net.pterodactylus.sone.notify.ListNotificationFilters;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.notify.PostVisibilityFilter;
 import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.collection.Pagination;
 import net.pterodactylus.util.template.Template;
@@ -37,8 +34,7 @@ import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * Page that displays all new posts and replies. The posts are filtered using
- * {@link ListNotificationFilters#filterPosts(java.util.Collection, net.pterodactylus.sone.data.Sone)}
- * and sorted by time.
+ * {@link PostVisibilityFilter#isPostVisible(Sone, Post)} and sorted by time.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
@@ -68,17 +64,16 @@ public class NewPage extends SoneTemplatePage {
                super.processTemplate(request, templateContext);
 
                /* collect new elements from notifications. */
-               Set<Post> posts = new HashSet<Post>(webInterface.getNewPosts());
-               for (PostReply reply : Collections2.filter(webInterface.getNewReplies(), PostReply.HAS_POST_FILTER)) {
+               List<Post> posts = new ArrayList<Post>(webInterface.getNewPosts(getCurrentSone(request.getToadletContext(), false)));
+               for (PostReply reply : webInterface.getNewReplies(getCurrentSone(request.getToadletContext(), false))) {
                        posts.add(reply.getPost().get());
                }
 
                /* filter and sort them. */
-               List<Post> sortedPosts = ListNotificationFilters.filterPosts(posts, webInterface.getCurrentSone(request.getToadletContext(), false));
-               Collections.sort(sortedPosts, Post.TIME_COMPARATOR);
+               Collections.sort(posts, Post.TIME_COMPARATOR);
 
                /* paginate them. */
-               Pagination<Post> pagination = new Pagination<Post>(sortedPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("page"), 0));
+               Pagination<Post> pagination = new Pagination<Post>(posts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(parseInt(request.getHttpRequest().getParam("page"), 0));
                templateContext.set("pagination", pagination);
                templateContext.set("posts", pagination.getItems());
        }
index 15810ca..18836e9 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.web;
 
 import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -27,20 +28,19 @@ import java.util.Map;
 
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.main.SonePlugin;
-import net.pterodactylus.sone.notify.ListNotificationFilters;
 import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.sone.web.page.FreenetTemplatePage;
 import net.pterodactylus.util.notify.Notification;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
 
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-
 import freenet.clients.http.SessionManager.Session;
 import freenet.clients.http.ToadletContext;
 import freenet.support.api.HTTPRequest;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
 /**
  * Base page for the Sone web interface.
  *
@@ -65,21 +65,6 @@ public class SoneTemplatePage extends FreenetTemplatePage {
         *            The path of the page
         * @param template
         *            The template to render
-        * @param webInterface
-        *            The Sone web interface
-        */
-       public SoneTemplatePage(String path, Template template, WebInterface webInterface) {
-               this(path, template, null, webInterface, false);
-       }
-
-       /**
-        * Creates a new template page for Sone that does not require the user to be
-        * logged in.
-        *
-        * @param path
-        *            The path of the page
-        * @param template
-        *            The template to render
         * @param pageTitleKey
         *            The l10n key of the page title
         * @param webInterface
@@ -263,7 +248,7 @@ public class SoneTemplatePage extends FreenetTemplatePage {
                templateContext.set("latestEdition", webInterface.getCore().getUpdateChecker().getLatestEdition());
                templateContext.set("latestVersion", webInterface.getCore().getUpdateChecker().getLatestVersion());
                templateContext.set("latestVersionTime", webInterface.getCore().getUpdateChecker().getLatestVersionDate());
-               List<Notification> notifications = ListNotificationFilters.filterNotifications(webInterface.getNotifications().getNotifications(), currentSone);
+               List<Notification> notifications = new ArrayList<Notification>(webInterface.getNotifications(currentSone));
                Collections.sort(notifications, Notification.CREATED_TIME_SORTER);
                templateContext.set("notifications", notifications);
                templateContext.set("notificationHash", notifications.hashCode());
index 4b4ac88..4e4f6fa 100644 (file)
@@ -17,6 +17,7 @@
 
 package net.pterodactylus.sone.web;
 
+import static com.google.common.collect.FluentIterable.from;
 import static java.util.logging.Logger.getLogger;
 import static net.pterodactylus.util.template.TemplateParser.parse;
 
@@ -37,6 +38,8 @@ import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.logging.Logger;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.core.event.ImageInsertAbortedEvent;
@@ -72,6 +75,9 @@ import net.pterodactylus.sone.main.Loaders;
 import net.pterodactylus.sone.main.ReparseFilter;
 import net.pterodactylus.sone.main.SonePlugin;
 import net.pterodactylus.sone.notify.ListNotification;
+import net.pterodactylus.sone.notify.ListNotificationFilters;
+import net.pterodactylus.sone.notify.PostVisibilityFilter;
+import net.pterodactylus.sone.notify.ReplyVisibilityFilter;
 import net.pterodactylus.sone.template.AlbumAccessor;
 import net.pterodactylus.sone.template.CollectionAccessor;
 import net.pterodactylus.sone.template.CssClassNameFilter;
@@ -147,11 +153,6 @@ import net.pterodactylus.util.template.XmlFilter;
 import net.pterodactylus.util.web.RedirectPage;
 import net.pterodactylus.util.web.TemplatePage;
 
-import com.google.common.collect.Collections2;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.eventbus.Subscribe;
-import com.google.inject.Inject;
-
 import freenet.clients.http.SessionManager;
 import freenet.clients.http.SessionManager.Session;
 import freenet.clients.http.ToadletContainer;
@@ -159,6 +160,12 @@ import freenet.clients.http.ToadletContext;
 import freenet.l10n.BaseL10n;
 import freenet.support.api.HTTPRequest;
 
+import com.google.common.base.Optional;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.eventbus.Subscribe;
+import com.google.inject.Inject;
+
 /**
  * Bundles functionality that a web interface of a Freenet plugin needs, e.g.
  * references to l10n helpers.
@@ -194,6 +201,10 @@ public class WebInterface {
        /** The parser filter. */
        private final ParserFilter parserFilter;
 
+       private final ListNotificationFilters listNotificationFilters;
+       private final PostVisibilityFilter postVisibilityFilter;
+       private final ReplyVisibilityFilter replyVisibilityFilter;
+
        /** The “new Sone” notification. */
        private final ListNotification<Sone> newSoneNotification;
 
@@ -243,9 +254,12 @@ public class WebInterface {
         *            The Sone plugin
         */
        @Inject
-       public WebInterface(SonePlugin sonePlugin, Loaders loaders) {
+       public WebInterface(SonePlugin sonePlugin, Loaders loaders, ListNotificationFilters listNotificationFilters, PostVisibilityFilter postVisibilityFilter, ReplyVisibilityFilter replyVisibilityFilter) {
                this.sonePlugin = sonePlugin;
                this.loaders = loaders;
+               this.listNotificationFilters = listNotificationFilters;
+               this.postVisibilityFilter = postVisibilityFilter;
+               this.replyVisibilityFilter = replyVisibilityFilter;
                formPassword = sonePlugin.pluginRespirator().getToadletContainer().getFormPassword();
                soneTextParser = new SoneTextParser(getCore(), getCore());
 
@@ -454,6 +468,16 @@ public class WebInterface {
                return notificationManager;
        }
 
+       @Nonnull
+       public Optional<Notification> getNotification(@Nonnull String notificationId) {
+               return Optional.fromNullable(notificationManager.getNotification(notificationId));
+       }
+
+       @Nonnull
+       public Collection<Notification> getNotifications(@Nullable Sone currentSone) {
+               return listNotificationFilters.filterNotifications(notificationManager.getNotifications(), currentSone);
+       }
+
        /**
         * Returns the l10n helper of the node.
         *
@@ -491,6 +515,15 @@ public class WebInterface {
                return ImmutableSet.<Post> builder().addAll(newPostNotification.getElements()).addAll(localPostNotification.getElements()).build();
        }
 
+       @Nonnull
+       public Collection<Post> getNewPosts(@Nullable Sone currentSone) {
+               Set<Post> allNewPosts = ImmutableSet.<Post> builder()
+                               .addAll(newPostNotification.getElements())
+                               .addAll(localPostNotification.getElements())
+                               .build();
+               return from(allNewPosts).filter(postVisibilityFilter.isVisible(currentSone)).toSet();
+       }
+
        /**
         * Returns the replies that have been announced as new in the
         * {@link #newReplyNotification}.
@@ -501,6 +534,15 @@ public class WebInterface {
                return ImmutableSet.<PostReply> builder().addAll(newReplyNotification.getElements()).addAll(localReplyNotification.getElements()).build();
        }
 
+       @Nonnull
+       public Collection<PostReply> getNewReplies(@Nullable Sone currentSone) {
+               Set<PostReply> allNewReplies = ImmutableSet.<PostReply>builder()
+                               .addAll(newReplyNotification.getElements())
+                               .addAll(localReplyNotification.getElements())
+                               .build();
+               return from(allNewReplies).filter(replyVisibilityFilter.isVisible(currentSone)).toSet();
+       }
+
        /**
         * Sets whether the current start of the plugin is the first start. It is
         * considered a first start if the configuration file does not exist.
@@ -638,7 +680,7 @@ public class WebInterface {
 
                PageToadletFactory pageToadletFactory = new PageToadletFactory(sonePlugin.pluginRespirator().getHLSimpleClient(), "/Sone/");
                pageToadlets.add(pageToadletFactory.createPageToadlet(new RedirectPage<FreenetRequest>("", "index.html")));
-               pageToadlets.add(pageToadletFactory.createPageToadlet(new IndexPage(indexTemplate, this), "Index"));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new IndexPage(indexTemplate, this, postVisibilityFilter), "Index"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new NewPage(newTemplate, this), "New"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateSonePage(createSoneTemplate, this), "CreateSone"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new KnownSonesPage(knownSonesTemplate, this), "KnownSones"));
index 4e731e8..7ee2005 100644 (file)
@@ -21,6 +21,8 @@ import net.pterodactylus.sone.web.WebInterface;
 import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.notify.Notification;
 
+import com.google.common.base.Optional;
+
 /**
  * AJAX page that lets the user dismiss a notification.
  *
@@ -44,14 +46,14 @@ public class DismissNotificationAjaxPage extends JsonPage {
        @Override
        protected JsonReturnObject createJsonObject(FreenetRequest request) {
                String notificationId = request.getHttpRequest().getParam("notification");
-               Notification notification = webInterface.getNotifications().getNotification(notificationId);
-               if (notification == null) {
+               Optional<Notification> notification = webInterface.getNotification(notificationId);
+               if (!notification.isPresent()) {
                        return createErrorJsonObject("invalid-notification-id");
                }
-               if (!notification.isDismissable()) {
+               if (!notification.get().isDismissable()) {
                        return createErrorJsonObject("not-dismissable");
                }
-               notification.dismiss();
+               notification.get().dismiss();
                return createSuccessJsonObject();
        }
 
index ee4683f..512a954 100644 (file)
@@ -21,13 +21,12 @@ import static com.fasterxml.jackson.databind.node.JsonNodeFactory.instance;
 
 import java.io.IOException;
 import java.io.StringWriter;
-import java.util.Collection;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.main.SonePlugin;
-import net.pterodactylus.sone.notify.ListNotificationFilters;
 import net.pterodactylus.sone.web.WebInterface;
 import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.notify.Notification;
@@ -81,14 +80,13 @@ public class GetNotificationsAjaxPage extends JsonPage {
        @Override
        protected JsonReturnObject createJsonObject(FreenetRequest request) {
                Sone currentSone = getCurrentSone(request.getToadletContext(), false);
-               Collection<Notification> notifications = webInterface.getNotifications().getNotifications();
-               List<Notification> filteredNotifications = ListNotificationFilters.filterNotifications(notifications, currentSone);
-               Collections.sort(filteredNotifications, Notification.CREATED_TIME_SORTER);
+               List<Notification> notifications = new ArrayList<Notification>(webInterface.getNotifications(currentSone));
+               Collections.sort(notifications, Notification.CREATED_TIME_SORTER);
                ArrayNode jsonNotifications = new ArrayNode(instance);
-               for (Notification notification : filteredNotifications) {
+               for (Notification notification : notifications) {
                        jsonNotifications.add(createJsonNotification(request, notification));
                }
-               return createSuccessJsonObject().put("notificationHash", filteredNotifications.hashCode()).put("notifications", jsonNotifications).put("options", createJsonOptions(currentSone));
+               return createSuccessJsonObject().put("notificationHash", notifications.hashCode()).put("notifications", jsonNotifications).put("options", createJsonOptions(currentSone));
        }
 
        //
index 6c5c0de..afba58f 100644 (file)
@@ -21,6 +21,7 @@ import static com.fasterxml.jackson.databind.node.JsonNodeFactory.instance;
 
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
@@ -31,7 +32,8 @@ import java.util.Set;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.PostReply;
 import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.notify.ListNotificationFilters;
+import net.pterodactylus.sone.notify.PostVisibilityFilter;
+import net.pterodactylus.sone.notify.ReplyVisibilityFilter;
 import net.pterodactylus.sone.template.SoneAccessor;
 import net.pterodactylus.sone.web.WebInterface;
 import net.pterodactylus.sone.web.page.FreenetRequest;
@@ -40,8 +42,6 @@ import net.pterodactylus.util.notify.Notification;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.google.common.base.Predicate;
-import com.google.common.collect.Collections2;
 
 /**
  * The “get status” AJAX handler returns all information that is necessary to
@@ -88,20 +88,11 @@ public class GetStatusAjaxPage extends JsonPage {
                        jsonSones.add(createJsonSone(sone));
                }
                /* load notifications. */
-               List<Notification> notifications = ListNotificationFilters.filterNotifications(webInterface.getNotifications().getNotifications(), currentSone);
+               List<Notification> notifications = new ArrayList<Notification>(webInterface.getNotifications(currentSone));
                Collections.sort(notifications, Notification.CREATED_TIME_SORTER);
                /* load new posts. */
-               Collection<Post> newPosts = webInterface.getNewPosts();
-               if (currentSone != null) {
-                       newPosts = Collections2.filter(newPosts, new Predicate<Post>() {
-
-                               @Override
-                               public boolean apply(Post post) {
-                                       return ListNotificationFilters.isPostVisible(currentSone, post);
-                               }
+               Collection<Post> newPosts = webInterface.getNewPosts(getCurrentSone(request.getToadletContext(), false));
 
-                       });
-               }
                ArrayNode jsonPosts = new ArrayNode(instance);
                for (Post post : newPosts) {
                        ObjectNode jsonPost = new ObjectNode(instance);
@@ -112,19 +103,8 @@ public class GetStatusAjaxPage extends JsonPage {
                        jsonPosts.add(jsonPost);
                }
                /* load new replies. */
-               Collection<PostReply> newReplies = webInterface.getNewReplies();
-               if (currentSone != null) {
-                       newReplies = Collections2.filter(newReplies, new Predicate<PostReply>() {
-
-                               @Override
-                               public boolean apply(PostReply reply) {
-                                       return ListNotificationFilters.isReplyVisible(currentSone, reply);
-                               }
+               Collection<PostReply> newReplies = webInterface.getNewReplies(getCurrentSone(request.getToadletContext(), false));
 
-                       });
-               }
-               /* remove replies to unknown posts. */
-               newReplies = Collections2.filter(newReplies, PostReply.HAS_POST_FILTER);
                ArrayNode jsonReplies = new ArrayNode(instance);
                for (PostReply reply : newReplies) {
                        ObjectNode jsonReply = new ObjectNode(instance);
diff --git a/src/test/java/net/pterodactylus/sone/notify/ListNotificationFiltersTest.java b/src/test/java/net/pterodactylus/sone/notify/ListNotificationFiltersTest.java
new file mode 100644 (file)
index 0000000..f6b7da5
--- /dev/null
@@ -0,0 +1,281 @@
+package net.pterodactylus.sone.notify;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.emptyIterable;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.PostReply;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.data.SoneOptions;
+import net.pterodactylus.sone.freenet.wot.OwnIdentity;
+import net.pterodactylus.util.notify.Notification;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import org.hamcrest.Matchers;
+import org.junit.Test;
+
+/**
+ * Unit test for {@link ListNotificationFiltersTest}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ListNotificationFiltersTest {
+
+       private static final String LOCAL_ID = "local-id";
+
+       private final PostVisibilityFilter postVisibilityFilter = mock(PostVisibilityFilter.class);
+       private final ReplyVisibilityFilter replyVisibilityFilter = mock(ReplyVisibilityFilter.class);
+       private final ListNotificationFilters listNotificationFilters = new ListNotificationFilters(postVisibilityFilter, replyVisibilityFilter);
+
+       private final Sone localSone = mock(Sone.class);
+       private final SoneOptions soneOptions = mock(SoneOptions.class);
+       private final OwnIdentity localIdentity = mock(OwnIdentity.class);
+       private final List<ListNotification<Post>> newPostNotifications = Arrays.asList(createNewPostNotification());
+       private final List<ListNotification<PostReply>> newReplyNotifications = Arrays.asList(createNewReplyNotification());
+       private final List<ListNotification<Post>> mentionNotifications = Arrays.asList(createMentionNotification());
+
+       public ListNotificationFiltersTest() {
+               when(localSone.getId()).thenReturn(LOCAL_ID);
+               when(localSone.isLocal()).thenReturn(true);
+               when(localSone.getIdentity()).thenReturn(localIdentity);
+               when(localIdentity.getId()).thenReturn(LOCAL_ID);
+               when(localSone.getOptions()).thenReturn(soneOptions);
+       }
+
+       @Test
+       public void filterIsOnlyCreatedOnce() {
+           Injector injector = Guice.createInjector();
+               ListNotificationFilters firstFilter = injector.getInstance(ListNotificationFilters.class);
+               ListNotificationFilters secondFilter = injector.getInstance(ListNotificationFilters.class);
+               assertThat(firstFilter, sameInstance(secondFilter));
+       }
+
+       @Test
+       public void newSoneNotificationsAreNotRemovedIfNotLoggedIn() {
+               List<Notification> notifications = Arrays.asList(createNewSoneNotification());
+               List<Notification> filteredNotifications = listNotificationFilters.filterNotifications(notifications, null);
+               assertThat(filteredNotifications, contains(notifications.get(0)));
+       }
+
+       private Notification createNewSoneNotification() {
+               ListNotification<Sone> newSoneNotification = mock(ListNotification.class);
+               when(newSoneNotification.getId()).thenReturn("new-sone-notification");
+               return newSoneNotification;
+       }
+
+       @Test
+       public void newSoneNotificationsAreRemovedIfLoggedInAndNewSonesShouldNotBeShown() {
+               List<Notification> notifications = Arrays.asList(createNewSoneNotification());
+               List<Notification> filteredNotifications = listNotificationFilters.filterNotifications(notifications, localSone);
+               assertThat(filteredNotifications, emptyIterable());
+       }
+
+       @Test
+       public void newSoneNotificationsAreNotRemovedIfLoggedInAndNewSonesShouldBeShown() {
+               List<Notification> notifications = Arrays.asList(createNewSoneNotification());
+               when(soneOptions.isShowNewSoneNotifications()).thenReturn(true);
+               List<Notification> filteredNotifications = listNotificationFilters.filterNotifications(notifications, localSone);
+               assertThat(filteredNotifications, contains(notifications.get(0)));
+       }
+
+       private ListNotification<Post> createNewPostNotification() {
+               ListNotification<Post> newSoneNotification = mock(ListNotification.class);
+               when(newSoneNotification.getElements()).thenReturn(new ArrayList<Post>());
+               when(newSoneNotification.getId()).thenReturn("new-post-notification");
+               return newSoneNotification;
+       }
+
+       @Test
+       public void newPostNotificationIsNotShownIfOptionsSetAccordingly() {
+               List<ListNotification<Post>> notifications = Arrays.asList(createNewPostNotification());
+               List<Notification> filteredNotifications = listNotificationFilters.filterNotifications(notifications, localSone);
+               assertThat(filteredNotifications, hasSize(0));
+       }
+
+       private void activateNewPostNotifications() {
+               when(soneOptions.isShowNewPostNotifications()).thenReturn(true);
+       }
+
+       private boolean addPostToPostNotification(List<ListNotification<Post>> notifications) {
+               return notifications.get(0).getElements().add(mock(Post.class));
+       }
+
+       private void setPostVisibilityPredicate(Predicate<Post> value) {
+               when(postVisibilityFilter.isVisible(any(Sone.class))).thenReturn(value);
+       }
+
+       @Test
+       public void newPostNotificationIsNotShownIfNoNewPostsAreVisible() {
+               activateNewPostNotifications();
+               addPostToPostNotification(newPostNotifications);
+               setPostVisibilityPredicate(Predicates.<Post>alwaysFalse());
+               List<Notification> filteredNotifications = listNotificationFilters.filterNotifications(newPostNotifications, localSone);
+               assertThat(filteredNotifications, hasSize(0));
+       }
+
+       @Test
+       public void newPostNotificationIsShownIfNewPostsAreVisible() {
+               activateNewPostNotifications();
+               addPostToPostNotification(newPostNotifications);
+               setPostVisibilityPredicate(Predicates.<Post>alwaysTrue());
+               List<Notification> filteredNotifications = listNotificationFilters.filterNotifications(newPostNotifications, localSone);
+               assertThat(filteredNotifications, contains((Notification) newPostNotifications.get(0)));
+       }
+
+       @Test
+       public void newPostNotificationIsNotShownIfNewPostsAreVisibleButLocalSoneIsNull() {
+               activateNewPostNotifications();
+               addPostToPostNotification(newPostNotifications);
+               setPostVisibilityPredicate(Predicates.<Post>alwaysTrue());
+               List<Notification> filteredNotifications = listNotificationFilters.filterNotifications(newPostNotifications, null);
+               assertThat(filteredNotifications, Matchers.<Notification>emptyIterable());
+       }
+
+       @Test
+       public void newPostNotificationContainsOnlyVisiblePosts() {
+               activateNewPostNotifications();
+               addPostToPostNotification(newPostNotifications);
+               addPostToPostNotification(newPostNotifications);
+               setPostVisibilityPredicate(new Predicate<Post>() {
+                       @Override
+                       public boolean apply(@Nullable Post post) {
+                               return post.equals(newPostNotifications.get(0).getElements().get(1));
+                       }
+               });
+               List<Notification> filteredNotifications = listNotificationFilters.filterNotifications(newPostNotifications, localSone);
+               assertThat(filteredNotifications, hasSize(1));
+               assertThat(((ListNotification<Post>) filteredNotifications.get(0)).getElements().get(0), is(newPostNotifications.get(0).getElements().get(1)));
+       }
+
+       private ListNotification<PostReply> createNewReplyNotification() {
+               ListNotification<PostReply> newReplyNotifications = mock(ListNotification.class);
+               when(newReplyNotifications.getElements()).thenReturn(new ArrayList<PostReply>());
+               when(newReplyNotifications.getId()).thenReturn("new-reply-notification");
+               return newReplyNotifications;
+       }
+
+       private void activateNewReplyNotifications() {
+               when(soneOptions.isShowNewReplyNotifications()).thenReturn(true);
+       }
+
+       private void addReplyToNewReplyNotification(List<ListNotification<PostReply>> notifications) {
+               notifications.get(0).getElements().add(mock(PostReply.class));
+       }
+
+       private void setReplyVisibilityPredicate(Predicate<PostReply> value) {
+               when(replyVisibilityFilter.isVisible(any(Sone.class))).thenReturn(value);
+       }
+
+       @Test
+       public void newReplyNotificationContainsOnlyVisibleReplies() {
+               activateNewReplyNotifications();
+               addReplyToNewReplyNotification(newReplyNotifications);
+               addReplyToNewReplyNotification(newReplyNotifications);
+               setReplyVisibilityPredicate(new Predicate<PostReply>() {
+                       @Override
+                       public boolean apply(@Nullable PostReply postReply) {
+                               return postReply.equals(newReplyNotifications.get(0).getElements().get(1));
+                       }
+               });
+               List<Notification> filteredNotifications = listNotificationFilters.filterNotifications(newReplyNotifications, localSone);
+               assertThat(filteredNotifications, hasSize(1));
+               assertThat(((ListNotification<PostReply>) filteredNotifications.get(0)).getElements().get(0), is(newReplyNotifications.get(0).getElements().get(1)));
+       }
+
+       @Test
+       public void newReplyNotificationIsNotModifiedIfAllRepliesAreVisible() {
+               activateNewReplyNotifications();
+               addReplyToNewReplyNotification(newReplyNotifications);
+               addReplyToNewReplyNotification(newReplyNotifications);
+               setReplyVisibilityPredicate(Predicates.<PostReply>alwaysTrue());
+               List<Notification> filteredNotifications = listNotificationFilters.filterNotifications(newReplyNotifications, localSone);
+               assertThat(filteredNotifications, hasSize(1));
+               assertThat(filteredNotifications.get(0), is((Notification) newReplyNotifications.get(0)));
+               assertThat(((ListNotification<PostReply>) filteredNotifications.get(0)).getElements(), hasSize(2));
+       }
+
+       @Test
+       public void newReplyNotificationIsNotShownIfNoRepliesAreVisible() {
+               activateNewReplyNotifications();
+               addReplyToNewReplyNotification(newReplyNotifications);
+               addReplyToNewReplyNotification(newReplyNotifications);
+               setReplyVisibilityPredicate(Predicates.<PostReply>alwaysFalse());
+               List<Notification> filteredNotifications = listNotificationFilters.filterNotifications(newReplyNotifications, localSone);
+               assertThat(filteredNotifications, hasSize(0));
+       }
+
+       @Test
+       public void newReplyNotificationIsNotShownIfDeactivatedInOptions() {
+               addReplyToNewReplyNotification(newReplyNotifications);
+               addReplyToNewReplyNotification(newReplyNotifications);
+               setReplyVisibilityPredicate(Predicates.<PostReply>alwaysTrue());
+               List<Notification> filteredNotifications = listNotificationFilters.filterNotifications(newReplyNotifications, localSone);
+               assertThat(filteredNotifications, hasSize(0));
+       }
+
+       @Test
+       public void newReplyNotificationIsNotShownIfCurrentSoneIsNull() {
+               addReplyToNewReplyNotification(newReplyNotifications);
+               addReplyToNewReplyNotification(newReplyNotifications);
+               setReplyVisibilityPredicate(Predicates.<PostReply>alwaysTrue());
+               List<Notification> filteredNotifications = listNotificationFilters.filterNotifications(newReplyNotifications, null);
+               assertThat(filteredNotifications, hasSize(0));
+       }
+
+       private ListNotification<Post> createMentionNotification() {
+               ListNotification<Post> newSoneNotification = mock(ListNotification.class);
+               when(newSoneNotification.getElements()).thenReturn(new ArrayList<Post>());
+               when(newSoneNotification.getId()).thenReturn("mention-notification");
+               return newSoneNotification;
+       }
+
+       @Test
+       public void mentionNotificationContainsOnlyVisiblePosts() {
+               addPostToPostNotification(mentionNotifications);
+               addPostToPostNotification(mentionNotifications);
+               setPostVisibilityPredicate(new Predicate<Post>() {
+                       @Override
+                       public boolean apply(@Nullable Post post) {
+                               return post.equals(mentionNotifications.get(0).getElements().get(1));
+                       }
+               });
+               List<Notification> filteredNotifications = listNotificationFilters.filterNotifications(mentionNotifications, localSone);
+               assertThat(filteredNotifications, hasSize(1));
+               assertThat(((ListNotification<Post>) filteredNotifications.get(0)).getElements().get(0), is(mentionNotifications.get(0).getElements().get(1)));
+       }
+
+       @Test
+       public void mentionNotificationIsNotShownIfNoPostsAreVisible() {
+               addPostToPostNotification(mentionNotifications);
+               addPostToPostNotification(mentionNotifications);
+               setPostVisibilityPredicate(Predicates.<Post>alwaysFalse());
+               List<Notification> filteredNotifications = listNotificationFilters.filterNotifications(mentionNotifications, localSone);
+               assertThat(filteredNotifications, hasSize(0));
+       }
+
+       @Test
+       public void unfilterableNotificationIsNotFiltered() {
+               Notification notification = mock(Notification.class);
+               when(notification.getId()).thenReturn("random-notification");
+               List<Notification> notifications = Arrays.asList(notification);
+               List<Notification> filteredNotifications = listNotificationFilters.filterNotifications(notifications, null);
+               assertThat(filteredNotifications, contains(notification));
+       }
+
+}
diff --git a/src/test/java/net/pterodactylus/sone/notify/PostVisibilityFilterTest.java b/src/test/java/net/pterodactylus/sone/notify/PostVisibilityFilterTest.java
new file mode 100644 (file)
index 0000000..d49e59b
--- /dev/null
@@ -0,0 +1,203 @@
+package net.pterodactylus.sone.notify;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.wot.Identity;
+import net.pterodactylus.sone.freenet.wot.OwnIdentity;
+import net.pterodactylus.sone.freenet.wot.Trust;
+
+import com.google.common.base.Optional;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import org.junit.Test;
+
+/**
+ * Unit test for {@link PostVisibilityFilterTest}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class PostVisibilityFilterTest {
+
+       private static final String LOCAL_ID = "local-id";
+       private static final String REMOTE_ID = "remote-id";
+
+       private final PostVisibilityFilter postVisibilityFilter = new PostVisibilityFilter();
+
+       private final Sone localSone = mock(Sone.class);
+       private final OwnIdentity localIdentity = mock(OwnIdentity.class);
+       private final Post post = mock(Post.class);
+       private final Sone remoteSone = mock(Sone.class);
+       private final Identity remoteIdentity = mock(Identity.class);
+
+       public PostVisibilityFilterTest() {
+               when(localSone.getId()).thenReturn(LOCAL_ID);
+               when(localSone.isLocal()).thenReturn(true);
+               when(localSone.getIdentity()).thenReturn(localIdentity);
+               when(localIdentity.getId()).thenReturn(LOCAL_ID);
+               when(remoteSone.getId()).thenReturn(REMOTE_ID);
+               when(remoteSone.getIdentity()).thenReturn(remoteIdentity);
+               when(remoteIdentity.getId()).thenReturn(REMOTE_ID);
+               when(post.getRecipientId()).thenReturn(Optional.<String>absent());
+       }
+
+       @Test
+       public void postVisibilityFilterIsOnlyCreatedOnce() {
+               Injector injector = Guice.createInjector();
+               PostVisibilityFilter firstFilter = injector.getInstance(PostVisibilityFilter.class);
+               PostVisibilityFilter secondFilter = injector.getInstance(PostVisibilityFilter.class);
+               assertThat(firstFilter, sameInstance(secondFilter));
+       }
+
+       @Test
+       public void postIsNotVisibleIfItIsNotLoaded() {
+               assertThat(postVisibilityFilter.isPostVisible(localSone, post), is(false));
+       }
+
+       private static void makePostLoaded(Post post) {
+               when(post.isLoaded()).thenReturn(true);
+       }
+
+       @Test
+       public void loadedPostIsVisibleWithoutSone() {
+               makePostLoaded(post);
+               assertThat(postVisibilityFilter.isPostVisible(null, post), is(true));
+       }
+
+       private void makePostComeFromTheFuture() {
+               when(post.getTime()).thenReturn(System.currentTimeMillis() + 1000);
+       }
+
+       @Test
+       public void loadedPostFromTheFutureIsNotVisible() {
+               makePostLoaded(post);
+               makePostComeFromTheFuture();
+               assertThat(postVisibilityFilter.isPostVisible(null, post), is(false));
+       }
+
+       private void makePostFromRemoteSone() {
+               when(post.getSone()).thenReturn(remoteSone);
+       }
+
+       private void giveRemoteIdentityNegativeExplicitTrust() {
+               when(remoteIdentity.getTrust(localIdentity)).thenReturn(new Trust(-1, null, null));
+       }
+
+       @Test
+       public void loadedPostFromExplicitelyNotTrustedSoneIsNotVisible() {
+               makePostLoaded(post);
+               makePostFromRemoteSone();
+               giveRemoteIdentityNegativeExplicitTrust();
+               assertThat(postVisibilityFilter.isPostVisible(localSone, post), is(false));
+       }
+
+       private void giveRemoteIdentityNegativeImplicitTrust() {
+               when(remoteIdentity.getTrust(localIdentity)).thenReturn(new Trust(null, -1, null));
+       }
+
+       @Test
+       public void loadedPostFromImplicitelyUntrustedSoneIsNotVisible() {
+               makePostLoaded(post);
+               makePostFromRemoteSone();
+               giveRemoteIdentityNegativeImplicitTrust();
+               assertThat(postVisibilityFilter.isPostVisible(localSone, post), is(false));
+       }
+
+       private void makeLocalSoneFollowRemoteSone() {
+               when(localSone.hasFriend(REMOTE_ID)).thenReturn(true);
+       }
+
+       private void giveRemoteIdentityPositiveExplicitTrustButNegativeImplicitTrust() {
+               when(remoteIdentity.getTrust(localIdentity)).thenReturn(new Trust(1, -1, null));
+       }
+
+       @Test
+       public void loadedPostFromExplicitelyTrustedButImplicitelyUntrustedSoneIsVisible() {
+               makePostLoaded(post);
+               makePostFromRemoteSone();
+               makeLocalSoneFollowRemoteSone();
+               giveRemoteIdentityPositiveExplicitTrustButNegativeImplicitTrust();
+               assertThat(postVisibilityFilter.isPostVisible(localSone, post), is(true));
+       }
+
+       private void giveTheRemoteIdentityPositiveImplicitTrust() {
+               when(remoteIdentity.getTrust(localIdentity)).thenReturn(new Trust(null, 1, null));
+       }
+
+       @Test
+       public void loadedPostFromImplicitelyTrustedSoneIsVisible() {
+               makePostLoaded(post);
+               makePostFromRemoteSone();
+               makeLocalSoneFollowRemoteSone();
+               giveTheRemoteIdentityPositiveImplicitTrust();
+               assertThat(postVisibilityFilter.isPostVisible(localSone, post), is(true));
+       }
+
+       private void giveTheRemoteIdentityUnknownTrust() {
+               when(remoteIdentity.getTrust(localIdentity)).thenReturn(new Trust(null, null, null));
+       }
+
+       @Test
+       public void loadedPostFromSoneWithUnknownTrustIsVisible() {
+               makePostLoaded(post);
+               makePostFromRemoteSone();
+               makeLocalSoneFollowRemoteSone();
+               giveTheRemoteIdentityUnknownTrust();
+               assertThat(postVisibilityFilter.isPostVisible(localSone, post), is(true));
+       }
+
+       @Test
+       public void loadedPostFromUnfollowedRemoteSoneThatIsNotDirectedAtLocalSoneIsNotVisible() {
+               makePostLoaded(post);
+               makePostFromRemoteSone();
+               assertThat(postVisibilityFilter.isPostVisible(localSone, post), is(false));
+       }
+
+       private void makePostFromLocalSone() {
+               makePostLoaded(post);
+               when(post.getSone()).thenReturn(localSone);
+       }
+
+       @Test
+       public void loadedPostFromLocalSoneIsVisible() {
+               makePostFromLocalSone();
+               assertThat(postVisibilityFilter.isPostVisible(localSone, post), is(true));
+       }
+
+       @Test
+       public void loadedPostFromFollowedRemoteSoneThatIsNotDirectedAtLocalSoneIsVisible() {
+               makePostLoaded(post);
+               makePostFromRemoteSone();
+               makeLocalSoneFollowRemoteSone();
+               assertThat(postVisibilityFilter.isPostVisible(localSone, post), is(true));
+       }
+
+       private void makePostDirectedAtLocalId() {
+               when(post.getRecipientId()).thenReturn(Optional.of(LOCAL_ID));
+       }
+
+       @Test
+       public void loadedPostFromRemoteSoneThatIsDirectedAtLocalSoneIsVisible() {
+               makePostLoaded(post);
+               makePostFromRemoteSone();
+               makePostDirectedAtLocalId();
+               assertThat(postVisibilityFilter.isPostVisible(localSone, post), is(true));
+       }
+
+       @Test
+       public void predicateWillCorrectlyRecognizeVisiblePost() {
+               makePostFromLocalSone();
+               assertThat(postVisibilityFilter.isVisible(null).apply(post), is(true));
+       }
+
+       @Test
+       public void predicateWillCorrectlyRecognizeNotVisiblePost() {
+               assertThat(postVisibilityFilter.isVisible(null).apply(post), is(false));
+       }
+
+}
diff --git a/src/test/java/net/pterodactylus/sone/notify/ReplyVisibilityFilterTest.java b/src/test/java/net/pterodactylus/sone/notify/ReplyVisibilityFilterTest.java
new file mode 100644 (file)
index 0000000..7187847
--- /dev/null
@@ -0,0 +1,106 @@
+package net.pterodactylus.sone.notify;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.PostReply;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.wot.OwnIdentity;
+
+import com.google.common.base.Optional;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import org.junit.Test;
+
+/**
+ * Unit test for {@link ReplyVisibilityFilterTest}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ReplyVisibilityFilterTest {
+
+       private static final String LOCAL_ID = "local-id";
+
+       private final PostVisibilityFilter postVisibilityFilter = mock(PostVisibilityFilter.class);
+       private final ReplyVisibilityFilter replyVisibilityFilter = new ReplyVisibilityFilter(postVisibilityFilter);
+
+       private final Sone localSone = mock(Sone.class);
+       private final OwnIdentity localIdentity = mock(OwnIdentity.class);
+       private final Post post = mock(Post.class);
+       private final PostReply postReply = mock(PostReply.class);
+
+       public ReplyVisibilityFilterTest() {
+               when(localSone.getId()).thenReturn(LOCAL_ID);
+               when(localSone.isLocal()).thenReturn(true);
+               when(localSone.getIdentity()).thenReturn(localIdentity);
+               when(post.getRecipientId()).thenReturn(Optional.<String>absent());
+       }
+
+       @Test
+       public void replyVisibilityFilterIsOnlyCreatedOnce() {
+               Injector injector = Guice.createInjector();
+               ReplyVisibilityFilter firstFilter = injector.getInstance(ReplyVisibilityFilter.class);
+               ReplyVisibilityFilter secondFilter = injector.getInstance(ReplyVisibilityFilter.class);
+               assertThat(firstFilter, sameInstance(secondFilter));
+       }
+
+       private void makePostPresent() {
+               when(postReply.getPost()).thenReturn(Optional.of(post));
+       }
+
+       @Test
+       public void replyIsNotVisibleIfPostIsNotVisible() {
+               makePostPresent();
+               assertThat(replyVisibilityFilter.isReplyVisible(localSone, postReply), is(false));
+       }
+
+       private void makePostAbsent() {
+               when(postReply.getPost()).thenReturn(Optional.<Post>absent());
+       }
+
+       @Test
+       public void replyIsNotVisibleIfPostIsNotPresent() {
+               makePostAbsent();
+               assertThat(replyVisibilityFilter.isReplyVisible(localSone, postReply), is(false));
+       }
+
+       private void makePostPresentAndVisible() {
+               makePostPresent();
+               when(postVisibilityFilter.isPostVisible(localSone, post)).thenReturn(true);
+       }
+
+       private void makeReplyComeFromFuture() {
+               when(postReply.getTime()).thenReturn(System.currentTimeMillis() + 1000);
+       }
+
+       @Test
+       public void replyIsNotVisibleIfItIsFromTheFuture() {
+               makePostPresentAndVisible();
+               makeReplyComeFromFuture();
+               assertThat(replyVisibilityFilter.isReplyVisible(localSone, postReply), is(false));
+       }
+
+       @Test
+       public void replyIsVisibleIfItIsNotFromTheFuture() {
+               makePostPresentAndVisible();
+               assertThat(replyVisibilityFilter.isReplyVisible(localSone, postReply), is(true));
+       }
+
+       @Test
+       public void predicateCorrectlyRecognizesVisibleReply() {
+               makePostPresentAndVisible();
+               assertThat(replyVisibilityFilter.isVisible(localSone).apply(postReply), is(true));
+       }
+
+       @Test
+       public void predicateCorrectlyRecognizesNotVisibleReply() {
+               makePostPresentAndVisible();
+               makeReplyComeFromFuture();
+               assertThat(replyVisibilityFilter.isVisible(localSone).apply(postReply), is(false));
+       }
+
+}