From: David ‘Bombe’ Roden Date: Sun, 10 Apr 2011 19:06:23 +0000 (+0200) Subject: Merge branch 'release-0.6.1' X-Git-Tag: 0.6.1^0 X-Git-Url: https://git.pterodactylus.net/?p=Sone.git;a=commitdiff_plain;h=58eeba9b521b0a0094ac90a37fd88811c6a27376;hp=ee05a67b3f1e796b6c4bdcd709ef0983103c455f Merge branch 'release-0.6.1' --- diff --git a/pom.xml b/pom.xml index 690bb17..33fd6be 100644 --- a/pom.xml +++ b/pom.xml @@ -2,12 +2,12 @@ 4.0.0 net.pterodactylus sone - 0.6 + 0.6.1 net.pterodactylus utils - 0.9.2 + 0.9.3 junit diff --git a/src/main/java/net/pterodactylus/sone/core/Core.java b/src/main/java/net/pterodactylus/sone/core/Core.java index c986d71..1fe843e 100644 --- a/src/main/java/net/pterodactylus/sone/core/Core.java +++ b/src/main/java/net/pterodactylus/sone/core/Core.java @@ -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 getDirectedPosts(Sone recipient) { + Validation.begin().isNotNull("Recipient", recipient).check(); + Set directedPosts = new HashSet(); + 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(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(10)); options.addIntegerOption("PositiveTrust", new DefaultOption(75)); - options.addIntegerOption("NegativeTrust", new DefaultOption(-100)); + options.addIntegerOption("NegativeTrust", new DefaultOption(-25)); options.addStringOption("TrustComment", new DefaultOption("Set from Sone Web Interface")); options.addBooleanOption("SoneRescueMode", new DefaultOption(false)); options.addBooleanOption("ClearOnNextRestart", new DefaultOption(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 diff --git a/src/main/java/net/pterodactylus/sone/core/SoneInserter.java b/src/main/java/net/pterodactylus/sone/core/SoneInserter.java index 05fa4b4..15e054f 100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneInserter.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneInserter.java @@ -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); + }} } /** diff --git a/src/main/java/net/pterodactylus/sone/data/Post.java b/src/main/java/net/pterodactylus/sone/data/Post.java index 964a054..bb431d0 100644 --- a/src/main/java/net/pterodactylus/sone/data/Post.java +++ b/src/main/java/net/pterodactylus/sone/data/Post.java @@ -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 FUTURE_POSTS_FILTER = new Filter() { + + @Override + public boolean filterObject(Post post) { + return post.getTime() <= System.currentTimeMillis(); + } + + }; + /** The GUID of the post. */ private final UUID id; diff --git a/src/main/java/net/pterodactylus/sone/data/Reply.java b/src/main/java/net/pterodactylus/sone/data/Reply.java index a106391..2dacfbe 100644 --- a/src/main/java/net/pterodactylus/sone/data/Reply.java +++ b/src/main/java/net/pterodactylus/sone/data/Reply.java @@ -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 FUTURE_REPLIES_FILTER = new Filter() { + + @Override + public boolean filterObject(Reply reply) { + return reply.getTime() <= System.currentTimeMillis(); + } + + }; + /** The ID of the reply. */ private final UUID id; diff --git a/src/main/java/net/pterodactylus/sone/data/Sone.java b/src/main/java/net/pterodactylus/sone/data/Sone.java index e5695fa..2dd4bc1 100644 --- a/src/main/java/net/pterodactylus/sone/data/Sone.java +++ b/src/main/java/net/pterodactylus/sone/data/Sone.java @@ -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 { /** The IDs of all liked replies. */ private final Set likedReplyIds = Collections.synchronizedSet(new HashSet()); + /** Sone-specific options. */ + private final Options options = new Options(); + /** * Creates a new Sone. * @@ -592,6 +596,15 @@ public class Sone implements Fingerprintable, Comparable { return this; } + /** + * Returns Sone-specific options. + * + * @return The options of this Sone + */ + public Options getOptions() { + return options; + } + // // FINGERPRINTABLE METHODS // diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.java b/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.java index eade03e..ab46b43 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.java +++ b/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.java @@ -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. */ diff --git a/src/main/java/net/pterodactylus/sone/main/SonePlugin.java b/src/main/java/net/pterodactylus/sone/main/SonePlugin.java index 3b6824b..de4fee8 100644 --- a/src/main/java/net/pterodactylus/sone/main/SonePlugin.java +++ b/src/main/java/net/pterodactylus/sone/main/SonePlugin.java @@ -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); diff --git a/src/main/java/net/pterodactylus/sone/notify/ListNotification.java b/src/main/java/net/pterodactylus/sone/notify/ListNotification.java index 2f43c58..c66d376 100644 --- a/src/main/java/net/pterodactylus/sone/notify/ListNotification.java +++ b/src/main/java/net/pterodactylus/sone/notify/ListNotification.java @@ -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 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 elements = new CopyOnWriteArrayList(); @@ -47,10 +51,42 @@ public class ListNotification 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 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 extends TemplateNotification { } /** + * Sets the elements to show in this notification. + * + * @param elements + * The elements to show + */ + public void setElements(Collection 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 index 0000000..2a98ec8 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/notify/ListNotificationFilters.java @@ -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 . + */ + +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 David ‘Bombe’ Roden + */ +public class ListNotificationFilters { + + /** + * Filters new-post and new-reply notifications in the given list of + * notifications. If {@code currentSone} is null, 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 filterNotifications(List notifications, Sone currentSone) { + ListNotification newPostNotification = getNotification(notifications, "new-post-notification", Post.class); + if (newPostNotification != null) { + ListNotification filteredNotification = filterNewPostNotification(newPostNotification, currentSone); + int notificationIndex = notifications.indexOf(newPostNotification); + if (filteredNotification == null) { + notifications.remove(notificationIndex); + } else { + notifications.set(notificationIndex, filteredNotification); + } + } + ListNotification newReplyNotification = getNotification(notifications, "new-replies-notification", Reply.class); + if (newReplyNotification != null) { + ListNotification 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 filterNewPostNotification(ListNotification newPostNotification, Sone currentSone) { + if (currentSone == null) { + return null; + } + List newPosts = new ArrayList(); + 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 filteredNotification = new ListNotification(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 filterNewReplyNotification(ListNotification newReplyNotification, Sone currentSone) { + if (currentSone == null) { + return null; + } + List newReplies = new ArrayList(); + 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 filteredNotification = new ListNotification(newReplyNotification); + filteredNotification.setElements(newReplies); + return filteredNotification; + } + + /** + * Finds the notification with the given ID in the list of notifications and + * returns it. + * + * @param + * 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 ListNotification getNotification(Collection notifications, String notificationId, Class notificationElementClass) { + for (Notification notification : notifications) { + if (!notificationId.equals(notification.getId())) { + continue; + } + return (ListNotification) notification; + } + return null; + } + +} diff --git a/src/main/java/net/pterodactylus/sone/template/NotificationManagerAccessor.java b/src/main/java/net/pterodactylus/sone/template/NotificationManagerAccessor.java index a8f34a8..c4468ea 100644 --- a/src/main/java/net/pterodactylus/sone/template/NotificationManagerAccessor.java +++ b/src/main/java/net/pterodactylus/sone/template/NotificationManagerAccessor.java @@ -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 notifications = new ArrayList(notificationManager.getNotifications()); + List notifications = ListNotificationFilters.filterNotifications(new ArrayList(notificationManager.getNotifications()), (Sone) templateContext.get("currentSone")); Collections.sort(notifications, Notification.CREATED_TIME_SORTER); return notifications; - } else if ("new".equals(member)) { - List notifications = new ArrayList(notificationManager.getChangedNotifications()); - Collections.sort(notifications, Notification.LAST_UPDATED_TIME_SORTER); - return notifications; } return super.get(templateContext, object, member); } diff --git a/src/main/java/net/pterodactylus/sone/template/ParserFilter.java b/src/main/java/net/pterodactylus/sone/template/ParserFilter.java index 1e274cc..477e972 100644 --- a/src/main/java/net/pterodactylus/sone/template/ParserFilter.java +++ b/src/main/java/net/pterodactylus/sone/template/ParserFilter.java @@ -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); } /** diff --git a/src/main/java/net/pterodactylus/sone/template/PostAccessor.java b/src/main/java/net/pterodactylus/sone/template/PostAccessor.java index ca0c84a..99d6845 100644 --- a/src/main/java/net/pterodactylus/sone/template/PostAccessor.java +++ b/src/main/java/net/pterodactylus/sone/template/PostAccessor.java @@ -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 index 0000000..2567284 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/template/ReplyGroupFilter.java @@ -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 . + */ + +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 David ‘Bombe’ Roden + */ +public class ReplyGroupFilter implements Filter { + + /** + * {@inheritDoc} + */ + @Override + public Object format(TemplateContext templateContext, Object data, Map parameters) { + @SuppressWarnings("unchecked") + List allReplies = (List) data; + Map> postSones = new HashMap>(); + Map> postReplies = new HashMap>(); + for (Reply reply : allReplies) { + Post post = reply.getPost(); + Set sones = postSones.get(post); + if (sones == null) { + sones = new HashSet(); + postSones.put(post, sones); + } + sones.add(reply.getSone()); + Set replies = postReplies.get(post); + if (replies == null) { + replies = new HashSet(); + postReplies.put(post, replies); + } + replies.add(reply); + } + Map>> result = new HashMap>>(); + for (Post post : postSones.keySet()) { + if (result.containsKey(post)) { + continue; + } + Map> postResult = new HashMap>(); + postResult.put("sones", postSones.get(post)); + postResult.put("replies", postReplies.get(post)); + result.put(post, postResult); + } + return result; + } + +} diff --git a/src/main/java/net/pterodactylus/sone/text/FreenetLinkParser.java b/src/main/java/net/pterodactylus/sone/text/FreenetLinkParser.java index 67cd7e3..cea4452 100644 --- a/src/main/java/net/pterodactylus/sone/text/FreenetLinkParser.java +++ b/src/main/java/net/pterodactylus/sone/text/FreenetLinkParser.java @@ -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 { 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 { 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 { 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 { } 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 { return new TemplatePart(templateContextFactory, TemplateParser.parse(new StringReader("\" title=\"<% link|html>\"><% name|html>"))).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("\" title=\"<%title|html>\"><%name|html>"))).set("link", link).set("name", name).set("title", title); + } + } diff --git a/src/main/java/net/pterodactylus/sone/text/PartContainer.java b/src/main/java/net/pterodactylus/sone/text/PartContainer.java index 4993324..d52658e 100644 --- a/src/main/java/net/pterodactylus/sone/text/PartContainer.java +++ b/src/main/java/net/pterodactylus/sone/text/PartContainer.java @@ -97,6 +97,9 @@ public class PartContainer implements Part { // OBJECT METHODS // + /** + * {@inheritDoc} + */ @Override public String toString() { StringWriter stringWriter = new StringWriter(); diff --git a/src/main/java/net/pterodactylus/sone/text/TemplatePart.java b/src/main/java/net/pterodactylus/sone/text/TemplatePart.java index b948f10..ac5694c 100644 --- a/src/main/java/net/pterodactylus/sone/text/TemplatePart.java +++ b/src/main/java/net/pterodactylus/sone/text/TemplatePart.java @@ -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(); diff --git a/src/main/java/net/pterodactylus/sone/web/IndexPage.java b/src/main/java/net/pterodactylus/sone/web/IndexPage.java index 2d85e02..e675311 100644 --- a/src/main/java/net/pterodactylus/sone/web/IndexPage.java +++ b/src/main/java/net/pterodactylus/sone/web/IndexPage.java @@ -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 pagination = new Pagination(allPosts, 25).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0)); + Pagination pagination = new Pagination(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 posts = (List) templateContext.get("posts"); - for (Post post : posts) { - webInterface.getCore().markPostKnown(post); - for (Reply reply : webInterface.getCore().getReplies(post)) { - webInterface.getCore().markReplyKnown(reply); - } - } - } - } diff --git a/src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java b/src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java index c0e6364..7c8f9ad 100644 --- a/src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java +++ b/src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java @@ -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 sones = (List) templateContext.get("knownSones"); - for (Sone sone : sones) { - webInterface.getCore().markSoneKnown(sone); - } - } - } diff --git a/src/main/java/net/pterodactylus/sone/web/OptionsPage.java b/src/main/java/net/pterodactylus/sone/web/OptionsPage.java index 84d7c79..39cacc6 100644 --- a/src/main/java/net/pterodactylus/sone/web/OptionsPage.java +++ b/src/main/java/net/pterodactylus/sone/web/OptionsPage.java @@ -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()); diff --git a/src/main/java/net/pterodactylus/sone/web/SearchPage.java b/src/main/java/net/pterodactylus/sone/web/SearchPage.java index 2b854f3..1d1aa36 100644 --- a/src/main/java/net/pterodactylus/sone/web/SearchPage.java +++ b/src/main/java/net/pterodactylus/sone/web/SearchPage.java @@ -86,7 +86,7 @@ public class SearchPage extends SoneTemplatePage { posts.addAll(sone.getPosts()); } @SuppressWarnings("synthetic-access") - Set> postHits = getHits(posts, phrases, new PostStringGenerator()); + Set> 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 resultPosts = Converters.convertList(sortedPostHits, new HitConverter()); /* pagination. */ - Pagination sonePagination = new Pagination(resultSones, 10).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("sonePage"), 0)); - Pagination postPagination = new Pagination(resultPosts, 10).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("postPage"), 0)); + Pagination sonePagination = new Pagination(resultSones, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("sonePage"), 0)); + Pagination postPagination = new Pagination(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()); } diff --git a/src/main/java/net/pterodactylus/sone/web/ViewPostPage.java b/src/main/java/net/pterodactylus/sone/web/ViewPostPage.java index e528709..b62e19c 100644 --- a/src/main/java/net/pterodactylus/sone/web/ViewPostPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ViewPostPage.java @@ -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); - } - } - } diff --git a/src/main/java/net/pterodactylus/sone/web/ViewSonePage.java b/src/main/java/net/pterodactylus/sone/web/ViewSonePage.java index 758b93d..14c4f94 100644 --- a/src/main/java/net/pterodactylus/sone/web/ViewSonePage.java +++ b/src/main/java/net/pterodactylus/sone/web/ViewSonePage.java @@ -81,14 +81,16 @@ public class ViewSonePage extends SoneTemplatePage { Sone sone = webInterface.getCore().getSone(soneId, false); templateContext.set("sone", sone); List sonePosts = sone.getPosts(); - Pagination postPagination = new Pagination(sonePosts, 10).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("postPage"), 0)); + sonePosts.addAll(webInterface.getCore().getDirectedPosts(sone)); + Collections.sort(sonePosts, Post.TIME_COMPARATOR); + Pagination postPagination = new Pagination(sonePosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("postPage"), 0)); templateContext.set("postPagination", postPagination); templateContext.set("posts", postPagination.getItems()); Set replies = sone.getReplies(); final Map> repliedPosts = new HashMap>(); 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 repliedPostPagination = new Pagination(posts, 10).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("repliedPostPage"), 0)); + Pagination repliedPostPagination = new Pagination(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 posts = (List) templateContext.get("posts"); - posts.addAll((List) 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); - } - } - } - } diff --git a/src/main/java/net/pterodactylus/sone/web/WebInterface.java b/src/main/java/net/pterodactylus/sone/web/WebInterface.java index acfdc7f..b3772f9 100644 --- a/src/main/java/net/pterodactylus/sone/web/WebInterface.java +++ b/src/main/java/net/pterodactylus/sone/web/WebInterface.java @@ -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("new-sone-notification", "sones", newSoneNotificationTemplate); + newSoneNotification = new ListNotification("new-sone-notification", "sones", newSoneNotificationTemplate, false); Template newPostNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newPostNotification.html")); - newPostNotification = new ListNotification("new-post-notification", "posts", newPostNotificationTemplate); + newPostNotification = new ListNotification("new-post-notification", "posts", newPostNotificationTemplate, false); Template newReplyNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newReplyNotification.html")); - newReplyNotification = new ListNotification("new-replies-notification", "replies", newReplyNotificationTemplate); + newReplyNotification = new ListNotification("new-replies-notification", "replies", newReplyNotificationTemplate, false); Template rescuingSonesTemplate = TemplateParser.parse(createReader("/templates/notify/rescuingSonesNotification.html")); rescuingSonesNotification = new ListNotification("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))); diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java index fd9ae76..426c08f 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java @@ -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 sones = new HashSet(Collections.singleton(getCurrentSone(request.getToadletContext(), false))); @@ -80,19 +84,24 @@ public class GetStatusAjaxPage extends JsonPage { jsonSones.add(jsonSone); } /* load notifications. */ - List notifications = new ArrayList(webInterface.getNotifications().getChangedNotifications()); - Set removedNotifications = webInterface.getNotifications().getRemovedNotifications(); + List notifications = ListNotificationFilters.filterNotifications(new ArrayList(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 newPosts = webInterface.getNewPosts(); + if (currentSone != null) { + newPosts = Filters.filteredSet(newPosts, new Filter() { + + @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 newReplies = webInterface.getNewReplies(); + if (currentSone != null) { + newReplies = Filters.filteredSet(newReplies, new Filter() { + + @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 index 0000000..b17e4a2 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/web/ajax/GetTimesAjaxPage.java @@ -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 . + */ + +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 David ‘Bombe’ Roden + */ +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 David ‘Bombe’ Roden + */ + 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; + } + + } + +} diff --git a/src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java b/src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java index 4334a7f..f7f59a6 100644 --- a/src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java +++ b/src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java @@ -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 index 0000000..2ce34f9 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/web/page/RedirectPage.java @@ -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 . + */ + +package net.pterodactylus.sone.web.page; + +/** + * Page implementation that redirects the user to another URL. + * + * @author David ‘Bombe’ Roden + */ +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); + } + +} diff --git a/src/main/resources/i18n/sone.en.properties b/src/main/resources/i18n/sone.en.properties index 104d3c7..a73432d 100644 --- a/src/main/resources/i18n/sone.en.properties +++ b/src/main/resources/i18n/sone.en.properties @@ -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! diff --git a/src/main/resources/static/css/sone.css b/src/main/resources/static/css/sone.css index 38fcd19..f6bb62d 100644 --- a/src/main/resources/static/css/sone.css +++ b/src/main/resources/static/css/sone.css @@ -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; diff --git a/src/main/resources/static/javascript/sone.js b/src/main/resources/static/javascript/sone.js index c0fbb6d..d60ee23 100644 --- a/src/main/resources/static/javascript/sone.js +++ b/src/main/resources/static/javascript/sone.js @@ -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) { diff --git a/src/main/resources/templates/about.html b/src/main/resources/templates/about.html index 96f4c8e..e04f4f2 100644 --- a/src/main/resources/templates/about.html +++ b/src/main/resources/templates/about.html @@ -2,17 +2,20 @@

<%= Page.About.Page.Title|l10n|html>

-

Sone – The Freenet Social Network Plugin, Version <% version|html>, © 2010 by David ‘Bombe’ Roden.

+

Sone – The Freenet Social Network Plugin, Version <% version|html>, © 2010–2011 by David ‘Bombe’ Roden.

- 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 flattr.com. + <%= Page.About.Flattr.Description|l10n|html|replace needle="{link}" replacement=''|replace needle="{/link}" replacement=''>

+

<%= Page.About.Homepage.Title|l10n|html>

+ +

+ <%= Page.About.Homepage.Description|l10n|html|replace needle="{link}" replacement=''|replace needle="{/link}" replacement=''> +

+ +

<%= Page.About.License.Title|l10n|html>

+

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 diff --git a/src/main/resources/templates/notify/newPostNotification.html b/src/main/resources/templates/notify/newPostNotification.html index 177b470..54008dc 100644 --- a/src/main/resources/templates/notify/newPostNotification.html +++ b/src/main/resources/templates/notify/newPostNotification.html @@ -12,6 +12,7 @@ <%= Notification.NewPost.Text|l10n|html> <%foreach posts post> +

<% post.sone.niceName|html><%notlast>,<%/notlast><%last>.<%/last> <%/foreach> diff --git a/src/main/resources/templates/notify/newReplyNotification.html b/src/main/resources/templates/notify/newReplyNotification.html index 21a5ca0..ee8c410 100644 --- a/src/main/resources/templates/notify/newReplyNotification.html +++ b/src/main/resources/templates/notify/newReplyNotification.html @@ -10,8 +10,9 @@ + <%foreach replies reply><%/foreach> <%= Notification.NewReply.Text|l10n|html> - <%foreach replies reply> - <% reply.sone.niceName|html><%notlast>,<%/notlast><%last>.<%/last> + <%foreach replies postGroup|replyGroup> + <% postGroup.key.sone.niceName|html> (<%foreach postGroup.value.sones sone><%sone.niceName|html><%notlast>, <%/notlast><%/foreach>)<%notlast>, <%/notlast><%last>.<%/last> <%/foreach> diff --git a/src/main/resources/templates/notify/newSoneNotification.html b/src/main/resources/templates/notify/newSoneNotification.html index 45357b1..aaa0d14 100644 --- a/src/main/resources/templates/notify/newSoneNotification.html +++ b/src/main/resources/templates/notify/newSoneNotification.html @@ -12,6 +12,7 @@ <%= Notification.NewSone.Text|l10n|html> <%foreach sones sone> + <% sone.niceName|html><%notlast>,<%/notlast><%last>.<%/last> <%/foreach> diff --git a/src/main/resources/templates/options.html b/src/main/resources/templates/options.html index 5c47764..db5eb80 100644 --- a/src/main/resources/templates/options.html +++ b/src/main/resources/templates/options.html @@ -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); + }); }); @@ -15,11 +27,27 @@
+

<%= Page.Options.Section.SoneSpecificOptions.Title|l10n|html>

+ + <%ifnull currentSone> +

<%= Page.Options.Section.SoneSpecificOptions.NotLoggedIn|l10n|html|replace needle="{link}" replacement=''|replace needle="{/link}" replacement=''>

+ <%else> +

<%= Page.Options.Section.SoneSpecificOptions.LoggedIn|l10n|html>

+ <%/if> + +

+ disabled="disabled"<%/if><%if auto-follow> checked="checked"<%/if> /> + <%= Page.Options.Option.AutoFollow.Description|l10n|html> +

+

<%= Page.Options.Section.RuntimeOptions.Title|l10n|html>

<%= Page.Options.Option.InsertionDelay.Description|l10n|html>

+

<%= Page.Options.Option.PostsPerPage.Description|l10n|html>

+

+

<%= Page.Options.Section.TrustOptions.Title|l10n|html>

<%= Page.Options.Option.PositiveTrust.Description|l10n|html>

diff --git a/src/test/java/net/pterodactylus/sone/text/FreenetLinkParserTest.java b/src/test/java/net/pterodactylus/sone/text/FreenetLinkParserTest.java index e350961..7e05fd5 100644 --- a/src/test/java/net/pterodactylus/sone/text/FreenetLinkParserTest.java +++ b/src/test/java/net/pterodactylus/sone/text/FreenetLinkParserTest.java @@ -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;