<modelVersion>4.0.0</modelVersion>
<groupId>net.pterodactylus</groupId>
<artifactId>sone</artifactId>
- <version>0.6</version>
+ <version>0.6.1</version>
<dependencies>
<dependency>
<groupId>net.pterodactylus</groupId>
<artifactId>utils</artifactId>
- <version>0.9.2</version>
+ <version>0.9.3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
}
/**
+ * 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.
*
}
if (newSone) {
coreListenerManager.fireNewSoneFound(sone);
+ for (Sone localSone : getLocalSones()) {
+ if (localSone.getOptions().getBooleanOption("AutoFollow").get()) {
+ localSone.addFriend(sone.getId());
+ }
+ }
}
}
remoteSones.put(identity.getId(), sone);
friends.add(friendId);
}
+ /* load options. */
+ sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
+ sone.getOptions().getBooleanOption("AutoFollow").set(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(null));
+
/* if we’re still here, Sone was loaded successfully. */
synchronized (sone) {
sone.setTime(soneTime);
}
configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter + "/ID").setValue(null);
+ /* save options. */
+ configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").setValue(sone.getOptions().getBooleanOption("AutoFollow").getReal());
+
configuration.save();
logger.log(Level.INFO, "Sone %s saved.", sone);
} catch (ConfigurationException ce1) {
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);
+ }}
}
/**
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;
/** The IDs of all liked replies. */
private final Set<String> likedReplyIds = Collections.synchronizedSet(new HashSet<String>());
+ /** Sone-specific options. */
+ private final Options options = new Options();
+
/**
* Creates a new Sone.
*
return this;
}
+ /**
+ * 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. */
}
/** The version. */
- public static final Version VERSION = new Version(0, 6);
+ public static final Version VERSION = new Version(0, 6, 1);
/** The logger. */
private static final Logger logger = Logging.getLogger(SonePlugin.class);
package net.pterodactylus.sone.notify;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
*/
public class ListNotification<T> extends TemplateNotification {
+ /** The key under which to store the elements in the template. */
+ private final String key;
+
/** The list of new elements. */
private final List<T> elements = new CopyOnWriteArrayList<T>();
* 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 (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;
+ }
+
+}
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;
}
int nextUsk = line.indexOf("USK@");
int nextHttp = line.indexOf("http://");
int nextHttps = line.indexOf("https://");
- if ((nextKsk == -1) && (nextChk == -1) && (nextSsk == -1) && (nextUsk == -1) && (nextHttp == -1) && (nextHttps == -1)) {
+ int nextSone = line.indexOf("sone://");
+ int nextPost = line.indexOf("post://");
+ if ((nextKsk == -1) && (nextChk == -1) && (nextSsk == -1) && (nextUsk == -1) && (nextHttp == -1) && (nextHttps == -1) && (nextSone == -1) && (nextPost == -1)) {
if (lineComplete && !lastLineEmpty) {
parts.add(createPlainTextPart("\n" + line));
} else {
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);
}
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 {
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);
+ }
+
}
// OBJECT METHODS
//
+ /**
+ * {@inheritDoc}
+ */
@Override
public String toString() {
StringWriter stringWriter = new StringWriter();
// OBJECT METHODS
//
+ /**
+ * {@inheritDoc}
+ */
@Override
public String toString() {
StringWriter stringWriter = new StringWriter();
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);
- }
- }
- }
-
}
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());
posts.addAll(sone.getPosts());
}
@SuppressWarnings("synthetic-access")
- Set<Hit<Post>> postHits = getHits(posts, phrases, new PostStringGenerator());
+ Set<Hit<Post>> postHits = getHits(Filters.filteredSet(posts, Post.FUTURE_POSTS_FILTER), phrases, new PostStringGenerator());
/* now filter. */
soneHits = Filters.filteredSet(soneHits, Hit.POSITIVE_FILTER);
List<Post> resultPosts = Converters.convertList(sortedPostHits, new HitConverter<Post>());
/* pagination. */
- Pagination<Sone> sonePagination = new Pagination<Sone>(resultSones, 10).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("sonePage"), 0));
- Pagination<Post> postPagination = new Pagination<Post>(resultPosts, 10).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("postPage"), 0));
+ Pagination<Sone> sonePagination = new Pagination<Sone>(resultSones, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("sonePage"), 0));
+ Pagination<Post> postPagination = new Pagination<Post>(resultPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("postPage"), 0));
templateContext.set("sonePagination", sonePagination);
templateContext.set("soneHits", sonePagination.getItems());
if (post.getRecipient() != null) {
postString.append(' ').append(SoneStringGenerator.NAME_GENERATOR.generateString(post.getRecipient()));
}
- for (Reply reply : webInterface.getCore().getReplies(post)) {
+ for (Reply reply : Filters.filteredList(webInterface.getCore().getReplies(post), Reply.FUTURE_REPLIES_FILTER)) {
postString.append(' ').append(SoneStringGenerator.NAME_GENERATOR.generateString(reply.getSone()));
postString.append(' ').append(reply.getText());
}
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;
templateContext.set("raw", raw);
}
- /**
- * {@inheritDoc}
- */
- @Override
- protected void postProcess(Request request, TemplateContext templateContext) {
- Post post = (Post) templateContext.get("post");
- if (post == null) {
- return;
- }
- webInterface.getCore().markPostKnown(post);
- for (Reply reply : webInterface.getCore().getReplies(post)) {
- webInterface.getCore().markReplyKnown(reply);
- }
- }
-
}
Sone sone = webInterface.getCore().getSone(soneId, false);
templateContext.set("sone", sone);
List<Post> sonePosts = sone.getPosts();
- Pagination<Post> postPagination = new Pagination<Post>(sonePosts, 10).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("postPage"), 0));
+ sonePosts.addAll(webInterface.getCore().getDirectedPosts(sone));
+ Collections.sort(sonePosts, Post.TIME_COMPARATOR);
+ Pagination<Post> postPagination = new Pagination<Post>(sonePosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("postPage"), 0));
templateContext.set("postPagination", postPagination);
templateContext.set("posts", postPagination.getItems());
Set<Reply> replies = sone.getReplies();
final Map<Post, List<Reply>> repliedPosts = new HashMap<Post, List<Reply>>();
for (Reply reply : replies) {
Post post = reply.getPost();
- if (repliedPosts.containsKey(post) || sone.equals(post.getSone())) {
+ if (repliedPosts.containsKey(post) || sone.equals(post.getSone()) || (sone.equals(post.getRecipient()))) {
continue;
}
repliedPosts.put(post, webInterface.getCore().getReplies(post));
});
- Pagination<Post> repliedPostPagination = new Pagination<Post>(posts, 10).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("repliedPostPage"), 0));
+ Pagination<Post> repliedPostPagination = new Pagination<Post>(posts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("repliedPostPage"), 0));
templateContext.set("repliedPostPagination", repliedPostPagination);
templateContext.set("repliedPosts", repliedPostPagination.getItems());
}
- /**
- * {@inheritDoc}
- */
- @Override
- @SuppressWarnings("unchecked")
- protected void postProcess(Request request, TemplateContext templateContext) {
- Sone sone = (Sone) templateContext.get("sone");
- if (sone == null) {
- return;
- }
- webInterface.getCore().markSoneKnown(sone);
- List<Post> posts = (List<Post>) templateContext.get("posts");
- posts.addAll((List<Post>) templateContext.get("repliedPosts"));
- for (Post post : posts) {
- if (post.getSone() != null) {
- webInterface.getCore().markPostKnown(post);
- }
- for (Reply reply : webInterface.getCore().getReplies(post)) {
- webInterface.getCore().markReplyKnown(reply);
- }
- }
- }
-
}
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;
templateContextFactory.addFilter("unknown", new UnknownDateFilter(getL10n(), "View.Sone.Text.UnknownDate"));
templateContextFactory.addFilter("format", new FormatFilter());
templateContextFactory.addFilter("sort", new CollectionSortFilter());
+ templateContextFactory.addFilter("replyGroup", new ReplyGroupFilter());
templateContextFactory.addProvider(Provider.TEMPLATE_CONTEXT_PROVIDER);
templateContextFactory.addProvider(new ClassPathTemplateProvider());
templateContextFactory.addTemplateObject("formPassword", formPassword);
/* create notifications. */
Template newSoneNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newSoneNotification.html"));
- newSoneNotification = new ListNotification<Sone>("new-sone-notification", "sones", newSoneNotificationTemplate);
+ newSoneNotification = new ListNotification<Sone>("new-sone-notification", "sones", newSoneNotificationTemplate, false);
Template newPostNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newPostNotification.html"));
- newPostNotification = new ListNotification<Post>("new-post-notification", "posts", newPostNotificationTemplate);
+ newPostNotification = new ListNotification<Post>("new-post-notification", "posts", newPostNotificationTemplate, false);
Template newReplyNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newReplyNotification.html"));
- newReplyNotification = new ListNotification<Reply>("new-replies-notification", "replies", newReplyNotificationTemplate);
+ newReplyNotification = new ListNotification<Reply>("new-replies-notification", "replies", newReplyNotificationTemplate, false);
Template rescuingSonesTemplate = TemplateParser.parse(createReader("/templates/notify/rescuingSonesNotification.html"));
rescuingSonesNotification = new ListNotification<Sone>("sones-being-rescued-notification", "sones", rescuingSonesTemplate);
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 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;
+ }
+
+ }
+
+}
* 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);
+ }
+
+}
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.
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.Trust.Tooltip.Distrust=Assign negative trust to this person
View.Trust.Tooltip.Untrust=Remove your trust assignment for this person
+View.Time.InTheFuture=in the future
+View.Time.AFewSecondsAgo=a few seconds ago
+View.Time.HalfAMinuteAgo=about half a minute ago
+View.Time.AMinuteAgo=about a minute ago
+View.Time.XMinutesAgo=${min} minutes ago
+View.Time.HalfAnHourAgo=half an hour ago
+View.Time.AnHourAgo=about an hour ago
+View.Time.XHoursAgo=${hour} hours ago
+View.Time.ADayAgo=about a day ago
+View.Time.XDaysAgo=${day} days ago
+View.Time.AWeekAgo=about a week ago
+View.Time.XWeeksAgo=${week} week ago
+View.Time.AMonthAgo=about a month ago
+View.Time.XMonthsAgo=${month} months ago
+View.Time.AYearAgo=about a year ago
+View.Time.XYearsAgo=${year} years ago
+
WebInterface.DefaultText.StatusUpdate=What’s on your mind?
WebInterface.DefaultText.Message=Write a Message…
WebInterface.DefaultText.Reply=Write a Reply…
WebInterface.DefaultText.BirthYear=Year
WebInterface.DefaultText.FieldName=Field name
WebInterface.DefaultText.Option.InsertionDelay=Time to wait after a Sone is modified before insert (in seconds)
+WebInterface.DefaultText.Option.PostsPerPage=Number of posts to show on a page
+WebInterface.DefaultText.Option.PositiveTrust=The positive trust to assign
+WebInterface.DefaultText.Option.NegativeTrust=The negative trust to assign
+WebInterface.DefaultText.Option.TrustComment=The comment to set in the web of trust
WebInterface.DefaultText.Search=What are you looking for?
WebInterface.Confirmation.DeletePostButton=Yes, delete!
WebInterface.Confirmation.DeleteReplyButton=Yes, delete!
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;
(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");
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
addCommentLink(getPostId(postElement), postElement, $(postElement).find(".post-status-line .time"));
/* process all replies. */
+ replyIds = [];
$(postElement).find(".reply").each(function() {
+ replyIds.push(getReplyId(this));
ajaxifyReply(this);
});
+ updateReplyTimes(replyIds.join(","));
/* process reply input fields. */
getTranslation("WebInterface.DefaultText.Reply", function(text) {
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;
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());
$("#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
</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>
public void testParser() throws IOException {
TemplateContextFactory templateContextFactory = new TemplateContextFactory();
templateContextFactory.addFilter("html", new HtmlFilter());
- FreenetLinkParser parser = new FreenetLinkParser(templateContextFactory);
+ FreenetLinkParser parser = new FreenetLinkParser(null, templateContextFactory);
FreenetLinkParserContext context = new FreenetLinkParserContext(null);
Part part;