Merge branch 'release-0.4.4' 0.4.4
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Tue, 8 Mar 2011 05:51:44 +0000 (06:51 +0100)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Tue, 8 Mar 2011 05:51:44 +0000 (06:51 +0100)
30 files changed:
pom.xml
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/core/FreenetInterface.java
src/main/java/net/pterodactylus/sone/core/SoneDownloader.java
src/main/java/net/pterodactylus/sone/data/Sone.java
src/main/java/net/pterodactylus/sone/main/SonePlugin.java
src/main/java/net/pterodactylus/sone/template/ParserFilter.java
src/main/java/net/pterodactylus/sone/template/PostAccessor.java
src/main/java/net/pterodactylus/sone/template/ReplyAccessor.java
src/main/java/net/pterodactylus/sone/web/BookmarkPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/BookmarksPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/OptionsPage.java
src/main/java/net/pterodactylus/sone/web/UnbookmarkPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ViewPostPage.java
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ajax/GetPostAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPage.java [new file with mode: 0644]
src/main/resources/i18n/sone.en.properties
src/main/resources/static/css/sone.css
src/main/resources/static/images/icon-activity.png [new file with mode: 0644]
src/main/resources/static/javascript/sone.js
src/main/resources/templates/bookmarks.html [new file with mode: 0644]
src/main/resources/templates/include/tail.html
src/main/resources/templates/include/viewPost.html
src/main/resources/templates/include/viewReply.html
src/main/resources/templates/notify/newPostNotification.html
src/main/resources/templates/notify/newReplyNotification.html
src/main/resources/templates/notify/newVersionNotification.html
src/main/resources/templates/viewSone.html

diff --git a/pom.xml b/pom.xml
index 3546ccc..78f1e32 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -2,12 +2,12 @@
        <modelVersion>4.0.0</modelVersion>
        <groupId>net.pterodactylus</groupId>
        <artifactId>sone</artifactId>
-       <version>0.4.3</version>
+       <version>0.4.4</version>
        <dependencies>
                <dependency>
                        <groupId>net.pterodactylus</groupId>
                        <artifactId>utils</artifactId>
-                       <version>0.8</version>
+                       <version>0.9</version>
                </dependency>
                <dependency>
                        <groupId>junit</groupId>
index 99a119b..7dc398e 100644 (file)
@@ -85,6 +85,9 @@ public class Core implements IdentityListener, UpdateListener {
        /** The options. */
        private final Options options = new Options();
 
+       /** The preferences. */
+       private final Preferences preferences = new Preferences(options);
+
        /** The core listener manager. */
        private final CoreListenerManager coreListenerManager = new CoreListenerManager(this);
 
@@ -155,6 +158,10 @@ public class Core implements IdentityListener, UpdateListener {
        /** All known replies. */
        private Set<String> knownReplies = new HashSet<String>();
 
+       /** All bookmarked posts. */
+       /* synchronize access on itself. */
+       private Set<String> bookmarkedPosts = new HashSet<String>();
+
        /** Trusted identities, sorted by own identities. */
        private Map<OwnIdentity, Set<Identity>> trustedIdentities = Collections.synchronizedMap(new HashMap<OwnIdentity, Set<Identity>>());
 
@@ -221,18 +228,8 @@ public class Core implements IdentityListener, UpdateListener {
         *
         * @return The options of the core
         */
-       public Options getOptions() {
-               return options;
-       }
-
-       /**
-        * Returns whether the “Sone rescue mode” is currently activated.
-        *
-        * @return {@code true} if the “Sone rescue mode” is currently activated,
-        *         {@code false} if it is not
-        */
-       public boolean isSoneRescueMode() {
-               return options.getBooleanOption("SoneRescueMode").get();
+       public Preferences getPreferences() {
+               return preferences;
        }
 
        /**
@@ -681,6 +678,50 @@ public class Core implements IdentityListener, UpdateListener {
                return sones;
        }
 
+       /**
+        * Returns whether the given post is bookmarked.
+        *
+        * @param post
+        *            The post to check
+        * @return {@code true} if the given post is bookmarked, {@code false}
+        *         otherwise
+        */
+       public boolean isBookmarked(Post post) {
+               return isPostBookmarked(post.getId());
+       }
+
+       /**
+        * Returns whether the post with the given ID is bookmarked.
+        *
+        * @param id
+        *            The ID of the post to check
+        * @return {@code true} if the post with the given ID is bookmarked,
+        *         {@code false} otherwise
+        */
+       public boolean isPostBookmarked(String id) {
+               synchronized (bookmarkedPosts) {
+                       return bookmarkedPosts.contains(id);
+               }
+       }
+
+       /**
+        * Returns all currently known bookmarked posts.
+        *
+        * @return All bookmarked posts
+        */
+       public Set<Post> getBookmarkedPosts() {
+               Set<Post> posts = new HashSet<Post>();
+               synchronized (bookmarkedPosts) {
+                       for (String bookmarkedPostId : bookmarkedPosts) {
+                               Post post = getPost(bookmarkedPostId, false);
+                               if (post != null) {
+                                       posts.add(post);
+                               }
+                       }
+               }
+               return posts;
+       }
+
        //
        // ACTIONS
        //
@@ -767,7 +808,7 @@ public class Core implements IdentityListener, UpdateListener {
                        soneInserters.put(sone, soneInserter);
                        setSoneStatus(sone, SoneStatus.idle);
                        loadSone(sone);
-                       if (!isSoneRescueMode()) {
+                       if (!preferences.isSoneRescueMode()) {
                                soneInserter.start();
                        }
                        new Thread(new Runnable() {
@@ -775,7 +816,7 @@ public class Core implements IdentityListener, UpdateListener {
                                @Override
                                @SuppressWarnings("synthetic-access")
                                public void run() {
-                                       if (!isSoneRescueMode()) {
+                                       if (!preferences.isSoneRescueMode()) {
                                                soneDownloader.fetchSone(sone);
                                                return;
                                        }
@@ -783,7 +824,7 @@ public class Core implements IdentityListener, UpdateListener {
                                        coreListenerManager.fireRescuingSone(sone);
                                        lockSone(sone);
                                        long edition = sone.getLatestEdition();
-                                       while (!stopped && (edition >= 0) && isSoneRescueMode()) {
+                                       while (!stopped && (edition >= 0) && preferences.isSoneRescueMode()) {
                                                logger.log(Level.FINE, "Downloading edition " + edition + "…");
                                                soneDownloader.fetchSone(sone, sone.getRequestUri().setKeyType("SSK").setDocName("Sone-" + edition));
                                                --edition;
@@ -893,7 +934,7 @@ public class Core implements IdentityListener, UpdateListener {
        public void setTrust(Sone origin, Sone target, int trustValue) {
                Validation.begin().isNotNull("Trust Origin", origin).check().isInstanceOf("Trust Origin", origin.getIdentity(), OwnIdentity.class).isNotNull("Trust Target", target).isLessOrEqual("Trust Value", trustValue, 100).isGreaterOrEqual("Trust Value", trustValue, -100).check();
                try {
-                       ((OwnIdentity) origin.getIdentity()).setTrust(target.getIdentity(), trustValue, options.getStringOption("TrustComment").get());
+                       ((OwnIdentity) origin.getIdentity()).setTrust(target.getIdentity(), trustValue, preferences.getTrustComment());
                } catch (WebOfTrustException wote1) {
                        logger.log(Level.WARNING, "Could not set trust for Sone: " + target, wote1);
                }
@@ -925,7 +966,7 @@ public class Core implements IdentityListener, UpdateListener {
         *            The trust target
         */
        public void trustSone(Sone origin, Sone target) {
-               setTrust(origin, target, options.getIntegerOption("PositiveTrust").get());
+               setTrust(origin, target, preferences.getPositiveTrust());
        }
 
        /**
@@ -937,7 +978,7 @@ public class Core implements IdentityListener, UpdateListener {
         *            The trust target
         */
        public void distrustSone(Sone origin, Sone target) {
-               setTrust(origin, target, options.getIntegerOption("NegativeTrust").get());
+               setTrust(origin, target, preferences.getNegativeTrust());
        }
 
        /**
@@ -960,7 +1001,7 @@ public class Core implements IdentityListener, UpdateListener {
         */
        public void updateSone(Sone sone) {
                if (hasSone(sone.getId())) {
-                       boolean soneRescueMode = isLocalSone(sone) && isSoneRescueMode();
+                       boolean soneRescueMode = isLocalSone(sone) && preferences.isSoneRescueMode();
                        Sone storedSone = getSone(sone.getId());
                        if (!soneRescueMode && !(sone.getTime() > storedSone.getTime())) {
                                logger.log(Level.FINE, "Downloaded Sone %s is not newer than stored Sone %s.", new Object[] { sone, storedSone });
@@ -1439,6 +1480,50 @@ public class Core implements IdentityListener, UpdateListener {
        }
 
        /**
+        * Bookmarks the given post.
+        *
+        * @param post
+        *            The post to bookmark
+        */
+       public void bookmark(Post post) {
+               bookmarkPost(post.getId());
+       }
+
+       /**
+        * Bookmarks the post with the given ID.
+        *
+        * @param id
+        *            The ID of the post to bookmark
+        */
+       public void bookmarkPost(String id) {
+               synchronized (bookmarkedPosts) {
+                       bookmarkedPosts.add(id);
+               }
+       }
+
+       /**
+        * Removes the given post from the bookmarks.
+        *
+        * @param post
+        *            The post to unbookmark
+        */
+       public void unbookmark(Post post) {
+               unbookmarkPost(post.getId());
+       }
+
+       /**
+        * Removes the post with the given ID from the bookmarks.
+        *
+        * @param id
+        *            The ID of the post to unbookmark
+        */
+       public void unbookmarkPost(String id) {
+               synchronized (bookmarkedPosts) {
+                       bookmarkedPosts.remove(id);
+               }
+       }
+
+       /**
         * Creates a new reply.
         *
         * @param sone
@@ -1594,6 +1679,15 @@ public class Core implements IdentityListener, UpdateListener {
                                configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
                        }
 
+                       /* save bookmarked posts. */
+                       int bookmarkedPostCounter = 0;
+                       synchronized (bookmarkedPosts) {
+                               for (String bookmarkedPostId : bookmarkedPosts) {
+                                       configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").setValue(bookmarkedPostId);
+                               }
+                       }
+                       configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").setValue(null);
+
                        /* now save it. */
                        configuration.save();
 
@@ -1684,6 +1778,18 @@ public class Core implements IdentityListener, UpdateListener {
                        }
                }
 
+               /* load bookmarked posts. */
+               int bookmarkedPostCounter = 0;
+               while (true) {
+                       String bookmarkedPostId = configuration.getStringValue("Bookmarks/Post/" + bookmarkedPostCounter++ + "/ID").getValue(null);
+                       if (bookmarkedPostId == null) {
+                               break;
+                       }
+                       synchronized (bookmarkedPosts) {
+                               bookmarkedPosts.add(bookmarkedPostId);
+                       }
+               }
+
        }
 
        /**
@@ -1750,6 +1856,7 @@ public class Core implements IdentityListener, UpdateListener {
                        public void run() {
                                Sone sone = getRemoteSone(identity.getId());
                                sone.setIdentity(identity);
+                               soneDownloader.addSone(sone);
                                soneDownloader.fetchSone(sone);
                        }
                }).start();
@@ -1775,4 +1882,191 @@ public class Core implements IdentityListener, UpdateListener {
                coreListenerManager.fireUpdateFound(version, releaseTime, latestEdition);
        }
 
+       /**
+        * Convenience interface for external classes that want to access the core’s
+        * configuration.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       public static class Preferences {
+
+               /** The wrapped options. */
+               private final Options options;
+
+               /**
+                * Creates a new preferences object wrapped around the given options.
+                *
+                * @param options
+                *            The options to wrap
+                */
+               public Preferences(Options options) {
+                       this.options = options;
+               }
+
+               /**
+                * Returns the insertion delay.
+                *
+                * @return The insertion delay
+                */
+               public int getInsertionDelay() {
+                       return options.getIntegerOption("InsertionDelay").get();
+               }
+
+               /**
+                * Sets the insertion delay
+                *
+                * @param insertionDelay
+                *            The new insertion delay, or {@code null} to restore it to
+                *            the default value
+                * @return This preferences
+                */
+               public Preferences setInsertionDelay(Integer insertionDelay) {
+                       options.getIntegerOption("InsertionDelay").set(insertionDelay);
+                       return this;
+               }
+
+               /**
+                * Returns the positive trust.
+                *
+                * @return The positive trust
+                */
+               public int getPositiveTrust() {
+                       return options.getIntegerOption("PositiveTrust").get();
+               }
+
+               /**
+                * Sets the positive trust.
+                *
+                * @param positiveTrust
+                *            The new positive trust, or {@code null} to restore it to
+                *            the default vlaue
+                * @return This preferences
+                */
+               public Preferences setPositiveTrust(Integer positiveTrust) {
+                       options.getIntegerOption("PositiveTrust").set(positiveTrust);
+                       return this;
+               }
+
+               /**
+                * Returns the negative trust.
+                *
+                * @return The negative trust
+                */
+               public int getNegativeTrust() {
+                       return options.getIntegerOption("NegativeTrust").get();
+               }
+
+               /**
+                * Sets the negative trust.
+                *
+                * @param negativeTrust
+                *            The negative trust, or {@code null} to restore it to the
+                *            default value
+                * @return The preferences
+                */
+               public Preferences setNegativeTrust(Integer negativeTrust) {
+                       options.getIntegerOption("NegativeTrust").set(negativeTrust);
+                       return this;
+               }
+
+               /**
+                * Returns the trust comment. This is the comment that is set in the web
+                * of trust when a trust value is assigned to an identity.
+                *
+                * @return The trust comment
+                */
+               public String getTrustComment() {
+                       return options.getStringOption("TrustComment").get();
+               }
+
+               /**
+                * Sets the trust comment.
+                *
+                * @param trustComment
+                *            The trust comment, or {@code null} to restore it to the
+                *            default value
+                * @return This preferences
+                */
+               public Preferences setTrustComment(String trustComment) {
+                       options.getStringOption("TrustComment").set(trustComment);
+                       return this;
+               }
+
+               /**
+                * Returns whether the rescue mode is active.
+                *
+                * @return {@code true} if the rescue mode is active, {@code false}
+                *         otherwise
+                */
+               public boolean isSoneRescueMode() {
+                       return options.getBooleanOption("SoneRescueMode").get();
+               }
+
+               /**
+                * Sets whether the rescue mode is active.
+                *
+                * @param soneRescueMode
+                *            {@code true} if the rescue mode is active, {@code false}
+                *            otherwise
+                * @return This preferences
+                */
+               public Preferences setSoneRescueMode(Boolean soneRescueMode) {
+                       options.getBooleanOption("SoneRescueMode").set(soneRescueMode);
+                       return this;
+               }
+
+               /**
+                * Returns whether Sone should clear its settings on the next restart.
+                * In order to be effective, {@link #isReallyClearOnNextRestart()} needs
+                * to return {@code true} as well!
+                *
+                * @return {@code true} if Sone should clear its settings on the next
+                *         restart, {@code false} otherwise
+                */
+               public boolean isClearOnNextRestart() {
+                       return options.getBooleanOption("ClearOnNextRestart").get();
+               }
+
+               /**
+                * Sets whether Sone will clear its settings on the next restart.
+                *
+                * @param clearOnNextRestart
+                *            {@code true} if Sone should clear its settings on the next
+                *            restart, {@code false} otherwise
+                * @return This preferences
+                */
+               public Preferences setClearOnNextRestart(Boolean clearOnNextRestart) {
+                       options.getBooleanOption("ClearOnNextRestart").set(clearOnNextRestart);
+                       return this;
+               }
+
+               /**
+                * Returns whether Sone should really clear its settings on next
+                * restart. This is a confirmation option that needs to be set in
+                * addition to {@link #isClearOnNextRestart()} in order to clear Sone’s
+                * settings on the next restart.
+                *
+                * @return {@code true} if Sone should really clear its settings on the
+                *         next restart, {@code false} otherwise
+                */
+               public boolean isReallyClearOnNextRestart() {
+                       return options.getBooleanOption("ReallyClearOnNextRestart").get();
+               }
+
+               /**
+                * Sets whether Sone should really clear its settings on the next
+                * restart.
+                *
+                * @param reallyClearOnNextRestart
+                *            {@code true} if Sone should really clear its settings on
+                *            the next restart, {@code false} otherwise
+                * @return This preferences
+                */
+               public Preferences setReallyClearOnNextRestart(Boolean reallyClearOnNextRestart) {
+                       options.getBooleanOption("ReallyClearOnNextRestart").set(reallyClearOnNextRestart);
+                       return this;
+               }
+
+       }
+
 }
index 3ccda5b..e22f407 100644 (file)
@@ -176,7 +176,7 @@ public class FreenetInterface {
                                }
                        };
                        soneUskCallbacks.put(sone.getId(), uskCallback);
-                       node.clientCore.uskManager.subscribe(USK.create(sone.getRequestUri()), uskCallback, true, (HighLevelSimpleClientImpl) client);
+                       node.clientCore.uskManager.subscribe(USK.create(sone.getRequestUri()), uskCallback, (System.currentTimeMillis() - sone.getTime()) < 7 * 24 * 60 * 60 * 1000, (HighLevelSimpleClientImpl) client);
                } catch (MalformedURLException mue1) {
                        logger.log(Level.WARNING, "Could not subscribe USK “" + sone.getRequestUri() + "”!", mue1);
                }
index c883301..b2d1da4 100644 (file)
@@ -25,6 +25,7 @@ import java.util.Set;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import net.pterodactylus.sone.core.Core.Preferences;
 import net.pterodactylus.sone.core.Core.SoneStatus;
 import net.pterodactylus.sone.data.Client;
 import net.pterodactylus.sone.data.Post;
@@ -93,6 +94,7 @@ public class SoneDownloader extends AbstractService {
         */
        public void addSone(Sone sone) {
                if (sones.add(sone)) {
+                       freenetInterface.unregisterUsk(sone);
                        freenetInterface.registerUsk(sone, this);
                }
        }
@@ -122,8 +124,8 @@ public class SoneDownloader extends AbstractService {
 
        /**
         * Fetches the updated Sone. This method can be used to fetch a Sone from a
-        * specific URI (which happens when {@link Core#isSoneRescueMode() „Sone
-        * rescue mode“} is active).
+        * specific URI (which happens when {@link Preferences#isSoneRescueMode()
+        * „Sone rescue mode“} is active).
         *
         * @param sone
         *            The Sone to fetch
@@ -131,9 +133,6 @@ public class SoneDownloader extends AbstractService {
         *            The URI to fetch the Sone from
         */
        public void fetchSone(Sone sone, FreenetURI soneUri) {
-               if (core.getSoneStatus(sone) == SoneStatus.downloading) {
-                       return;
-               }
                logger.log(Level.FINE, "Starting fetch for Sone “%s” from %s…", new Object[] { sone, soneUri });
                FreenetURI requestUri = soneUri.setMetaString(new String[] { "sone.xml" });
                core.setSoneStatus(sone, SoneStatus.downloading);
@@ -146,6 +145,7 @@ public class SoneDownloader extends AbstractService {
                        logger.log(Level.FINEST, "Got %d bytes back.", fetchResults.getRight().size());
                        Sone parsedSone = parseSone(sone, fetchResults.getRight(), fetchResults.getLeft());
                        if (parsedSone != null) {
+                               addSone(parsedSone);
                                core.updateSone(parsedSone);
                        }
                } finally {
index f183b39..cc54697 100644 (file)
@@ -40,7 +40,7 @@ import freenet.keys.FreenetURI;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class Sone implements Fingerprintable {
+public class Sone implements Fingerprintable, Comparable<Sone> {
 
        /** comparator that sorts Sones by their nice name. */
        public static final Comparator<Sone> NICE_NAME_COMPARATOR = new Comparator<Sone>() {
@@ -626,6 +626,18 @@ public class Sone implements Fingerprintable {
        }
 
        //
+       // INTERFACE Comparable<Sone>
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public int compareTo(Sone sone) {
+               return NICE_NAME_COMPARATOR.compare(this, sone);
+       }
+
+       //
        // OBJECT METHODS
        //
 
index e51f9a1..4bdce70 100644 (file)
@@ -78,7 +78,7 @@ public class SonePlugin implements FredPlugin, FredPluginL10n, FredPluginBaseL10
        }
 
        /** The version. */
-       public static final Version VERSION = new Version(0, 4, 3);
+       public static final Version VERSION = new Version(0, 4, 4);
 
        /** The logger. */
        private static final Logger logger = Logging.getLogger(SonePlugin.class);
index f1190e0..1e274cc 100644 (file)
@@ -21,6 +21,7 @@ import java.io.IOException;
 import java.io.StringReader;
 import java.util.Map;
 
+import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.text.FreenetLinkParser;
 import net.pterodactylus.sone.text.FreenetLinkParserContext;
@@ -35,6 +36,9 @@ import net.pterodactylus.util.template.TemplateContextFactory;
  */
 public class ParserFilter implements Filter {
 
+       /** The core. */
+       private final Core core;
+
        /** The link parser. */
        private final FreenetLinkParser linkParser;
 
@@ -42,10 +46,13 @@ public class ParserFilter implements Filter {
         * Creates a new filter that runs its input through a
         * {@link FreenetLinkParser}.
         *
+        * @param core
+        *            The core
         * @param templateContextFactory
         *            The context factory for rendering the parts
         */
-       public ParserFilter(TemplateContextFactory templateContextFactory) {
+       public ParserFilter(Core core, TemplateContextFactory templateContextFactory) {
+               this.core = core;
                linkParser = new FreenetLinkParser(templateContextFactory);
        }
 
@@ -60,6 +67,9 @@ public class ParserFilter implements Filter {
                        soneKey = "sone";
                }
                Sone sone = (Sone) templateContext.get(soneKey);
+               if (sone == null) {
+                       sone = core.getSone(soneKey, false);
+               }
                FreenetLinkParserContext context = new FreenetLinkParserContext(sone);
                try {
                        return linkParser.parse(context, new StringReader(text));
index f6a9c06..ca0c84a 100644 (file)
@@ -62,6 +62,10 @@ public class PostAccessor extends ReflectionAccessor {
                        return (currentSone != null) && (currentSone.isLikedPostId(post.getId()));
                } else if (member.equals("new")) {
                        return core.isNewPost(post.getId());
+               } else if (member.equals("bookmarked")) {
+                       return core.isBookmarked(post);
+               } else if (member.equals("loaded")) {
+                       return post.getSone() != null;
                }
                return super.get(templateContext, object, member);
        }
index f439abe..1e54d53 100644 (file)
@@ -58,6 +58,8 @@ public class ReplyAccessor extends ReflectionAccessor {
                        return (currentSone != null) && (currentSone.isLikedReplyId(reply.getId()));
                } else if (member.equals("new")) {
                        return core.isNewReply(reply.getId());
+               } else if (member.equals("loaded")) {
+                       return reply.getSone() != null;
                }
                return super.get(templateContext, object, member);
        }
diff --git a/src/main/java/net/pterodactylus/sone/web/BookmarkPage.java b/src/main/java/net/pterodactylus/sone/web/BookmarkPage.java
new file mode 100644 (file)
index 0000000..12a0934
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Sone - BookmarkPage.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;
+
+import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Page that lets the user bookmark a post.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class BookmarkPage extends SoneTemplatePage {
+
+       /**
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public BookmarkPage(Template template, WebInterface webInterface) {
+               super("bookmark.html", template, "Page.Bookmark.Title", webInterface);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               if (request.getMethod() == Method.POST) {
+                       String id = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
+                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+                       webInterface.getCore().bookmarkPost(id);
+                       throw new RedirectException(returnPage);
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/BookmarksPage.java b/src/main/java/net/pterodactylus/sone/web/BookmarksPage.java
new file mode 100644 (file)
index 0000000..d8a18c8
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * Sone - BookmarksPage.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;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.util.collection.Pagination;
+import net.pterodactylus.util.number.Numbers;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Page that lets the user browse all his bookmarked posts.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class BookmarksPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new bookmarks page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public BookmarksPage(Template template, WebInterface webInterface) {
+               super("bookmarks.html", template, "Page.Bookmarks.Title", webInterface);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               Set<Post> posts = webInterface.getCore().getBookmarkedPosts();
+               List<Post> sortedPosts = new ArrayList<Post>(posts);
+               Collections.sort(sortedPosts, Post.TIME_COMPARATOR);
+               Pagination<Post> pagination = new Pagination<Post>(sortedPosts, 25).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
+               templateContext.set("pagination", pagination);
+               templateContext.set("posts", pagination.getItems());
+       }
+
+}
index 421b2a2..84d7c79 100644 (file)
@@ -17,7 +17,7 @@
 
 package net.pterodactylus.sone.web;
 
-import net.pterodactylus.sone.core.Options;
+import net.pterodactylus.sone.core.Core.Preferences;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
 import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.template.Template;
@@ -52,35 +52,35 @@ public class OptionsPage extends SoneTemplatePage {
        @Override
        protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
-               Options options = webInterface.getCore().getOptions();
+               Preferences preferences = webInterface.getCore().getPreferences();
                if (request.getMethod() == Method.POST) {
                        Integer insertionDelay = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("insertion-delay", 16));
-                       options.getIntegerOption("InsertionDelay").set(insertionDelay);
-                       Integer positiveTrust = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("positive-trust", 3), options.getIntegerOption("PositiveTrust").getReal());
-                       options.getIntegerOption("PositiveTrust").set(positiveTrust);
-                       Integer negativeTrust = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("negative-trust", 3), options.getIntegerOption("NegativeTrust").getReal());
-                       options.getIntegerOption("NegativeTrust").set(negativeTrust);
+                       preferences.setInsertionDelay(insertionDelay);
+                       Integer positiveTrust = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("positive-trust", 3));
+                       preferences.setPositiveTrust(positiveTrust);
+                       Integer negativeTrust = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("negative-trust", 4));
+                       preferences.setNegativeTrust(negativeTrust);
                        String trustComment = request.getHttpRequest().getPartAsStringFailsafe("trust-comment", 256);
                        if (trustComment.trim().length() == 0) {
                                trustComment = null;
                        }
-                       options.getStringOption("TrustComment").set(trustComment);
+                       preferences.setTrustComment(trustComment);
                        boolean soneRescueMode = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("sone-rescue-mode", 5));
-                       options.getBooleanOption("SoneRescueMode").set(soneRescueMode);
+                       preferences.setSoneRescueMode(soneRescueMode);
                        boolean clearOnNextRestart = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("clear-on-next-restart", 5));
-                       options.getBooleanOption("ClearOnNextRestart").set(clearOnNextRestart);
+                       preferences.setClearOnNextRestart(clearOnNextRestart);
                        boolean reallyClearOnNextRestart = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("really-clear-on-next-restart", 5));
-                       options.getBooleanOption("ReallyClearOnNextRestart").set(reallyClearOnNextRestart);
+                       preferences.setReallyClearOnNextRestart(reallyClearOnNextRestart);
                        webInterface.getCore().saveConfiguration();
                        throw new RedirectException(getPath());
                }
-               templateContext.set("insertion-delay", options.getIntegerOption("InsertionDelay").get());
-               templateContext.set("positive-trust", options.getIntegerOption("PositiveTrust").get());
-               templateContext.set("negative-trust", options.getIntegerOption("NegativeTrust").get());
-               templateContext.set("trust-comment", options.getStringOption("TrustComment").get());
-               templateContext.set("sone-rescue-mode", options.getBooleanOption("SoneRescueMode").get());
-               templateContext.set("clear-on-next-restart", options.getBooleanOption("ClearOnNextRestart").get());
-               templateContext.set("really-clear-on-next-restart", options.getBooleanOption("ReallyClearOnNextRestart").get());
+               templateContext.set("insertion-delay", preferences.getInsertionDelay());
+               templateContext.set("positive-trust", preferences.getPositiveTrust());
+               templateContext.set("negative-trust", preferences.getNegativeTrust());
+               templateContext.set("trust-comment", preferences.getTrustComment());
+               templateContext.set("sone-rescue-mode", preferences.isSoneRescueMode());
+               templateContext.set("clear-on-next-restart", preferences.isClearOnNextRestart());
+               templateContext.set("really-clear-on-next-restart", preferences.isReallyClearOnNextRestart());
        }
 
 }
diff --git a/src/main/java/net/pterodactylus/sone/web/UnbookmarkPage.java b/src/main/java/net/pterodactylus/sone/web/UnbookmarkPage.java
new file mode 100644 (file)
index 0000000..85a0359
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Sone - BookmarkPage.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;
+
+import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Page that lets the user unbookmark a post.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UnbookmarkPage extends SoneTemplatePage {
+
+       /**
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public UnbookmarkPage(Template template, WebInterface webInterface) {
+               super("unbookmark.html", template, "Page.Unbookmark.Title", webInterface);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               if (request.getMethod() == Method.POST) {
+                       String id = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
+                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+                       webInterface.getCore().unbookmarkPost(id);
+                       throw new RedirectException(returnPage);
+               }
+       }
+
+}
index 864dc84..967b9c5 100644 (file)
@@ -52,8 +52,10 @@ public class ViewPostPage extends SoneTemplatePage {
        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);
        }
 
        /**
index 9023ccf..89cec92 100644 (file)
@@ -58,6 +58,7 @@ import net.pterodactylus.sone.template.SoneAccessor;
 import net.pterodactylus.sone.template.SubstringFilter;
 import net.pterodactylus.sone.template.TrustAccessor;
 import net.pterodactylus.sone.template.UnknownDateFilter;
+import net.pterodactylus.sone.web.ajax.BookmarkAjaxPage;
 import net.pterodactylus.sone.web.ajax.CreatePostAjaxPage;
 import net.pterodactylus.sone.web.ajax.CreateReplyAjaxPage;
 import net.pterodactylus.sone.web.ajax.DeletePostAjaxPage;
@@ -77,6 +78,7 @@ import net.pterodactylus.sone.web.ajax.LockSoneAjaxPage;
 import net.pterodactylus.sone.web.ajax.MarkAsKnownAjaxPage;
 import net.pterodactylus.sone.web.ajax.MoveProfileFieldAjaxPage;
 import net.pterodactylus.sone.web.ajax.TrustAjaxPage;
+import net.pterodactylus.sone.web.ajax.UnbookmarkAjaxPage;
 import net.pterodactylus.sone.web.ajax.UnfollowSoneAjaxPage;
 import net.pterodactylus.sone.web.ajax.UnlikeAjaxPage;
 import net.pterodactylus.sone.web.ajax.UnlockSoneAjaxPage;
@@ -94,6 +96,7 @@ import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.notify.Notification;
 import net.pterodactylus.util.notify.NotificationManager;
 import net.pterodactylus.util.notify.TemplateNotification;
+import net.pterodactylus.util.template.CollectionSortFilter;
 import net.pterodactylus.util.template.DateFilter;
 import net.pterodactylus.util.template.FormatFilter;
 import net.pterodactylus.util.template.HtmlFilter;
@@ -198,9 +201,10 @@ public class WebInterface implements CoreListener {
                templateContextFactory.addFilter("match", new MatchFilter());
                templateContextFactory.addFilter("css", new CssClassNameFilter());
                templateContextFactory.addFilter("js", new JavascriptFilter());
-               templateContextFactory.addFilter("parse", new ParserFilter(templateContextFactory));
+               templateContextFactory.addFilter("parse", new ParserFilter(getCore(), templateContextFactory));
                templateContextFactory.addFilter("unknown", new UnknownDateFilter(getL10n(), "View.Sone.Text.UnknownDate"));
                templateContextFactory.addFilter("format", new FormatFilter());
+               templateContextFactory.addFilter("sort", new CollectionSortFilter());
                templateContextFactory.addPlugin("getpage", new GetPagePlugin());
                templateContextFactory.addPlugin("paginate", new PaginationPlugin());
                templateContextFactory.addProvider(Provider.TEMPLATE_CONTEXT_PROVIDER);
@@ -518,6 +522,7 @@ public class WebInterface implements CoreListener {
                Template createSoneTemplate = TemplateParser.parse(createReader("/templates/createSone.html"));
                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 editProfileTemplate = TemplateParser.parse(createReader("/templates/editProfile.html"));
                Template editProfileFieldTemplate = TemplateParser.parse(createReader("/templates/editProfileField.html"));
                Template deleteProfileFieldTemplate = TemplateParser.parse(createReader("/templates/deleteProfileField.html"));
@@ -556,6 +561,9 @@ public class WebInterface implements CoreListener {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DistrustPage(emptyTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UntrustPage(emptyTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new MarkAsKnownPage(emptyTemplate, this)));
+               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 DeleteSonePage(deleteSoneTemplate, this), "DeleteSone"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new LoginPage(loginTemplate, this), "Login"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new LogoutPage(emptyTemplate, this), "Logout"));
@@ -587,6 +595,8 @@ public class WebInterface implements CoreListener {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new LikeAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UnlikeAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetLikesAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new BookmarkAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new UnbookmarkAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new EditProfileFieldAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteProfileFieldAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new MoveProfileFieldAjaxPage(this)));
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/BookmarkAjaxPage.java
new file mode 100644 (file)
index 0000000..760bd34
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Sone - BookmarkAjaxPage.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.ajax;
+
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * AJAX page that lets the user bookmark a post.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class BookmarkAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new bookmark AJAX page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public BookmarkAjaxPage(WebInterface webInterface) {
+               super("bookmark.ajax", webInterface);
+       }
+
+       //
+       // JSONPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected JsonObject createJsonObject(Request request) {
+               String id = request.getHttpRequest().getParam("post", null);
+               if ((id == null) || (id.length() == 0)) {
+                       return createErrorJsonObject("invalid-post-id");
+               }
+               webInterface.getCore().bookmarkPost(id);
+               return createSuccessJsonObject();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
+}
index 127834b..4e5f1b8 100644 (file)
@@ -97,6 +97,7 @@ public class GetPostAjaxPage extends JsonPage {
                TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext();
                templateContext.set("post", post);
                templateContext.set("currentSone", currentSone);
+               templateContext.set("localSones", webInterface.getCore().getLocalSones());
                try {
                        postTemplate.render(templateContext, stringWriter);
                } catch (TemplateException te1) {
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/UnbookmarkAjaxPage.java
new file mode 100644 (file)
index 0000000..ad1689e
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Sone - BookmarkAjaxPage.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.ajax;
+
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * AJAX page that lets the user unbookmark a post.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UnbookmarkAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new unbookmark AJAX page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public UnbookmarkAjaxPage(WebInterface webInterface) {
+               super("unbookmark.ajax", webInterface);
+       }
+
+       //
+       // JSONPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected JsonObject createJsonObject(Request request) {
+               String id = request.getHttpRequest().getParam("post", null);
+               if ((id == null) || (id.length() == 0)) {
+                       return createErrorJsonObject("invalid-post-id");
+               }
+               webInterface.getCore().unbookmarkPost(id);
+               return createSuccessJsonObject();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
+}
index 35bc436..4d912e9 100644 (file)
@@ -8,6 +8,8 @@ Navigation.Menu.Item.CreateSone.Name=Create Sone
 Navigation.Menu.Item.CreateSone.Tooltip=Create a new Sone
 Navigation.Menu.Item.KnownSones.Name=Known Sones
 Navigation.Menu.Item.KnownSones.Tooltip=Shows all known Sones
+Navigation.Menu.Item.Bookmarks.Name=Bookmarks
+Navigation.Menu.Item.Bookmarks.Tooltip=Show bookmarked posts
 Navigation.Menu.Item.EditProfile.Name=Edit Profile
 Navigation.Menu.Item.EditProfile.Tooltip=Edit the Profile of your Sone
 Navigation.Menu.Item.DeleteSone.Name=Delete Sone
@@ -160,6 +162,13 @@ Page.Untrust.Title=Untrust Sone - Sone
 
 Page.MarkAsKnown.Title=Mark as Known - Sone
 
+Page.Bookmark.Title=Bookmark - Sone
+Page.Unbookmark.Title=Remove Bookmark - Sone
+Page.Bookmarks.Title=Bookmarks - Sone
+Page.Bookmarks.Page.Title=Bookmarks
+Page.Bookmarks.Text.NoBookmarks=You don’t have any bookmarks defined right now. You can bookmark posts by clicking the star below the post.
+Page.Bookmarks.Text.PostsNotLoaded=Some of your bookmarked posts have not been shown because they could not be loaded. This can happen if your restarted Sone recently or if the originating Sone has deleted the post.
+
 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!
@@ -197,11 +206,14 @@ View.Sone.Status.Downloading=This Sone is currently being downloaded.
 View.Sone.Status.Inserting=This Sone is currently being inserted.
 
 View.Post.UnknownAuthor=(unknown)
+View.Post.Bookmarks.PostIsBookmarked=Post is bookmarked, click to remove from bookmarks
+View.Post.Bookmarks.PostIsNotBookmarked=Post is not bookmarked, click to bookmark
 View.Post.DeleteLink=Delete
 View.Post.SendReply=Post Reply!
 View.Post.Reply.DeleteLink=Delete
 View.Post.LikeLink=Like
 View.Post.UnlikeLink=Unlike
+View.Post.ShowSource=Toggle Parser
 
 View.UpdateStatus.Text.ChooseSenderIdentity=Choose the sender identity
 
index c5d8e9a..8eb8449 100644 (file)
@@ -164,7 +164,7 @@ textarea {
        margin-bottom: 1em;
 }
 
-#sone #update-status label {
+#sone #update-status label, #sone #post-message label {
        display: none;
 }
 
@@ -180,11 +180,11 @@ textarea {
        float: right;
 }
 
-#sone #update-status .select-sender, #sone .create-reply .select-sender {
+#sone #update-status .select-sender, #sone .create-reply .select-sender, #sone #post-message .select-sender {
        display: none;
 }
 
-#sone #update-status .select-sender button {
+#sone #update-status .select-sender button, #sone #post-message .select-sender button {
        display: inline;
        float: left;
 }
@@ -239,11 +239,15 @@ textarea {
        font-weight: bold;
 }
 
-#sone .post .text {
+#sone .post .text, #sone .post .raw-text {
        display: inline;
        white-space: pre-wrap;
 }
 
+#sone .post .text.hidden, #sone .post .raw-text.hidden {
+       display: none;
+}
+
 #sone .post .status-line {
        margin-top: 0.5ex;
        font-size: 85%;
@@ -254,6 +258,21 @@ textarea {
        color: rgb(28, 131, 191);
 }
 
+#sone .show-source, #sone .show-reply-source {
+       display: inline;
+}
+
+#sone .post .bookmarks {
+       display: inline;
+       color: rgb(28, 131, 191);
+}
+
+#sone .post .bookmark, #sone .post .unbookmark {
+       display: inline;
+       font: inherit;
+       margin: 0px;
+}
+
 #sone .post .time {
        display: inline;
        color: #666;
@@ -269,11 +288,11 @@ textarea {
        display: none;
 }
 
-#sone .post .like.hidden, #sone .post .unlike.hidden, #sone .post .trust.hidden, #sone .post .distrust.hidden, #sone .post .untrust.hidden {
+#sone .post .like.hidden, #sone .post .unlike.hidden, #sone .post .trust.hidden, #sone .post .distrust.hidden, #sone .post .untrust.hidden, #sone .post .bookmark.hidden, #sone .post .unbookmark.hidden {
        display: none;
 }
 
-#sone .post .delete button, #sone .post .like button, #sone .post .unlike button, #sone .post .trust button, #sone .post .distrust button, #sone .post .untrust button {
+#sone .post .delete button, #sone .post .like button, #sone .post .unlike button, #sone .post .trust button, #sone .post .distrust button, #sone .post .untrust button, #sone .post .bookmark button, #sone .post .unbookmark button {
        border: 0px;
        background: none;
        padding: 0px;
@@ -299,7 +318,7 @@ textarea {
        color: rgb(64, 64, 64);
 }
 
-#sone .post .delete button:hover, #sone .post .like button:hover, #sone .post .unlike button:hover, #sone .post .trust button:hover, #sone .post .distrust button:hover, #sone .post .untrust button:hover {
+#sone .post .delete button:hover, #sone .post .like button:hover, #sone .post .unlike button:hover, #sone .post .trust button:hover, #sone .post .distrust button:hover, #sone .post .untrust button:hover, #sone .post .bookmark button:hover, #sone .post .unbookmark button:hover {
        border: 0px;
        background: none;
        padding: 0px;
@@ -392,7 +411,7 @@ textarea {
        float: right;
 }
 
-#sone .create-reply .select-sender button {
+#sone .create-reply .select-sender button, #sone #post-message .select-sender button {
        display: inline;
        float: left;
 }
diff --git a/src/main/resources/static/images/icon-activity.png b/src/main/resources/static/images/icon-activity.png
new file mode 100644 (file)
index 0000000..5803edb
Binary files /dev/null and b/src/main/resources/static/images/icon-activity.png differ
index 251105a..9ef6812 100644 (file)
@@ -297,6 +297,17 @@ function getSoneId(element) {
        return getSoneElement(element).find(".id").text();
 }
 
+/**
+ * Returns the element of the post with the given ID.
+ *
+ * @param postId
+ *            The ID of the post
+ * @returns The element of the post
+ */
+function getPost(postId) {
+       return $("#sone .post#" + postId);
+}
+
 function getPostElement(element) {
        return $(element).closest(".post");
 }
@@ -475,6 +486,38 @@ function updateTrustControls(soneId, trustValue) {
        });
 }
 
+/**
+ * Bookmarks the post with the given ID.
+ *
+ * @param postId
+ *            The ID of the post to bookmark
+ */
+function bookmarkPost(postId) {
+       (function(postId) {
+               $.getJSON("bookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
+                       if ((data != null) && data.success) {
+                               getPost(postId).find(".bookmark").toggleClass("hidden", true);
+                               getPost(postId).find(".unbookmark").toggleClass("hidden", false);
+                       }
+               });
+       })(postId);
+}
+
+/**
+ * Unbookmarks the post with the given ID.
+ *
+ * @param postId
+ *            The ID of the post to unbookmark
+ */
+function unbookmarkPost(postId) {
+       $.getJSON("unbookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
+               if ((data != null) && data.success) {
+                       getPost(postId).find(".bookmark").toggleClass("hidden", false);
+                       getPost(postId).find(".unbookmark").toggleClass("hidden", true);
+               }
+       });
+}
+
 function updateReplyLikes(replyId) {
        $.getJSON("getLikes.ajax", { "type": "reply", "reply": replyId }, function(data, textStatus) {
                if ((data != null) && data.success) {
@@ -649,6 +692,25 @@ function ajaxifyPost(postElement) {
                return false;
        });
 
+       /* convert bookmark/unbookmark buttons to javascript functions. */
+       $(postElement).find(".bookmark").submit(function() {
+               bookmarkPost(getPostId(this));
+               return false;
+       });
+       $(postElement).find(".unbookmark").submit(function() {
+               unbookmarkPost(getPostId(this));
+               return false;
+       });
+
+       /* convert “show source” link into javascript function. */
+       $(postElement).find(".show-source").each(function() {
+               $("a", this).click(function() {
+                       $(".post-text.text", getPostElement(this)).toggleClass("hidden");
+                       $(".post-text.raw-text", getPostElement(this)).toggleClass("hidden");
+                       return false;
+               });
+       });
+
        /* add “comment” link. */
        addCommentLink(getPostId(postElement), postElement, $(postElement).find(".post-status-line .time"));
 
@@ -709,6 +771,15 @@ function ajaxifyReply(replyElement) {
        })(replyElement);
        addCommentLink(getPostId(replyElement), replyElement, $(replyElement).find(".reply-status-line .time"));
 
+       /* convert “show source” link into javascript function. */
+       $(replyElement).find(".show-reply-source").each(function() {
+               $("a", this).click(function() {
+                       $(".reply-text.text", getReplyElement(this)).toggleClass("hidden");
+                       $(".reply-text.raw-text", getReplyElement(this)).toggleClass("hidden");
+                       return false;
+               });
+       });
+
        /* convert trust control buttons to javascript functions. */
        $(replyElement).find(".reply-trust").submit(function() {
                trustSone(getReplyAuthor(this));
@@ -742,6 +813,15 @@ function ajaxifyNotification(notification) {
        notification.find("form.mark-as-read button").click(function() {
                $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": $(":input[name=type]", this.form).val(), "id": $(":input[name=id]", this.form).val()});
        });
+       notification.find("a[class^='link-']").each(function() {
+               linkElement = $(this);
+               if (linkElement.is("[href^='viewPost']")) {
+                       id = linkElement.attr("class").substr(5);
+                       if (hasPost(id)) {
+                               linkElement.attr("href", "#post-" + id);
+                       }
+               }
+       });
        notification.find("form.dismiss button").click(function() {
                $.getJSON("dismissNotification.ajax", { "formPassword" : getFormPassword(), "notification" : notification.attr("id") }, function(data, textStatus) {
                        /* dismiss in case of error, too. */
@@ -1026,7 +1106,7 @@ function markReplyAsKnown(replyElements) {
 function resetActivity() {
        title = document.title;
        if (title.indexOf('(') == 0) {
-               document.title = title.substr(title.indexOf(' ') + 1);
+               setTitle(title.substr(title.indexOf(' ') + 1));
        }
 }
 
@@ -1034,12 +1114,65 @@ function setActivity() {
        if (!focus) {
                title = document.title;
                if (title.indexOf('(') != 0) {
-                       document.title = "(!) " + title;
+                       setTitle("(!) " + title);
+               }
+               if (!iconBlinking) {
+                       setTimeout(toggleIcon, 1500);
+                       iconBlinking = true;
+               }
+       }
+}
+
+/**
+ * Sets the window title after a small delay to prevent race-condition issues.
+ *
+ * @param title
+ *            The title to set
+ */
+function setTitle(title) {
+       setTimeout(function() {
+               document.title = title;
+       }, 50);
+}
+
+/** Whether the icon is currently showing activity. */
+var iconActive = false;
+
+/** Whether the icon is currently supposed to blink. */
+var iconBlinking = false;
+
+/**
+ * Toggles the icon. If the window has gained focus and the icon is still
+ * showing the activity state, it is returned to normal.
+ */
+function toggleIcon() {
+       if (focus) {
+               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);
        }
 }
 
 /**
+ * Changes the icon of the page.
+ *
+ * @param iconUrl
+ *            The new URL of the icon
+ */
+function changeIcon(iconUrl) {
+       $("link[rel=icon]").remove();
+       $("head").append($("<link>").attr("rel", "icon").attr("type", "image/png").attr("href", iconUrl));
+       $("iframe[id=icon-update]")[0].src += "";
+}
+
+/**
  * Creates a new notification.
  *
  * @param id
@@ -1186,14 +1319,25 @@ $(document).ready(function() {
        /* 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 .select-sender").css("display", "inline");
+               $("#sone #post-message .sender").hide();
+               $("#sone #post-message .select-sender button").click(function() {
+                       $("#sone #post-message .sender").show();
+                       $("#sone #post-message .select-sender").hide();
+                       return false;
+               });
                $("#sone #post-message").submit(function() {
-                       text = $(this).find(":input:enabled").val();
-                       $.getJSON("createPost.ajax", { "formPassword": getFormPassword(), "recipient": getShownSoneId(), "text": text }, function(data, textStatus) {
+                       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());
                                }
                        });
-                       $(this).find(":input:enabled").val("").blur();
+                       $(this).find(":input[name=sender]").val(getCurrentSoneId());
+                       $(this).find(":input[name=text]:enabled").val("").blur();
+                       $(this).find(".sender").hide();
+                       $(this).find(".select-sender").show();
                        return false;
                });
        });
diff --git a/src/main/resources/templates/bookmarks.html b/src/main/resources/templates/bookmarks.html
new file mode 100644 (file)
index 0000000..af592db
--- /dev/null
@@ -0,0 +1,27 @@
+<%include include/head.html>
+
+       <div class="page-id hidden">bookmarks</div>
+
+       <h1><%= Page.Bookmarks.Page.Title|l10n|html></h1>
+
+       <div id="posts">
+               <%include include/pagination.html>
+               <%foreach posts post>
+                       <%if post.loaded>
+                               <%= true|store key=postShown>
+                               <%include include/viewPost.html>
+                       <%else>
+                               <%= true|store key=postNotLoaded>
+                       <%/if>
+               <%/foreach>
+               <%if postNotLoaded>
+                       <p><%= Page.Bookmarks.Text.PostsNotLoaded|l10n|html></p>
+               <%else>
+                       <%if !postShown>
+                               <p><%= Page.Bookmarks.Text.NoBookmarks|l10n|html></p>
+                       <%/if>
+               <%/if>
+               <%include include/pagination.html>
+       </div>
+
+<%include include/tail.html>
index dd2e25e..ec2b587 100644 (file)
@@ -16,4 +16,6 @@
                </div>
        </div>
 
+       <iframe id="icon-update" class="hidden" src="about:blank"></iframe>
+
 </div>
index 74e3450..3023566 100644 (file)
@@ -1,5 +1,5 @@
-<a name="post-<% post.id|html>"></a>
 <div id="<% post.id|html>" class="post<%if loop.last> last<%/if><%if post.new> new<%/if>">
+       <a name="post-<% post.id|html>"></a>
        <div class="post-time hidden"><% post.time|html></div>
        <div class="post-author hidden"><% post.sone.id|html></div>
        <div class="avatar">
                                        <div class="recipient profile-link"><a href="viewSone.html?sone=<% post.recipient.id|html>"><% post.recipient.niceName|html></a></div>
                                <%/if>
                        <%/if>
-                       <div class="text"><% post.text|parse sone=post.sone></div>
+                       <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>
                </div>
                <div class="post-status-line status-line">
+                       <div class="bookmarks">
+                               <form class="unbookmark<%if !post.bookmarked> hidden<%/if>" action="unbookmark.html" method="post">
+                                       <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+                                       <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+                                       <input type="hidden" name="post" value="<% post.id|html>" />
+                                       <button type="submit" title="<%= View.Post.Bookmarks.PostIsBookmarked|l10n|html>">★</button>
+                               </form>
+                               <form class="bookmark<%if post.bookmarked> hidden<%/if>" action="bookmark.html" method="post">
+                                       <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+                                       <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+                                       <input type="hidden" name="post" value="<% post.id|html>" />
+                                       <button type="submit" title="<%= View.Post.Bookmarks.PostIsNotBookmarked|l10n|html>">☆</button>
+                               </form>
+                       </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>&amp;raw=<%if raw>false<%else>true<%/if>"><%= View.Post.ShowSource|l10n|html></a></div>
                        <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>
                                                <input type="hidden" name="post" value="<% post.id|html>" />
                                                <div class="sender">
                                                        <select name="sender" title="<%= View.UpdateStatus.Text.ChooseSenderIdentity|l10n|html>">
-                                                               <%foreach localSones localSone>
+                                                               <%foreach localSones localSone|sort>
                                                                        <option value="<% localSone.id|html>"<%if localSone.current> selected="selected"<%/if>><% localSone.niceName|html></option>
                                                                <%/foreach>
                                                        </select>
index 6b13776..cdb837c 100644 (file)
@@ -1,5 +1,5 @@
-<a name="reply-<% reply.id|html>"></a>
 <div id="<% reply.id|html>" class="reply<%if reply.new> new<%/if>">
+       <a name="reply-<% reply.id|html>"></a>
        <div class="reply-time hidden"><% reply.time|html></div>
        <div class="reply-author hidden"><% reply.sone.id|html></div>
        <div class="avatar">
@@ -8,10 +8,13 @@
        <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="text"><% reply.text|parse sone=reply.sone></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>
                </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>&amp;raw=<%if raw>false<%else>true<%/if>"><%= View.Post.ShowSource|l10n|html></a></div>
                        <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>
index e5d7792..177b470 100644 (file)
@@ -12,6 +12,6 @@
        </form>
        <%= Notification.NewPost.Text|l10n|html>
        <%foreach posts post>
-               <a href="viewPost.html?post=<% post.id|html>"><% post.sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
+               <a class="link-<% post.id|html>" href="viewPost.html?post=<% post.id|html>"><% post.sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
        <%/foreach>
 </div>
index 2a194f2..21a5ca0 100644 (file)
@@ -12,6 +12,6 @@
        </form>
        <%= Notification.NewReply.Text|l10n|html>
        <%foreach replies reply>
-               <a href="viewPost.html?post=<% reply.post.id|html>"><% reply.sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
+               <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>
 </div>
index fdb8b2a..5dfc25a 100644 (file)
@@ -1 +1 @@
-<div class="text"><%= Notification.NewVersion.Text|l10n|replace needle="{version}" replacementKey=latestVersion|replace needle="{edition}" replacementKey=latestEdition|parse sone=currentSone></div>
+<div class="text"><%= Notification.NewVersion.Text|l10n|replace needle="{version}" replacementKey=latestVersion|replace needle="{edition}" replacementKey=latestEdition|parse sone="nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI"></div>
index 4b2502a..d2a8f6e 100644 (file)
                                        <input type="hidden" name="formPassword" value="<% formPassword|html>" />
                                        <input type="hidden" name="returnPage" value="<% request.uri|html>" />
                                        <input type="hidden" name="recipient" value="<% sone.id|html>" />
+                                       <label for="sender"><%= Page.Index.Label.Sender|l10n|html></label>
+                                       <div class="sender">
+                                               <select name="sender" title="<%= View.UpdateStatus.Text.ChooseSenderIdentity|l10n|html>">
+                                                       <%foreach localSones localSone>
+                                                               <option value="<% localSone.id|html>"<%if localSone.current> selected="selected"<%/if>><% localSone.niceName|html></option>
+                                                       <%/foreach>
+                                               </select>
+                                       </div>
+                                       <div class="select-sender"><button type="button" title="<%= View.UpdateStatus.Text.ChooseSenderIdentity|l10n|html>">+</button></div><label for="text"><%= Page.Index.Label.Text|l10n|html></label>
                                        <input type="text" name="text" value="" />
                                        <button type="submit"><%= Page.CreatePost.Button.Post|l10n|html></button>
                                </form>