<modelVersion>4.0.0</modelVersion>
<groupId>net.pterodactylus</groupId>
<artifactId>sone</artifactId>
- <version>0.5.1</version>
+ <version>0.6.1</version>
<dependencies>
<dependency>
<groupId>net.pterodactylus</groupId>
<artifactId>utils</artifactId>
- <version>0.9.1</version>
+ <version>0.9.3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
* @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());
}
/**
*
* @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);
}
/**
+ * Returns all posts that have the given Sone as recipient.
+ *
+ * @see Post#getRecipient()
+ * @param recipient
+ * The recipient of the posts
+ * @return All posts that have the given Sone as recipient
+ */
+ public Set<Post> getDirectedPosts(Sone recipient) {
+ Validation.begin().isNotNull("Recipient", recipient).check();
+ Set<Post> directedPosts = new HashSet<Post>();
+ synchronized (posts) {
+ for (Post post : posts.values()) {
+ if (recipient.equals(post.getRecipient())) {
+ directedPosts.add(post);
+ }
+ }
+ }
+ return directedPosts;
+ }
+
+ /**
* Returns the reply with the given ID. If there is no reply with the given
* ID yet, a new one is created.
*
@SuppressWarnings("synthetic-access")
public void run() {
if (!preferences.isSoneRescueMode()) {
- soneDownloader.fetchSone(sone);
return;
}
logger.log(Level.INFO, "Trying to restore Sone from Freenet…");
return null;
}
Sone sone = addLocalSone(ownIdentity);
+ sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
+ saveSone(sone);
return sone;
}
}
if (newSone) {
coreListenerManager.fireNewSoneFound(sone);
+ for (Sone localSone : getLocalSones()) {
+ if (localSone.getOptions().getBooleanOption("AutoFollow").get()) {
+ localSone.addFriend(sone.getId());
+ }
+ }
}
}
remoteSones.put(identity.getId(), sone);
posts.put(post.getId(), post);
}
synchronized (newPosts) {
- knownPosts.add(post.getId());
+ newPosts.add(post.getId());
+ coreListenerManager.fireNewPostFound(post);
}
sone.addPost(post);
saveSone(sone);
synchronized (posts) {
posts.remove(post.getId());
}
+ synchronized (newPosts) {
+ markPostKnown(post);
+ knownPosts.remove(post.getId());
+ }
saveSone(post.getSone());
}
replies.put(reply.getId(), reply);
}
synchronized (newReplies) {
- knownReplies.add(reply.getId());
+ newReplies.add(reply.getId());
+ coreListenerManager.fireNewReplyFound(reply);
}
sone.addReply(reply);
saveSone(sone);
synchronized (replies) {
replies.remove(reply.getId());
}
+ synchronized (newReplies) {
+ markReplyKnown(reply);
+ knownReplies.remove(reply.getId());
+ }
sone.removeReply(reply);
saveSone(sone);
}
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());
}
}));
+ options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(10));
options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75));
- options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-100));
+ options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-25));
options.addStringOption("TrustComment", new DefaultOption<String>("Set from Sone Web Interface"));
options.addBooleanOption("SoneRescueMode", new DefaultOption<Boolean>(false));
options.addBooleanOption("ClearOnNextRestart", new DefaultOption<Boolean>(false));
}
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));
}
/**
+ * 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
protected void serviceRun() {
long lastModificationTime = 0;
String lastFingerprint = "";
- while (!shouldStop()) {
+ while (!shouldStop()) { try {
/* check every seconds. */
sleep(1000);
}
}
}
- }
+ } catch (Throwable t1) {
+ logger.log(Level.SEVERE, "SoneInserter threw an Exception!", t1);
+ }}
}
/**
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
- private class InsertInformation {
+ private static class InsertInformation {
/** All properties of the Sone, copied for thread safety. */
private final Map<String, Object> soneProperties = new HashMap<String, Object>();
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.
};
+ /** Filter for posts with timestamps from the future. */
+ public static final Filter<Post> FUTURE_POSTS_FILTER = new Filter<Post>() {
+
+ @Override
+ public boolean filterObject(Post post) {
+ return post.getTime() <= System.currentTimeMillis();
+ }
+
+ };
+
/** The GUID of the post. */
private final UUID id;
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}.
};
+ /** Filter for replies with timestamps from the future. */
+ public static final Filter<Reply> FUTURE_REPLIES_FILTER = new Filter<Reply>() {
+
+ @Override
+ public boolean filterObject(Reply reply) {
+ return reply.getTime() <= System.currentTimeMillis();
+ }
+
+ };
+
/** The ID of the reply. */
private final UUID id;
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;
};
+ /** Filter to remove Sones that have not been downloaded. */
+ public static final Filter<Sone> EMPTY_SONE_FILTER = new Filter<Sone>() {
+
+ @Override
+ public boolean filterObject(Sone sone) {
+ return sone.getTime() != 0;
+ }
+ };
+
/** The logger. */
private static final Logger logger = Logging.getLogger(Sone.class);
/** The albums of this Sone. */
private final List<Album> albums = Collections.synchronizedList(new ArrayList<Album>());
+ /** Sone-specific options. */
+ private final Options options = new Options();
+
/**
* Creates a new Sone.
*
* @return This Sone (for method chaining)
*/
public synchronized Sone setPosts(Collection<Post> posts) {
- this.posts.clear();
- this.posts.addAll(posts);
+ synchronized (this) {
+ this.posts.clear();
+ this.posts.addAll(posts);
+ }
return this;
}
albums.remove(album);
}
+ /**
+ * Returns Sone-specific options.
+ *
+ * @return The options of this Sone
+ */
+ public Options getOptions() {
+ return options;
+ }
+
//
// FINGERPRINTABLE METHODS
//
}
}
+ //
+ // 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);
+ }
+
}
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());
* 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");
}
/** 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);
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;
*/
public class ListNotification<T> extends TemplateNotification {
+ /** The key under which to store the elements in the template. */
+ private final String key;
+
/** The list of new elements. */
- private final List<T> elements = Collections.synchronizedList(new ArrayList<T>());
+ private final List<T> elements = new CopyOnWriteArrayList<T>();
/**
* Creates a new list notification.
* The template to render
*/
public ListNotification(String id, String key, Template template) {
- super(id, template);
+ this(id, key, template, true);
+ }
+
+ /**
+ * Creates a new list notification.
+ *
+ * @param id
+ * The ID of the notification
+ * @param key
+ * The key under which to store the elements in the template
+ * @param template
+ * The template to render
+ * @param dismissable
+ * {@code true} if this notification should be dismissable by the
+ * user, {@code false} otherwise
+ */
+ public ListNotification(String id, String key, Template template, boolean dismissable) {
+ super(id, System.currentTimeMillis(), System.currentTimeMillis(), dismissable, template);
+ this.key = key;
template.getInitialContext().set(key, elements);
}
+ /**
+ * Creates a new list notification that copies its ID and the template from
+ * the given list notification.
+ *
+ * @param listNotification
+ * The list notification to copy
+ */
+ public ListNotification(ListNotification<T> listNotification) {
+ super(listNotification.getId(), listNotification.getCreatedTime(), listNotification.getLastUpdatedTime(), listNotification.isDismissable(), new Template());
+ this.key = listNotification.key;
+ getTemplate().add(listNotification.getTemplate());
+ getTemplate().getInitialContext().set(key, elements);
+ }
+
//
// ACTIONS
//
}
/**
+ * Sets the elements to show in this notification.
+ *
+ * @param elements
+ * The elements to show
+ */
+ public void setElements(Collection<? extends T> elements) {
+ this.elements.clear();
+ this.elements.addAll(elements);
+ touch();
+ }
+
+ /**
* Returns whether there are any new elements.
*
* @return {@code true} if there are no new elements, {@code false} if there
--- /dev/null
+/*
+ * Sone - ListNotificationFilters.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.notify;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.notify.Notification;
+
+/**
+ * Filter for {@link ListNotification}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ListNotificationFilters {
+
+ /**
+ * Filters new-post and new-reply notifications in the given list of
+ * notifications. If {@code currentSone} is <code>null</code>, new-post and
+ * new-reply notifications are removed completely. If {@code currentSone} is
+ * not {@code null}, only posts that are posted by a friend Sone or the Sone
+ * itself, and replies that are replies to posts of friend Sones or the Sone
+ * itself will be retained in the notifications.
+ *
+ * @param notifications
+ * The notifications to filter
+ * @param currentSone
+ * The current Sone, or {@code null} if not logged in
+ * @return The filtered notifications
+ */
+ public static List<Notification> filterNotifications(List<Notification> notifications, Sone currentSone) {
+ ListNotification<Post> newPostNotification = getNotification(notifications, "new-post-notification", Post.class);
+ if (newPostNotification != null) {
+ ListNotification<Post> filteredNotification = filterNewPostNotification(newPostNotification, currentSone);
+ int notificationIndex = notifications.indexOf(newPostNotification);
+ if (filteredNotification == null) {
+ notifications.remove(notificationIndex);
+ } else {
+ notifications.set(notificationIndex, filteredNotification);
+ }
+ }
+ ListNotification<Reply> newReplyNotification = getNotification(notifications, "new-replies-notification", Reply.class);
+ if (newReplyNotification != null) {
+ ListNotification<Reply> filteredNotification = filterNewReplyNotification(newReplyNotification, currentSone);
+ int notificationIndex = notifications.indexOf(newReplyNotification);
+ if (filteredNotification == null) {
+ notifications.remove(notificationIndex);
+ } else {
+ notifications.set(notificationIndex, filteredNotification);
+ }
+ }
+ return notifications;
+ }
+
+ /**
+ * Filters the new posts of the given notification. If {@code currentSone}
+ * is {@code null}, {@code null} is returned and the notification is
+ * subsequently removed. Otherwise only posts that are posted by friend
+ * Sones of the given Sone are retained; all other posts are removed.
+ *
+ * @param newPostNotification
+ * The new-post notification
+ * @param currentSone
+ * The current Sone, or {@code null} if not logged in
+ * @return The filtered new-post notification, or {@code null} if the
+ * notification should be removed
+ */
+ private static ListNotification<Post> filterNewPostNotification(ListNotification<Post> newPostNotification, Sone currentSone) {
+ if (currentSone == null) {
+ return null;
+ }
+ List<Post> newPosts = new ArrayList<Post>();
+ for (Post post : newPostNotification.getElements()) {
+ if (currentSone.hasFriend(post.getSone().getId()) || currentSone.equals(post.getSone()) || currentSone.equals(post.getRecipient())) {
+ newPosts.add(post);
+ }
+ }
+ if (newPosts.isEmpty()) {
+ return null;
+ }
+ if (newPosts.size() == newPostNotification.getElements().size()) {
+ return newPostNotification;
+ }
+ ListNotification<Post> filteredNotification = new ListNotification<Post>(newPostNotification);
+ filteredNotification.setElements(newPosts);
+ return filteredNotification;
+ }
+
+ /**
+ * Filters the new replies of the given notification. If {@code currentSone}
+ * is {@code null}, {@code null} is returned and the notification is
+ * subsequently removed. Otherwise only replies that are replies to posts
+ * that are posted by friend Sones of the given Sone are retained; all other
+ * replies are removed.
+ *
+ * @param newReplyNotification
+ * The new-reply notification
+ * @param currentSone
+ * The current Sone, or {@code null} if not logged in
+ * @return The filtered new-reply notification, or {@code null} if the
+ * notification should be removed
+ */
+ private static ListNotification<Reply> filterNewReplyNotification(ListNotification<Reply> newReplyNotification, Sone currentSone) {
+ if (currentSone == null) {
+ return null;
+ }
+ List<Reply> newReplies = new ArrayList<Reply>();
+ for (Reply reply : newReplyNotification.getElements()) {
+ if (((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<Reply> filteredNotification = new ListNotification<Reply>(newReplyNotification);
+ filteredNotification.setElements(newReplies);
+ return filteredNotification;
+ }
+
+ /**
+ * Finds the notification with the given ID in the list of notifications and
+ * returns it.
+ *
+ * @param <T>
+ * The type of the item in the notification
+ * @param notifications
+ * The notification to search
+ * @param notificationId
+ * The ID of the requested notification
+ * @param notificationElementClass
+ * The class of the notification item
+ * @return The requested notification, or {@code null} if no notification
+ * with the given ID could be found
+ */
+ @SuppressWarnings("unchecked")
+ private static <T> ListNotification<T> getNotification(Collection<? extends Notification> notifications, String notificationId, Class<T> notificationElementClass) {
+ for (Notification notification : notifications) {
+ if (!notificationId.equals(notification.getId())) {
+ continue;
+ }
+ return (ListNotification<T>) notification;
+ }
+ return null;
+ }
+
+}
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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);
+ }
+
+}
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;
public Object get(TemplateContext templateContext, Object object, String member) {
NotificationManager notificationManager = (NotificationManager) object;
if ("all".equals(member)) {
- List<Notification> notifications = new ArrayList<Notification>(notificationManager.getNotifications());
+ List<Notification> notifications = ListNotificationFilters.filterNotifications(new ArrayList<Notification>(notificationManager.getNotifications()), (Sone) templateContext.get("currentSone"));
Collections.sort(notifications, Notification.CREATED_TIME_SORTER);
return notifications;
- } else if ("new".equals(member)) {
- List<Notification> notifications = new ArrayList<Notification>(notificationManager.getChangedNotifications());
- Collections.sort(notifications, Notification.LAST_UPDATED_TIME_SORTER);
- return notifications;
}
return super.get(templateContext, object, member);
}
*/
public ParserFilter(Core core, TemplateContextFactory templateContextFactory) {
this.core = core;
- linkParser = new FreenetLinkParser(templateContextFactory);
+ linkParser = new FreenetLinkParser(core, templateContextFactory);
}
/**
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;
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")) {
--- /dev/null
+/*
+ * Sone - ReplyGroupFilter.java - Copyright © 2010 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.template;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.template.Filter;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * {@link Filter} implementation that groups replies by the post the are in
+ * reply to, returning a map with the post as key and the list of replies as
+ * values.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ReplyGroupFilter implements Filter {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Object format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
+ @SuppressWarnings("unchecked")
+ List<Reply> allReplies = (List<Reply>) data;
+ Map<Post, Set<Sone>> postSones = new HashMap<Post, Set<Sone>>();
+ Map<Post, Set<Reply>> postReplies = new HashMap<Post, Set<Reply>>();
+ for (Reply reply : allReplies) {
+ Post post = reply.getPost();
+ Set<Sone> sones = postSones.get(post);
+ if (sones == null) {
+ sones = new HashSet<Sone>();
+ postSones.put(post, sones);
+ }
+ sones.add(reply.getSone());
+ Set<Reply> replies = postReplies.get(post);
+ if (replies == null) {
+ replies = new HashSet<Reply>();
+ postReplies.put(post, replies);
+ }
+ replies.add(reply);
+ }
+ Map<Post, Map<String, Set<?>>> result = new HashMap<Post, Map<String, Set<?>>>();
+ for (Post post : postSones.keySet()) {
+ if (result.containsKey(post)) {
+ continue;
+ }
+ Map<String, Set<?>> postResult = new HashMap<String, Set<?>>();
+ postResult.put("sones", postSones.get(post));
+ postResult.put("replies", postReplies.get(post));
+ result.put(post, postResult);
+ }
+ return result;
+ }
+
+}
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;
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;
}
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@");
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;
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);
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);
}
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;
}
return new TemplatePart(templateContextFactory, TemplateParser.parse(new StringReader("<a class=\"freenet-trusted\" href=\"/<% link|html>\" title=\"<% link|html>\"><% name|html></a>"))).set("link", link).set("name", name);
}
+ /**
+ * Creates a new part based on a template that links to a page in Sone.
+ *
+ * @param link
+ * The target of the link
+ * @param name
+ * The name of the link
+ * @return The part that displays the link
+ */
+ private Part createInSoneLinkPart(String link, String name) {
+ return createInSoneLinkPart(link, name, name);
+ }
+
+ /**
+ * Creates a new part based on a template that links to a page in Sone.
+ *
+ * @param link
+ * The target of the link
+ * @param name
+ * The name of the link
+ * @param title
+ * The title attribute of the link
+ * @return The part that displays the link
+ */
+ private Part createInSoneLinkPart(String link, String name, String title) {
+ return new TemplatePart(templateContextFactory, TemplateParser.parse(new StringReader("<a class=\"in-sone\" href=\"<%link|html>\" title=\"<%title|html>\"><%name|html></a>"))).set("link", link).set("name", name).set("title", title);
+ }
+
}
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;
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
//
}
}
+ //
+ // OBJECT METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ StringWriter stringWriter = new StringWriter();
+ try {
+ render(stringWriter);
+ } catch (IOException ioe1) {
+ /* should never throw, ignore. */
+ }
+ return stringWriter.toString();
+ }
+
}
package net.pterodactylus.sone.text;
import java.io.IOException;
+import java.io.StringWriter;
import java.io.Writer;
import net.pterodactylus.util.template.Template;
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();
+ }
+
}
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;
}
}
}
+ allPosts = Filters.filteredList(allPosts, Post.FUTURE_POSTS_FILTER);
Collections.sort(allPosts, Post.TIME_COMPARATOR);
- Pagination<Post> pagination = new Pagination<Post>(allPosts, 25).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
+ Pagination<Post> pagination = new Pagination<Post>(allPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
templateContext.set("pagination", pagination);
templateContext.set("posts", pagination.getItems());
}
- /**
- * {@inheritDoc}
- */
- @Override
- protected void postProcess(Request request, TemplateContext templateContext) {
- @SuppressWarnings("unchecked")
- List<Post> posts = (List<Post>) templateContext.get("posts");
- for (Post post : posts) {
- webInterface.getCore().markPostKnown(post);
- for (Reply reply : webInterface.getCore().getReplies(post)) {
- webInterface.getCore().markReplyKnown(reply);
- }
- }
- }
-
}
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;
@Override
protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
- List<Sone> knownSones = new ArrayList<Sone>(webInterface.getCore().getSones());
+ List<Sone> knownSones = Filters.filteredList(new ArrayList<Sone>(webInterface.getCore().getSones()), Sone.EMPTY_SONE_FILTER);
Collections.sort(knownSones, Sone.NICE_NAME_COMPARATOR);
Pagination<Sone> sonePagination = new Pagination<Sone>(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<Sone> sones = (List<Sone>) templateContext.get("knownSones");
- for (Sone sone : sones) {
- webInterface.getCore().markSoneKnown(sone);
- }
- }
-
}
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;
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));
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());
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<Phrase> phrases = parseSearchPhrases(query);
+
+ Set<Sone> sones = webInterface.getCore().getSones();
+ Set<Hit<Sone>> soneHits = getHits(sones, phrases, SoneStringGenerator.COMPLETE_GENERATOR);
+
+ Set<Post> posts = new HashSet<Post>();
+ for (Sone sone : sones) {
+ posts.addAll(sone.getPosts());
+ }
+ @SuppressWarnings("synthetic-access")
+ Set<Hit<Post>> 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<Hit<Sone>> sortedSoneHits = new ArrayList<Hit<Sone>>(soneHits);
+ Collections.sort(sortedSoneHits, Hit.DESCENDING_COMPARATOR);
+ List<Hit<Post>> sortedPostHits = new ArrayList<Hit<Post>>(postHits);
+ Collections.sort(sortedPostHits, Hit.DESCENDING_COMPARATOR);
+
+ /* extract Sones and posts. */
+ List<Sone> resultSones = Converters.convertList(sortedSoneHits, new HitConverter<Sone>());
+ List<Post> resultPosts = Converters.convertList(sortedPostHits, new HitConverter<Post>());
+
+ /* pagination. */
+ Pagination<Sone> sonePagination = new Pagination<Sone>(resultSones, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("sonePage"), 0));
+ Pagination<Post> postPagination = new Pagination<Post>(resultPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("postPage"), 0));
+
+ templateContext.set("sonePagination", sonePagination);
+ templateContext.set("soneHits", sonePagination.getItems());
+ 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 <T>
+ * 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 <T> Set<Hit<T>> getHits(Collection<T> objects, List<Phrase> phrases, StringGenerator<T> stringGenerator) {
+ Set<Hit<T>> hits = new HashSet<Hit<T>>();
+ for (T object : objects) {
+ String objectString = stringGenerator.generateString(object);
+ int score = calculateScore(phrases, objectString);
+ hits.add(new Hit<T>(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<Phrase> parseSearchPhrases(String query) {
+ List<String> parsedPhrases = null;
+ try {
+ parsedPhrases = StringEscaper.parseLine(query);
+ } catch (TextException te1) {
+ /* invalid query. */
+ return Collections.emptyList();
+ }
+
+ List<Phrase> phrases = new ArrayList<Phrase>();
+ 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<Phrase> 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 <T>
+ * The type of the objects
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ private static interface StringGenerator<T> {
+
+ /**
+ * 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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ private static class SoneStringGenerator implements StringGenerator<Sone> {
+
+ /** 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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ private class PostStringGenerator implements StringGenerator<Post> {
+
+ /**
+ * {@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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ private static class Phrase {
+
+ /**
+ * The optionality of a search phrase.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’
+ * Roden</a>
+ */
+ 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 <T>
+ * The type of the searched object
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ private static class Hit<T> {
+
+ /** Filter for {@link Hit}s with a score of more than 0. */
+ public static final Filter<Hit<?>> POSITIVE_FILTER = new Filter<Hit<?>>() {
+
+ @Override
+ public boolean filterObject(Hit<?> hit) {
+ return hit.getScore() > 0;
+ }
+
+ };
+
+ /** Comparator that sorts {@link Hit}s descending by score. */
+ public static final Comparator<Hit<?>> DESCENDING_COMPARATOR = new Comparator<Hit<?>>() {
+
+ @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 <T>
+ * The type of the object to extract
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ public static class HitConverter<T> implements Converter<Hit<T>, T> {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public T convert(Hit<T> input) {
+ return input.getObject();
+ }
+
+ }
+
+}
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;
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
-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;
* 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
* 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
* 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);
* {@inheritDoc}
*/
@Override
+ protected String getPageTitle(Request request) {
+ if (pageTitleKey != null) {
+ return webInterface.getL10n().getString(pageTitleKey);
+ }
+ return "";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected List<Map<String, String>> getAdditionalLinkNodes(Request request) {
+ return new ListBuilder<Map<String, String>>().add(new MapBuilder<String, String>().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<String> getStyleSheets() {
return Arrays.asList("css/sone.css");
}
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;
* {@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);
}
}
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;
* 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);
}
//
* {@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<Post> 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<Post> sonePosts = sone.getPosts();
+ sonePosts.addAll(webInterface.getCore().getDirectedPosts(sone));
+ Collections.sort(sonePosts, Post.TIME_COMPARATOR);
+ Pagination<Post> postPagination = new Pagination<Post>(sonePosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("postPage"), 0));
+ templateContext.set("postPagination", postPagination);
+ templateContext.set("posts", postPagination.getItems());
+ Set<Reply> replies = sone.getReplies();
+ final Map<Post, List<Reply>> repliedPosts = new HashMap<Post, List<Reply>>();
+ for (Reply reply : replies) {
+ Post post = reply.getPost();
+ if (repliedPosts.containsKey(post) || sone.equals(post.getSone()) || (sone.equals(post.getRecipient()))) {
+ continue;
}
+ repliedPosts.put(post, webInterface.getCore().getReplies(post));
}
+ List<Post> posts = new ArrayList<Post>(repliedPosts.keySet());
+ Collections.sort(posts, new Comparator<Post>() {
+
+ @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<Post> repliedPostPagination = new Pagination<Post>(posts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("repliedPostPage"), 0));
+ templateContext.set("repliedPostPagination", repliedPostPagination);
+ templateContext.set("repliedPosts", repliedPostPagination.getItems());
}
}
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;
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;
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;
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;
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;
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.
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());
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<Sone>("new-sone-notification", "sones", newSoneNotificationTemplate);
+ newSoneNotification = new ListNotification<Sone>("new-sone-notification", "sones", newSoneNotificationTemplate, false);
Template newPostNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newPostNotification.html"));
- newPostNotification = new ListNotification<Post>("new-post-notification", "posts", newPostNotificationTemplate);
+ newPostNotification = new ListNotification<Post>("new-post-notification", "posts", newPostNotificationTemplate, false);
Template newReplyNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newReplyNotification.html"));
- newReplyNotification = new ListNotification<Reply>("new-replies-notification", "replies", newReplyNotificationTemplate);
+ newReplyNotification = new ListNotification<Reply>("new-replies-notification", "replies", newReplyNotificationTemplate, false);
Template rescuingSonesTemplate = TemplateParser.parse(createReader("/templates/notify/rescuingSonesNotification.html"));
rescuingSonesNotification = new ListNotification<Sone>("sones-being-rescued-notification", "sones", rescuingSonesTemplate);
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"));
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"));
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"));
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)));
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;
*/
@Override
protected JsonObject createJsonObject(Request request) {
+ final Sone currentSone = getCurrentSone(request.getToadletContext(), false);
/* load Sones. */
boolean loadAllSones = Boolean.parseBoolean(request.getHttpRequest().getParam("loadAllSones", "true"));
Set<Sone> sones = new HashSet<Sone>(Collections.singleton(getCurrentSone(request.getToadletContext(), false)));
jsonSones.add(jsonSone);
}
/* load notifications. */
- List<Notification> notifications = new ArrayList<Notification>(webInterface.getNotifications().getChangedNotifications());
- Set<Notification> removedNotifications = webInterface.getNotifications().getRemovedNotifications();
+ List<Notification> notifications = ListNotificationFilters.filterNotifications(new ArrayList<Notification>(webInterface.getNotifications().getNotifications()), currentSone);
Collections.sort(notifications, Notification.LAST_UPDATED_TIME_SORTER);
JsonArray jsonNotifications = new JsonArray();
for (Notification notification : notifications) {
jsonNotifications.add(createJsonNotification(notification));
}
- JsonArray jsonRemovedNotifications = new JsonArray();
- for (Notification notification : removedNotifications) {
- jsonRemovedNotifications.add(createJsonNotification(notification));
- }
/* load new posts. */
Set<Post> newPosts = webInterface.getNewPosts();
+ if (currentSone != null) {
+ newPosts = Filters.filteredSet(newPosts, new Filter<Post>() {
+
+ @Override
+ public boolean filterObject(Post post) {
+ return currentSone.hasFriend(post.getSone().getId()) || currentSone.equals(post.getSone()) || currentSone.equals(post.getRecipient());
+ }
+
+ });
+ }
JsonArray jsonPosts = new JsonArray();
for (Post post : newPosts) {
JsonObject jsonPost = new JsonObject();
}
/* load new replies. */
Set<Reply> newReplies = webInterface.getNewReplies();
+ if (currentSone != null) {
+ newReplies = Filters.filteredSet(newReplies, new Filter<Reply>() {
+
+ @Override
+ public boolean filterObject(Reply reply) {
+ return currentSone.hasFriend(reply.getPost().getSone().getId()) || currentSone.equals(reply.getPost().getSone()) || currentSone.equals(reply.getPost().getRecipient());
+ }
+
+ });
+ }
JsonArray jsonReplies = new JsonArray();
for (Reply reply : newReplies) {
JsonObject jsonReply = new JsonObject();
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);
}
/**
--- /dev/null
+/*
+ * Sone - GetTimesAjaxPage.java - Copyright © 2010–2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later
+ * version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.ajax;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+import net.pterodactylus.util.number.Digits;
+
+/**
+ * Ajax page that returns a formatted, relative timestamp for replies or posts.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class GetTimesAjaxPage extends JsonPage {
+
+ /** Formatter for tooltips. */
+ private static final DateFormat dateFormat = new SimpleDateFormat("MMM d, yyyy, HH:mm:ss");
+
+ /**
+ * Creates a new get times AJAX page.
+ *
+ * @param webInterface
+ * The Sone web interface
+ */
+ public GetTimesAjaxPage(WebInterface webInterface) {
+ super("getTimes.ajax", webInterface);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected JsonObject createJsonObject(Request request) {
+ long now = System.currentTimeMillis();
+ String allIds = request.getHttpRequest().getParam("posts");
+ JsonObject postTimes = new JsonObject();
+ if (allIds.length() > 0) {
+ String[] ids = allIds.split(",");
+ for (String id : ids) {
+ Post post = webInterface.getCore().getPost(id, false);
+ if (post == null) {
+ continue;
+ }
+ long age = now - post.getTime();
+ JsonObject postTime = new JsonObject();
+ Time time = getTime(age);
+ postTime.put("timeText", time.getText());
+ postTime.put("refreshTime", time.getRefresh() / Time.SECOND);
+ postTime.put("tooltip", dateFormat.format(new Date(post.getTime())));
+ postTimes.put(id, postTime);
+ }
+ }
+ JsonObject replyTimes = new JsonObject();
+ allIds = request.getHttpRequest().getParam("replies");
+ if (allIds.length() > 0) {
+ String[] ids = allIds.split(",");
+ for (String id : ids) {
+ Reply reply = webInterface.getCore().getReply(id, false);
+ if (reply == null) {
+ continue;
+ }
+ long age = now - reply.getTime();
+ JsonObject replyTime = new JsonObject();
+ Time time = getTime(age);
+ replyTime.put("timeText", time.getText());
+ replyTime.put("refreshTime", time.getRefresh() / Time.SECOND);
+ replyTime.put("tooltip", dateFormat.format(new Date(reply.getTime())));
+ replyTimes.put(id, replyTime);
+ }
+ }
+ return createSuccessJsonObject().put("postTimes", postTimes).put("replyTimes", replyTimes);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected boolean needsFormPassword() {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected boolean requiresLogin() {
+ return false;
+ }
+
+ //
+ // PRIVATE METHODS
+ //
+
+ /**
+ * Returns the formatted relative time for a given age.
+ *
+ * @param age
+ * The age to format (in milliseconds)
+ * @return The formatted age
+ */
+ private Time getTime(long age) {
+ String text;
+ long refresh;
+ if (age < 0) {
+ text = webInterface.getL10n().getDefaultString("View.Time.InTheFuture");
+ refresh = 5 * Time.MINUTE;
+ } else if (age < 20 * Time.SECOND) {
+ text = webInterface.getL10n().getDefaultString("View.Time.AFewSecondsAgo");
+ refresh = 10 * Time.SECOND;
+ } else if (age < 45 * Time.SECOND) {
+ text = webInterface.getL10n().getString("View.Time.HalfAMinuteAgo");
+ refresh = 20 * Time.SECOND;
+ } else if (age < 90 * Time.SECOND) {
+ text = webInterface.getL10n().getString("View.Time.AMinuteAgo");
+ refresh = Time.MINUTE;
+ } else if (age < 30 * Time.MINUTE) {
+ text = webInterface.getL10n().getString("View.Time.XMinutesAgo", "min", String.valueOf((int) Digits.round(age / Time.MINUTE, 1)));
+ refresh = 1 * Time.MINUTE;
+ } else if (age < 45 * Time.MINUTE) {
+ text = webInterface.getL10n().getString("View.Time.HalfAnHourAgo");
+ refresh = 10 * Time.MINUTE;
+ } else if (age < 90 * Time.MINUTE) {
+ text = webInterface.getL10n().getString("View.Time.AnHourAgo");
+ refresh = Time.HOUR;
+ } else if (age < 21 * Time.HOUR) {
+ text = webInterface.getL10n().getString("View.Time.XHoursAgo", "hour", String.valueOf((int) Digits.round(age / Time.HOUR, 1)));
+ refresh = Time.HOUR;
+ } else if (age < 42 * Time.HOUR) {
+ text = webInterface.getL10n().getString("View.Time.ADayAgo");
+ refresh = Time.DAY;
+ } else if (age < 6 * Time.DAY) {
+ text = webInterface.getL10n().getString("View.Time.XDaysAgo", "day", String.valueOf((int) Digits.round(age / Time.DAY, 1)));
+ refresh = Time.DAY;
+ } else if (age < 11 * Time.DAY) {
+ text = webInterface.getL10n().getString("View.Time.AWeekAgo");
+ refresh = Time.DAY;
+ } else if (age < 4 * Time.WEEK) {
+ text = webInterface.getL10n().getString("View.Time.XWeeksAgo", "week", String.valueOf((int) Digits.round(age / Time.WEEK, 1)));
+ refresh = Time.DAY;
+ } else if (age < 6 * Time.WEEK) {
+ text = webInterface.getL10n().getString("View.Time.AMonthAgo");
+ refresh = Time.DAY;
+ } else if (age < 11 * Time.MONTH) {
+ text = webInterface.getL10n().getString("View.Time.XMonthsAgo", "month", String.valueOf((int) Digits.round(age / Time.MONTH, 1)));
+ refresh = Time.DAY;
+ } else if (age < 18 * Time.MONTH) {
+ text = webInterface.getL10n().getString("View.Time.AYearAgo");
+ refresh = Time.WEEK;
+ } else {
+ text = webInterface.getL10n().getString("View.Time.XYearsAgo", "year", String.valueOf((int) Digits.round(age / Time.YEAR, 1)));
+ refresh = Time.WEEK;
+ }
+ return new Time(text, refresh);
+ }
+
+ /**
+ * Container for a formatted time.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ private static class Time {
+
+ /** Number of milliseconds in a second. */
+ private static final long SECOND = 1000;
+
+ /** Number of milliseconds in a minute. */
+ private static final long MINUTE = 60 * SECOND;
+
+ /** Number of milliseconds in an hour. */
+ private static final long HOUR = 60 * MINUTE;
+
+ /** Number of milliseconds in a day. */
+ private static final long DAY = 24 * HOUR;
+
+ /** Number of milliseconds in a week. */
+ private static final long WEEK = 7 * DAY;
+
+ /** Number of milliseconds in a 30-day month. */
+ private static final long MONTH = 30 * DAY;
+
+ /** Number of milliseconds in a year. */
+ private static final long YEAR = 365 * DAY;
+
+ /** The formatted time. */
+ private final String text;
+
+ /** The time after which to refresh the time. */
+ private final long refresh;
+
+ /**
+ * Creates a new formatted time container.
+ *
+ * @param text
+ * The formatted time
+ * @param refresh
+ * The time after which to refresh the time (in milliseconds)
+ */
+ public Time(String text, long refresh) {
+ this.text = text;
+ this.refresh = refresh;
+ }
+
+ /**
+ * Returns the formatted time.
+ *
+ * @return The formatted time
+ */
+ public String getText() {
+ return text;
+ }
+
+ /**
+ * Returns the time after which to refresh the time.
+ *
+ * @return The time after which to refresh the time (in milliseconds)
+ */
+ public long getRefresh() {
+ return refresh;
+ }
+
+ }
+
+}
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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<String, String> linkNodeParameters : getAdditionalLinkNodes(request)) {
+ HTMLNode linkNode = pageNode.headNode.addChild("link");
+ for (Entry<String, String> 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<String> 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<Map<String, String>> 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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ 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;
+ }
+
+ }
+
+}
* 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
--- /dev/null
+/*
+ * Sone - RedirectPage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.page;
+
+/**
+ * Page implementation that redirects the user to another URL.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class RedirectPage implements Page {
+
+ /** The original path. */
+ private String originalPath;
+
+ /** The path to redirect the browser to. */
+ private String newPath;
+
+ /**
+ * Creates a new redirect page.
+ *
+ * @param originalPath
+ * The original path
+ * @param newPath
+ * The path to redirect the browser to
+ */
+ public RedirectPage(String originalPath, String newPath) {
+ this.originalPath = originalPath;
+ this.newPath = newPath;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPath() {
+ return originalPath;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Response handleRequest(Request request) {
+ return new RedirectResponse(newPath);
+ }
+
+}
/*
- * 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
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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
-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;
}
/**
*/
@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<String> 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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
- 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);
}
}
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.
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}
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!
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.
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
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…
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…
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!
float: right;
}
+#sone #notification-area .notification .hidden {
+ display: none;
+}
+
#sone #plugin-warning {
border: solid 0.5em red;
padding: 0.5em;
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;
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");
$(inputField.get(0).form).submit(function() {
inputField.attr("disabled", "disabled");
if (!optional && (textarea.val() == "")) {
+ inputField.removeAttr("disabled").focus();
return false;
}
});
* @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 = $("<span> · </span>").addClass("separator");
var commentElement = $("<div><span>Comment</span></div>").addClass("show-reply-form").click(function() {
replyElement = $("#sone .post#" + postId + " .create-reply");
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);
}
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") {
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") {
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");
}
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");
}
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) {
}
/**
- * 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
/* mark Sone as known when clicking it. */
$(soneElement).click(function() {
- markSoneAsKnown(soneElement);
+ markSoneAsKnown(this);
});
}
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);
} else {
alert(error);
}
+ button.removeAttr("disabled");
});
})(sender, postId, text, inputField);
return false;
});
/* 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) {
});
});
})(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() {
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) {
$.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)) {
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) {
newPost.insertBefore(firstOlderPost);
}
ajaxifyPost(newPost);
+ updatePostTimes(data.post.id);
newPost.slideDown();
setActivity();
}
}
}
ajaxifyReply(newReply);
+ updateReplyTimes(data.reply.id);
newReply.slideDown();
setActivity();
return false;
*
* @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() {
* showing the activity state, it is returned to normal.
*/
function toggleIcon() {
- if (focus) {
+ if (focus || !iconBlinking) {
if (iconActive) {
changeIcon("images/icon.png");
iconActive = false;
iconBlinking = false;
} else {
iconActive = !iconActive;
- console.log("showing icon: " + iconActive);
changeIcon(iconActive ? "images/icon-activity.png" : "images/icon.png");
setTimeout(toggleIcon, 1500);
}
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();
});
});
+ /* 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);
$("#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();
});
});
+ /* 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) {
<h1><%= Page.About.Page.Title|l10n|html></h1>
- <p>Sone – The Freenet Social Network Plugin, Version <% version|html>, © 2010 by David ‘Bombe’ Roden.</p>
+ <p>Sone – The Freenet Social Network Plugin, Version <% version|html>, © 2010–2011 by David ‘Bombe’ Roden.</p>
<p>
- If you like Sone and you would like to reward me, you can use the
- Flattr button at the bottom of each page. Flattr is a non-anonymous
- micro payment that acts like an internet tip jar where the amount
- each user spends is limited (lowest being 2 € per month). More
- information can be found on <a href="/?_CHECKED_HTTP_=https://www.flattr.com/"
- title="Flattr Homepage" target="_blank">flattr.com</a>.
+ <%= Page.About.Flattr.Description|l10n|html|replace needle="{link}" replacement='<a href="/?_CHECKED_HTTP_=https://www.flattr.com/" title="Flattr Homepage" target="_blank">'|replace needle="{/link}" replacement='</a>'>
</p>
+ <h2><%= Page.About.Homepage.Title|l10n|html></h2>
+
+ <p>
+ <%= Page.About.Homepage.Description|l10n|html|replace needle="{link}" replacement='<a href="/USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/33/">'|replace needle="{/link}" replacement='</a>'>
+ </p>
+
+ <h2><%= Page.About.License.Title|l10n|html></h2>
+
<p>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
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>
<%if !identitiesWithoutSone.empty>
<h1><%= Page.Login.CreateSone.Title|l10n|html></h1>
- <p><%= View.CreateSone.Text.WotIdentityRequired|l10n|html|replace needle="{link}" replacement='<a href="/WoT/">'|replace needle="{/link}" replacement='</a>'></p>
+ <p><%= View.CreateSone.Text.WotIdentityRequired|l10n|html|replace needle="{link}" replacement='<a href="/WebOfTrust/">'|replace needle="{/link}" replacement='</a>'></p>
<form id="create-sone" action="createSone.html" method="post">
<input type="hidden" name="formPassword" value="<% formPassword|html>" />
</form>
<%else>
<%if !sones.empty>
- <p><%= View.CreateSone.Text.NoNonSoneIdentities|l10n|html|replace needle="{link}" replacement='<a href="/WoT/OwnIdentities">'|replace needle="{/link}" replacement="</a>"></p>
+ <p><%= View.CreateSone.Text.NoNonSoneIdentities|l10n|html|replace needle="{link}" replacement='<a href="/WebOfTrust/OwnIdentities">'|replace needle="{/link}" replacement="</a>"></p>
<%else>
- <p><%= View.CreateSone.Text.NoIdentities|l10n|html|replace needle="{link}" replacement='<a href="/WoT/OwnIdentities">'|replace needle="{/link}" replacement="</a>"></p>
+ <p><%= View.CreateSone.Text.NoIdentities|l10n|html|replace needle="{link}" replacement='<a href="/WebOfTrust/OwnIdentities">'|replace needle="{/link}" replacement="</a>"></p>
<%/if>
<%/if>
<div id="profile" class="<%ifnull currentSone>offline<%else>online<%/if>">
<a class="picture" href="index.html">
<%ifnull !currentSone>
- <img src="/WoT/GetIdenticon?identity=<% currentSone.id|html>&width=80&height=80" width="80" height="80" alt="Profile Avatar" />
+ <img src="/WebOfTrust/GetIdenticon?identity=<% currentSone.id|html>&width=80&height=80" width="80" height="80" alt="Profile Avatar" />
<%else>
<img src="images/sone.png" width="80" height="80" alt="Sone is offline" />
<%/if>
<%include include/viewSone.html>
</div>
<%/if>
+ <form id="search" action="search.html" method="get">
+ <input type="text" name="query" value="" />
+ <button type="submit"><%= View.Search.Button.Search|l10n|html></button>
+ </form>
</div>
<div id="tail">
<div class="flattr-button">
<a href="/?_CHECKED_HTTP_=http://flattr.com/thing/81996/Sone-The-Freenet-Social-Network-Plugin" target="_blank">
- <img src="images/flattr-badge-large.png" alt="Flattr Sone" title="Flattr Sone" />
+ <img src="images/flattr-badge-large.png" width="93" height="20" alt="Flattr Sone" title="Flattr Sone" />
</a>
</div>
<div class="post-time hidden"><% post.time|html></div>
<div class="post-author hidden"><% post.sone.id|html></div>
<div class="avatar">
- <img src="/WoT/GetIdenticon?identity=<% post.sone.id|html>&width=48&height=48" width="48" height="48" alt="Avatar Image" />
+ <%if post.loaded>
+ <img src="/WebOfTrust/GetIdenticon?identity=<% post.sone.id|html>&width=48&height=48" width="48" height="48" alt="Avatar Image" />
+ <%else>
+ <img src="images/sone-avatar.png" width="48" height="48" alt="Avatar Image" />
+ <%/if>
</div>
<div class="inner-part">
- <div>
+ <div<%if !post.loaded> class="hidden"<%/if>>
<div class="author profile-link"><a href="viewSone.html?sone=<% post.sone.id|html>"><% post.sone.niceName|html></a></div>
<%ifnull !post.recipient>
<span class="recipient-to">→</span>
<div class="recipient profile-link"><a href="viewSone.html?sone=<% post.recipient.id|html>"><% post.recipient.niceName|html></a></div>
<%/if>
<%/if>
- <div class="post-text raw-text<%if !raw> hidden<%/if>"><% post.text|html></div>
- <div class="post-text text<%if raw> hidden<%/if>"><% post.text|parse sone=post.sone></div>
+ <% post.text|html|store key=originalText text=true>
+ <% post.text|parse sone=post.sone|store key=parsedText text=true>
+ <div class="post-text raw-text<%if !raw> hidden<%/if>"><% originalText></div>
+ <div class="post-text text<%if raw> hidden<%/if>"><% parsedText></div>
</div>
- <div class="post-status-line status-line">
+ <div class="post-status-line status-line<%if !post.loaded> hidden<%/if>">
<div class="bookmarks">
<form class="unbookmark<%if !post.bookmarked> hidden<%/if>" action="unbookmark.html" method="post">
<input type="hidden" name="formPassword" value="<% formPassword|html>" />
</div>
<span class='separator'>·</span>
<div class="time"><a href="viewPost.html?post=<% post.id|html>"><% post.time|date format="MMM d, yyyy, HH:mm:ss"></a></div>
- <span class='separator'>·</span>
- <div class="show-source"><a href="viewPost.html?post=<% post.id|html>&raw=<%if raw>false<%else>true<%/if>"><%= View.Post.ShowSource|l10n|html></a></div>
+ <%if ! originalText|match key=parsedText>
+ <span class='separator'>·</span>
+ <div class="show-source"><a href="viewPost.html?post=<% post.id|html>&raw=<%if raw>false<%else>true<%/if>"><%= View.Post.ShowSource|l10n|html></a></div>
+ <%/if>
<div class="likes<%if post.likes.size|match value=0> hidden<%/if>">
<span class='separator'>·</span>
<span title="<% post.likes.soneNames|html>">↑<span class="like-count"><% post.likes.size></span></span>
</form>
<%/if>
</div>
+ <div<%if post.loaded> class="hidden"<%/if>>
+ <%= View.Post.NotDownloaded|l10n|html>
+ </div>
<div class="replies">
<%foreach post.replies reply>
<%include include/viewReply.html>
<div class="reply-time hidden"><% reply.time|html></div>
<div class="reply-author hidden"><% reply.sone.id|html></div>
<div class="avatar">
- <img src="/WoT/GetIdenticon?identity=<% reply.sone.id|html>&width=36&height=36" width="36" height="36" alt="Avatar Image" />
+ <img src="/WebOfTrust/GetIdenticon?identity=<% reply.sone.id|html>&width=36&height=36" width="36" height="36" alt="Avatar Image" />
</div>
<div class="inner-part">
<div>
<div class="author profile-link"><a href="viewSone.html?sone=<% reply.sone.id|html>"><% reply.sone.niceName|html></a></div>
- <div class="reply-text raw-text<%if !raw> hidden<%/if>"><% reply.text|html></div>
- <div class="reply-text text<%if raw> hidden<%/if>"><% reply.text|parse sone=reply.sone></div>
+ <% reply.text|html|store key=originalText text=true>
+ <% reply.text|parse sone=reply.sone|store key=parsedText text=true>
+ <div class="reply-text raw-text<%if !raw> hidden<%/if>"><% originalText></div>
+ <div class="reply-text text<%if raw> hidden<%/if>"><% parsedText></div>
</div>
<div class="reply-status-line status-line">
<div class="time"><% reply.time|date format="MMM d, yyyy, HH:mm:ss"></div>
- <span class='separator'>·</span>
- <div class="show-reply-source"><a href="viewPost.html?post=<% post.id|html>&raw=<%if raw>false<%else>true<%/if>"><%= View.Post.ShowSource|l10n|html></a></div>
+ <%if ! originalText|match key=parsedText>
+ <span class='separator'>·</span>
+ <div class="show-reply-source"><a href="viewPost.html?post=<% post.id|html>&raw=<%if raw>false<%else>true<%/if>"><%= View.Post.ShowSource|l10n|html></a></div>
+ <%/if>
<div class="likes<%if reply.likes.size|match value=0> hidden<%/if>">
<span class='separator'>·</span>
<span title="<% reply.likes.soneNames|html>">↑<span class="like-count"><% reply.likes.size></span></span>
</form>
<%= Notification.NewPost.Text|l10n|html>
<%foreach posts post>
+ <div class="hidden post-id"><%post.id|html></div>
<a class="link-<% post.id|html>" href="viewPost.html?post=<% post.id|html>"><% post.sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
<%/foreach>
</div>
<input type="hidden" name="id" value="<%foreach replies reply><% reply.id|html><%notlast> <%/notlast><%/foreach>" />
<button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
</form>
+ <%foreach replies reply><div class="hidden reply-id"><%reply.id|html></div><%/foreach>
<%= Notification.NewReply.Text|l10n|html>
- <%foreach replies reply>
- <a class="link-<% reply.post.id|html>" href="viewPost.html?post=<% reply.post.id|html>"><% reply.sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
+ <%foreach replies postGroup|replyGroup>
+ <a class="link-<% postGroup.key.id|html>" href="viewPost.html?post=<% postGroup.key.id|html>"><% postGroup.key.sone.niceName|html></a> (<%foreach postGroup.value.sones sone><%sone.niceName|html><%notlast>, <%/notlast><%/foreach>)<%notlast>, <%/notlast><%last>.<%/last>
<%/foreach>
</div>
</form>
<%= Notification.NewSone.Text|l10n|html>
<%foreach sones sone>
+ <div class="hidden sone-id"><% sone.id|html></div>
<a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
<%/foreach>
</div>
getTranslation("WebInterface.DefaultText.Option.InsertionDelay", function(insertionDelayDefaultText) {
registerInputTextareaSwap("#sone #options input[name=insertion-delay]", insertionDelayDefaultText, "insertion-delay", true, true);
});
+ getTranslation("WebInterface.DefaultText.Option.PostsPerPage", function(postsPerPageText) {
+ registerInputTextareaSwap("#sone #options input[name=posts-per-page]", postsPerPageText, "posts-per-page", true, true);
+ });
+ getTranslation("WebInterface.DefaultText.Option.PositiveTrust", function(positiveTrustText) {
+ registerInputTextareaSwap("#sone #options input[name=positive-trust]", positiveTrustText, "positive-trust", true, true);
+ });
+ getTranslation("WebInterface.DefaultText.Option.NegativeTrust", function(negativeTrustText) {
+ registerInputTextareaSwap("#sone #options input[name=negative-trust]", negativeTrustText, "negative-trust", true, true);
+ });
+ getTranslation("WebInterface.DefaultText.Option.TrustComment", function(trustCommentText) {
+ registerInputTextareaSwap("#sone #options input[name=trust-comment]", trustCommentText, "trust-comment", true, true);
+ });
});
</script>
<form id="options" method="post">
<input type="hidden" name="formPassword" value="<% formPassword|html>" />
+ <h2><%= Page.Options.Section.SoneSpecificOptions.Title|l10n|html></h2>
+
+ <%ifnull currentSone>
+ <p><%= Page.Options.Section.SoneSpecificOptions.NotLoggedIn|l10n|html|replace needle="{link}" replacement='<a href="login.html">'|replace needle="{/link}" replacement='</a>'></p>
+ <%else>
+ <p><%= Page.Options.Section.SoneSpecificOptions.LoggedIn|l10n|html></p>
+ <%/if>
+
+ <p>
+ <input type="checkbox" name="auto-follow"<%ifnull currentSone> disabled="disabled"<%/if><%if auto-follow> checked="checked"<%/if> />
+ <%= Page.Options.Option.AutoFollow.Description|l10n|html>
+ </p>
+
<h2><%= Page.Options.Section.RuntimeOptions.Title|l10n|html></h2>
<p><%= Page.Options.Option.InsertionDelay.Description|l10n|html></p>
<p><input type="text" name="insertion-delay" value="<% insertion-delay|html>" /></p>
+ <p><%= Page.Options.Option.PostsPerPage.Description|l10n|html></p>
+ <p><input type="text" name="posts-per-page" value="<% posts-per-page|html>" /></p>
+
<h2><%= Page.Options.Section.TrustOptions.Title|l10n|html></h2>
<p><%= Page.Options.Option.PositiveTrust.Description|l10n|html></p>
--- /dev/null
+<%include include/head.html>
+
+ <h1><%= Page.Search.Page.Title|l10n|html></h1>
+
+ <%foreach soneHits sone>
+ <%first>
+ <div id="sone-results">
+ <p><%= Page.Search.Text.SoneHits|l10n|html></p>
+ <%include include/pagination.html pagination=sonePagination pageParameter==sonePage>
+ <%/first>
+ <%include include/viewSone.html>
+ <%last>
+ <%include include/pagination.html pagination=sonePagination pageParameter==sonePage>
+ </div>
+ <%/last>
+ <%/foreach>
+
+ <%foreach postHits post>
+ <%first>
+ <div id="post-results">
+ <p><%= Page.Search.Text.PostHits|l10n|html></p>
+ <%include include/pagination.html pagination=postPagination pageParameter==postPage>
+ <%/first>
+ <%include include/viewPost.html>
+ <%last>
+ <%include include/pagination.html pagination=postPagination pageParameter==postPage>
+ </div>
+ <%/last>
+ <%/foreach>
+
+ <%if soneHits.empty><%if postHits.empty>
+ <p><%= Page.Search.Text.NoHits|l10n|html></p>
+ <%/if><%/if>
+
+<%include include/tail.html>
<div class="profile-field">
<div class="name"><%= Page.ViewSone.Profile.Label.Name|l10n|html></div>
- <div class="value"><a href="/WoT/ShowIdentity?id=<% sone.id|html>"><% sone.niceName|html></a></div>
+ <div class="value"><a href="/WebOfTrust/ShowIdentity?id=<% sone.id|html>"><% sone.niceName|html></a></div>
</div>
<%foreach sone.profile.fields field>
<h1><%= Page.ViewSone.PostList.Title|l10n|replace needle="{sone}" replacementKey=sone.niceName|html></h1>
- <div id="posts">
- <%: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>
- <div><%= Page.ViewSone.PostList.Text.NoPostYet|l10n|html></div>
- <%/foreach>
- <%include include/pagination.html>
- </div>
+ <%foreach posts post>
+ <%first>
+ <div id="posts">
+ <%include include/pagination.html pagination=postPagination pageParameter==postPage>
+ <%/first>
+ <%include include/viewPost.html>
+ <%last>
+ <%include include/pagination.html pagination=postPagination pageParameter==postPage>
+ </div>
+ <%/last>
+ <%foreachelse>
+ <div><%= Page.ViewSone.PostList.Text.NoPostYet|l10n|html></div>
+ <%/foreach>
+
+ <%foreach repliedPosts post>
+ <%first>
+ <h2><%= Page.ViewSone.Replies.Title|l10n|html></h2>
+ <div id="replied-posts">
+ <%include include/pagination.html pagination=repliedPostPagination pageParameter==repliedPostPage>
+ <%/first>
+ <%include include/viewPost.html>
+ <%last>
+ <%include include/pagination.html pagination=repliedPostPagination pageParameter==repliedPostPage>
+ </div>
+ <%/last>
+ <%/foreach>
<%/if>
--- /dev/null
+<?xml version="1.0" encoding="utf-8" ?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
+ <ShortName>Sone</ShortName>
+ <Description>Search Sone Profiles and Posts</Description>
+ <Image width="32" height="32" type="image/png">http://<%request.httpRequest.host>/Sone/images/icon.png</Image>
+ <Url type="text/html" method="get" template="http://<%request.httpRequest.host>/Sone/search.html?query={searchTerms}" />
+</OpenSearchDescription>
--- /dev/null
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+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 class=\"freenet\" href=\"/KSK@a\" title=\"KSK@a\">a</a> text.\n\nText.", part.toString());
+ }
+
+}