From: David ‘Bombe’ Roden Date: Wed, 13 Apr 2011 04:46:32 +0000 (+0200) Subject: Bring image-management up to speed. X-Git-Tag: beta-freefall-0.6.2-1~8 X-Git-Url: https://git.pterodactylus.net/?p=Sone.git;a=commitdiff_plain;h=a23c4f218c3adf236d89d5927cae37d6e6e4feda;hp=b8545de14af229c6b557ef2229ee0166459d3a16 Bring image-management up to speed. Conflicts: src/main/java/net/pterodactylus/sone/core/Core.java src/main/java/net/pterodactylus/sone/data/Sone.java src/main/java/net/pterodactylus/sone/web/WebInterface.java src/main/resources/i18n/sone.en.properties src/main/resources/static/css/sone.css --- diff --git a/pom.xml b/pom.xml index 229f2ef..33fd6be 100644 --- a/pom.xml +++ b/pom.xml @@ -2,12 +2,12 @@ 4.0.0 net.pterodactylus sone - 0.5.1 + 0.6.1 net.pterodactylus utils - 0.9.1 + 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 f5d695c..a67177b 100644 --- a/src/main/java/net/pterodactylus/sone/core/Core.java +++ b/src/main/java/net/pterodactylus/sone/core/Core.java @@ -541,7 +541,8 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen * @return {@code true} if the target Sone is trusted by the origin Sone */ public boolean isSoneTrusted(Sone origin, Sone target) { - return trustedIdentities.containsKey(origin) && trustedIdentities.get(origin.getIdentity()).contains(target); + Validation.begin().isNotNull("Origin", origin).isNotNull("Target", target).check().isInstanceOf("Origin’s OwnIdentity", origin.getIdentity(), OwnIdentity.class).check(); + return trustedIdentities.containsKey(origin.getIdentity()) && trustedIdentities.get(origin.getIdentity()).contains(target.getIdentity()); } /** @@ -549,7 +550,7 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen * * @param postId * The ID of the post to get - * @return The post, or {@code null} if there is no such post + * @return The post with the given ID, or a new post with the given ID */ public Post getPost(String postId) { return getPost(postId, true); @@ -591,6 +592,27 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen } /** + * 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. * @@ -916,7 +938,6 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen @SuppressWarnings("synthetic-access") public void run() { if (!preferences.isSoneRescueMode()) { - soneDownloader.fetchSone(sone); return; } logger.log(Level.INFO, "Trying to restore Sone from Freenet…"); @@ -954,6 +975,8 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen return null; } Sone sone = addLocalSone(ownIdentity); + sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption(false)); + saveSone(sone); return sone; } @@ -983,6 +1006,11 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen } if (newSone) { coreListenerManager.fireNewSoneFound(sone); + for (Sone localSone : getLocalSones()) { + if (localSone.getOptions().getBooleanOption("AutoFollow").get()) { + localSone.addFriend(sone.getId()); + } + } } } remoteSones.put(identity.getId(), sone); @@ -1644,7 +1672,8 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen posts.put(post.getId(), post); } synchronized (newPosts) { - knownPosts.add(post.getId()); + newPosts.add(post.getId()); + coreListenerManager.fireNewPostFound(post); } sone.addPost(post); saveSone(sone); @@ -1666,6 +1695,10 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen synchronized (posts) { posts.remove(post.getId()); } + synchronized (newPosts) { + markPostKnown(post); + knownPosts.remove(post.getId()); + } saveSone(post.getSone()); } @@ -1768,7 +1801,8 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen replies.put(reply.getId(), reply); } synchronized (newReplies) { - knownReplies.add(reply.getId()); + newReplies.add(reply.getId()); + coreListenerManager.fireNewReplyFound(reply); } sone.addReply(reply); saveSone(sone); @@ -1790,6 +1824,10 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen synchronized (replies) { replies.remove(reply.getId()); } + synchronized (newReplies) { + markReplyKnown(reply); + knownReplies.remove(reply.getId()); + } sone.removeReply(reply); saveSone(sone); } @@ -1996,6 +2034,7 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen 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()); @@ -2069,8 +2108,9 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen } })); + 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)); @@ -2088,6 +2128,7 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen } 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)); @@ -2320,6 +2361,27 @@ public class Core implements IdentityListener, UpdateListener, ImageInsertListen } /** + * 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 b16d374..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); + }} } /** @@ -246,7 +248,7 @@ public class SoneInserter extends AbstractService { * * @author David ‘Bombe’ Roden */ - private class InsertInformation { + private static class InsertInformation { /** All properties of the Sone, copied for thread safety. */ private final Map soneProperties = new HashMap(); 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 1a8c7df..aa4a155 100644 --- a/src/main/java/net/pterodactylus/sone/data/Sone.java +++ b/src/main/java/net/pterodactylus/sone/data/Sone.java @@ -27,8 +27,10 @@ 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; import net.pterodactylus.util.logging.Logging; import net.pterodactylus.util.validation.Validation; import freenet.keys.FreenetURI; @@ -57,6 +59,15 @@ public class Sone implements Fingerprintable, Comparable { }; + /** Filter to remove Sones that have not been downloaded. */ + public static final Filter EMPTY_SONE_FILTER = new Filter() { + + @Override + public boolean filterObject(Sone sone) { + return sone.getTime() != 0; + } + }; + /** The logger. */ private static final Logger logger = Logging.getLogger(Sone.class); @@ -103,6 +114,9 @@ public class Sone implements Fingerprintable, Comparable { /** The albums of this Sone. */ private final List albums = Collections.synchronizedList(new ArrayList()); + /** Sone-specific options. */ + private final Options options = new Options(); + /** * Creates a new Sone. * @@ -391,8 +405,10 @@ public class Sone implements Fingerprintable, Comparable { * @return This Sone (for method chaining) */ public synchronized Sone setPosts(Collection posts) { - this.posts.clear(); - this.posts.addAll(posts); + synchronized (this) { + this.posts.clear(); + this.posts.addAll(posts); + } return this; } @@ -629,6 +645,15 @@ public class Sone implements Fingerprintable, Comparable { albums.remove(album); } + /** + * 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 ab96756..ab46b43 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.java +++ b/src/main/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.java @@ -167,4 +167,26 @@ 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. */ + return super.equals(object); + } + } diff --git a/src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.java b/src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.java index b6d68d3..59da039 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.java +++ b/src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.java @@ -42,7 +42,7 @@ public class WebOfTrustConnector implements ConnectorListener { private static final Logger logger = Logging.getLogger(WebOfTrustConnector.class); /** The name of the WoT plugin. */ - private static final String WOT_PLUGIN_NAME = "plugins.WoT.WoT"; + private static final String WOT_PLUGIN_NAME = "plugins.WebOfTrust.WebOfTrust"; /** A random connection identifier. */ private static final String PLUGIN_CONNECTION_IDENTIFIER = "Sone-WoT-Connector-" + Math.abs(Math.random()); @@ -230,7 +230,7 @@ public class WebOfTrustConnector implements ConnectorListener { * if an error occured talking to the Web of Trust plugin */ public Trust getTrust(OwnIdentity ownIdentity, Identity identity) throws PluginException { - Reply getTrustReply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetIdentity").put("TreeOwner", ownIdentity.getId()).put("Identity", identity.getId()).get()); + Reply getTrustReply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetIdentity").put("Truster", ownIdentity.getId()).put("Identity", identity.getId()).get()); String trust = getTrustReply.getFields().get("Trust"); String score = getTrustReply.getFields().get("Score"); String rank = getTrustReply.getFields().get("Rank"); diff --git a/src/main/java/net/pterodactylus/sone/main/SonePlugin.java b/src/main/java/net/pterodactylus/sone/main/SonePlugin.java index 0f7f0e8..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, 5, 1); + 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 c7e5b7c..c66d376 100644 --- a/src/main/java/net/pterodactylus/sone/notify/ListNotification.java +++ b/src/main/java/net/pterodactylus/sone/notify/ListNotification.java @@ -18,8 +18,9 @@ package net.pterodactylus.sone.notify; import java.util.ArrayList; -import java.util.Collections; +import java.util.Collection; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import net.pterodactylus.util.notify.TemplateNotification; import net.pterodactylus.util.template.Template; @@ -33,8 +34,11 @@ 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 = Collections.synchronizedList(new ArrayList()); + private final List elements = new CopyOnWriteArrayList(); /** * Creates a new list notification. @@ -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..a6ef8aa --- /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 (((reply.getPost().getSone() != null) && 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/HttpRequestAccessor.java b/src/main/java/net/pterodactylus/sone/template/HttpRequestAccessor.java new file mode 100644 index 0000000..c5e38df --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/template/HttpRequestAccessor.java @@ -0,0 +1,47 @@ +/* + * Sone - HttpRequestAccessor.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.template; + +import net.pterodactylus.util.template.Accessor; +import net.pterodactylus.util.template.ReflectionAccessor; +import net.pterodactylus.util.template.TemplateContext; +import freenet.support.api.HTTPRequest; + +/** + * {@link Accessor} implementation that can parse headers from + * {@link HTTPRequest}s. + * + * @see HTTPRequest#getHeader(String) + * @author David ‘Bombe’ Roden + */ +public class HttpRequestAccessor extends ReflectionAccessor { + + /** + * {@inheritDoc} + */ + @Override + public Object get(TemplateContext templateContext, Object object, String member) { + Object parentValue = super.get(templateContext, object, member); + if (parentValue != null) { + return parentValue; + } + HTTPRequest httpRequest = (HTTPRequest) object; + return httpRequest.getHeader(member); + } + +} 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 0a50a62..7c30b76 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; } @@ -97,8 +113,20 @@ public class FreenetLinkParser implements Parser { PartContainer parts = new PartContainer(); BufferedReader bufferedReader = (source instanceof BufferedReader) ? (BufferedReader) source : new BufferedReader(source); String line; + boolean lastLineEmpty = true; + int emptyLines = 0; while ((line = bufferedReader.readLine()) != null) { - line = line.trim() + "\n"; + if (line.trim().length() == 0) { + if (lastLineEmpty) { + continue; + } + parts.add(createPlainTextPart("\n")); + ++emptyLines; + lastLineEmpty = emptyLines == 2; + continue; + } + emptyLines = 0; + boolean lineComplete = true; while (line.length() > 0) { int nextKsk = line.indexOf("KSK@"); int nextChk = line.indexOf("CHK@"); @@ -106,8 +134,14 @@ 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)) { - parts.add(createPlainTextPart(line)); + 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 { + parts.add(createPlainTextPart(line)); + } break; } int next = Integer.MAX_VALUE; @@ -136,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); @@ -143,7 +185,11 @@ public class FreenetLinkParser implements Parser { Matcher matcher = whitespacePattern.matcher(line); int nextSpace = matcher.find(next) ? matcher.start() : line.length(); if (nextSpace > (next + 4)) { - parts.add(createPlainTextPart(line.substring(0, next))); + if (!lastLineEmpty && lineComplete) { + parts.add(createPlainTextPart("\n" + line.substring(0, next))); + } else { + parts.add(createPlainTextPart(line.substring(0, next))); + } String link = line.substring(next, nextSpace); String name = link; logger.log(Level.FINER, "Found link: %s", link); @@ -196,13 +242,44 @@ 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 { - parts.add(createPlainTextPart(line.substring(0, next + 4))); + if (!lastLineEmpty && lineComplete) { + parts.add(createPlainTextPart("\n" + line.substring(0, next + 4))); + } else { + parts.add(createPlainTextPart(line.substring(0, next + 4))); + } line = line.substring(next + 4); } + lineComplete = false; } + lastLineEmpty = false; + } + for (int partIndex = parts.size() - 1; partIndex >= 0; --partIndex) { + if (!parts.getPart(partIndex).toString().equals("\n")) { + break; + } + parts.removePart(partIndex); } return parts; } @@ -264,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 7399859..d52658e 100644 --- a/src/main/java/net/pterodactylus/sone/text/PartContainer.java +++ b/src/main/java/net/pterodactylus/sone/text/PartContainer.java @@ -18,6 +18,7 @@ package net.pterodactylus.sone.text; import java.io.IOException; +import java.io.StringWriter; import java.io.Writer; import java.util.ArrayList; import java.util.List; @@ -48,6 +49,36 @@ public class PartContainer implements Part { parts.add(part); } + /** + * Returns the part at the given index. + * + * @param index + * The index of the part + * @return The part + */ + public Part getPart(int index) { + return parts.get(index); + } + + /** + * Removes the part at the given index. + * + * @param index + * The index of the part to remove + */ + public void removePart(int index) { + parts.remove(index); + } + + /** + * Returns the number of parts. + * + * @return The number of parts + */ + public int size() { + return parts.size(); + } + // // PART METHODS // @@ -62,4 +93,22 @@ public class PartContainer implements Part { } } + // + // OBJECT METHODS + // + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + StringWriter stringWriter = new StringWriter(); + try { + render(stringWriter); + } catch (IOException ioe1) { + /* should never throw, ignore. */ + } + return stringWriter.toString(); + } + } diff --git a/src/main/java/net/pterodactylus/sone/text/TemplatePart.java b/src/main/java/net/pterodactylus/sone/text/TemplatePart.java index 1ac1fdd..ac5694c 100644 --- a/src/main/java/net/pterodactylus/sone/text/TemplatePart.java +++ b/src/main/java/net/pterodactylus/sone/text/TemplatePart.java @@ -18,6 +18,7 @@ package net.pterodactylus.sone.text; import java.io.IOException; +import java.io.StringWriter; import java.io.Writer; import net.pterodactylus.util.template.Template; @@ -89,4 +90,22 @@ public class TemplatePart implements Part, net.pterodactylus.util.template.Part template.render(templateContext.mergeContext(template.getInitialContext()), writer); } + // + // OBJECT METHODS + // + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + StringWriter stringWriter = new StringWriter(); + try { + render(stringWriter); + } catch (IOException ioe1) { + /* should never throw, ignore. */ + } + return stringWriter.toString(); + } + } 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 b08b6e3..7c8f9ad 100644 --- a/src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java +++ b/src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java @@ -23,6 +23,7 @@ import java.util.List; 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; @@ -56,24 +57,11 @@ public class KnownSonesPage extends SoneTemplatePage { @Override protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException { super.processTemplate(request, templateContext); - List knownSones = new ArrayList(webInterface.getCore().getSones()); + List knownSones = Filters.filteredList(new ArrayList(webInterface.getCore().getSones()), Sone.EMPTY_SONE_FILTER); Collections.sort(knownSones, Sone.NICE_NAME_COMPARATOR); Pagination sonePagination = new Pagination(knownSones, 25).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0)); templateContext.set("pagination", sonePagination); 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 new file mode 100644 index 0000000..1d1aa36 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/web/SearchPage.java @@ -0,0 +1,484 @@ +/* + * Sone - OptionsPage.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.web; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.Profile; +import net.pterodactylus.sone.data.Profile.Field; +import net.pterodactylus.sone.data.Reply; +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.util.collection.Converter; +import net.pterodactylus.util.collection.Converters; +import net.pterodactylus.util.collection.Pagination; +import net.pterodactylus.util.filter.Filter; +import net.pterodactylus.util.filter.Filters; +import net.pterodactylus.util.number.Numbers; +import net.pterodactylus.util.template.Template; +import net.pterodactylus.util.template.TemplateContext; +import net.pterodactylus.util.text.StringEscaper; +import net.pterodactylus.util.text.TextException; + +/** + * This page lets the user search for posts and replies that contain certain + * words. + * + * @author David ‘Bombe’ Roden + */ +public class SearchPage extends SoneTemplatePage { + + /** + * Creates a new search page. + * + * @param template + * The template to render + * @param webInterface + * The Sone web interface + */ + public SearchPage(Template template, WebInterface webInterface) { + super("search.html", template, "Page.Search.Title", webInterface); + } + + // + // SONETEMPLATEPAGE METHODS + // + + /** + * {@inheritDoc} + */ + @Override + protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException { + super.processTemplate(request, templateContext); + String query = request.getHttpRequest().getParam("query").trim(); + if (query.length() == 0) { + throw new RedirectException("index.html"); + } + + List phrases = parseSearchPhrases(query); + + Set sones = webInterface.getCore().getSones(); + Set> soneHits = getHits(sones, phrases, SoneStringGenerator.COMPLETE_GENERATOR); + + Set posts = new HashSet(); + for (Sone sone : sones) { + posts.addAll(sone.getPosts()); + } + @SuppressWarnings("synthetic-access") + Set> postHits = getHits(Filters.filteredSet(posts, Post.FUTURE_POSTS_FILTER), phrases, new PostStringGenerator()); + + /* now filter. */ + soneHits = Filters.filteredSet(soneHits, Hit.POSITIVE_FILTER); + postHits = Filters.filteredSet(postHits, Hit.POSITIVE_FILTER); + + /* now sort. */ + List> sortedSoneHits = new ArrayList>(soneHits); + Collections.sort(sortedSoneHits, Hit.DESCENDING_COMPARATOR); + List> sortedPostHits = new ArrayList>(postHits); + Collections.sort(sortedPostHits, Hit.DESCENDING_COMPARATOR); + + /* extract Sones and posts. */ + List resultSones = Converters.convertList(sortedSoneHits, new HitConverter()); + List resultPosts = Converters.convertList(sortedPostHits, new HitConverter()); + + /* pagination. */ + 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()); + templateContext.set("postPagination", postPagination); + templateContext.set("postHits", postPagination.getItems()); + } + + // + // PRIVATE METHODS + // + + /** + * Collects hit information for the given objects. The objects are converted + * to a {@link String} using the given {@link StringGenerator}, and the + * {@link #calculateScore(List, String) calculated score} is stored together + * with the object in a {@link Hit}, and all resulting {@link Hit}s are then + * returned. + * + * @param + * The type of the objects + * @param objects + * The objects to search over + * @param phrases + * The phrases to search for + * @param stringGenerator + * The string generator for the objects + * @return The hits for the given phrases + */ + private Set> getHits(Collection objects, List phrases, StringGenerator stringGenerator) { + Set> hits = new HashSet>(); + for (T object : objects) { + String objectString = stringGenerator.generateString(object); + int score = calculateScore(phrases, objectString); + hits.add(new Hit(object, score)); + } + return hits; + } + + /** + * Parses the given query into search phrases. The query is split on + * whitespace while allowing to group words using single or double quotes. + * Isolated phrases starting with a “+” are + * {@link Phrase.Optionality#REQUIRED}, phrases with a “-” are + * {@link Phrase.Optionality#FORBIDDEN}. + * + * @param query + * The query to parse + * @return The parsed phrases + */ + private List parseSearchPhrases(String query) { + List parsedPhrases = null; + try { + parsedPhrases = StringEscaper.parseLine(query); + } catch (TextException te1) { + /* invalid query. */ + return Collections.emptyList(); + } + + List phrases = new ArrayList(); + for (String phrase : parsedPhrases) { + if (phrase.startsWith("+")) { + phrases.add(new Phrase(phrase.substring(1), Phrase.Optionality.REQUIRED)); + } else if (phrase.startsWith("-")) { + phrases.add(new Phrase(phrase.substring(1), Phrase.Optionality.FORBIDDEN)); + } + phrases.add(new Phrase(phrase, Phrase.Optionality.OPTIONAL)); + } + return phrases; + } + + /** + * Calculates the score for the given expression when using the given + * phrases. + * + * @param phrases + * The phrases to search for + * @param expression + * The expression to search + * @return The score of the expression + */ + private int calculateScore(List phrases, String expression) { + int optionalHits = 0; + int requiredHits = 0; + int forbiddenHits = 0; + int requiredPhrases = 0; + for (Phrase phrase : phrases) { + String phraseString = phrase.getPhrase().toLowerCase(); + if (phrase.getOptionality() == Phrase.Optionality.REQUIRED) { + ++requiredPhrases; + } + int matches = 0; + int index = 0; + while (index < expression.length()) { + int position = expression.toLowerCase().indexOf(phraseString, index); + if (position == -1) { + break; + } + index = position + phraseString.length(); + ++matches; + } + if (matches == 0) { + continue; + } + if (phrase.getOptionality() == Phrase.Optionality.REQUIRED) { + requiredHits += matches; + } + if (phrase.getOptionality() == Phrase.Optionality.OPTIONAL) { + optionalHits += matches; + } + if (phrase.getOptionality() == Phrase.Optionality.FORBIDDEN) { + forbiddenHits += matches; + } + } + return requiredHits * 3 + optionalHits + (requiredHits - requiredPhrases) * 5 - (forbiddenHits * 2); + } + + /** + * Converts a given object into a {@link String}. + * + * @param + * The type of the objects + * @author David ‘Bombe’ Roden + */ + private static interface StringGenerator { + + /** + * Generates a {@link String} for the given object. + * + * @param object + * The object to generate the {@link String} for + * @return The generated {@link String} + */ + public String generateString(T object); + + } + + /** + * Generates a {@link String} from a {@link Sone}, concatenating the name of + * the Sone and all {@link Profile} {@link Field} values. + * + * @author David ‘Bombe’ Roden + */ + private static class SoneStringGenerator implements StringGenerator { + + /** A static instance of a complete Sone string generator. */ + public static final SoneStringGenerator COMPLETE_GENERATOR = new SoneStringGenerator(true); + + /** + * A static instance of a Sone string generator that will only use the + * name of the Sone. + */ + public static final SoneStringGenerator NAME_GENERATOR = new SoneStringGenerator(false); + + /** Whether to generate a string from all data of a Sone. */ + private final boolean complete; + + /** + * Creates a new Sone string generator. + * + * @param complete + * {@code true} to use the profile’s fields, {@code false} to + * not to use the profile‘s fields + */ + private SoneStringGenerator(boolean complete) { + this.complete = complete; + } + + /** + * {@inheritDoc} + */ + @Override + public String generateString(Sone sone) { + StringBuilder soneString = new StringBuilder(); + soneString.append(sone.getName()); + Profile soneProfile = sone.getProfile(); + if (soneProfile.getFirstName() != null) { + soneString.append(' ').append(soneProfile.getFirstName()); + } + if (soneProfile.getMiddleName() != null) { + soneString.append(' ').append(soneProfile.getMiddleName()); + } + if (soneProfile.getLastName() != null) { + soneString.append(' ').append(soneProfile.getLastName()); + } + if (complete) { + for (Field field : soneProfile.getFields()) { + soneString.append(' ').append(field.getValue()); + } + } + return soneString.toString(); + } + + } + + /** + * Generates a {@link String} from a {@link Post}, concatenating the text of + * the post, the text of all {@link Reply}s, and the name of all + * {@link Sone}s that have replied. + * + * @author David ‘Bombe’ Roden + */ + private class PostStringGenerator implements StringGenerator { + + /** + * {@inheritDoc} + */ + @Override + public String generateString(Post post) { + StringBuilder postString = new StringBuilder(); + postString.append(post.getText()); + if (post.getRecipient() != null) { + postString.append(' ').append(SoneStringGenerator.NAME_GENERATOR.generateString(post.getRecipient())); + } + 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()); + } + return postString.toString(); + } + + } + + /** + * A search phrase. + * + * @author David ‘Bombe’ Roden + */ + private static class Phrase { + + /** + * The optionality of a search phrase. + * + * @author David ‘Bombe’ + * Roden + */ + public enum Optionality { + + /** The phrase is optional. */ + OPTIONAL, + + /** The phrase is required. */ + REQUIRED, + + /** The phrase is forbidden. */ + FORBIDDEN + + } + + /** The phrase to search for. */ + private final String phrase; + + /** The optionality of the phrase. */ + private final Optionality optionality; + + /** + * Creates a new phrase. + * + * @param phrase + * The phrase to search for + * @param optionality + * The optionality of the phrase + */ + public Phrase(String phrase, Optionality optionality) { + this.optionality = optionality; + this.phrase = phrase; + } + + /** + * Returns the phrase to search for. + * + * @return The phrase to search for + */ + public String getPhrase() { + return phrase; + } + + /** + * Returns the optionality of the phrase. + * + * @return The optionality of the phrase + */ + public Optionality getOptionality() { + return optionality; + } + + } + + /** + * A hit consists of a searched object and the score it got for the phrases + * of the search. + * + * @see SearchPage#calculateScore(List, String) + * @param + * The type of the searched object + * @author David ‘Bombe’ Roden + */ + private static class Hit { + + /** Filter for {@link Hit}s with a score of more than 0. */ + public static final Filter> POSITIVE_FILTER = new Filter>() { + + @Override + public boolean filterObject(Hit hit) { + return hit.getScore() > 0; + } + + }; + + /** Comparator that sorts {@link Hit}s descending by score. */ + public static final Comparator> DESCENDING_COMPARATOR = new Comparator>() { + + @Override + public int compare(Hit leftHit, Hit rightHit) { + return rightHit.getScore() - leftHit.getScore(); + } + + }; + + /** The object that was searched. */ + private final T object; + + /** The score of the object. */ + private final int score; + + /** + * Creates a new hit. + * + * @param object + * The object that was searched + * @param score + * The score of the object + */ + public Hit(T object, int score) { + this.object = object; + this.score = score; + } + + /** + * Returns the object that was searched. + * + * @return The object that was searched + */ + public T getObject() { + return object; + } + + /** + * Returns the score of the object. + * + * @return The score of the object + */ + public int getScore() { + return score; + } + + } + + /** + * Extracts the object from a {@link Hit}. + * + * @param + * The type of the object to extract + * @author David ‘Bombe’ Roden + */ + public static class HitConverter implements Converter, T> { + + /** + * {@inheritDoc} + */ + @Override + public T convert(Hit input) { + return input.getObject(); + } + + } + +} diff --git a/src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java b/src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java index a04873d..d6c41c1 100644 --- a/src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java +++ b/src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java @@ -21,11 +21,15 @@ import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.Arrays; import java.util.Collection; +import java.util.List; +import java.util.Map; import net.pterodactylus.sone.data.Sone; import net.pterodactylus.sone.main.SonePlugin; import net.pterodactylus.sone.web.page.Page; -import net.pterodactylus.sone.web.page.TemplatePage; +import net.pterodactylus.sone.web.page.FreenetTemplatePage; +import net.pterodactylus.util.collection.ListBuilder; +import net.pterodactylus.util.collection.MapBuilder; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; import freenet.clients.http.SessionManager.Session; @@ -37,11 +41,14 @@ import freenet.support.api.HTTPRequest; * * @author David ‘Bombe’ Roden */ -public class SoneTemplatePage extends TemplatePage { +public class SoneTemplatePage extends FreenetTemplatePage { /** The Sone core. */ protected final WebInterface webInterface; + /** The page title l10n key. */ + private final String pageTitleKey; + /** Whether to require a login. */ private final boolean requireLogin; @@ -53,6 +60,21 @@ public class SoneTemplatePage extends TemplatePage { * 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 Freetalk 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 @@ -69,6 +91,22 @@ public class SoneTemplatePage extends TemplatePage { * The path of the page * @param template * The template to render + * @param webInterface + * The Sone web interface + * @param requireLogin + * Whether this page requires a login + */ + public SoneTemplatePage(String path, Template template, WebInterface webInterface, boolean requireLogin) { + this(path, template, null, webInterface, requireLogin); + } + + /** + * Creates a new template page for Freetalk. + * + * @param path + * The path of the page + * @param template + * The template to render * @param pageTitleKey * The l10n key of the page title * @param webInterface @@ -77,7 +115,8 @@ public class SoneTemplatePage extends TemplatePage { * Whether this page requires a login */ public SoneTemplatePage(String path, Template template, String pageTitleKey, WebInterface webInterface, boolean requireLogin) { - super(path, webInterface.getTemplateContextFactory(), template, webInterface.getL10n(), pageTitleKey, "noPermission.html"); + super(path, webInterface.getTemplateContextFactory(), template, "noPermission.html"); + this.pageTitleKey = pageTitleKey; this.webInterface = webInterface; this.requireLogin = requireLogin; template.getInitialContext().set("webInterface", webInterface); @@ -163,6 +202,25 @@ public class SoneTemplatePage extends TemplatePage { * {@inheritDoc} */ @Override + protected String getPageTitle(Request request) { + if (pageTitleKey != null) { + return webInterface.getL10n().getString(pageTitleKey); + } + return ""; + } + + /** + * {@inheritDoc} + */ + @Override + protected List> getAdditionalLinkNodes(Request request) { + return new ListBuilder>().add(new MapBuilder().put("rel", "search").put("type", "application/opensearchdescription+xml").put("title", "Sone").put("href", "http://" + request.getHttpRequest().getHeader("host") + "/Sone/OpenSearch.xml").get()).get(); + } + + /** + * {@inheritDoc} + */ + @Override protected Collection getStyleSheets() { return Arrays.asList("css/sone.css"); } diff --git a/src/main/java/net/pterodactylus/sone/web/ViewPostPage.java b/src/main/java/net/pterodactylus/sone/web/ViewPostPage.java index 967b9c5..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,7 @@ 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; @@ -49,28 +49,29 @@ public class ViewPostPage extends SoneTemplatePage { * {@inheritDoc} */ @Override - protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException { - super.processTemplate(request, templateContext); + protected String getPageTitle(Request request) { String postId = request.getHttpRequest().getParam("post"); - boolean raw = request.getHttpRequest().getParam("raw").equals("true"); - Post post = webInterface.getCore().getPost(postId); - templateContext.set("post", post); - templateContext.set("raw", raw); + Post post = webInterface.getCore().getPost(postId, false); + String title = ""; + if ((post != null) && (post.getSone() != null)) { + title = post.getText().substring(0, Math.min(20, post.getText().length())) + "…"; + title += " - " + SoneAccessor.getNiceName(post.getSone()) + " - "; + } + title += webInterface.getL10n().getString("Page.ViewPost.Title"); + return title; } /** * {@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); - } + protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException { + super.processTemplate(request, templateContext); + String postId = request.getHttpRequest().getParam("post"); + boolean raw = request.getHttpRequest().getParam("raw").equals("true"); + Post post = webInterface.getCore().getPost(postId); + templateContext.set("post", post); + templateContext.set("raw", raw); } } diff --git a/src/main/java/net/pterodactylus/sone/web/ViewSonePage.java b/src/main/java/net/pterodactylus/sone/web/ViewSonePage.java index 0792a6f..14c4f94 100644 --- a/src/main/java/net/pterodactylus/sone/web/ViewSonePage.java +++ b/src/main/java/net/pterodactylus/sone/web/ViewSonePage.java @@ -17,11 +17,20 @@ package net.pterodactylus.sone.web; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; 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.sone.template.SoneAccessor; +import net.pterodactylus.util.collection.Pagination; +import net.pterodactylus.util.number.Numbers; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; @@ -41,7 +50,7 @@ public class ViewSonePage extends SoneTemplatePage { * The Sone web interface */ public ViewSonePage(Template template, WebInterface webInterface) { - super("viewSone.html", template, "Page.ViewSone.Title", webInterface, false); + super("viewSone.html", template, webInterface, false); } // @@ -52,30 +61,53 @@ public class ViewSonePage extends SoneTemplatePage { * {@inheritDoc} */ @Override - protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException { - super.processTemplate(request, templateContext); + protected String getPageTitle(Request request) { String soneId = request.getHttpRequest().getParam("sone"); Sone sone = webInterface.getCore().getSone(soneId, false); - templateContext.set("sone", sone); + if ((sone != null) && (sone.getTime() > 0)) { + String soneName = SoneAccessor.getNiceName(sone); + return soneName + " - " + webInterface.getL10n().getString("Page.ViewSone.Title"); + } + return webInterface.getL10n().getString("Page.ViewSone.Page.TitleWithoutSone"); } /** * {@inheritDoc} */ @Override - protected void postProcess(Request request, TemplateContext templateContext) { - Sone sone = (Sone) templateContext.get("sone"); - if (sone == null) { - return; - } - webInterface.getCore().markSoneKnown(sone); - List posts = sone.getPosts(); - for (Post post : posts) { - webInterface.getCore().markPostKnown(post); - for (Reply reply : webInterface.getCore().getReplies(post)) { - webInterface.getCore().markReplyKnown(reply); + protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException { + super.processTemplate(request, templateContext); + String soneId = request.getHttpRequest().getParam("sone"); + Sone sone = webInterface.getCore().getSone(soneId, false); + templateContext.set("sone", sone); + List sonePosts = sone.getPosts(); + 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()) || (sone.equals(post.getRecipient()))) { + continue; } + repliedPosts.put(post, webInterface.getCore().getReplies(post)); } + List posts = new ArrayList(repliedPosts.keySet()); + Collections.sort(posts, new Comparator() { + + @Override + public int compare(Post leftPost, Post rightPost) { + return (int) Math.min(Integer.MAX_VALUE, Math.max(Integer.MIN_VALUE, repliedPosts.get(rightPost).get(0).getTime() - repliedPosts.get(leftPost).get(0).getTime())); + } + + }); + + 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()); } } diff --git a/src/main/java/net/pterodactylus/sone/web/WebInterface.java b/src/main/java/net/pterodactylus/sone/web/WebInterface.java index 376ff09..f5b111f 100644 --- a/src/main/java/net/pterodactylus/sone/web/WebInterface.java +++ b/src/main/java/net/pterodactylus/sone/web/WebInterface.java @@ -49,7 +49,7 @@ import net.pterodactylus.sone.notify.ListNotification; import net.pterodactylus.sone.template.AlbumAccessor; import net.pterodactylus.sone.template.CollectionAccessor; import net.pterodactylus.sone.template.CssClassNameFilter; -import net.pterodactylus.sone.template.GetPagePlugin; +import net.pterodactylus.sone.template.HttpRequestAccessor; import net.pterodactylus.sone.template.IdentityAccessor; import net.pterodactylus.sone.template.ImageLinkFilter; import net.pterodactylus.sone.template.JavascriptFilter; @@ -57,6 +57,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; @@ -76,6 +77,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; @@ -89,7 +91,9 @@ 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; import net.pterodactylus.util.cache.CacheException; import net.pterodactylus.util.cache.CacheItem; @@ -105,7 +109,6 @@ import net.pterodactylus.util.template.DateFilter; import net.pterodactylus.util.template.FormatFilter; import net.pterodactylus.util.template.HtmlFilter; import net.pterodactylus.util.template.MatchFilter; -import net.pterodactylus.util.template.PaginationPlugin; import net.pterodactylus.util.template.Provider; import net.pterodactylus.util.template.ReflectionAccessor; import net.pterodactylus.util.template.ReplaceFilter; @@ -123,6 +126,7 @@ import freenet.clients.http.SessionManager.Session; import freenet.clients.http.ToadletContainer; import freenet.clients.http.ToadletContext; import freenet.l10n.BaseL10n; +import freenet.support.api.HTTPRequest; /** * Bundles functionality that a web interface of a Freenet plugin needs, e.g. @@ -204,6 +208,7 @@ public class WebInterface implements CoreListener { templateContextFactory.addAccessor(Identity.class, new IdentityAccessor(getCore())); templateContextFactory.addAccessor(NotificationManager.class, new NotificationManagerAccessor()); templateContextFactory.addAccessor(Trust.class, new TrustAccessor()); + templateContextFactory.addAccessor(HTTPRequest.class, new HttpRequestAccessor()); templateContextFactory.addFilter("date", new DateFilter()); templateContextFactory.addFilter("html", new HtmlFilter()); templateContextFactory.addFilter("replace", new ReplaceFilter()); @@ -220,19 +225,20 @@ public class WebInterface implements CoreListener { templateContextFactory.addFilter("format", new FormatFilter()); templateContextFactory.addFilter("sort", new CollectionSortFilter()); templateContextFactory.addFilter("image-link", new ImageLinkFilter(templateContextFactory)); + 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); @@ -545,6 +551,7 @@ public class WebInterface implements CoreListener { Template createPostTemplate = TemplateParser.parse(createReader("/templates/createPost.html")); Template createReplyTemplate = TemplateParser.parse(createReader("/templates/createReply.html")); Template bookmarksTemplate = TemplateParser.parse(createReader("/templates/bookmarks.html")); + Template searchTemplate = TemplateParser.parse(createReader("/templates/search.html")); Template editProfileTemplate = TemplateParser.parse(createReader("/templates/editProfile.html")); Template editProfileFieldTemplate = TemplateParser.parse(createReader("/templates/editProfileField.html")); Template deleteProfileFieldTemplate = TemplateParser.parse(createReader("/templates/deleteProfileField.html")); @@ -563,8 +570,10 @@ public class WebInterface implements CoreListener { Template invalidTemplate = TemplateParser.parse(createReader("/templates/invalid.html")); Template postTemplate = TemplateParser.parse(createReader("/templates/include/viewPost.html")); Template replyTemplate = TemplateParser.parse(createReader("/templates/include/viewReply.html")); + 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")); @@ -597,6 +606,7 @@ public class WebInterface implements CoreListener { pageToadlets.add(pageToadletFactory.createPageToadlet(new BookmarkPage(emptyTemplate, this))); pageToadlets.add(pageToadletFactory.createPageToadlet(new UnbookmarkPage(emptyTemplate, this))); pageToadlets.add(pageToadletFactory.createPageToadlet(new BookmarksPage(bookmarksTemplate, this), "Bookmarks")); + pageToadlets.add(pageToadletFactory.createPageToadlet(new SearchPage(searchTemplate, this))); pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteSonePage(deleteSoneTemplate, this), "DeleteSone")); pageToadlets.add(pageToadletFactory.createPageToadlet(new LoginPage(loginTemplate, this), "Login")); pageToadlets.add(pageToadletFactory.createPageToadlet(new LogoutPage(emptyTemplate, this), "Logout")); @@ -617,6 +627,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/FreenetTemplatePage.java b/src/main/java/net/pterodactylus/sone/web/page/FreenetTemplatePage.java new file mode 100644 index 0000000..0668154 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/web/page/FreenetTemplatePage.java @@ -0,0 +1,276 @@ +/* + * shortener - TemplatePage.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.web.page; + +import java.io.StringWriter; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.logging.Level; +import java.util.logging.Logger; + +import net.pterodactylus.sone.web.page.Page.Request.Method; +import net.pterodactylus.util.logging.Logging; +import net.pterodactylus.util.template.Template; +import net.pterodactylus.util.template.TemplateContext; +import net.pterodactylus.util.template.TemplateContextFactory; +import freenet.clients.http.LinkEnabledCallback; +import freenet.clients.http.PageMaker; +import freenet.clients.http.PageNode; +import freenet.clients.http.ToadletContext; +import freenet.support.HTMLNode; + +/** + * Base class for all {@link Page}s that are rendered with {@link Template}s and + * fit into Freenet’s web interface. + * + * @author David ‘Bombe’ Roden + */ +public class FreenetTemplatePage implements Page, LinkEnabledCallback { + + /** The logger. */ + private static final Logger logger = Logging.getLogger(FreenetTemplatePage.class); + + /** The path of the page. */ + private final String path; + + /** The template context factory. */ + private final TemplateContextFactory templateContextFactory; + + /** The template to render. */ + private final Template template; + + /** Where to redirect for invalid form passwords. */ + private final String invalidFormPasswordRedirectTarget; + + /** + * Creates a new template page. + * + * @param path + * The path of the page + * @param templateContextFactory + * The template context factory + * @param template + * The template to render + * @param invalidFormPasswordRedirectTarget + * The target to redirect to if a POST request does not contain + * the correct form password + */ + public FreenetTemplatePage(String path, TemplateContextFactory templateContextFactory, Template template, String invalidFormPasswordRedirectTarget) { + this.path = path; + this.templateContextFactory = templateContextFactory; + this.template = template; + this.invalidFormPasswordRedirectTarget = invalidFormPasswordRedirectTarget; + } + + /** + * {@inheritDoc} + */ + @Override + public String getPath() { + return path; + } + + /** + * Returns the title of the page. + * + * @param request + * The request to serve + * @return The title of the page + */ + protected String getPageTitle(Request request) { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public Response handleRequest(Request request) { + String redirectTarget = getRedirectTarget(request); + if (redirectTarget != null) { + return new RedirectResponse(redirectTarget); + } + + ToadletContext toadletContext = request.getToadletContext(); + if (request.getMethod() == Method.POST) { + /* require form password. */ + String formPassword = request.getHttpRequest().getPartAsStringFailsafe("formPassword", 32); + if (!formPassword.equals(toadletContext.getContainer().getFormPassword())) { + return new RedirectResponse(invalidFormPasswordRedirectTarget); + } + } + PageMaker pageMaker = toadletContext.getPageMaker(); + PageNode pageNode = pageMaker.getPageNode(getPageTitle(request), toadletContext); + for (String styleSheet : getStyleSheets()) { + pageNode.addCustomStyleSheet(styleSheet); + } + for (Map linkNodeParameters : getAdditionalLinkNodes(request)) { + HTMLNode linkNode = pageNode.headNode.addChild("link"); + for (Entry parameter : linkNodeParameters.entrySet()) { + linkNode.addAttribute(parameter.getKey(), parameter.getValue()); + } + } + String shortcutIcon = getShortcutIcon(); + if (shortcutIcon != null) { + pageNode.addForwardLink("icon", shortcutIcon); + } + + TemplateContext templateContext = templateContextFactory.createTemplateContext(); + templateContext.mergeContext(template.getInitialContext()); + try { + long start = System.nanoTime(); + processTemplate(request, templateContext); + long finish = System.nanoTime(); + logger.log(Level.FINEST, "Template was rendered in " + ((finish - start) / 1000) / 1000.0 + "ms."); + } catch (RedirectException re1) { + return new RedirectResponse(re1.getTarget()); + } + + StringWriter stringWriter = new StringWriter(); + template.render(templateContext, stringWriter); + pageNode.content.addChild("%", stringWriter.toString()); + + postProcess(request, templateContext); + + return new Response(200, "OK", "text/html", pageNode.outer.generate()); + } + + /** + * Can be overridden to return a custom set of style sheets that are to be + * included in the page’s header. + * + * @return Additional style sheets to load + */ + protected Collection getStyleSheets() { + return Collections.emptySet(); + } + + /** + * Returns the name of the shortcut icon to include in the page’s header. + * + * @return The URL of the shortcut icon, or {@code null} for no icon + */ + protected String getShortcutIcon() { + return null; + } + + /** + * Can be overridden when extending classes need to set variables in the + * template before it is rendered. + * + * @param request + * The request that is rendered + * @param templateContext + * The template context to set variables in + * @throws RedirectException + * if the processing page wants to redirect after processing + */ + protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException { + /* do nothing. */ + } + + /** + * This method will be called after + * {@link #processTemplate(net.pterodactylus.sone.web.page.Page.Request, TemplateContext)} + * has processed the template and the template was rendered. This method + * will not be called if + * {@link #processTemplate(net.pterodactylus.sone.web.page.Page.Request, TemplateContext)} + * throws a {@link RedirectException}! + * + * @param request + * The request being processed + * @param templateContext + * The template context that supplied the rendered data + */ + protected void postProcess(Request request, TemplateContext templateContext) { + /* do nothing. */ + } + + /** + * Can be overridden to redirect the user to a different page, in case a log + * in is required, or something else is wrong. + * + * @param request + * The request that is processed + * @return The URL to redirect to, or {@code null} to not redirect + */ + protected String getRedirectTarget(Page.Request request) { + return null; + } + + /** + * Returns additional <link> nodes for the HTML’s <head> node. + * + * @param request + * The request for which to return the link nodes + * @return All link nodes that should be added to the HTML head + */ + protected List> getAdditionalLinkNodes(Request request) { + return Collections.emptyList(); + } + + // + // INTERFACE LinkEnabledCallback + // + + /** + * {@inheritDoc} + */ + @Override + public boolean isEnabled(ToadletContext toadletContext) { + return true; + } + + /** + * Exception that can be thrown to signal that a subclassed {@link Page} + * wants to redirect the user during the + * {@link FreenetTemplatePage#processTemplate(net.pterodactylus.sone.web.page.Page.Request, TemplateContext)} + * method call. + * + * @author David ‘Bombe’ Roden + */ + public static class RedirectException extends Exception { + + /** The target to redirect to. */ + private final String target; + + /** + * Creates a new redirect exception. + * + * @param target + * The target of the redirect + */ + public RedirectException(String target) { + this.target = target; + } + + /** + * Returns the target to redirect to. + * + * @return The target to redirect to + */ + public String getTarget() { + return target; + } + + } + +} 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/java/net/pterodactylus/sone/web/page/TemplatePage.java b/src/main/java/net/pterodactylus/sone/web/page/TemplatePage.java index 9b40deb..06fc9fc 100644 --- a/src/main/java/net/pterodactylus/sone/web/page/TemplatePage.java +++ b/src/main/java/net/pterodactylus/sone/web/page/TemplatePage.java @@ -1,5 +1,5 @@ /* - * shortener - TemplatePage.java - Copyright © 2010 David Roden + * Sone - StaticTemplatePage.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 @@ -17,75 +17,59 @@ package net.pterodactylus.sone.web.page; -import java.io.StringWriter; -import java.util.Collection; -import java.util.Collections; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; import java.util.logging.Level; import java.util.logging.Logger; -import net.pterodactylus.sone.web.page.Page.Request.Method; +import net.pterodactylus.util.io.Closer; import net.pterodactylus.util.logging.Logging; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; import net.pterodactylus.util.template.TemplateContextFactory; -import freenet.clients.http.LinkEnabledCallback; -import freenet.clients.http.PageMaker; -import freenet.clients.http.PageNode; -import freenet.clients.http.ToadletContext; -import freenet.l10n.BaseL10n; /** - * Base class for all {@link Page}s that are rendered with {@link Template}s. + * A template page is a single page that is created from a {@link Template} but + * does not necessarily return HTML. * * @author David ‘Bombe’ Roden */ -public class TemplatePage implements Page, LinkEnabledCallback { +public class TemplatePage implements Page { /** The logger. */ private static final Logger logger = Logging.getLogger(TemplatePage.class); - /** The path of the page. */ + /** The path of this page. */ private final String path; + /** The content type of this page. */ + private final String contentType; + /** The template context factory. */ private final TemplateContextFactory templateContextFactory; /** The template to render. */ private final Template template; - /** The L10n handler. */ - private final BaseL10n l10n; - - /** The l10n key for the page title. */ - private final String pageTitleKey; - - /** Where to redirect for invalid form passwords. */ - private final String invalidFormPasswordRedirectTarget; - /** * Creates a new template page. * * @param path * The path of the page + * @param contentType + * The content type of the page * @param templateContextFactory * The template context factory * @param template * The template to render - * @param l10n - * The L10n handler - * @param pageTitleKey - * The l10n key of the title page - * @param invalidFormPasswordRedirectTarget - * The target to redirect to if a POST request does not contain - * the correct form password */ - public TemplatePage(String path, TemplateContextFactory templateContextFactory, Template template, BaseL10n l10n, String pageTitleKey, String invalidFormPasswordRedirectTarget) { + public TemplatePage(String path, String contentType, TemplateContextFactory templateContextFactory, Template template) { this.path = path; + this.contentType = contentType; this.templateContextFactory = templateContextFactory; this.template = template; - this.l10n = l10n; - this.pageTitleKey = pageTitleKey; - this.invalidFormPasswordRedirectTarget = invalidFormPasswordRedirectTarget; } /** @@ -101,156 +85,22 @@ public class TemplatePage implements Page, LinkEnabledCallback { */ @Override public Response handleRequest(Request request) { - String redirectTarget = getRedirectTarget(request); - if (redirectTarget != null) { - return new RedirectResponse(redirectTarget); - } - - ToadletContext toadletContext = request.getToadletContext(); - if (request.getMethod() == Method.POST) { - /* require form password. */ - String formPassword = request.getHttpRequest().getPartAsStringFailsafe("formPassword", 32); - if (!formPassword.equals(toadletContext.getContainer().getFormPassword())) { - return new RedirectResponse(invalidFormPasswordRedirectTarget); - } - } - PageMaker pageMaker = toadletContext.getPageMaker(); - PageNode pageNode = pageMaker.getPageNode(l10n.getString(pageTitleKey), toadletContext); - for (String styleSheet : getStyleSheets()) { - pageNode.addCustomStyleSheet(styleSheet); - } - String shortcutIcon = getShortcutIcon(); - if (shortcutIcon != null) { - pageNode.addForwardLink("icon", shortcutIcon); - } - - TemplateContext templateContext = templateContextFactory.createTemplateContext(); - templateContext.mergeContext(template.getInitialContext()); + ByteArrayOutputStream responseOutputStream = new ByteArrayOutputStream(); + OutputStreamWriter responseWriter = null; try { - long start = System.nanoTime(); - processTemplate(request, templateContext); - long finish = System.nanoTime(); - logger.log(Level.FINEST, "Template was rendered in " + ((finish - start) / 1000) / 1000.0 + "ms."); - } catch (RedirectException re1) { - return new RedirectResponse(re1.getTarget()); - } - - StringWriter stringWriter = new StringWriter(); - template.render(templateContext, stringWriter); - pageNode.content.addChild("%", stringWriter.toString()); - - postProcess(request, templateContext); - - return new Response(200, "OK", "text/html", pageNode.outer.generate()); - } - - /** - * Can be overridden to return a custom set of style sheets that are to be - * included in the page’s header. - * - * @return Additional style sheets to load - */ - protected Collection getStyleSheets() { - return Collections.emptySet(); - } - - /** - * Returns the name of the shortcut icon to include in the page’s header. - * - * @return The URL of the shortcut icon, or {@code null} for no icon - */ - protected String getShortcutIcon() { - return null; - } - - /** - * Can be overridden when extending classes need to set variables in the - * template before it is rendered. - * - * @param request - * The request that is rendered - * @param templateContext - * The template context to set variables in - * @throws RedirectException - * if the processing page wants to redirect after processing - */ - protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException { - /* do nothing. */ - } - - /** - * This method will be called after - * {@link #processTemplate(net.pterodactylus.sone.web.page.Page.Request, TemplateContext)} - * has processed the template and the template was rendered. This method - * will not be called if - * {@link #processTemplate(net.pterodactylus.sone.web.page.Page.Request, TemplateContext)} - * throws a {@link RedirectException}! - * - * @param request - * The request being processed - * @param templateContext - * The template context that supplied the rendered data - */ - protected void postProcess(Request request, TemplateContext templateContext) { - /* do nothing. */ - } - - /** - * Can be overridden to redirect the user to a different page, in case a log - * in is required, or something else is wrong. - * - * @param request - * The request that is processed - * @return The URL to redirect to, or {@code null} to not redirect - */ - protected String getRedirectTarget(Page.Request request) { - return null; - } - - // - // INTERFACE LinkEnabledCallback - // - - /** - * {@inheritDoc} - */ - @Override - public boolean isEnabled(ToadletContext toadletContext) { - return true; - } - - /** - * Exception that can be thrown to signal that a subclassed {@link Page} - * wants to redirect the user during the - * {@link TemplatePage#processTemplate(net.pterodactylus.sone.web.page.Page.Request, TemplateContext)} - * method call. - * - * @author David ‘Bombe’ Roden - */ - public class RedirectException extends Exception { - - /** The target to redirect to. */ - private final String target; - - /** - * Creates a new redirect exception. - * - * @param target - * The target of the redirect - */ - public RedirectException(String target) { - this.target = target; + responseWriter = new OutputStreamWriter(responseOutputStream, "UTF-8"); + TemplateContext templateContext = templateContextFactory.createTemplateContext(); + templateContext.set("request", request); + template.render(templateContext, responseWriter); + } catch (IOException ioe1) { + logger.log(Level.WARNING, "Could not render template for path “" + path + "”!", ioe1); + } finally { + Closer.close(responseWriter); + Closer.close(responseOutputStream); } - - /** - * Returns the target to redirect to. - * - * @return The target to redirect to - */ - public String getTarget() { - return target; - } - + ByteArrayInputStream responseInputStream = new ByteArrayInputStream(responseOutputStream.toByteArray()); + /* no need to close a ByteArrayInputStream. */ + return new Response(200, "OK", contentType, null, responseInputStream); } } diff --git a/src/main/resources/i18n/sone.en.properties b/src/main/resources/i18n/sone.en.properties index a5ab0c1..a5f423e 100644 --- a/src/main/resources/i18n/sone.en.properties +++ b/src/main/resources/i18n/sone.en.properties @@ -25,12 +25,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. @@ -127,6 +136,7 @@ Page.ViewSone.PostList.Title=Posts by {sone} Page.ViewSone.PostList.Text.NoPostYet=This Sone has not yet posted anything. Page.ViewSone.Profile.Title=Profile Page.ViewSone.Profile.Label.Name=Name +Page.ViewSone.Replies.Title=Replies to Posts Page.ViewPost.Title=View Post - Sone Page.ViewPost.Page.Title=View Post by {sone} @@ -212,6 +222,12 @@ Page.Bookmarks.Page.Title=Bookmarks Page.Bookmarks.Text.NoBookmarks=You don’t have any bookmarks defined right now. You can bookmark posts by clicking the star below the post. Page.Bookmarks.Text.PostsNotLoaded=Some of your bookmarked posts have not been shown because they could not be loaded. This can happen if you restarted Sone recently or if the originating Sone has deleted the post. If you are reasonable sure that these posts do not exist anymore, you can {link}unbookmark them{/link}. +Page.Search.Title=Search - Sone +Page.Search.Page.Title=Search Results +Page.Search.Text.SoneHits=The following Sones match your search terms. +Page.Search.Text.PostHits=The following posts match your search terms. +Page.Search.Text.NoHits=No Sones or posts matched your search terms. + Page.NoPermission.Title=Unauthorized Access - Sone Page.NoPermission.Page.Title=Unauthorized Access Page.NoPermission.Text.NoPermission=You tried to do something that you do not have sufficient authorization for. Please refrain from such actions in the future or we will be forced to take counter-measures! @@ -227,10 +243,12 @@ Page.Invalid.Title=Invalid Action Performed Page.Invalid.Page.Title=Invalid Action Performed Page.Invalid.Text=An invalid action was performed, or the action was valid but the parameters were not. Please go back to the {link}index page{/link} and try again. If the error persists you have probably found a bug. +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. @@ -257,6 +275,7 @@ View.Post.Reply.DeleteLink=Delete View.Post.LikeLink=Like View.Post.UnlikeLink=Unlike View.Post.ShowSource=Toggle Parser +View.Post.NotDownloaded=This post has not yet been downloaded, or it has been deleted. View.UpdateStatus.Text.ChooseSenderIdentity=Choose the sender identity @@ -273,6 +292,23 @@ View.UploadImage.Label.Title=Title: View.UploadImage.Label.Description=Description: View.UploadImage.Button.UploadImage=Upload Image +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… @@ -293,6 +329,10 @@ WebInterface.DefaultText.UploadImage.Title=Image title WebInterface.DefaultText.UploadImage.Description=Image description WebInterface.DefaultText.EditImage.Title=Image title WebInterface.DefaultText.EditImage.Description=Image description +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.Confirmation.DeletePostButton=Yes, delete! WebInterface.Confirmation.DeleteReplyButton=Yes, delete! WebInterface.SelectBox.Choose=Choose… @@ -313,7 +353,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 ab242a2..9470850 100644 --- a/src/main/resources/static/css/sone.css +++ b/src/main/resources/static/css/sone.css @@ -125,6 +125,10 @@ textarea { float: right; } +#sone #notification-area .notification .hidden { + display: none; +} + #sone #plugin-warning { border: solid 0.5em red; padding: 0.5em; @@ -579,6 +583,19 @@ textarea { display: inline; } +#sone #search { + text-align: right; +} + +#sone #search input[type=text] { + width: 35em; +} + +#sone #sone-results + #sone #post-results { + clear: both; + padding-top: 1em; +} + #sone #tail { margin-top: 1em; border-top: solid 1px #ccc; diff --git a/src/main/resources/static/images/sone-avatar.png b/src/main/resources/static/images/sone-avatar.png new file mode 100644 index 0000000..0339b5a Binary files /dev/null and b/src/main/resources/static/images/sone-avatar.png differ diff --git a/src/main/resources/static/javascript/sone.js b/src/main/resources/static/javascript/sone.js index 9ef6812..d58f17f 100644 --- a/src/main/resources/static/javascript/sone.js +++ b/src/main/resources/static/javascript/sone.js @@ -33,11 +33,12 @@ function registerInputTextareaSwap(inputElement, defaultText, inputFieldName, op inputField.val(defaultText); } }).hide().data("inputField", $(this)).val($(this).val()); - $(this).after(textarea); + $(this).data("textarea", textarea).after(textarea); (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"); @@ -49,6 +50,7 @@ function registerInputTextareaSwap(inputElement, defaultText, inputFieldName, op $(inputField.get(0).form).submit(function() { inputField.attr("disabled", "disabled"); if (!optional && (textarea.val() == "")) { + inputField.removeAttr("disabled").focus(); return false; } }); @@ -64,11 +66,11 @@ function registerInputTextareaSwap(inputElement, defaultText, inputFieldName, op * @param element * The element to add a “comment” link to */ -function addCommentLink(postId, element, insertAfterThisElement) { +function addCommentLink(postId, author, element, insertAfterThisElement) { if (($(element).find(".show-reply-form").length > 0) || (getPostElement(element).find(".create-reply").length == 0)) { return; } - commentElement = (function(postId) { + commentElement = (function(postId, author) { separator = $(" · ").addClass("separator"); var commentElement = $("
Comment
").addClass("show-reply-form").click(function() { replyElement = $("#sone .post#" + postId + " .create-reply"); @@ -83,10 +85,11 @@ function addCommentLink(postId, element, insertAfterThisElement) { replyElement.removeClass("light"); }); })(replyElement); - replyElement.find("input.reply-input").focus(); + textArea = replyElement.find("input.reply-input").focus().data("textarea"); + textArea.val(textArea.val() + "@sone://" + author + " "); }); return commentElement; - })(postId); + })(postId, author); $(insertAfterThisElement).after(commentElement.clone(true)); $(insertAfterThisElement).after(separator); } @@ -216,7 +219,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") { @@ -247,7 +251,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") { @@ -263,6 +268,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"); } @@ -331,6 +349,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"); } @@ -354,6 +383,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) { @@ -560,25 +622,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 @@ -624,7 +667,7 @@ function ajaxifySone(soneElement) { /* mark Sone as known when clicking it. */ $(soneElement).click(function() { - markSoneAsKnown(soneElement); + markSoneAsKnown(this); }); } @@ -639,6 +682,8 @@ function ajaxifyPost(postElement) { return false; }); $(postElement).find(".create-reply button:submit").click(function() { + button = $(this); + button.attr("disabled", "disabled"); sender = $(this.form).find(":input[name=sender]").val(); inputField = $(this.form).find(":input[name=text]:enabled").get(0); postId = getPostId(this); @@ -655,6 +700,7 @@ function ajaxifyPost(postElement) { } else { alert(error); } + button.removeAttr("disabled"); }); })(sender, postId, text, inputField); return false; @@ -712,12 +758,15 @@ function ajaxifyPost(postElement) { }); /* add “comment” link. */ - addCommentLink(getPostId(postElement), postElement, $(postElement).find(".post-status-line .time")); + addCommentLink(getPostId(postElement), getPostAuthor(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) { @@ -769,7 +818,7 @@ function ajaxifyReply(replyElement) { }); }); })(replyElement); - addCommentLink(getPostId(replyElement), replyElement, $(replyElement).find(".reply-status-line .time")); + addCommentLink(getPostId(replyElement), getReplyAuthor(replyElement), replyElement, $(replyElement).find(".reply-status-line .time")); /* convert “show source” link into javascript function. */ $(replyElement).find(".show-reply-source").each(function() { @@ -833,6 +882,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) { @@ -840,9 +973,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)) { @@ -850,15 +1019,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) { @@ -1017,6 +1186,7 @@ function loadNewPost(postId, soneId, recipientId, time) { newPost.insertBefore(firstOlderPost); } ajaxifyPost(newPost); + updatePostTimes(data.post.id); newPost.slideDown(); setActivity(); } @@ -1055,6 +1225,7 @@ function loadNewReply(replyId, soneId, postId, postSoneId) { } } ajaxifyReply(newReply); + updateReplyTimes(data.reply.id); newReply.slideDown(); setActivity(); return false; @@ -1068,46 +1239,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() { @@ -1146,7 +1404,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; @@ -1154,7 +1412,6 @@ function toggleIcon() { iconBlinking = false; } else { iconActive = !iconActive; - console.log("showing icon: " + iconActive); changeIcon(iconActive ? "images/icon-activity.png" : "images/icon.png"); setTimeout(toggleIcon, 1500); } @@ -1298,15 +1555,15 @@ $(document).ready(function() { return false; }); $("#sone #update-status").submit(function() { + button = $("button:submit", this); + button.attr("disabled", "disabled"); if ($(this).find(":input.default:enabled").length > 0) { return false; } 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()); $(this).find(":input[name=text]:enabled").val("").blur(); @@ -1316,6 +1573,11 @@ $(document).ready(function() { }); }); + /* ajaxify the search input field. */ + getTranslation("WebInterface.DefaultText.Search", function(defaultText) { + registerInputTextareaSwap("#sone #search input[name=query]", defaultText, "query", false, true); + }); + /* ajaxify input field on “view Sone” page. */ getTranslation("WebInterface.DefaultText.Message", function(defaultText) { registerInputTextareaSwap("#sone #post-message input[name=text]", defaultText, "text", false, false); @@ -1329,11 +1591,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(); @@ -1354,6 +1612,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/editProfile.html b/src/main/resources/templates/editProfile.html index ecde2f7..fea58a8 100644 --- a/src/main/resources/templates/editProfile.html +++ b/src/main/resources/templates/editProfile.html @@ -28,7 +28,7 @@ registerInputTextareaSwap("#sone #edit-profile input[name=birth-year]", birthYearDefaultText, "birth-year", true, true); }); getTranslation("WebInterface.DefaultText.FieldName", function(fieldNameDefaultText) { - registerInputTextareaSwap("#sone #add-profile-field input[name=field-name]", fieldNameDefaultText, "field-name", true, true); + registerInputTextareaSwap("#sone #add-profile-field input[name=field-name]", fieldNameDefaultText, "field-name", false, true); }); <%foreach fields field> diff --git a/src/main/resources/templates/include/createSone.html b/src/main/resources/templates/include/createSone.html index 30ce0f7..c0df5c0 100644 --- a/src/main/resources/templates/include/createSone.html +++ b/src/main/resources/templates/include/createSone.html @@ -1,7 +1,7 @@ <%if !identitiesWithoutSone.empty>

<%= Page.Login.CreateSone.Title|l10n|html>

-

<%= View.CreateSone.Text.WotIdentityRequired|l10n|html|replace needle="{link}" replacement=''|replace needle="{/link}" replacement=''>

+

<%= View.CreateSone.Text.WotIdentityRequired|l10n|html|replace needle="{link}" replacement=''|replace needle="{/link}" replacement=''>

@@ -23,8 +23,8 @@
<%else> <%if !sones.empty> -

<%= View.CreateSone.Text.NoNonSoneIdentities|l10n|html|replace needle="{link}" replacement=''|replace needle="{/link}" replacement="">

+

<%= View.CreateSone.Text.NoNonSoneIdentities|l10n|html|replace needle="{link}" replacement=''|replace needle="{/link}" replacement="">

<%else> -

<%= View.CreateSone.Text.NoIdentities|l10n|html|replace needle="{link}" replacement=''|replace needle="{/link}" replacement="">

+

<%= View.CreateSone.Text.NoIdentities|l10n|html|replace needle="{link}" replacement=''|replace needle="{/link}" replacement="">

<%/if> <%/if> diff --git a/src/main/resources/templates/include/head.html b/src/main/resources/templates/include/head.html index a9fbb49..fe9cf34 100644 --- a/src/main/resources/templates/include/head.html +++ b/src/main/resources/templates/include/head.html @@ -36,7 +36,7 @@ <%/if> + diff --git a/src/main/resources/templates/include/tail.html b/src/main/resources/templates/include/tail.html index ec2b587..7ab6b22 100644 --- a/src/main/resources/templates/include/tail.html +++ b/src/main/resources/templates/include/tail.html @@ -4,7 +4,7 @@
diff --git a/src/main/resources/templates/include/viewPost.html b/src/main/resources/templates/include/viewPost.html index 3023566..4be1bcc 100644 --- a/src/main/resources/templates/include/viewPost.html +++ b/src/main/resources/templates/include/viewPost.html @@ -3,10 +3,14 @@
- Avatar Image + <%if post.loaded> + Avatar Image + <%else> + Avatar Image + <%/if>
-
+ class="hidden"<%/if>> <%ifnull !post.recipient> → @@ -16,10 +20,12 @@ <%/if> <%/if> -
<% post.text|html>
-
<% post.text|parse sone=post.sone>
+ <% post.text|html|store key=originalText text=true> + <% post.text|parse sone=post.sone|store key=parsedText text=true> +
<% originalText>
+
<% parsedText>
-
+
@@ -36,8 +42,10 @@
· - · - + <%if ! originalText|match key=parsedText> + · + + <%/if>
· ↑ @@ -90,6 +98,9 @@ <%/if>
+ class="hidden"<%/if>> + <%= View.Post.NotDownloaded|l10n|html> +
<%foreach post.replies reply> <%include include/viewReply.html> diff --git a/src/main/resources/templates/include/viewReply.html b/src/main/resources/templates/include/viewReply.html index cdb837c..5dc26fb 100644 --- a/src/main/resources/templates/include/viewReply.html +++ b/src/main/resources/templates/include/viewReply.html @@ -3,18 +3,22 @@
- Avatar Image + Avatar Image
-
<% reply.text|html>
-
<% reply.text|parse sone=reply.sone>
+ <% reply.text|html|store key=originalText text=true> + <% reply.text|parse sone=reply.sone|store key=parsedText text=true> +
<% originalText>
+
<% parsedText>
<% reply.time|date format="MMM d, yyyy, HH:mm:ss">
- · - + <%if ! originalText|match key=parsedText> + · + + <%/if>
· ↑ 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/main/resources/templates/search.html b/src/main/resources/templates/search.html new file mode 100644 index 0000000..9f87072 --- /dev/null +++ b/src/main/resources/templates/search.html @@ -0,0 +1,35 @@ +<%include include/head.html> + +

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

+ + <%foreach soneHits sone> + <%first> +
+

<%= Page.Search.Text.SoneHits|l10n|html>

+ <%include include/pagination.html pagination=sonePagination pageParameter==sonePage> + <%/first> + <%include include/viewSone.html> + <%last> + <%include include/pagination.html pagination=sonePagination pageParameter==sonePage> +
+ <%/last> + <%/foreach> + + <%foreach postHits post> + <%first> +
+

<%= Page.Search.Text.PostHits|l10n|html>

+ <%include include/pagination.html pagination=postPagination pageParameter==postPage> + <%/first> + <%include include/viewPost.html> + <%last> + <%include include/pagination.html pagination=postPagination pageParameter==postPage> +
+ <%/last> + <%/foreach> + + <%if soneHits.empty><%if postHits.empty> +

<%= Page.Search.Text.NoHits|l10n|html>

+ <%/if><%/if> + +<%include include/tail.html> diff --git a/src/main/resources/templates/viewSone.html b/src/main/resources/templates/viewSone.html index d2a8f6e..066adef 100644 --- a/src/main/resources/templates/viewSone.html +++ b/src/main/resources/templates/viewSone.html @@ -25,7 +25,7 @@
<%= Page.ViewSone.Profile.Label.Name|l10n|html>
- +
<%foreach sone.profile.fields field> @@ -60,18 +60,32 @@

<%= Page.ViewSone.PostList.Title|l10n|replace needle="{sone}" replacementKey=sone.niceName|html>

-
- <%:getpage parameter=postPage> - <%:paginate list=sone.posts pagesize=25> - <%= postPage|store key=pageParameter> - <%include include/pagination.html> - <%foreach pagination.items post> - <%include include/viewPost.html> - <%foreachelse> -
<%= Page.ViewSone.PostList.Text.NoPostYet|l10n|html>
- <%/foreach> - <%include include/pagination.html> -
+ <%foreach posts post> + <%first> +
+ <%include include/pagination.html pagination=postPagination pageParameter==postPage> + <%/first> + <%include include/viewPost.html> + <%last> + <%include include/pagination.html pagination=postPagination pageParameter==postPage> +
+ <%/last> + <%foreachelse> +
<%= Page.ViewSone.PostList.Text.NoPostYet|l10n|html>
+ <%/foreach> + + <%foreach repliedPosts post> + <%first> +

<%= Page.ViewSone.Replies.Title|l10n|html>

+
+ <%include include/pagination.html pagination=repliedPostPagination pageParameter==repliedPostPage> + <%/first> + <%include include/viewPost.html> + <%last> + <%include include/pagination.html pagination=repliedPostPagination pageParameter==repliedPostPage> +
+ <%/last> + <%/foreach> <%/if> diff --git a/src/main/resources/templates/xml/OpenSearch.xml b/src/main/resources/templates/xml/OpenSearch.xml new file mode 100644 index 0000000..37f0d35 --- /dev/null +++ b/src/main/resources/templates/xml/OpenSearch.xml @@ -0,0 +1,7 @@ + + + Sone + Search Sone Profiles and Posts + http://<%request.httpRequest.host>/Sone/images/icon.png + + diff --git a/src/test/java/net/pterodactylus/sone/text/FreenetLinkParserTest.java b/src/test/java/net/pterodactylus/sone/text/FreenetLinkParserTest.java new file mode 100644 index 0000000..7e05fd5 --- /dev/null +++ b/src/test/java/net/pterodactylus/sone/text/FreenetLinkParserTest.java @@ -0,0 +1,78 @@ +/* + * Sone - FreenetLinkParserTest.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.text; + +import java.io.IOException; +import java.io.StringReader; + +import junit.framework.TestCase; +import net.pterodactylus.util.template.HtmlFilter; +import net.pterodactylus.util.template.TemplateContextFactory; + +/** + * JUnit test case for {@link FreenetLinkParser}. + * + * @author David ‘Bombe’ Roden + */ +public class FreenetLinkParserTest extends TestCase { + + /** + * Tests the parser. + * + * @throws IOException + * if an I/O error occurs + */ + public void testParser() throws IOException { + TemplateContextFactory templateContextFactory = new TemplateContextFactory(); + templateContextFactory.addFilter("html", new HtmlFilter()); + FreenetLinkParser parser = new FreenetLinkParser(null, templateContextFactory); + FreenetLinkParserContext context = new FreenetLinkParserContext(null); + Part part; + + part = parser.parse(context, new StringReader("Text.")); + assertEquals("Text.", part.toString()); + + part = parser.parse(context, new StringReader("Text.\nText.")); + assertEquals("Text.\nText.", part.toString()); + + part = parser.parse(context, new StringReader("Text.\n\nText.")); + assertEquals("Text.\n\nText.", part.toString()); + + part = parser.parse(context, new StringReader("Text.\n\n\nText.")); + assertEquals("Text.\n\nText.", part.toString()); + + part = parser.parse(context, new StringReader("\nText.\n\n\nText.")); + assertEquals("Text.\n\nText.", part.toString()); + + part = parser.parse(context, new StringReader("\nText.\n\n\nText.\n")); + assertEquals("Text.\n\nText.", part.toString()); + + part = parser.parse(context, new StringReader("\nText.\n\n\nText.\n\n")); + assertEquals("Text.\n\nText.", part.toString()); + + part = parser.parse(context, new StringReader("\nText.\n\n\n\nText.\n\n\n")); + assertEquals("Text.\n\nText.", part.toString()); + + part = parser.parse(context, new StringReader("\n\nText.\n\n\n\nText.\n\n\n")); + assertEquals("Text.\n\nText.", part.toString()); + + part = parser.parse(context, new StringReader("\n\nText. KSK@a text.\n\n\n\nText.\n\n\n")); + assertEquals("Text. a text.\n\nText.", part.toString()); + } + +}