Merge branch 'release-0.6.6' 0.6.6
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Mon, 12 Sep 2011 11:40:37 +0000 (13:40 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Mon, 12 Sep 2011 11:40:37 +0000 (13:40 +0200)
57 files changed:
pom.xml
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/core/CoreListener.java
src/main/java/net/pterodactylus/sone/core/CoreListenerManager.java
src/main/java/net/pterodactylus/sone/core/SoneDownloader.java
src/main/java/net/pterodactylus/sone/core/SoneInsertListener.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/SoneInsertListenerManager.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/SoneInserter.java
src/main/java/net/pterodactylus/sone/core/SoneRescuer.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/data/Sone.java
src/main/java/net/pterodactylus/sone/freenet/L10nFilter.java
src/main/java/net/pterodactylus/sone/main/SonePlugin.java
src/main/java/net/pterodactylus/sone/notify/ListNotificationFilters.java
src/main/java/net/pterodactylus/sone/template/ParserFilter.java
src/main/java/net/pterodactylus/sone/template/SoneAccessor.java
src/main/java/net/pterodactylus/sone/template/UniqueElementFilter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/text/SoneTextParser.java
src/main/java/net/pterodactylus/sone/web/CreateReplyPage.java
src/main/java/net/pterodactylus/sone/web/EditProfilePage.java
src/main/java/net/pterodactylus/sone/web/FollowSonePage.java
src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java
src/main/java/net/pterodactylus/sone/web/OptionsPage.java
src/main/java/net/pterodactylus/sone/web/RescuePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/SearchPage.java
src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java
src/main/java/net/pterodactylus/sone/web/UnfollowSonePage.java
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/java/net/pterodactylus/sone/web/ajax/CreateReplyAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/FollowSoneAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetPostAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetReplyAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetTimesAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/LikeAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/UnfollowSoneAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/UnlikeAjaxPage.java
src/main/resources/i18n/sone.en.properties
src/main/resources/static/css/sone.css
src/main/resources/static/javascript/jquery.fieldselection.js [new file with mode: 0644]
src/main/resources/static/javascript/sone.js
src/main/resources/templates/include/head.html
src/main/resources/templates/include/soneMenu.html [new file with mode: 0644]
src/main/resources/templates/include/updateStatus.html
src/main/resources/templates/include/viewPost.html
src/main/resources/templates/include/viewReply.html
src/main/resources/templates/include/viewSone.html
src/main/resources/templates/knownSones.html
src/main/resources/templates/notify/mentionNotification.html
src/main/resources/templates/notify/rescuingSonesNotification.html [deleted file]
src/main/resources/templates/notify/soneInsertNotification.html [new file with mode: 0644]
src/main/resources/templates/notify/sonesRescuedNotification.html [deleted file]
src/main/resources/templates/options.html
src/main/resources/templates/rescue.html [new file with mode: 0644]
src/main/resources/templates/viewSone.html

diff --git a/pom.xml b/pom.xml
index 36a51ff..a087474 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.6.5</version>
+       <version>0.6.6</version>
        <dependencies>
                <dependency>
                        <groupId>net.pterodactylus</groupId>
                        <artifactId>utils</artifactId>
-                       <version>0.9.6</version>
+                       <version>0.10.0</version>
                </dependency>
                <dependency>
                        <groupId>junit</groupId>
index 3c99eef..c8d7588 100644 (file)
@@ -49,16 +49,17 @@ import net.pterodactylus.sone.freenet.wot.OwnIdentity;
 import net.pterodactylus.sone.freenet.wot.Trust;
 import net.pterodactylus.sone.freenet.wot.WebOfTrustException;
 import net.pterodactylus.sone.main.SonePlugin;
-import net.pterodactylus.util.collection.Pair;
 import net.pterodactylus.util.config.Configuration;
 import net.pterodactylus.util.config.ConfigurationException;
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.number.Numbers;
+import net.pterodactylus.util.service.AbstractService;
 import net.pterodactylus.util.thread.Ticker;
+import net.pterodactylus.util.validation.EqualityValidator;
 import net.pterodactylus.util.validation.IntegerRangeValidator;
+import net.pterodactylus.util.validation.OrValidator;
 import net.pterodactylus.util.validation.Validation;
 import net.pterodactylus.util.version.Version;
-import freenet.client.FetchResult;
 import freenet.keys.FreenetURI;
 
 /**
@@ -66,7 +67,7 @@ import freenet.keys.FreenetURI;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class Core implements IdentityListener, UpdateListener, SoneProvider, PostProvider {
+public class Core extends AbstractService implements IdentityListener, UpdateListener, SoneProvider, PostProvider, SoneInsertListener {
 
        /**
         * Enumeration for the possible states of a {@link Sone}.
@@ -124,9 +125,6 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
        /** The FCP interface. */
        private volatile FcpInterface fcpInterface;
 
-       /** Whether the core has been stopped. */
-       private volatile boolean stopped;
-
        /** The Sones’ statuses. */
        /* synchronize access on itself. */
        private final Map<Sone, SoneStatus> soneStatuses = new HashMap<Sone, SoneStatus>();
@@ -139,6 +137,10 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
        /* synchronize access on this on localSones. */
        private final Map<Sone, SoneInserter> soneInserters = new HashMap<Sone, SoneInserter>();
 
+       /** Sone rescuers. */
+       /* synchronize access on this on localSones. */
+       private final Map<Sone, SoneRescuer> soneRescuers = new HashMap<Sone, SoneRescuer>();
+
        /** All local Sones. */
        /* synchronize access on this on itself. */
        private Map<String, Sone> localSones = new HashMap<String, Sone>();
@@ -183,6 +185,9 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
        /** Ticker for threads that mark own elements as known. */
        private Ticker localElementTicker = new Ticker();
 
+       /** The time the configuration was last touched. */
+       private volatile long lastConfigurationUpdate;
+
        /**
         * Creates a new core.
         *
@@ -194,6 +199,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
         *            The identity manager
         */
        public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager) {
+               super("Sone Core");
                this.configuration = configuration;
                this.freenetInterface = freenetInterface;
                this.identityManager = identityManager;
@@ -238,7 +244,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
         */
        public void setConfiguration(Configuration configuration) {
                this.configuration = configuration;
-               saveConfiguration();
+               touchConfiguration();
        }
 
        /**
@@ -306,6 +312,26 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
        }
 
        /**
+        * Returns the Sone rescuer for the given local Sone.
+        *
+        * @param sone
+        *            The local Sone to get the rescuer for
+        * @return The Sone rescuer for the given Sone
+        */
+       public SoneRescuer getSoneRescuer(Sone sone) {
+               Validation.begin().isNotNull("Sone", sone).check().is("Local Sone", isLocalSone(sone)).check();
+               synchronized (localSones) {
+                       SoneRescuer soneRescuer = soneRescuers.get(sone);
+                       if (soneRescuer == null) {
+                               soneRescuer = new SoneRescuer(this, soneDownloader, sone);
+                               soneRescuers.put(sone, soneRescuer);
+                               soneRescuer.start();
+                       }
+                       return soneRescuer;
+               }
+       }
+
+       /**
         * Returns whether the given Sone is currently locked.
         *
         * @param sone
@@ -857,44 +883,11 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
                        /* TODO - load posts ’n stuff */
                        localSones.put(ownIdentity.getId(), sone);
                        final SoneInserter soneInserter = new SoneInserter(this, freenetInterface, sone);
+                       soneInserter.addSoneInsertListener(this);
                        soneInserters.put(sone, soneInserter);
                        setSoneStatus(sone, SoneStatus.idle);
                        loadSone(sone);
-                       if (!preferences.isSoneRescueMode()) {
-                               soneInserter.start();
-                       }
-                       new Thread(new Runnable() {
-
-                               @Override
-                               @SuppressWarnings("synthetic-access")
-                               public void run() {
-                                       if (!preferences.isSoneRescueMode()) {
-                                               return;
-                                       }
-                                       logger.log(Level.INFO, "Trying to restore Sone from Freenet…");
-                                       coreListenerManager.fireRescuingSone(sone);
-                                       lockSone(sone);
-                                       long edition = sone.getLatestEdition();
-                                       /* find the latest edition the node knows about. */
-                                       Pair<FreenetURI, FetchResult> currentUri = freenetInterface.fetchUri(sone.getRequestUri());
-                                       if (currentUri != null) {
-                                               long currentEdition = currentUri.getLeft().getEdition();
-                                               if (currentEdition > edition) {
-                                                       edition = currentEdition;
-                                               }
-                                       }
-                                       while (!stopped && (edition >= 0) && preferences.isSoneRescueMode()) {
-                                               logger.log(Level.FINE, "Downloading edition " + edition + "…");
-                                               soneDownloader.fetchSone(sone, sone.getRequestUri().setKeyType("SSK").setDocName("Sone-" + edition));
-                                               --edition;
-                                       }
-                                       logger.log(Level.INFO, "Finished restoring Sone from Freenet, starting Inserter…");
-                                       saveSone(sone);
-                                       coreListenerManager.fireRescuedSone(sone);
-                                       soneInserter.start();
-                               }
-
-                       }, "Sone Downloader").start();
+                       soneInserter.start();
                        return sone;
                }
        }
@@ -916,7 +909,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
                Sone sone = addLocalSone(ownIdentity);
                sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
                sone.addFriend("nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI");
-               saveSone(sone);
+               touchConfiguration();
                return sone;
        }
 
@@ -949,7 +942,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
                                        for (Sone localSone : getLocalSones()) {
                                                if (localSone.getOptions().getBooleanOption("AutoFollow").get()) {
                                                        localSone.addFriend(sone.getId());
-                                                       saveSone(localSone);
+                                                       touchConfiguration();
                                                }
                                        }
                                }
@@ -1062,14 +1055,28 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
        }
 
        /**
-        * Updates the stores Sone with the given Sone.
+        * Updates the stored Sone with the given Sone.
         *
         * @param sone
         *            The updated Sone
         */
        public void updateSone(Sone sone) {
+               updateSone(sone, false);
+       }
+
+       /**
+        * Updates the stored Sone with the given Sone. If {@code soneRescueMode} is
+        * {@code true}, an older Sone than the current Sone can be given to restore
+        * an old state.
+        *
+        * @param sone
+        *            The Sone to update
+        * @param soneRescueMode
+        *            {@code true} if the stored Sone should be updated regardless
+        *            of the age of the given Sone
+        */
+       public void updateSone(Sone sone, boolean soneRescueMode) {
                if (hasSone(sone.getId())) {
-                       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 });
@@ -1166,7 +1173,9 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
                                return;
                        }
                        localSones.remove(sone.getId());
-                       soneInserters.remove(sone).stop();
+                       SoneInserter soneInserter = soneInserters.remove(sone);
+                       soneInserter.removeSoneInsertListener(this);
+                       soneInserter.stop();
                }
                try {
                        ((OwnIdentity) sone.getIdentity()).removeContext("Sone");
@@ -1193,7 +1202,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
                        if (newSones.remove(sone.getId())) {
                                knownSones.add(sone.getId());
                                coreListenerManager.fireMarkSoneKnown(sone);
-                               saveConfiguration();
+                               touchConfiguration();
                        }
                }
        }
@@ -1213,6 +1222,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
 
                /* initialize options. */
                sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
+               sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption<Boolean>(false));
 
                /* load Sone. */
                String sonePrefix = "Sone/" + sone.getId();
@@ -1315,6 +1325,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
 
                /* load options. */
                sone.getOptions().getBooleanOption("AutoFollow").set(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(null));
+               sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").set(configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").getValue(null));
 
                /* if we’re still here, Sone was loaded successfully. */
                synchronized (sone) {
@@ -1345,105 +1356,6 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
        }
 
        /**
-        * Saves the given Sone. This will persist all local settings for the given
-        * Sone, such as the friends list and similar, private options.
-        *
-        * @param sone
-        *            The Sone to save
-        */
-       public synchronized void saveSone(Sone sone) {
-               if (!isLocalSone(sone)) {
-                       logger.log(Level.FINE, "Tried to save non-local Sone: %s", sone);
-                       return;
-               }
-               if (!(sone.getIdentity() instanceof OwnIdentity)) {
-                       logger.log(Level.WARNING, "Local Sone without OwnIdentity found, refusing to save: %s", sone);
-                       return;
-               }
-
-               logger.log(Level.INFO, "Saving Sone: %s", sone);
-               try {
-                       ((OwnIdentity) sone.getIdentity()).setProperty("Sone.LatestEdition", String.valueOf(sone.getLatestEdition()));
-
-                       /* save Sone into configuration. */
-                       String sonePrefix = "Sone/" + sone.getId();
-                       configuration.getLongValue(sonePrefix + "/Time").setValue(sone.getTime());
-                       configuration.getStringValue(sonePrefix + "/LastInsertFingerprint").setValue(soneInserters.get(sone).getLastInsertFingerprint());
-
-                       /* save profile. */
-                       Profile profile = sone.getProfile();
-                       configuration.getStringValue(sonePrefix + "/Profile/FirstName").setValue(profile.getFirstName());
-                       configuration.getStringValue(sonePrefix + "/Profile/MiddleName").setValue(profile.getMiddleName());
-                       configuration.getStringValue(sonePrefix + "/Profile/LastName").setValue(profile.getLastName());
-                       configuration.getIntValue(sonePrefix + "/Profile/BirthDay").setValue(profile.getBirthDay());
-                       configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").setValue(profile.getBirthMonth());
-                       configuration.getIntValue(sonePrefix + "/Profile/BirthYear").setValue(profile.getBirthYear());
-
-                       /* save profile fields. */
-                       int fieldCounter = 0;
-                       for (Field profileField : profile.getFields()) {
-                               String fieldPrefix = sonePrefix + "/Profile/Fields/" + fieldCounter++;
-                               configuration.getStringValue(fieldPrefix + "/Name").setValue(profileField.getName());
-                               configuration.getStringValue(fieldPrefix + "/Value").setValue(profileField.getValue());
-                       }
-                       configuration.getStringValue(sonePrefix + "/Profile/Fields/" + fieldCounter + "/Name").setValue(null);
-
-                       /* save posts. */
-                       int postCounter = 0;
-                       for (Post post : sone.getPosts()) {
-                               String postPrefix = sonePrefix + "/Posts/" + postCounter++;
-                               configuration.getStringValue(postPrefix + "/ID").setValue(post.getId());
-                               configuration.getStringValue(postPrefix + "/Recipient").setValue((post.getRecipient() != null) ? post.getRecipient().getId() : null);
-                               configuration.getLongValue(postPrefix + "/Time").setValue(post.getTime());
-                               configuration.getStringValue(postPrefix + "/Text").setValue(post.getText());
-                       }
-                       configuration.getStringValue(sonePrefix + "/Posts/" + postCounter + "/ID").setValue(null);
-
-                       /* save replies. */
-                       int replyCounter = 0;
-                       for (Reply reply : sone.getReplies()) {
-                               String replyPrefix = sonePrefix + "/Replies/" + replyCounter++;
-                               configuration.getStringValue(replyPrefix + "/ID").setValue(reply.getId());
-                               configuration.getStringValue(replyPrefix + "/Post/ID").setValue(reply.getPost().getId());
-                               configuration.getLongValue(replyPrefix + "/Time").setValue(reply.getTime());
-                               configuration.getStringValue(replyPrefix + "/Text").setValue(reply.getText());
-                       }
-                       configuration.getStringValue(sonePrefix + "/Replies/" + replyCounter + "/ID").setValue(null);
-
-                       /* save post likes. */
-                       int postLikeCounter = 0;
-                       for (String postId : sone.getLikedPostIds()) {
-                               configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter++ + "/ID").setValue(postId);
-                       }
-                       configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter + "/ID").setValue(null);
-
-                       /* save reply likes. */
-                       int replyLikeCounter = 0;
-                       for (String replyId : sone.getLikedReplyIds()) {
-                               configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter++ + "/ID").setValue(replyId);
-                       }
-                       configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter + "/ID").setValue(null);
-
-                       /* save friends. */
-                       int friendCounter = 0;
-                       for (String friendId : sone.getFriends()) {
-                               configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter++ + "/ID").setValue(friendId);
-                       }
-                       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) {
-                       logger.log(Level.WARNING, "Could not save Sone: " + sone, ce1);
-               } catch (WebOfTrustException wote1) {
-                       logger.log(Level.WARNING, "Could not set WoT property for Sone: " + sone, wote1);
-               }
-       }
-
-       /**
         * Creates a new post.
         *
         * @param sone
@@ -1518,7 +1430,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
                        coreListenerManager.fireNewPostFound(post);
                }
                sone.addPost(post);
-               saveSone(sone);
+               touchConfiguration();
                localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() {
 
                        /**
@@ -1552,7 +1464,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
                        markPostKnown(post);
                        knownPosts.remove(post.getId());
                }
-               saveSone(post.getSone());
+               touchConfiguration();
        }
 
        /**
@@ -1567,7 +1479,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
                        if (newPosts.remove(post.getId())) {
                                knownPosts.add(post.getId());
                                coreListenerManager.fireMarkPostKnown(post);
-                               saveConfiguration();
+                               touchConfiguration();
                        }
                }
        }
@@ -1658,7 +1570,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
                        coreListenerManager.fireNewReplyFound(reply);
                }
                sone.addReply(reply);
-               saveSone(sone);
+               touchConfiguration();
                localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() {
 
                        /**
@@ -1692,7 +1604,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
                        knownReplies.remove(reply.getId());
                }
                sone.removeReply(reply);
-               saveSone(sone);
+               touchConfiguration();
        }
 
        /**
@@ -1707,43 +1619,177 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
                        if (newReplies.remove(reply.getId())) {
                                knownReplies.add(reply.getId());
                                coreListenerManager.fireMarkReplyKnown(reply);
-                               saveConfiguration();
+                               touchConfiguration();
                        }
                }
        }
 
        /**
+        * Notifies the core that the configuration, either of the core or of a
+        * single local Sone, has changed, and that the configuration should be
+        * saved.
+        */
+       public void touchConfiguration() {
+               lastConfigurationUpdate = System.currentTimeMillis();
+       }
+
+       //
+       // SERVICE METHODS
+       //
+
+       /**
         * Starts the core.
         */
-       public void start() {
+       @Override
+       public void serviceStart() {
                loadConfiguration();
                updateChecker.addUpdateListener(this);
                updateChecker.start();
        }
 
        /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void serviceRun() {
+               long lastSaved = System.currentTimeMillis();
+               while (!shouldStop()) {
+                       sleep(1000);
+                       long now = System.currentTimeMillis();
+                       if (shouldStop() || ((lastConfigurationUpdate > lastSaved) && ((now - lastConfigurationUpdate) > 5000))) {
+                               for (Sone localSone : getLocalSones()) {
+                                       saveSone(localSone);
+                               }
+                               saveConfiguration();
+                               lastSaved = now;
+                       }
+               }
+       }
+
+       /**
         * Stops the core.
         */
-       public void stop() {
+       @Override
+       public void serviceStop() {
                synchronized (localSones) {
                        for (SoneInserter soneInserter : soneInserters.values()) {
+                               soneInserter.removeSoneInsertListener(this);
                                soneInserter.stop();
                        }
-                       for (Sone localSone : localSones.values()) {
-                               saveSone(localSone);
-                       }
                }
                updateChecker.stop();
                updateChecker.removeUpdateListener(this);
                soneDownloader.stop();
-               saveConfiguration();
-               stopped = true;
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Saves the given Sone. This will persist all local settings for the given
+        * Sone, such as the friends list and similar, private options.
+        *
+        * @param sone
+        *            The Sone to save
+        */
+       private synchronized void saveSone(Sone sone) {
+               if (!isLocalSone(sone)) {
+                       logger.log(Level.FINE, "Tried to save non-local Sone: %s", sone);
+                       return;
+               }
+               if (!(sone.getIdentity() instanceof OwnIdentity)) {
+                       logger.log(Level.WARNING, "Local Sone without OwnIdentity found, refusing to save: %s", sone);
+                       return;
+               }
+
+               logger.log(Level.INFO, "Saving Sone: %s", sone);
+               try {
+                       ((OwnIdentity) sone.getIdentity()).setProperty("Sone.LatestEdition", String.valueOf(sone.getLatestEdition()));
+
+                       /* save Sone into configuration. */
+                       String sonePrefix = "Sone/" + sone.getId();
+                       configuration.getLongValue(sonePrefix + "/Time").setValue(sone.getTime());
+                       configuration.getStringValue(sonePrefix + "/LastInsertFingerprint").setValue(soneInserters.get(sone).getLastInsertFingerprint());
+
+                       /* save profile. */
+                       Profile profile = sone.getProfile();
+                       configuration.getStringValue(sonePrefix + "/Profile/FirstName").setValue(profile.getFirstName());
+                       configuration.getStringValue(sonePrefix + "/Profile/MiddleName").setValue(profile.getMiddleName());
+                       configuration.getStringValue(sonePrefix + "/Profile/LastName").setValue(profile.getLastName());
+                       configuration.getIntValue(sonePrefix + "/Profile/BirthDay").setValue(profile.getBirthDay());
+                       configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").setValue(profile.getBirthMonth());
+                       configuration.getIntValue(sonePrefix + "/Profile/BirthYear").setValue(profile.getBirthYear());
+
+                       /* save profile fields. */
+                       int fieldCounter = 0;
+                       for (Field profileField : profile.getFields()) {
+                               String fieldPrefix = sonePrefix + "/Profile/Fields/" + fieldCounter++;
+                               configuration.getStringValue(fieldPrefix + "/Name").setValue(profileField.getName());
+                               configuration.getStringValue(fieldPrefix + "/Value").setValue(profileField.getValue());
+                       }
+                       configuration.getStringValue(sonePrefix + "/Profile/Fields/" + fieldCounter + "/Name").setValue(null);
+
+                       /* save posts. */
+                       int postCounter = 0;
+                       for (Post post : sone.getPosts()) {
+                               String postPrefix = sonePrefix + "/Posts/" + postCounter++;
+                               configuration.getStringValue(postPrefix + "/ID").setValue(post.getId());
+                               configuration.getStringValue(postPrefix + "/Recipient").setValue((post.getRecipient() != null) ? post.getRecipient().getId() : null);
+                               configuration.getLongValue(postPrefix + "/Time").setValue(post.getTime());
+                               configuration.getStringValue(postPrefix + "/Text").setValue(post.getText());
+                       }
+                       configuration.getStringValue(sonePrefix + "/Posts/" + postCounter + "/ID").setValue(null);
+
+                       /* save replies. */
+                       int replyCounter = 0;
+                       for (Reply reply : sone.getReplies()) {
+                               String replyPrefix = sonePrefix + "/Replies/" + replyCounter++;
+                               configuration.getStringValue(replyPrefix + "/ID").setValue(reply.getId());
+                               configuration.getStringValue(replyPrefix + "/Post/ID").setValue(reply.getPost().getId());
+                               configuration.getLongValue(replyPrefix + "/Time").setValue(reply.getTime());
+                               configuration.getStringValue(replyPrefix + "/Text").setValue(reply.getText());
+                       }
+                       configuration.getStringValue(sonePrefix + "/Replies/" + replyCounter + "/ID").setValue(null);
+
+                       /* save post likes. */
+                       int postLikeCounter = 0;
+                       for (String postId : sone.getLikedPostIds()) {
+                               configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter++ + "/ID").setValue(postId);
+                       }
+                       configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter + "/ID").setValue(null);
+
+                       /* save reply likes. */
+                       int replyLikeCounter = 0;
+                       for (String replyId : sone.getLikedReplyIds()) {
+                               configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter++ + "/ID").setValue(replyId);
+                       }
+                       configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter + "/ID").setValue(null);
+
+                       /* save friends. */
+                       int friendCounter = 0;
+                       for (String friendId : sone.getFriends()) {
+                               configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter++ + "/ID").setValue(friendId);
+                       }
+                       configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter + "/ID").setValue(null);
+
+                       /* save options. */
+                       configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").setValue(sone.getOptions().getBooleanOption("AutoFollow").getReal());
+                       configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").setValue(sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").getReal());
+
+                       configuration.save();
+                       logger.log(Level.INFO, "Sone %s saved.", sone);
+               } catch (ConfigurationException ce1) {
+                       logger.log(Level.WARNING, "Could not save Sone: " + sone, ce1);
+               } catch (WebOfTrustException wote1) {
+                       logger.log(Level.WARNING, "Could not set WoT property for Sone: " + sone, wote1);
+               }
        }
 
        /**
         * Saves the current options.
         */
-       public void saveConfiguration() {
+       private void saveConfiguration() {
                synchronized (configuration) {
                        if (storingConfiguration) {
                                logger.log(Level.FINE, "Already storing configuration…");
@@ -1757,6 +1803,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
                        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/CharactersPerPost").setValue(options.getIntegerOption("CharactersPerPost").getReal());
                        configuration.getBooleanValue("Option/RequireFullAccess").setValue(options.getBooleanOption("RequireFullAccess").getReal());
                        configuration.getIntValue("Option/PositiveTrust").setValue(options.getIntegerOption("PositiveTrust").getReal());
                        configuration.getIntValue("Option/NegativeTrust").setValue(options.getIntegerOption("NegativeTrust").getReal());
@@ -1815,10 +1862,6 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
                }
        }
 
-       //
-       // PRIVATE METHODS
-       //
-
        /**
         * Loads the configuration.
         */
@@ -1834,6 +1877,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
 
                }));
                options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(10, new IntegerRangeValidator(1, Integer.MAX_VALUE)));
+               options.addIntegerOption("CharactersPerPost", new DefaultOption<Integer>(200, new OrValidator<Integer>(new IntegerRangeValidator(50, Integer.MAX_VALUE), new EqualityValidator<Integer>(-1))));
                options.addBooleanOption("RequireFullAccess", new DefaultOption<Boolean>(false));
                options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75, new IntegerRangeValidator(0, 100)));
                options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-25, new IntegerRangeValidator(-100, 100)));
@@ -1872,6 +1916,7 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
 
                loadConfigurationValue("InsertionDelay");
                loadConfigurationValue("PostsPerPage");
+               loadConfigurationValue("CharactersPerPost");
                options.getBooleanOption("RequireFullAccess").set(configuration.getBooleanValue("Option/RequireFullAccess").getValue(null));
                loadConfigurationValue("PositiveTrust");
                loadConfigurationValue("NegativeTrust");
@@ -2079,6 +2124,33 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
                coreListenerManager.fireUpdateFound(version, releaseTime, latestEdition);
        }
 
+       //
+       // SONEINSERTLISTENER METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       public void insertStarted(Sone sone) {
+               coreListenerManager.fireSoneInserting(sone);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void insertFinished(Sone sone, long insertDuration) {
+               coreListenerManager.fireSoneInserted(sone, insertDuration);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void insertAborted(Sone sone, Throwable cause) {
+               coreListenerManager.fireSoneInsertAborted(sone, cause);
+       }
+
        /**
         * Convenience interface for external classes that want to access the core’s
         * configuration.
@@ -2168,6 +2240,41 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
                }
 
                /**
+                * Returns the number of characters per post, or <code>-1</code> if the
+                * posts should not be cut off.
+                *
+                * @return The numbers of characters per post
+                */
+               public int getCharactersPerPost() {
+                       return options.getIntegerOption("CharactersPerPost").get();
+               }
+
+               /**
+                * Validates the number of characters per post.
+                *
+                * @param charactersPerPost
+                *            The number of characters per post
+                * @return {@code true} if the number of characters per post was valid,
+                *         {@code false} otherwise
+                */
+               public boolean validateCharactersPerPost(Integer charactersPerPost) {
+                       return options.getIntegerOption("CharactersPerPost").validate(charactersPerPost);
+               }
+
+               /**
+                * Sets the number of characters per post.
+                *
+                * @param charactersPerPost
+                *            The number of characters per post, or <code>-1</code> to
+                *            not cut off the posts
+                * @return This preferences objects
+                */
+               public Preferences setCharactersPerPost(Integer charactersPerPost) {
+                       options.getIntegerOption("CharactersPerPost").set(charactersPerPost);
+                       return this;
+               }
+
+               /**
                 * Returns whether Sone requires full access to be even visible.
                 *
                 * @return {@code true} if Sone requires full access, {@code false}
@@ -2331,29 +2438,6 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos
                }
 
                /**
-                * 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!
index d34ac36..d5120ac 100644 (file)
@@ -33,22 +33,6 @@ import net.pterodactylus.util.version.Version;
 public interface CoreListener extends EventListener {
 
        /**
-        * Notifies a listener that a Sone is now being rescued.
-        *
-        * @param sone
-        *            The Sone that is rescued
-        */
-       public void rescuingSone(Sone sone);
-
-       /**
-        * Notifies a listener that the Sone was rescued and can now be unlocked.
-        *
-        * @param sone
-        *            The Sone that was rescued
-        */
-       public void rescuedSone(Sone sone);
-
-       /**
         * Notifies a listener that a new Sone has been discovered.
         *
         * @param sone
@@ -137,6 +121,38 @@ public interface CoreListener extends EventListener {
        public void soneUnlocked(Sone sone);
 
        /**
+        * Notifies a listener that the insert of the given Sone has started.
+        *
+        * @see SoneInsertListener#insertStarted(Sone)
+        * @param sone
+        *            The Sone that is being inserted
+        */
+       public void soneInserting(Sone sone);
+
+       /**
+        * Notifies a listener that the insert of the given Sone has finished
+        * successfully.
+        *
+        * @see SoneInsertListener#insertFinished(Sone, long)
+        * @param sone
+        *            The Sone that has been inserted
+        * @param insertDuration
+        *            The insert duration (in milliseconds)
+        */
+       public void soneInserted(Sone sone, long insertDuration);
+
+       /**
+        * Notifies a listener that the insert of the given Sone was aborted.
+        *
+        * @see SoneInsertListener#insertAborted(Sone, Throwable)
+        * @param sone
+        *            The Sone that was inserted
+        * @param cause
+        *            The cause for the abortion (may be {@code null})
+        */
+       public void soneInsertAborted(Sone sone, Throwable cause);
+
+       /**
         * Notifies a listener that a new version has been found.
         *
         * @param version
index 786cfb8..875a2b6 100644 (file)
@@ -45,32 +45,6 @@ public class CoreListenerManager extends AbstractListenerManager<Core, CoreListe
        //
 
        /**
-        * Notifies all listeners that the given Sone is now being rescued.
-        *
-        * @see CoreListener#rescuingSone(Sone)
-        * @param sone
-        *            The Sone that is being rescued
-        */
-       void fireRescuingSone(Sone sone) {
-               for (CoreListener coreListener : getListeners()) {
-                       coreListener.rescuingSone(sone);
-               }
-       }
-
-       /**
-        * Notifies all listeners that the given Sone was rescued.
-        *
-        * @see CoreListener#rescuedSone(Sone)
-        * @param sone
-        *            The Sone that was rescued
-        */
-       void fireRescuedSone(Sone sone) {
-               for (CoreListener coreListener : getListeners()) {
-                       coreListener.rescuedSone(sone);
-               }
-       }
-
-       /**
         * Notifies all listeners that a new Sone has been discovered.
         *
         * @see CoreListener#newSoneFound(Sone)
@@ -212,6 +186,50 @@ public class CoreListenerManager extends AbstractListenerManager<Core, CoreListe
        }
 
        /**
+        * Notifies all listeners that the insert of the given Sone has started.
+        *
+        * @see SoneInsertListener#insertStarted(Sone)
+        * @param sone
+        *            The Sone being inserted
+        */
+       void fireSoneInserting(Sone sone) {
+               for (CoreListener coreListener : getListeners()) {
+                       coreListener.soneInserting(sone);
+               }
+       }
+
+       /**
+        * Notifies all listeners that the insert of the given Sone has finished
+        * successfully.
+        *
+        * @see SoneInsertListener#insertFinished(Sone, long)
+        * @param sone
+        *            The Sone that was inserted
+        * @param insertDuration
+        *            The insert duration (in milliseconds)
+        */
+       void fireSoneInserted(Sone sone, long insertDuration) {
+               for (CoreListener coreListener : getListeners()) {
+                       coreListener.soneInserted(sone, insertDuration);
+               }
+       }
+
+       /**
+        * Notifies all listeners that the insert of the given Sone was aborted.
+        *
+        * @see SoneInsertListener#insertStarted(Sone)
+        * @param sone
+        *            The Sone being inserted
+        * @param cause
+        *            The cause for the abortion (may be {@code null}
+        */
+       void fireSoneInsertAborted(Sone sone, Throwable cause) {
+               for (CoreListener coreListener : getListeners()) {
+                       coreListener.soneInsertAborted(sone, cause);
+               }
+       }
+
+       /**
         * Notifies all listeners that a new version was found.
         *
         * @see CoreListener#updateFound(Version, long, long)
index 56c1e4a..0812f66 100644 (file)
@@ -25,7 +25,6 @@ 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;
@@ -124,8 +123,7 @@ 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 Preferences#isSoneRescueMode()
-        * „Sone rescue mode“} is active).
+        * specific URI.
         *
         * @param sone
         *            The Sone to fetch
@@ -133,6 +131,23 @@ public class SoneDownloader extends AbstractService {
         *            The URI to fetch the Sone from
         */
        public void fetchSone(Sone sone, FreenetURI soneUri) {
+               fetchSone(sone, soneUri, false);
+       }
+
+       /**
+        * Fetches the Sone from the given URI.
+        *
+        * @param sone
+        *            The Sone to fetch
+        * @param soneUri
+        *            The URI of the Sone to fetch
+        * @param fetchOnly
+        *            {@code true} to only fetch and parse the Sone, {@code false}
+        *            to {@link Core#updateSone(Sone) update} it in the core
+        * @return The downloaded Sone, or {@code null} if the Sone could not be
+        *         downloaded
+        */
+       public Sone fetchSone(Sone sone, FreenetURI soneUri, boolean fetchOnly) {
                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);
@@ -140,14 +155,17 @@ public class SoneDownloader extends AbstractService {
                        Pair<FreenetURI, FetchResult> fetchResults = freenetInterface.fetchUri(requestUri);
                        if (fetchResults == null) {
                                /* TODO - mark Sone as bad. */
-                               return;
+                               return null;
                        }
                        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);
+                               if (!fetchOnly) {
+                                       core.updateSone(parsedSone);
+                                       addSone(parsedSone);
+                               }
                        }
+                       return parsedSone;
                } finally {
                        core.setSoneStatus(sone, (sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
                }
diff --git a/src/main/java/net/pterodactylus/sone/core/SoneInsertListener.java b/src/main/java/net/pterodactylus/sone/core/SoneInsertListener.java
new file mode 100644 (file)
index 0000000..6391d8a
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Sone - SoneInsertListener.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.core;
+
+import java.util.EventListener;
+
+import net.pterodactylus.sone.data.Sone;
+
+/**
+ * Listener for Sone insert events.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface SoneInsertListener extends EventListener {
+
+       /**
+        * Notifies a listener that a Sone is now being inserted.
+        *
+        * @param sone
+        *            The Sone being inserted
+        */
+       public void insertStarted(Sone sone);
+
+       /**
+        * Notifies a listener that a Sone has been successfully inserted.
+        *
+        * @param sone
+        *            The Sone that was inserted
+        * @param insertDuration
+        *            The duration of the insert (in milliseconds)
+        */
+       public void insertFinished(Sone sone, long insertDuration);
+
+       /**
+        * Notifies a listener that the insert of the given Sone was aborted.
+        *
+        * @param sone
+        *            The Sone that was being inserted
+        * @param cause
+        *            The cause of the abortion (may be {@code null})
+        */
+       public void insertAborted(Sone sone, Throwable cause);
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/core/SoneInsertListenerManager.java b/src/main/java/net/pterodactylus/sone/core/SoneInsertListenerManager.java
new file mode 100644 (file)
index 0000000..9e4ea2a
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * Sone - SoneInsertListenerManager.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.core;
+
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.event.AbstractListenerManager;
+
+/**
+ * Manager for {@link SoneInsertListener}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SoneInsertListenerManager extends AbstractListenerManager<Sone, SoneInsertListener> {
+
+       /**
+        * Creates a new Sone insert listener manager.
+        *
+        * @param sone
+        *            The sone being inserted
+        */
+       public SoneInsertListenerManager(Sone sone) {
+               super(sone);
+       }
+
+       //
+       // ACTIONS
+       //
+
+       /**
+        * Notifies all listeners that the insert of the Sone has started.
+        *
+        * @see SoneInsertListener#insertStarted(Sone)
+        */
+       void fireInsertStarted() {
+               for (SoneInsertListener soneInsertListener : getListeners()) {
+                       soneInsertListener.insertStarted(getSource());
+               }
+       }
+
+       /**
+        * Notifies all listeners that the insert of the Sone has finished
+        * successfully.
+        *
+        * @see SoneInsertListener#insertFinished(Sone, long)
+        * @param insertDuration
+        *            The insert duration (in milliseconds)
+        */
+       void fireInsertFinished(long insertDuration) {
+               for (SoneInsertListener soneInsertListener : getListeners()) {
+                       soneInsertListener.insertFinished(getSource(), insertDuration);
+               }
+       }
+
+       /**
+        * Notifies all listeners that the insert of the Sone was aborted.
+        *
+        * @see SoneInsertListener#insertAborted(Sone, Throwable)
+        * @param cause
+        *            The cause of the abortion (may be {@code null}
+        */
+       void fireInsertAborted(Throwable cause) {
+               for (SoneInsertListener soneInsertListener : getListeners()) {
+                       soneInsertListener.insertAborted(getSource(), cause);
+               }
+       }
+
+}
index cc5f689..f654d09 100644 (file)
@@ -83,6 +83,9 @@ public class SoneInserter extends AbstractService {
        /** The Sone to insert. */
        private final Sone sone;
 
+       /** The insert listener manager. */
+       private SoneInsertListenerManager soneInsertListenerManager;
+
        /** Whether a modification has been detected. */
        private volatile boolean modified = false;
 
@@ -104,6 +107,31 @@ public class SoneInserter extends AbstractService {
                this.core = core;
                this.freenetInterface = freenetInterface;
                this.sone = sone;
+               this.soneInsertListenerManager = new SoneInsertListenerManager(sone);
+       }
+
+       //
+       // LISTENER MANAGEMENT
+       //
+
+       /**
+        * Adds a listener for Sone insert events.
+        *
+        * @param soneInsertListener
+        *            The Sone insert listener
+        */
+       public void addSoneInsertListener(SoneInsertListener soneInsertListener) {
+               soneInsertListenerManager.addListener(soneInsertListener);
+       }
+
+       /**
+        * Removes a listener for Sone insert events.
+        *
+        * @param soneInsertListener
+        *            The Sone insert listener
+        */
+       public void removeSoneInsertListener(SoneInsertListener soneInsertListener) {
+               soneInsertListenerManager.removeListener(soneInsertListener);
        }
 
        //
@@ -206,7 +234,9 @@ public class SoneInserter extends AbstractService {
                                        core.setSoneStatus(sone, SoneStatus.inserting);
                                        long insertTime = System.currentTimeMillis();
                                        insertInformation.setTime(insertTime);
+                                       soneInsertListenerManager.fireInsertStarted();
                                        FreenetURI finalUri = freenetInterface.insertDirectory(insertInformation.getInsertUri(), insertInformation.generateManifestEntries(), "index.html");
+                                       soneInsertListenerManager.fireInsertFinished(System.currentTimeMillis() - insertTime);
                                        /* at this point we might already be stopped. */
                                        if (shouldStop()) {
                                                /* if so, bail out, don’t change anything. */
@@ -214,10 +244,11 @@ public class SoneInserter extends AbstractService {
                                        }
                                        sone.setTime(insertTime);
                                        sone.setLatestEdition(finalUri.getEdition());
-                                       core.saveSone(sone);
+                                       core.touchConfiguration();
                                        success = true;
                                        logger.log(Level.INFO, "Inserted Sone “%s” at %s.", new Object[] { sone.getName(), finalUri });
                                } catch (SoneException se1) {
+                                       soneInsertListenerManager.fireInsertAborted(se1);
                                        logger.log(Level.WARNING, "Could not insert Sone “" + sone.getName() + "”!", se1);
                                } finally {
                                        core.setSoneStatus(sone, SoneStatus.idle);
@@ -347,6 +378,7 @@ public class SoneInserter extends AbstractService {
                        }
 
                        TemplateContext templateContext = templateContextFactory.createTemplateContext();
+                       templateContext.set("core", core);
                        templateContext.set("currentSone", soneProperties);
                        templateContext.set("currentEdition", core.getUpdateChecker().getLatestEdition());
                        templateContext.set("version", SonePlugin.VERSION);
diff --git a/src/main/java/net/pterodactylus/sone/core/SoneRescuer.java b/src/main/java/net/pterodactylus/sone/core/SoneRescuer.java
new file mode 100644 (file)
index 0000000..542ec4a
--- /dev/null
@@ -0,0 +1,173 @@
+/*
+ * Sone - SoneRescuer.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.core;
+
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.service.AbstractService;
+import freenet.keys.FreenetURI;
+
+/**
+ * The Sone rescuer downloads older editions of a Sone and updates the currently
+ * stored Sone with it.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SoneRescuer extends AbstractService {
+
+       /** The core. */
+       private final Core core;
+
+       /** The Sone downloader. */
+       private final SoneDownloader soneDownloader;
+
+       /** The Sone being rescued. */
+       private final Sone sone;
+
+       /** Whether the rescuer is currently fetching a Sone. */
+       private volatile boolean fetching;
+
+       /** The currently tried edition. */
+       private volatile long currentEdition;
+
+       /** Whether the last fetch was successful. */
+       private volatile boolean lastFetchSuccessful = true;
+
+       /**
+        * Creates a new Sone rescuer.
+        *
+        * @param core
+        *            The core
+        * @param soneDownloader
+        *            The Sone downloader
+        * @param sone
+        *            The Sone to rescue
+        */
+       public SoneRescuer(Core core, SoneDownloader soneDownloader, Sone sone) {
+               super("Sone Rescuer for " + sone.getName());
+               this.core = core;
+               this.soneDownloader = soneDownloader;
+               this.sone = sone;
+               currentEdition = sone.getRequestUri().getEdition();
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns whether the Sone rescuer is currently fetching a Sone.
+        *
+        * @return {@code true} if the Sone rescuer is currently fetching a Sone
+        */
+       public boolean isFetching() {
+               return fetching;
+       }
+
+       /**
+        * Returns the edition that is currently being downloaded.
+        *
+        * @return The edition that is currently being downloaded
+        */
+       public long getCurrentEdition() {
+               return currentEdition;
+       }
+
+       /**
+        * Returns whether the Sone rescuer can download a next edition.
+        *
+        * @return {@code true} if the Sone rescuer can download a next edition,
+        *         {@code false} if the last edition was already tried
+        */
+       public boolean hasNextEdition() {
+               return currentEdition > 0;
+       }
+
+       /**
+        * Returns the next edition the Sone rescuer can download.
+        *
+        * @return The next edition the Sone rescuer can download
+        */
+       public long getNextEdition() {
+               return currentEdition - 1;
+       }
+
+       /**
+        * Sets the edition to rescue.
+        *
+        * @param edition
+        *            The edition to rescue
+        * @return This Sone rescuer
+        */
+       public SoneRescuer setEdition(long edition) {
+               currentEdition = edition;
+               return this;
+       }
+
+       /**
+        * Sets whether the last fetch was successful.
+        *
+        * @return {@code true} if the last fetch was successful, {@code false}
+        *         otherwise
+        */
+       public boolean isLastFetchSuccessful() {
+               return lastFetchSuccessful;
+       }
+
+       //
+       // ACTIONS
+       //
+
+       /**
+        * Starts the next fetch. If you want to fetch a different edition than “the
+        * next older one,” remember to call {@link #setEdition(long)} before
+        * calling this method.
+        */
+       public void startNextFetch() {
+               fetching = true;
+               notifySyncObject();
+       }
+
+       //
+       // SERVICE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void serviceRun() {
+               while (!shouldStop()) {
+                       while (!shouldStop() && !fetching) {
+                               sleep();
+                       }
+                       if (fetching) {
+                               core.lockSone(sone);
+                               FreenetURI soneUri = sone.getRequestUri().setKeyType("SSK").setDocName("Sone-" + currentEdition).setMetaString(new String[] { "sone.xml" });
+                               System.out.println("URI: " + soneUri);
+                               Sone fetchedSone = soneDownloader.fetchSone(sone, soneUri, true);
+                               System.out.println("Sone: " + fetchedSone);
+                               lastFetchSuccessful = (fetchedSone != null);
+                               if (lastFetchSuccessful) {
+                                       core.updateSone(fetchedSone, true);
+                               }
+                               fetching = false;
+                       }
+               }
+       }
+
+}
index e1c0391..3477346 100644 (file)
@@ -60,6 +60,29 @@ public class Sone implements Fingerprintable, Comparable<Sone> {
 
        };
 
+       /**
+        * Comparator that sorts Sones by last activity (least recent active first).
+        */
+       public static final Comparator<Sone> LAST_ACTIVITY_COMPARATOR = new Comparator<Sone>() {
+
+               @Override
+               public int compare(Sone firstSone, Sone secondSone) {
+                       return (int) Math.min(Integer.MAX_VALUE, Math.max(Integer.MIN_VALUE, secondSone.getTime() - firstSone.getTime()));
+               }
+       };
+
+       /** Comparator that sorts Sones by numbers of posts (descending). */
+       public static final Comparator<Sone> POST_COUNT_COMPARATOR = new Comparator<Sone>() {
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public int compare(Sone leftSone, Sone rightSone) {
+                       return (leftSone.getPosts().size() != rightSone.getPosts().size()) ? (rightSone.getPosts().size() - leftSone.getPosts().size()) : (rightSone.getReplies().size() - leftSone.getReplies().size());
+               }
+       };
+
        /** Filter to remove Sones that have not been downloaded. */
        public static final Filter<Sone> EMPTY_SONE_FILTER = new Filter<Sone>() {
 
index fa7385c..910e0d6 100644 (file)
 
 package net.pterodactylus.sone.freenet;
 
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 
 import net.pterodactylus.util.template.Filter;
@@ -49,7 +53,21 @@ public class L10nFilter implements Filter {
         */
        @Override
        public String format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
-               return l10n.getString(String.valueOf(data));
+               if (parameters.isEmpty()) {
+                       return l10n.getString(String.valueOf(data));
+               }
+               List<Object> parameterValues = new ArrayList<Object>();
+               int parameterIndex = 0;
+               while (parameters.containsKey(String.valueOf(parameterIndex))) {
+                       Object value = parameters.get(String.valueOf(parameterIndex));
+                       if (((String) value).startsWith("=")) {
+                               value = ((String) value).substring(1);
+                       } else {
+                               value = templateContext.get((String) value);
+                       }
+                       parameterValues.add(value);
+                       ++parameterIndex;
+               }
+               return new MessageFormat(l10n.getString(String.valueOf(data)), new Locale(l10n.getSelectedLanguage().shortCode)).format(parameterValues.toArray());
        }
-
 }
index b5a7fcc..738c0d1 100644 (file)
@@ -83,7 +83,7 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
        }
 
        /** The version. */
-       public static final Version VERSION = new Version(0, 6, 5);
+       public static final Version VERSION = new Version(0, 6, 6);
 
        /** The logger. */
        private static final Logger logger = Logging.getLogger(SonePlugin.class);
index 37cff6f..3db8912 100644 (file)
@@ -55,7 +55,7 @@ public class ListNotificationFilters {
                List<Notification> filteredNotifications = new ArrayList<Notification>();
                for (Notification notification : notifications) {
                        if (notification.getId().equals("new-post-notification")) {
-                               ListNotification<Post> filteredNotification = filterNewPostNotification((ListNotification<Post>) notification, currentSone);
+                               ListNotification<Post> filteredNotification = filterNewPostNotification((ListNotification<Post>) notification, currentSone, true);
                                if (filteredNotification != null) {
                                        filteredNotifications.add(filteredNotification);
                                }
@@ -64,6 +64,11 @@ public class ListNotificationFilters {
                                if (filteredNotification != null) {
                                        filteredNotifications.add(filteredNotification);
                                }
+                       } else if (notification.getId().equals("mention-notification")) {
+                               ListNotification<Post> filteredNotification = filterNewPostNotification((ListNotification<Post>) notification, null, false);
+                               if (filteredNotification != null) {
+                                       filteredNotifications.add(filteredNotification);
+                               }
                        } else {
                                filteredNotifications.add(notification);
                        }
@@ -73,19 +78,23 @@ public class ListNotificationFilters {
 
        /**
         * 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.
+        * is {@code null} and {@code soneRequired} is {@code true}, {@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
+        * @param soneRequired
+        *            Whether a non-{@code null} Sone in {@code currentSone} is
+        *            required
         * @return The filtered new-post notification, or {@code null} if the
         *         notification should be removed
         */
-       public static ListNotification<Post> filterNewPostNotification(ListNotification<Post> newPostNotification, Sone currentSone) {
-               if (currentSone == null) {
+       public static ListNotification<Post> filterNewPostNotification(ListNotification<Post> newPostNotification, Sone currentSone, boolean soneRequired) {
+               if (soneRequired && (currentSone == null)) {
                        return null;
                }
                List<Post> newPosts = new ArrayList<Post>();
@@ -102,6 +111,7 @@ public class ListNotificationFilters {
                }
                ListNotification<Post> filteredNotification = new ListNotification<Post>(newPostNotification);
                filteredNotification.setElements(newPosts);
+               filteredNotification.setLastUpdateTime(newPostNotification.getLastUpdatedTime());
                return filteredNotification;
        }
 
@@ -137,6 +147,7 @@ public class ListNotificationFilters {
                }
                ListNotification<Reply> filteredNotification = new ListNotification<Reply>(newReplyNotification);
                filteredNotification.setElements(newReplies);
+               filteredNotification.setLastUpdateTime(newReplyNotification.getLastUpdatedTime());
                return filteredNotification;
        }
 
@@ -145,6 +156,13 @@ public class ListNotificationFilters {
         * considered visible if one of the following statements is true:
         * <ul>
         * <li>The post does not have a Sone.</li>
+        * <li>The post’s {@link Post#getTime() time} is in the future.</li>
+        * </ul>
+        * <p>
+        * If {@code post} is not {@code null} more checks are performed, and the
+        * post will be invisible if:
+        * </p>
+        * <ul>
         * <li>The Sone of the post is not the given Sone, the given Sone does not
         * follow the post’s Sone, and the given Sone is not the recipient of the
         * post.</li>
@@ -153,36 +171,38 @@ public class ListNotificationFilters {
         * Sone.</li>
         * <li>The given Sone has not explicitely assigned negative trust to the
         * post’s Sone but the implicit trust is negative.</li>
-        * <li>The post’s {@link Post#getTime() time} is in the future.</li>
         * </ul>
         * If none of these statements is true the post is considered visible.
         *
         * @param sone
-        *            The Sone that checks for a post’s visibility
+        *            The Sone that checks for a post’s visibility (may be
+        *            {@code null} to skip Sone-specific checks, such as trust)
         * @param post
         *            The post to check for visibility
         * @return {@code true} if the post is considered visible, {@code false}
         *         otherwise
         */
        public static boolean isPostVisible(Sone sone, Post post) {
-               Validation.begin().isNotNull("Sone", sone).isNotNull("Post", post).check().isNotNull("Sone’s Identity", sone.getIdentity()).check().isInstanceOf("Sone’s Identity", sone.getIdentity(), OwnIdentity.class).check();
+               Validation.begin().isNotNull("Post", post).check();
                Sone postSone = post.getSone();
                if (postSone == null) {
                        return false;
                }
-               Trust trust = postSone.getIdentity().getTrust((OwnIdentity) sone.getIdentity());
-               if (trust != null) {
-                       if ((trust.getExplicit() != null) && (trust.getExplicit() < 0)) {
+               if (sone != null) {
+                       Trust trust = postSone.getIdentity().getTrust((OwnIdentity) sone.getIdentity());
+                       if (trust != null) {
+                               if ((trust.getExplicit() != null) && (trust.getExplicit() < 0)) {
+                                       return false;
+                               }
+                               if ((trust.getExplicit() == null) && (trust.getImplicit() != null) && (trust.getImplicit() < 0)) {
+                                       return false;
+                               }
+                       } else {
                                return false;
                        }
-                       if ((trust.getExplicit() == null) && (trust.getImplicit() != null) && (trust.getImplicit() < 0)) {
+                       if ((!postSone.equals(sone)) && !sone.hasFriend(postSone.getId()) && !sone.equals(post.getRecipient())) {
                                return false;
                        }
-               } else {
-                       return false;
-               }
-               if ((!postSone.equals(sone)) && !sone.hasFriend(postSone.getId()) && !sone.equals(post.getRecipient())) {
-                       return false;
                }
                if (post.getTime() > System.currentTimeMillis()) {
                        return false;
@@ -210,14 +230,15 @@ public class ListNotificationFilters {
         * If none of these statements is true the reply is considered visible.
         *
         * @param sone
-        *            The Sone that checks for a post’s visibility
+        *            The Sone that checks for a post’s visibility (may be
+        *            {@code null} to skip Sone-specific checks, such as trust)
         * @param reply
         *            The reply to check for visibility
         * @return {@code true} if the reply is considered visible, {@code false}
         *         otherwise
         */
        public static boolean isReplyVisible(Sone sone, Reply reply) {
-               Validation.begin().isNotNull("Sone", sone).isNotNull("Reply", reply).check().isNotNull("Sone’s Identity", sone.getIdentity()).check().isInstanceOf("Sone’s Identity", sone.getIdentity(), OwnIdentity.class).check();
+               Validation.begin().isNotNull("Reply", reply).check();
                Post post = reply.getPost();
                if (post == null) {
                        return false;
index 26afe2e..332f504 100644 (file)
@@ -21,6 +21,8 @@ import java.io.IOException;
 import java.io.StringReader;
 import java.io.StringWriter;
 import java.io.Writer;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 
 import net.pterodactylus.sone.core.Core;
@@ -85,6 +87,19 @@ public class ParserFilter implements Filter {
        @Override
        public Object format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
                String text = String.valueOf(data);
+               int length = -1;
+               try {
+                       length = Integer.parseInt(parameters.get("length"));
+               } catch (NumberFormatException nfe1) {
+                       /* ignore. */
+               }
+               if ((length == -1) && (parameters.get("length") != null)) {
+                       try {
+                               length = Integer.parseInt(String.valueOf(templateContext.get(parameters.get("length"))));
+                       } catch (NumberFormatException nfe1) {
+                               /* ignore. */
+                       }
+               }
                String soneKey = parameters.get("sone");
                if (soneKey == null) {
                        soneKey = "sone";
@@ -97,7 +112,31 @@ public class ParserFilter implements Filter {
                SoneTextParserContext context = new SoneTextParserContext(request, sone);
                StringWriter parsedTextWriter = new StringWriter();
                try {
-                       render(parsedTextWriter, soneTextParser.parse(context, new StringReader(text)));
+                       Iterable<Part> parts = soneTextParser.parse(context, new StringReader(text));
+                       if (length > -1) {
+                               List<Part> shortenedParts = new ArrayList<Part>();
+                               for (Part part : parts) {
+                                       if (part instanceof PlainTextPart) {
+                                               String longText = ((PlainTextPart) part).getText();
+                                               if (length >= longText.length()) {
+                                                       shortenedParts.add(part);
+                                               } else {
+                                                       shortenedParts.add(new PlainTextPart(longText.substring(0, length) + "…"));
+                                               }
+                                               length -= longText.length();
+                                       } else if (part instanceof LinkPart) {
+                                               shortenedParts.add(part);
+                                               length -= ((LinkPart) part).getText().length();
+                                       } else {
+                                               shortenedParts.add(part);
+                                       }
+                                       if (length <= 0) {
+                                               break;
+                                       }
+                               }
+                               parts = shortenedParts;
+                       }
+                       render(parsedTextWriter, parts);
                } catch (IOException ioe1) {
                        /* no exceptions in a StringReader or StringWriter, ignore. */
                }
@@ -247,10 +286,11 @@ public class ParserFilter implements Filter {
         * @return The excerpt of the text
         */
        private static String getExcerpt(String text, int length) {
-               if (text.length() > length) {
-                       return text.substring(0, length) + "…";
+               String filteredText = text.replaceAll("(\r\n)+", "\r\n").replaceAll("\n+", "\n").replace("\r\n", " ").replace('\n', ' ');
+               if (filteredText.length() > length) {
+                       return filteredText.substring(0, length) + "…";
                }
-               return text;
+               return filteredText;
        }
 
 }
index 76fdcc1..ead6066 100644 (file)
@@ -100,7 +100,7 @@ public class SoneAccessor extends ReflectionAccessor {
                } else if (member.equals("locked")) {
                        return core.isLocked(sone);
                } else if (member.equals("lastUpdatedText")) {
-                       return GetTimesAjaxPage.getTime((WebInterface) templateContext.get("webInterface"), System.currentTimeMillis() - sone.getTime());
+                       return GetTimesAjaxPage.getTime((WebInterface) templateContext.get("webInterface"), sone.getTime());
                } else if (member.equals("trust")) {
                        Sone currentSone = (Sone) templateContext.get("currentSone");
                        if (currentSone == null) {
diff --git a/src/main/java/net/pterodactylus/sone/template/UniqueElementFilter.java b/src/main/java/net/pterodactylus/sone/template/UniqueElementFilter.java
new file mode 100644 (file)
index 0000000..70dae70
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Sone - UniqueElementFilter.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.template;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import net.pterodactylus.util.template.Filter;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Filter that reduces a collection to a {@link Set}, removing duplicates.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UniqueElementFilter implements Filter {
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Object format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
+               if (!(data instanceof Collection<?>)) {
+                       return data;
+               }
+               return new HashSet<Object>((Collection<?>) data);
+       }
+
+}
index 94c3db4..4195516 100644 (file)
@@ -182,7 +182,7 @@ public class SoneTextParser implements Parser<SoneTextParserContext> {
                                        if (line.length() >= (next + 7 + 43)) {
                                                String soneId = line.substring(next + 7, next + 50);
                                                Sone sone = soneProvider.getSone(soneId, false);
-                                               if (sone != null) {
+                                               if ((sone != null) && (sone.getName() != null)) {
                                                        parts.add(new SonePart(sone));
                                                } else {
                                                        parts.add(new PlainTextPart(line.substring(next, next + 50)));
@@ -202,8 +202,6 @@ public class SoneTextParser implements Parser<SoneTextParserContext> {
                                                String postId = line.substring(next + 7, next + 43);
                                                Post post = postProvider.getPost(postId, false);
                                                if ((post != null) && (post.getSone() != null)) {
-                                                       String postText = post.getText();
-                                                       postText = postText.substring(0, Math.min(postText.length(), 20)) + "…";
                                                        parts.add(new PostPart(post));
                                                } else {
                                                        parts.add(new PlainTextPart(line.substring(next, next + 43)));
index aae5e83..2b12352 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.text.TextFilter;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
@@ -63,6 +64,7 @@ public class CreateReplyPage extends SoneTemplatePage {
                                if (sender == null) {
                                        sender = getCurrentSone(request.getToadletContext());
                                }
+                               text = TextFilter.filter(request.getHttpRequest().getHeader("host"), text);
                                webInterface.getCore().createReply(sender, post, text);
                                throw new RedirectException(returnPage);
                        }
index 52468a3..43dd15b 100644 (file)
@@ -84,7 +84,7 @@ public class EditProfilePage extends SoneTemplatePage {
                                        field.setValue(value);
                                }
                                currentSone.setProfile(profile);
-                               webInterface.getCore().saveSone(currentSone);
+                               webInterface.getCore().touchConfiguration();
                                throw new RedirectException("editProfile.html");
                        } else if (request.getHttpRequest().getPartAsStringFailsafe("add-field", 4).equals("true")) {
                                String fieldName = request.getHttpRequest().getPartAsStringFailsafe("field-name", 256).trim();
@@ -92,7 +92,7 @@ public class EditProfilePage extends SoneTemplatePage {
                                        profile.addField(fieldName);
                                        currentSone.setProfile(profile);
                                        fields = profile.getFields();
-                                       webInterface.getCore().saveSone(currentSone);
+                                       webInterface.getCore().touchConfiguration();
                                        throw new RedirectException("editProfile.html#profile-fields");
                                } catch (IllegalArgumentException iae1) {
                                        templateContext.set("fieldName", fieldName);
index 9cd1f2a..81225e9 100644 (file)
@@ -50,11 +50,13 @@ public class FollowSonePage extends SoneTemplatePage {
        protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
-                       String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
                        String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
                        Sone currentSone = getCurrentSone(request.getToadletContext());
-                       currentSone.addFriend(soneId);
-                       webInterface.getCore().saveSone(currentSone);
+                       String soneIds = request.getHttpRequest().getPartAsStringFailsafe("sone", 1200);
+                       for (String soneId : soneIds.split("[ ,]+")) {
+                               currentSone.addFriend(soneId);
+                       }
+                       webInterface.getCore().touchConfiguration();
                        throw new RedirectException(returnPage);
                }
        }
index 7c8f9ad..ad63791 100644 (file)
@@ -23,6 +23,8 @@ import java.util.List;
 
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.util.collection.Pagination;
+import net.pterodactylus.util.collection.ReverseComparator;
+import net.pterodactylus.util.filter.Filter;
 import net.pterodactylus.util.filter.Filters;
 import net.pterodactylus.util.number.Numbers;
 import net.pterodactylus.util.template.Template;
@@ -57,11 +59,52 @@ public class KnownSonesPage extends SoneTemplatePage {
        @Override
        protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
+               String sortField = request.getHttpRequest().getParam("sort");
+               String sortOrder = request.getHttpRequest().getParam("order");
+               String followedSones = request.getHttpRequest().getParam("followedSones");
+               templateContext.set("sort", (sortField != null) ? sortField : "name");
+               templateContext.set("order", (sortOrder != null) ? sortOrder : "asc");
+               templateContext.set("followedSones", followedSones);
+               final Sone currentSone = getCurrentSone(request.getToadletContext(), false);
                List<Sone> knownSones = Filters.filteredList(new ArrayList<Sone>(webInterface.getCore().getSones()), Sone.EMPTY_SONE_FILTER);
-               Collections.sort(knownSones, Sone.NICE_NAME_COMPARATOR);
+               if ((currentSone != null) && "show-only".equals(followedSones)) {
+                       knownSones = Filters.filteredList(knownSones, new Filter<Sone>() {
+
+                               @Override
+                               public boolean filterObject(Sone sone) {
+                                       return currentSone.hasFriend(sone.getId());
+                               }
+                       });
+               } else if ((currentSone != null) && "hide".equals(followedSones)) {
+                       knownSones = Filters.filteredList(knownSones, new Filter<Sone>() {
+
+                               @Override
+                               public boolean filterObject(Sone sone) {
+                                       return !currentSone.hasFriend(sone.getId());
+                               }
+                       });
+               }
+               if ("activity".equals(sortField)) {
+                       if ("asc".equals(sortOrder)) {
+                               Collections.sort(knownSones, new ReverseComparator<Sone>(Sone.LAST_ACTIVITY_COMPARATOR));
+                       } else {
+                               Collections.sort(knownSones, Sone.LAST_ACTIVITY_COMPARATOR);
+                       }
+               } else if ("posts".equals(sortField)) {
+                       if ("asc".equals(sortOrder)) {
+                               Collections.sort(knownSones, new ReverseComparator<Sone>(Sone.POST_COUNT_COMPARATOR));
+                       } else {
+                               Collections.sort(knownSones, Sone.POST_COUNT_COMPARATOR);
+                       }
+               } else {
+                       if ("desc".equals(sortOrder)) {
+                               Collections.sort(knownSones, new ReverseComparator<Sone>(Sone.NICE_NAME_COMPARATOR));
+                       } else {
+                               Collections.sort(knownSones, Sone.NICE_NAME_COMPARATOR);
+                       }
+               }
                Pagination<Sone> sonePagination = new Pagination<Sone>(knownSones, 25).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
                templateContext.set("pagination", sonePagination);
                templateContext.set("knownSones", sonePagination.getItems());
        }
-
 }
index 6785e81..fda6afd 100644 (file)
@@ -64,7 +64,9 @@ public class OptionsPage extends SoneTemplatePage {
                        if (currentSone != null) {
                                boolean autoFollow = request.getHttpRequest().isPartSet("auto-follow");
                                currentSone.getOptions().getBooleanOption("AutoFollow").set(autoFollow);
-                               webInterface.getCore().saveSone(currentSone);
+                               boolean enableSoneInsertNotifications = request.getHttpRequest().isPartSet("enable-sone-insert-notifications");
+                               currentSone.getOptions().getBooleanOption("EnableSoneInsertNotifications").set(enableSoneInsertNotifications);
+                               webInterface.getCore().touchConfiguration();
                        }
                        Integer insertionDelay = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("insertion-delay", 16));
                        if (!preferences.validateInsertionDelay(insertionDelay)) {
@@ -78,6 +80,12 @@ public class OptionsPage extends SoneTemplatePage {
                        } else {
                                preferences.setPostsPerPage(postsPerPage);
                        }
+                       Integer charactersPerPost = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("characters-per-post", 10), null);
+                       if (!preferences.validateCharactersPerPost(charactersPerPost)) {
+                               fieldErrors.add("characters-per-post");
+                       } else {
+                               preferences.setCharactersPerPost(charactersPerPost);
+                       }
                        boolean requireFullAccess = request.getHttpRequest().isPartSet("require-full-access");
                        preferences.setRequireFullAccess(requireFullAccess);
                        Integer positiveTrust = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("positive-trust", 3));
@@ -102,13 +110,11 @@ public class OptionsPage extends SoneTemplatePage {
                        Integer fcpFullAccessRequiredInteger = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("fcp-full-access-required", 1), preferences.getFcpFullAccessRequired().ordinal());
                        FullAccessRequired fcpFullAccessRequired = FullAccessRequired.values()[fcpFullAccessRequiredInteger];
                        preferences.setFcpFullAccessRequired(fcpFullAccessRequired);
-                       boolean soneRescueMode = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("sone-rescue-mode", 5));
-                       preferences.setSoneRescueMode(soneRescueMode);
                        boolean clearOnNextRestart = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("clear-on-next-restart", 5));
                        preferences.setClearOnNextRestart(clearOnNextRestart);
                        boolean reallyClearOnNextRestart = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("really-clear-on-next-restart", 5));
                        preferences.setReallyClearOnNextRestart(reallyClearOnNextRestart);
-                       webInterface.getCore().saveConfiguration();
+                       webInterface.getCore().touchConfiguration();
                        if (fieldErrors.isEmpty()) {
                                throw new RedirectException(getPath());
                        }
@@ -116,16 +122,17 @@ public class OptionsPage extends SoneTemplatePage {
                }
                if (currentSone != null) {
                        templateContext.set("auto-follow", currentSone.getOptions().getBooleanOption("AutoFollow").get());
+                       templateContext.set("enable-sone-insert-notifications", currentSone.getOptions().getBooleanOption("EnableSoneInsertNotifications").get());
                }
                templateContext.set("insertion-delay", preferences.getInsertionDelay());
                templateContext.set("posts-per-page", preferences.getPostsPerPage());
+               templateContext.set("characters-per-post", preferences.getCharactersPerPost());
                templateContext.set("require-full-access", preferences.isRequireFullAccess());
                templateContext.set("positive-trust", preferences.getPositiveTrust());
                templateContext.set("negative-trust", preferences.getNegativeTrust());
                templateContext.set("trust-comment", preferences.getTrustComment());
                templateContext.set("fcp-interface-active", preferences.isFcpInterfaceActive());
                templateContext.set("fcp-full-access-required", preferences.getFcpFullAccessRequired().ordinal());
-               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/RescuePage.java b/src/main/java/net/pterodactylus/sone/web/RescuePage.java
new file mode 100644 (file)
index 0000000..40b03f1
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * Sone - RescuePage.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.core.SoneRescuer;
+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;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Page that lets the user control the rescue mode for a Sone.
+ *
+ * @see SoneRescuer
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class RescuePage extends SoneTemplatePage {
+
+       /**
+        * Creates a new rescue page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public RescuePage(Template template, WebInterface webInterface) {
+               super("rescue.html", template, "Page.Rescue.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               Sone currentSone = getCurrentSone(request.getToadletContext(), false);
+               SoneRescuer soneRescuer = webInterface.getCore().getSoneRescuer(currentSone);
+               if (request.getMethod() == Method.POST) {
+                       if ("true".equals(request.getHttpRequest().getPartAsStringFailsafe("fetch", 4))) {
+                               long edition = Numbers.safeParseLong(request.getHttpRequest().getPartAsStringFailsafe("edition", 8), -1L);
+                               if (edition > -1) {
+                                       soneRescuer.setEdition(edition);
+                               }
+                               soneRescuer.startNextFetch();
+                       }
+                       throw new RedirectException("rescue.html");
+               }
+               templateContext.set("soneRescuer", soneRescuer);
+       }
+
+}
index 6f5e001..fab7a57 100644 (file)
@@ -32,9 +32,16 @@ import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.cache.Cache;
+import net.pterodactylus.util.cache.CacheException;
+import net.pterodactylus.util.cache.CacheItem;
+import net.pterodactylus.util.cache.DefaultCacheItem;
+import net.pterodactylus.util.cache.MemoryCache;
+import net.pterodactylus.util.cache.ValueRetriever;
 import net.pterodactylus.util.collection.Mapper;
 import net.pterodactylus.util.collection.Mappers;
 import net.pterodactylus.util.collection.Pagination;
+import net.pterodactylus.util.collection.TimedMap;
 import net.pterodactylus.util.filter.Filter;
 import net.pterodactylus.util.filter.Filters;
 import net.pterodactylus.util.logging.Logging;
@@ -55,6 +62,20 @@ public class SearchPage extends SoneTemplatePage {
        /** The logger. */
        private static final Logger logger = Logging.getLogger(SearchPage.class);
 
+       /** Short-term cache. */
+       private final Cache<List<Phrase>, Set<Hit<Post>>> hitCache = new MemoryCache<List<Phrase>, Set<Hit<Post>>>(new ValueRetriever<List<Phrase>, Set<Hit<Post>>>() {
+
+               @SuppressWarnings("synthetic-access")
+               public CacheItem<Set<Hit<Post>>> retrieve(List<Phrase> phrases) throws CacheException {
+                       Set<Post> posts = new HashSet<Post>();
+                       for (Sone sone : webInterface.getCore().getSones()) {
+                               posts.addAll(sone.getPosts());
+                       }
+                       return new DefaultCacheItem<Set<Hit<Post>>>(getHits(Filters.filteredSet(posts, Post.FUTURE_POSTS_FILTER), phrases, new PostStringGenerator()));
+               }
+
+       }, new TimedMap<List<Phrase>, CacheItem<Set<Hit<Post>>>>(300000));
+
        /**
         * Creates a new search page.
         *
@@ -83,16 +104,21 @@ public class SearchPage extends SoneTemplatePage {
                }
 
                List<Phrase> phrases = parseSearchPhrases(query);
+               if (phrases.isEmpty()) {
+                       throw new RedirectException("index.html");
+               }
 
                Set<Sone> sones = webInterface.getCore().getSones();
                Set<Hit<Sone>> soneHits = getHits(sones, phrases, SoneStringGenerator.COMPLETE_GENERATOR);
 
-               Set<Post> posts = new HashSet<Post>();
-               for (Sone sone : sones) {
-                       posts.addAll(sone.getPosts());
+               Set<Hit<Post>> postHits;
+               try {
+                       postHits = hitCache.get(phrases);
+               } catch (CacheException ce1) {
+                       /* should never happen. */
+                       logger.log(Level.SEVERE, "Could not get search results from cache!", ce1);
+                       postHits = Collections.emptySet();
                }
-               @SuppressWarnings("synthetic-access")
-               Set<Hit<Post>> postHits = getHits(Filters.filteredSet(posts, Post.FUTURE_POSTS_FILTER), phrases, new PostStringGenerator());
 
                /* now filter. */
                soneHits = Filters.filteredSet(soneHits, Hit.POSITIVE_FILTER);
@@ -172,11 +198,20 @@ public class SearchPage extends SoneTemplatePage {
                List<Phrase> phrases = new ArrayList<Phrase>();
                for (String phrase : parsedPhrases) {
                        if (phrase.startsWith("+")) {
-                               phrases.add(new Phrase(phrase.substring(1), Phrase.Optionality.REQUIRED));
+                               if (phrase.length() > 1) {
+                                       phrases.add(new Phrase(phrase.substring(1), Phrase.Optionality.REQUIRED));
+                               } else {
+                                       phrases.add(new Phrase("+", Phrase.Optionality.OPTIONAL));
+                               }
                        } else if (phrase.startsWith("-")) {
-                               phrases.add(new Phrase(phrase.substring(1), Phrase.Optionality.FORBIDDEN));
+                               if (phrase.length() > 1) {
+                                       phrases.add(new Phrase(phrase.substring(1), Phrase.Optionality.FORBIDDEN));
+                               } else {
+                                       phrases.add(new Phrase("-", Phrase.Optionality.OPTIONAL));
+                               }
+                       } else {
+                               phrases.add(new Phrase(phrase, Phrase.Optionality.OPTIONAL));
                        }
-                       phrases.add(new Phrase(phrase, Phrase.Optionality.OPTIONAL));
                }
                return phrases;
        }
@@ -401,6 +436,30 @@ public class SearchPage extends SoneTemplatePage {
                        return optionality;
                }
 
+               //
+               // OBJECT METHODS
+               //
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public int hashCode() {
+                       return phrase.hashCode() ^ ((optionality == Optionality.FORBIDDEN) ? (0xaaaaaaaa) : ((optionality == Optionality.REQUIRED) ? 0x55555555 : 0));
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public boolean equals(Object object) {
+                       if (!(object instanceof Phrase)) {
+                               return false;
+                       }
+                       Phrase phrase = (Phrase) object;
+                       return (this.optionality == phrase.optionality) && this.phrase.equals(phrase.phrase);
+               }
+
        }
 
        /**
index a0174a9..b7e36f4 100644 (file)
@@ -250,6 +250,7 @@ public class SoneTemplatePage extends FreenetTemplatePage {
        protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                Sone currentSone = getCurrentSone(request.getToadletContext(), false);
+               templateContext.set("core", webInterface.getCore());
                templateContext.set("currentSone", currentSone);
                templateContext.set("localSones", webInterface.getCore().getLocalSones());
                templateContext.set("request", request);
index a7836f5..33b88a1 100644 (file)
@@ -50,11 +50,13 @@ public class UnfollowSonePage extends SoneTemplatePage {
        protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
-                       String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
                        String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
                        Sone currentSone = getCurrentSone(request.getToadletContext());
-                       currentSone.removeFriend(soneId);
-                       webInterface.getCore().saveSone(currentSone);
+                       String soneIds = request.getHttpRequest().getPartAsStringFailsafe("sone", 2000);
+                       for (String soneId : soneIds.split("[ ,]+")) {
+                               currentSone.removeFriend(soneId);
+                       }
+                       webInterface.getCore().touchConfiguration();
                        throw new RedirectException(returnPage);
                }
        }
index 73436c7..08202d4 100644 (file)
@@ -58,6 +58,7 @@ import net.pterodactylus.sone.template.RequestChangeFilter;
 import net.pterodactylus.sone.template.SoneAccessor;
 import net.pterodactylus.sone.template.SubstringFilter;
 import net.pterodactylus.sone.template.TrustAccessor;
+import net.pterodactylus.sone.template.UniqueElementFilter;
 import net.pterodactylus.sone.template.UnknownDateFilter;
 import net.pterodactylus.sone.text.Part;
 import net.pterodactylus.sone.text.SonePart;
@@ -178,11 +179,8 @@ public class WebInterface implements CoreListener {
        /** The “you have been mentioned” notification. */
        private final ListNotification<Post> mentionNotification;
 
-       /** The “rescuing Sone” notification. */
-       private final ListNotification<Sone> rescuingSonesNotification;
-
-       /** The “Sone rescued” notification. */
-       private final ListNotification<Sone> sonesRescuedNotification;
+       /** Notifications for sone inserts. */
+       private final Map<Sone, TemplateNotification> soneInsertNotifications = new HashMap<Sone, TemplateNotification>();
 
        /** Sone locked notification ticker objects. */
        private final Map<Sone, Object> lockedSonesTickerObjects = Collections.synchronizedMap(new HashMap<Sone, Object>());
@@ -231,6 +229,7 @@ public class WebInterface implements CoreListener {
                templateContextFactory.addFilter("sort", new CollectionSortFilter());
                templateContextFactory.addFilter("replyGroup", new ReplyGroupFilter());
                templateContextFactory.addFilter("in", new ContainsFilter());
+               templateContextFactory.addFilter("unique", new UniqueElementFilter());
                templateContextFactory.addProvider(Provider.TEMPLATE_CONTEXT_PROVIDER);
                templateContextFactory.addProvider(new ClassPathTemplateProvider());
                templateContextFactory.addTemplateObject("webInterface", this);
@@ -255,12 +254,6 @@ public class WebInterface implements CoreListener {
                Template mentionNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/mentionNotification.html"));
                mentionNotification = new ListNotification<Post>("mention-notification", "posts", mentionNotificationTemplate, false);
 
-               Template rescuingSonesTemplate = TemplateParser.parse(createReader("/templates/notify/rescuingSonesNotification.html"));
-               rescuingSonesNotification = new ListNotification<Sone>("sones-being-rescued-notification", "sones", rescuingSonesTemplate);
-
-               Template sonesRescuedTemplate = TemplateParser.parse(createReader("/templates/notify/sonesRescuedNotification.html"));
-               sonesRescuedNotification = new ListNotification<Sone>("sones-rescued-notification", "sones", sonesRescuedTemplate);
-
                Template lockedSonesTemplate = TemplateParser.parse(createReader("/templates/notify/lockedSonesNotification.html"));
                lockedSonesNotification = new ListNotification<Sone>("sones-locked-notification", "sones", lockedSonesTemplate);
 
@@ -568,6 +561,7 @@ public class WebInterface implements CoreListener {
                Template deleteSoneTemplate = TemplateParser.parse(createReader("/templates/deleteSone.html"));
                Template noPermissionTemplate = TemplateParser.parse(createReader("/templates/noPermission.html"));
                Template optionsTemplate = TemplateParser.parse(createReader("/templates/options.html"));
+               Template rescueTemplate = TemplateParser.parse(createReader("/templates/rescue.html"));
                Template aboutTemplate = TemplateParser.parse(createReader("/templates/about.html"));
                Template invalidTemplate = TemplateParser.parse(createReader("/templates/invalid.html"));
                Template postTemplate = TemplateParser.parse(createReader("/templates/include/viewPost.html"));
@@ -606,6 +600,7 @@ public class WebInterface implements CoreListener {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new LoginPage(loginTemplate, this), "Login"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new LogoutPage(emptyTemplate, this), "Logout"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new OptionsPage(optionsTemplate, this), "Options"));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new RescuePage(rescueTemplate, this), "Rescue"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new AboutPage(aboutTemplate, this, SonePlugin.VERSION), "About"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("noPermission.html", noPermissionTemplate, "Page.NoPermission.Title", this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DismissNotificationPage(emptyTemplate, this)));
@@ -705,28 +700,30 @@ public class WebInterface implements CoreListener {
                return Filters.filteredSet(mentionedSones, Sone.LOCAL_SONE_FILTER);
        }
 
-       //
-       // CORELISTENER METHODS
-       //
-
        /**
-        * {@inheritDoc}
-        */
-       @Override
-       public void rescuingSone(Sone sone) {
-               rescuingSonesNotification.add(sone);
-               notificationManager.addNotification(rescuingSonesNotification);
+        * Returns the Sone insert notification for the given Sone. If no
+        * notification for the given Sone exists, a new notification is created and
+        * cached.
+        *
+        * @param sone
+        *            The Sone to get the insert notification for
+        * @return The Sone insert notification
+        */
+       private TemplateNotification getSoneInsertNotification(Sone sone) {
+               synchronized (soneInsertNotifications) {
+                       TemplateNotification templateNotification = soneInsertNotifications.get(sone);
+                       if (templateNotification == null) {
+                               templateNotification = new TemplateNotification(TemplateParser.parse(createReader("/templates/notify/soneInsertNotification.html")));
+                               templateNotification.set("insertSone", sone);
+                               soneInsertNotifications.put(sone, templateNotification);
+                       }
+                       return templateNotification;
+               }
        }
 
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public void rescuedSone(Sone sone) {
-               rescuingSonesNotification.remove(sone);
-               sonesRescuedNotification.add(sone);
-               notificationManager.addNotification(sonesRescuedNotification);
-       }
+       //
+       // CORELISTENER METHODS
+       //
 
        /**
         * {@inheritDoc}
@@ -766,9 +763,6 @@ public class WebInterface implements CoreListener {
         */
        @Override
        public void newReplyFound(Reply reply) {
-               if (reply.getPost().getSone() == null) {
-                       return;
-               }
                boolean isLocal = getCore().isLocalSone(reply.getSone());
                if (isLocal) {
                        localReplyNotification.add(reply);
@@ -871,6 +865,44 @@ public class WebInterface implements CoreListener {
         * {@inheritDoc}
         */
        @Override
+       public void soneInserting(Sone sone) {
+               TemplateNotification soneInsertNotification = getSoneInsertNotification(sone);
+               soneInsertNotification.set("soneStatus", "inserting");
+               if (sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").get()) {
+                       notificationManager.addNotification(soneInsertNotification);
+               }
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void soneInserted(Sone sone, long insertDuration) {
+               TemplateNotification soneInsertNotification = getSoneInsertNotification(sone);
+               soneInsertNotification.set("soneStatus", "inserted");
+               soneInsertNotification.set("insertDuration", insertDuration / 1000);
+               if (sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").get()) {
+                       notificationManager.addNotification(soneInsertNotification);
+               }
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void soneInsertAborted(Sone sone, Throwable cause) {
+               TemplateNotification soneInsertNotification = getSoneInsertNotification(sone);
+               soneInsertNotification.set("soneStatus", "insert-aborted");
+               soneInsertNotification.set("insert-error", cause);
+               if (sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").get()) {
+                       notificationManager.addNotification(soneInsertNotification);
+               }
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
        public void updateFound(Version version, long releaseTime, long latestEdition) {
                newVersionNotification.getTemplateContext().set("latestVersion", version);
                newVersionNotification.getTemplateContext().set("latestEdition", latestEdition);
index 9ed960f..ce78f58 100644 (file)
@@ -20,6 +20,7 @@ package net.pterodactylus.sone.web.ajax;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.text.TextFilter;
 import net.pterodactylus.sone.web.WebInterface;
 import net.pterodactylus.util.json.JsonObject;
 
@@ -60,6 +61,7 @@ public class CreateReplyAjaxPage extends JsonPage {
                if ((post == null) || (post.getSone() == null)) {
                        return createErrorJsonObject("invalid-post-id");
                }
+               text = TextFilter.filter(request.getHttpRequest().getHeader("host"), text);
                Reply reply = webInterface.getCore().createReply(sender, post, text);
                return createSuccessJsonObject().put("reply", reply.getId()).put("sone", sender.getId());
        }
index 383d2d4..4625c13 100644 (file)
@@ -54,7 +54,7 @@ public class DeleteProfileFieldAjaxPage extends JsonPage {
                }
                profile.removeField(field);
                currentSone.setProfile(profile);
-               webInterface.getCore().saveSone(currentSone);
+               webInterface.getCore().touchConfiguration();
                return createSuccessJsonObject().put("field", new JsonObject().put("id", field.getId()));
        }
 
index 1269b41..d82ab1d 100644 (file)
@@ -52,7 +52,7 @@ public class FollowSoneAjaxPage extends JsonPage {
                        return createErrorJsonObject("auth-required");
                }
                currentSone.addFriend(soneId);
-               webInterface.getCore().saveSone(currentSone);
+               webInterface.getCore().touchConfiguration();
                return createSuccessJsonObject();
        }
 
index 1406c3a..8bfe472 100644 (file)
@@ -82,9 +82,11 @@ public class GetNotificationAjaxPage extends JsonPage {
                for (String notificationId : notificationIds) {
                        Notification notification = webInterface.getNotifications().getNotification(notificationId);
                        if ("new-post-notification".equals(notificationId)) {
-                               notification = ListNotificationFilters.filterNewPostNotification((ListNotification<Post>) notification, currentSone);
+                               notification = ListNotificationFilters.filterNewPostNotification((ListNotification<Post>) notification, currentSone, false);
                        } else if ("new-reply-notification".equals(notificationId)) {
                                notification = ListNotificationFilters.filterNewReplyNotification((ListNotification<Reply>) notification, currentSone);
+                       } else if ("mention-notification".equals(notificationId)) {
+                               notification = ListNotificationFilters.filterNewPostNotification((ListNotification<Post>) notification, currentSone, false);
                        }
                        if (notification == null) {
                                // TODO - show error
@@ -115,6 +117,7 @@ public class GetNotificationAjaxPage extends JsonPage {
                try {
                        if (notification instanceof TemplateNotification) {
                                TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext().mergeContext(((TemplateNotification) notification).getTemplateContext());
+                               templateContext.set("core", webInterface.getCore());
                                templateContext.set("currentSone", webInterface.getCurrentSone(request.getToadletContext(), false));
                                templateContext.set("localSones", webInterface.getCore().getLocalSones());
                                templateContext.set("request", request);
index f46ec6b..f56c5b7 100644 (file)
@@ -97,6 +97,7 @@ public class GetPostAjaxPage extends JsonPage {
                jsonPost.put("time", post.getTime());
                StringWriter stringWriter = new StringWriter();
                TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext();
+               templateContext.set("core", webInterface.getCore());
                templateContext.set("request", request);
                templateContext.set("post", post);
                templateContext.set("currentSone", currentSone);
index 24bbbe2..6cc7d47 100644 (file)
@@ -99,6 +99,7 @@ public class GetReplyAjaxPage extends JsonPage {
                jsonReply.put("time", reply.getTime());
                StringWriter stringWriter = new StringWriter();
                TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext();
+               templateContext.set("core", webInterface.getCore());
                templateContext.set("request", request);
                templateContext.set("reply", reply);
                templateContext.set("currentSone", currentSone);
index 246cbed..f704905 100644 (file)
@@ -132,7 +132,7 @@ public class GetStatusAjaxPage extends JsonPage {
 
                        @Override
                        public boolean filterObject(Reply reply) {
-                               return reply.getPost() != null;
+                               return (reply.getPost() != null) && (reply.getPost().getSone() != null);
                        }
                });
                JsonArray jsonReplies = new JsonArray();
@@ -186,7 +186,7 @@ public class GetStatusAjaxPage extends JsonPage {
                synchronized (dateFormat) {
                        jsonSone.put("lastUpdated", dateFormat.format(new Date(sone.getTime())));
                }
-               jsonSone.put("lastUpdatedText", GetTimesAjaxPage.getTime(webInterface, System.currentTimeMillis() - sone.getTime()).getText());
+               jsonSone.put("lastUpdatedText", GetTimesAjaxPage.getTime(webInterface, sone.getTime()).getText());
                return jsonSone;
        }
 
index 0be0c46..31ec399 100644 (file)
@@ -62,9 +62,8 @@ public class GetTimesAjaxPage extends JsonPage {
                                if (post == null) {
                                        continue;
                                }
-                               long age = now - post.getTime();
                                JsonObject postTime = new JsonObject();
-                               Time time = getTime(age);
+                               Time time = getTime(post.getTime());
                                postTime.put("timeText", time.getText());
                                postTime.put("refreshTime", time.getRefresh() / Time.SECOND);
                                postTime.put("tooltip", dateFormat.format(new Date(post.getTime())));
@@ -113,14 +112,14 @@ public class GetTimesAjaxPage extends JsonPage {
        //
 
        /**
-        * Returns the formatted relative time for a given age.
+        * Returns the formatted relative time for a given time.
         *
-        * @param age
-        *            The age to format (in milliseconds)
+        * @param time
+        *            The time to format the difference from (in milliseconds)
         * @return The formatted age
         */
-       private Time getTime(long age) {
-               return getTime(webInterface, age);
+       private Time getTime(long time) {
+               return getTime(webInterface, time);
        }
 
        //
@@ -128,15 +127,19 @@ public class GetTimesAjaxPage extends JsonPage {
        //
 
        /**
-        * Returns the formatted relative time for a given age.
+        * Returns the formatted relative time for a given time.
         *
         * @param webInterface
         *            The Sone web interface (for l10n access)
-        * @param age
-        *            The age to format (in milliseconds)
+        * @param time
+        *            The time to format the difference from (in milliseconds)
         * @return The formatted age
         */
-       public static Time getTime(WebInterface webInterface, long age) {
+       public static Time getTime(WebInterface webInterface, long time) {
+               if (time == 0) {
+                       return new Time(webInterface.getL10n().getString("View.Sone.Text.UnknownDate"), 12 * Time.HOUR);
+               }
+               long age = System.currentTimeMillis() - time;
                String text;
                long refresh;
                if (age < 0) {
index efd1439..bf87ead 100644 (file)
@@ -55,10 +55,10 @@ public class LikeAjaxPage extends JsonPage {
                }
                if ("post".equals(type)) {
                        currentSone.addLikedPostId(id);
-                       webInterface.getCore().saveSone(currentSone);
+                       webInterface.getCore().touchConfiguration();
                } else if ("reply".equals(type)) {
                        currentSone.addLikedReplyId(id);
-                       webInterface.getCore().saveSone(currentSone);
+                       webInterface.getCore().touchConfiguration();
                } else {
                        return createErrorJsonObject("invalid-type");
                }
index 780e4d1..092c1f7 100644 (file)
@@ -71,7 +71,7 @@ public class MoveProfileFieldAjaxPage extends JsonPage {
                        return createErrorJsonObject("not-possible");
                }
                currentSone.setProfile(profile);
-               webInterface.getCore().saveSone(currentSone);
+               webInterface.getCore().touchConfiguration();
                return createSuccessJsonObject();
        }
 
index c2379e8..7f719b7 100644 (file)
@@ -52,7 +52,7 @@ public class UnfollowSoneAjaxPage extends JsonPage {
                        return createErrorJsonObject("auth-required");
                }
                currentSone.removeFriend(soneId);
-               webInterface.getCore().saveSone(currentSone);
+               webInterface.getCore().touchConfiguration();
                return createSuccessJsonObject();
        }
 
index e5c933d..84b0593 100644 (file)
@@ -55,10 +55,10 @@ public class UnlikeAjaxPage extends JsonPage {
                }
                if ("post".equals(type)) {
                        currentSone.removeLikedPostId(id);
-                       webInterface.getCore().saveSone(currentSone);
+                       webInterface.getCore().touchConfiguration();
                } else if ("reply".equals(type)) {
                        currentSone.removeLikedReplyId(id);
-                       webInterface.getCore().saveSone(currentSone);
+                       webInterface.getCore().touchConfiguration();
                } else {
                        return createErrorJsonObject("invalid-type");
                }
index 3610afc..739c615 100644 (file)
@@ -18,6 +18,8 @@ Navigation.Menu.Item.Logout.Name=Logout
 Navigation.Menu.Item.Logout.Tooltip=Logs you out of the current Sone
 Navigation.Menu.Item.Options.Name=Options
 Navigation.Menu.Item.Options.Tooltip=Options for the Sone plugin
+Navigation.Menu.Item.Rescue.Name=Rescue
+Navigation.Menu.Item.Rescue.Tooltip=Rescue Sone
 Navigation.Menu.Item.About.Name=About
 Navigation.Menu.Item.About.Tooltip=Information about Sone
 
@@ -35,9 +37,11 @@ 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. Note that this will only follow Sones that are discovered after you activate this option!
+Page.Options.Option.EnableSoneInsertNotifications.Description=If enabled, this will display notifications every time your Sone is being inserted or finishes inserting.
 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.Option.CharactersPerPost.Description=The number of characters to display from a post before cutting it off and showing a link to expand it (-1 to disable).
 Page.Options.Option.RequireFullAccess.Description=Whether to deny access to Sone to any host that has not been granted full access.
 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.
@@ -49,10 +53,6 @@ Page.Options.Option.FcpFullAccessRequired.Description=Require FCP connection fro
 Page.Options.Option.FcpFullAccessRequired.Value.No=No
 Page.Options.Option.FcpFullAccessRequired.Value.Writing=For Write Access
 Page.Options.Option.FcpFullAccessRequired.Value.Always=Always
-Page.Options.Section.RescueOptions.Title=Rescue Settings
-Page.Options.Option.SoneRescueMode.Description1=Try to rescue your Sones at the next start of the Sone plugin. The Rescue Mode will start at the latest known edition and will try to download all editions sequentially backwards, merging all discovered posts and replies together, until it is stopped or it has reached the first edition.
-Page.Options.Option.SoneRescueMode.Description2=When using the Rescue Mode because Sone lost its configuration it usually suffices to let the Rescue Mode only run for a short time; use a second tab to control how many posts of your Sone are visible again. As soon as the last valid edition is loaded you can then deactivate the Rescue Mode.
-Page.Options.Option.SoneRescueMode.Description3=Note that when you use the Rescue Mode posts that you have deleted after they have been inserted will have to be deleted again. Unfortunately this is an unavoidable side effect of the Rescue Mode.
 Page.Options.Section.Cleaning.Title=Clean Up
 Page.Options.Option.ClearOnNextRestart.Description=Resets the configuration of the Sone plugin at the next restart. Warning! {strong}This will destroy all of your Sones{/strong} so make sure you have backed up everyhing you still need! Also, you need to set the next option to true to actually do it.
 Page.Options.Option.ReallyClearOnNextRestart.Description=This option needs to be set to “yes” if you really, {strong}really{/strong} want to clear the plugin configuration on the next restart.
@@ -84,6 +84,8 @@ Page.Index.PostList.Text.NoPostYet=Nobody has written any posts yet. You should
 Page.KnownSones.Title=Known Sones - Sone
 Page.KnownSones.Page.Title=Known Sones
 Page.KnownSones.Text.NoKnownSones=There are currently no known Sones.
+Page.KnownSones.Button.FollowAllSones=Follow all Sones
+Page.KnownSones.Button.UnfollowAllSones=Unfollow all Sones
 
 Page.EditProfile.Title=Edit Profile - Sone
 Page.EditProfile.Page.Title=Edit Profile
@@ -139,12 +141,13 @@ Page.ViewSone.Title=View Sone - Sone
 Page.ViewSone.Page.TitleWithoutSone=View unknown Sone
 Page.ViewSone.NoSone.Description=There is currently no known Sone with the ID {sone}. If you were looking for a specific Sone, make sure that it is visible in your web of trust!
 Page.ViewSone.UnknownSone.Description=This Sone has not yet been retrieved. Please check back in a short time.
+Page.ViewSone.UnknownSone.LinkToWebOfTrust=Even though the Sone is still unknown, its Web of Trust profile might already be available:
 Page.ViewSone.WriteAMessage=You can write a message to this Sone here. Please note that everybody will be able to read this message!
 Page.ViewSone.PostList.Title=Posts by {sone}
 Page.ViewSone.PostList.Text.NoPostYet=This Sone has not yet posted anything.
 Page.ViewSone.Profile.Title=Profile
 Page.ViewSone.Profile.Label.Name=Name
-Page.ViewSone.Profile.Name.WoTLink=Web of trust profile
+Page.ViewSone.Profile.Name.WoTLink=web of trust profile
 Page.ViewSone.Replies.Title=Posts {sone} has replied to
 
 Page.ViewPost.Title=View Post - Sone
@@ -196,6 +199,17 @@ Page.Search.Text.SoneHits=The following Sones match your search terms.
 Page.Search.Text.PostHits=The following posts match your search terms.
 Page.Search.Text.NoHits=No Sones or posts matched your search terms.
 
+Page.Rescue.Title=Rescue Sone
+Page.Rescue.Page.Title=Rescue Sone “{0}”
+Page.Rescue.Text.Description=The Rescue Mode lets you restore previous versions of your Sone. This can be necessary if your configuration was lost.
+Page.Rescue.Text.Procedure=The Rescue Mode works by fetching the latest inserted edition of your Sone. If an edition was successfully fetched it will be loaded into your Sone, letting you control your posts, profile, and other settings (you could do that in a second browser tab or window). If the fetched edition is not the one you want to restore, instruct the Rescue Mode to fetch the next older edition below.
+Page.Rescue.Text.Fetching=The Sone Rescuer is currently fetching edition {0} of your Sone.
+Page.Rescue.Text.Fetched=The Sone Rescuer has downloaded edition {0} of your Sone. Please check your posts, replies, and profile. If you like what the current Sone contains, just unlock it.
+Page.Rescue.Text.FetchedLast=The Sone rescuer has downloaded the last available edition. If it did not manage to restore your Sone you are probably out of luck now.
+Page.Rescue.Text.NotFetched=The Sone Rescuer could not download edition {0} of your Sone. Please either try again with edition {0}, or try the next older edition.
+Page.Rescue.Label.NextEdition=Next edition:
+Page.Rescue.Button.Fetch=Fetch edition
+
 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!
@@ -222,6 +236,8 @@ View.CreateSone.Text.Error.NoIdentity=You have not selected an identity.
 
 View.Sone.Label.LastUpdate=Last update:
 View.Sone.Text.UnknownDate=unknown
+View.Sone.Stats.Posts={0,number} {0,choice,0#posts|1#post|1<posts}
+View.Sone.Stats.Replies={0,number} {0,choice,0#replies|1#reply|1<replies}
 View.Sone.Button.UnlockSone=unlock
 View.Sone.Button.UnlockSone.Tooltip=Allow this Sone to be inserted now
 View.Sone.Button.LockSone=lock
@@ -235,6 +251,7 @@ 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.WebOfTrustLink=web of trust profile
 View.Post.Permalink=link post
 View.Post.PermalinkAuthor=link author
 View.Post.Bookmarks.PostIsBookmarked=Post is bookmarked, click to remove from bookmarks
@@ -246,6 +263,8 @@ View.Post.LikeLink=Like
 View.Post.UnlikeLink=Unlike
 View.Post.ShowSource=Toggle Parser
 View.Post.NotDownloaded=This post has not yet been downloaded, or it has been deleted.
+View.Post.ShowMore=show more
+View.Post.ShowLess=show less
 
 View.UpdateStatus.Text.ChooseSenderIdentity=Choose the sender identity
 
@@ -282,6 +301,7 @@ 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.CharactersPerPost=Number of characters per post after which to cut the post off
 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
@@ -314,3 +334,4 @@ Notification.LockedSones.Text=The following Sones have been locked for more than
 Notification.NewVersion.Text=Version {version} of the Sone plugin was found. Download it from USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/{edition}​!
 Notification.Mention.ShortText=You have been mentioned.
 Notification.Mention.Text=You have been mentioned in the following posts:
+Notification.SoneInsert.Duration={0,number} {0,choice,0#seconds|1#second|1<seconds}
index 1dda47e..a5328a0 100644 (file)
@@ -145,7 +145,7 @@ textarea {
 
 #sone #notification-area #local-post-notification, #sone #notification-area #local-reply-notification {
        display: none;
-} 
+}
 
 #sone #plugin-warning {
        border: solid 0.5em red;
@@ -238,6 +238,7 @@ textarea {
        padding: 1ex 0px;
        border-bottom: solid 1px #ccc;
        clear: both;
+       position: relative;
 }
 
 #sone .post.new {
@@ -250,6 +251,33 @@ textarea {
        border-bottom: none;
 }
 
+#sone .post .sone-menu {
+       position: absolute;
+       top: 0;
+       left: -1ex;
+       padding: 1ex 1ex;
+       margin: -1px -1px;
+       display: none;
+       background-color: rgb(255, 255, 224);
+       border: solid 1px rgb(0, 0, 0);
+       z-index: 1;
+}
+
+#sone .post .sone-menu .avatar {
+       position: absolute;
+       margin-right: 1ex;
+}
+
+#sone .post .sone-menu .inner-menu {
+       margin-left: 64px;
+       padding-left: 1ex;
+       min-height: 64px;
+}
+
+#sone .sone-menu .follow, #sone .sone-menu .unfollow {
+       cursor: pointer;
+}
+
 #sone .post > .avatar {
        position: absolute;
 }
@@ -265,16 +293,32 @@ textarea {
        font-weight: bold;
 }
 
-#sone .post .text, #sone .post .raw-text {
+#sone .post .author-wot-link {
+       font-size: 90%;
+}
+
+#sone .post .text, #sone .post .raw-text, #sone .post .short-text {
        display: inline;
        white-space: pre-wrap;
        word-wrap: break-word;
 }
 
-#sone .post .text.hidden, #sone .post .raw-text.hidden {
+#sone .post .text.hidden, #sone .post .raw-text.hidden, #sone .post .short-text.hidden {
        display: none;
 }
 
+#sone .post .expand-post-text:before, #sone .post .expand-reply-text:before {
+       content: "» ";
+}
+
+#sone .post .shrink-post-text:before, #sone .post .shrink-reply-text:before {
+       content: "« ";
+}
+
+#sone .post .shrink-post-text {
+       cursor: pointer;
+}
+
 #sone .post .status-line {
        margin-top: 0.5ex;
        font-size: 85%;
@@ -389,13 +433,17 @@ textarea {
 }
 
 #sone .post .reply {
+       position: relative;
        clear: both;
        background-color: #f0f0ff;
-       font-size: 85%;
        margin: 1ex 0px;
        padding: 1ex;
 }
 
+#sone .post .reply .inner-part {
+       font-size: 85%;
+}
+
 #sone .post .reply.new {
        background-color: #ffffa0;
 }
@@ -464,7 +512,11 @@ textarea {
 }
 
 #sone .sone .profile-link {
-       display: block;
+       display: inline;
+}
+
+#sone .sone .sone-stats {
+       display: inline;
 }
 
 #sone .sone .short-request-uri {
@@ -680,3 +732,7 @@ textarea {
        color: red;
        font-style: italic;
 }
+
+#sone #sort-options {
+       margin-bottom: 1em;
+}
diff --git a/src/main/resources/static/javascript/jquery.fieldselection.js b/src/main/resources/static/javascript/jquery.fieldselection.js
new file mode 100644 (file)
index 0000000..47f25a9
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * jQuery plugin: fieldSelection - v0.1.0 - last change: 2006-12-16
+ * (c) 2006 Alex Brem <alex@0xab.cd> - http://blog.0xab.cd
+ */
+
+(function() {
+
+       var fieldSelection = {
+
+               getSelection: function() {
+
+                       var e = this.jquery ? this[0] : this;
+
+                       return (
+
+                               /* mozilla / dom 3.0 */
+                               ('selectionStart' in e && function() {
+                                       var l = e.selectionEnd - e.selectionStart;
+                                       return { start: e.selectionStart, end: e.selectionEnd, length: l, text: e.value.substr(e.selectionStart, l) };
+                               }) ||
+
+                               /* exploder */
+                               (document.selection && function() {
+
+                                       e.focus();
+
+                                       var r = document.selection.createRange();
+                                       if (r == null) {
+                                               return { start: 0, end: e.value.length, length: 0 }
+                                       }
+
+                                       var re = e.createTextRange();
+                                       var rc = re.duplicate();
+                                       re.moveToBookmark(r.getBookmark());
+                                       rc.setEndPoint('EndToStart', re);
+
+                                       return { start: rc.text.length, end: rc.text.length + r.text.length, length: r.text.length, text: r.text };
+                               }) ||
+
+                               /* browser not supported */
+                               function() {
+                                       return { start: 0, end: e.value.length, length: 0 };
+                               }
+
+                       )();
+
+               },
+
+               replaceSelection: function() {
+
+                       var e = this.jquery ? this[0] : this;
+                       var text = arguments[0] || '';
+
+                       return (
+
+                               /* mozilla / dom 3.0 */
+                               ('selectionStart' in e && function() {
+                                       e.value = e.value.substr(0, e.selectionStart) + text + e.value.substr(e.selectionEnd, e.value.length);
+                                       return this;
+                               }) ||
+
+                               /* exploder */
+                               (document.selection && function() {
+                                       e.focus();
+                                       document.selection.createRange().text = text;
+                                       return this;
+                               }) ||
+
+                               /* browser not supported */
+                               function() {
+                                       e.value += text;
+                                       return this;
+                               }
+
+                       )();
+
+               }
+
+       };
+
+       jQuery.each(fieldSelection, function(i) { jQuery.fn[i] = this; });
+
+})();
index d6c83ae..ca80b48 100644 (file)
@@ -283,6 +283,18 @@ function getSoneElement(element) {
 }
 
 /**
+ * Returns the ID of the sone of the context menu that contains the given
+ * element.
+ *
+ * @param element
+ *            The element within a context menu to get the Sone ID for
+ * @return The Sone ID
+ */
+function getMenuSone(element) {
+       return $(element).closest(".sone-menu").find(".sone-id").text();
+}
+
+/**
  * Generates a list of Sones by concatening the names of the given sones with a
  * new line character (“\n”).
  *
@@ -759,11 +771,52 @@ function ajaxifyPost(postElement) {
        /* convert “show source” link into javascript function. */
        $(postElement).find(".show-source").each(function() {
                $("a", this).click(function() {
+                       post = getPostElement(this);
+                       rawPostText = $(".post-text.raw-text", post);
+                       rawPostText.toggleClass("hidden");
+                       if (rawPostText.hasClass("hidden")) {
+                               $(".post-text.short-text", post).removeClass("hidden");
+                               $(".post-text.text", post).addClass("hidden");
+                               $(".expand-post-text", post).removeClass("hidden");
+                               $(".shrink-post-text", post).addClass("hidden");
+                       } else {
+                               $(".post-text.short-text", post).addClass("hidden");
+                               $(".post-text.text", post).addClass("hidden");
+                               $(".expand-post-text", post).addClass("hidden");
+                               $(".shrink-post-text", post).addClass("hidden");
+                       }
+                       return false;
+               });
+       });
+
+       /* convert “show more” link into javascript function. */
+       $(postElement).find(".expand-post-text").each(function() {
+               $(this).click(function() {
                        $(".post-text.text", getPostElement(this)).toggleClass("hidden");
-                       $(".post-text.raw-text", getPostElement(this)).toggleClass("hidden");
+                       $(".post-text.short-text", getPostElement(this)).toggleClass("hidden");
+                       $(".expand-post-text", getPostElement(this)).toggleClass("hidden");
+                       $(".shrink-post-text", getPostElement(this)).toggleClass("hidden");
                        return false;
                });
        });
+       $(postElement).find(".shrink-post-text").each(function() {
+               $(this).click(function() {
+                       $(".post-text.text", getPostElement(this)).toggleClass("hidden");
+                       $(".post-text.short-text", getPostElement(this)).toggleClass("hidden");
+                       $(".expand-post-text", getPostElement(this)).toggleClass("hidden");
+                       $(".shrink-post-text", getPostElement(this)).toggleClass("hidden");
+                       return false;
+               })
+       });
+
+       /* ajaxify author/post links */
+       $(".post-status-line .permalink a", postElement).click(function() {
+               if (!$(".create-reply", postElement).hasClass("hidden")) {
+                       textArea = $("input.reply-input", postElement).focus().data("textarea");
+                       $(textArea).replaceSelection($(this).attr("href"));
+               }
+               return false;
+       });
 
        /* add “comment” link. */
        addCommentLink(getPostId(postElement), getPostAuthor(postElement), postElement, $(postElement).find(".post-status-line .permalink-author"));
@@ -802,6 +855,46 @@ function ajaxifyPost(postElement) {
 
        /* hide reply input field. */
        $(postElement).find(".create-reply").addClass("hidden");
+
+       /* show Sone menu when hovering over the avatar. */
+       $(postElement).find(".post-avatar").mouseover(function() {
+               $(".sone-menu:visible").fadeOut();
+               $(".sone-post-menu", postElement).mouseleave(function() {
+                       $(this).fadeOut();
+               }).fadeIn();
+               return false;
+       });
+       (function(postElement) {
+               var soneId = $(".sone-id", postElement).text();
+               $(".sone-post-menu .follow", postElement).click(function() {
+                       var followElement = this;
+                       ajaxGet("followSone.ajax", { "sone": soneId, "formPassword": getFormPassword() }, function() {
+                               $(followElement).addClass("hidden");
+                               $(followElement).parent().find(".unfollow").removeClass("hidden");
+                               $("#sone .sone-menu").each(function() {
+                                       if (getMenuSone(this) == soneId) {
+                                               $(".follow", this).toggleClass("hidden", true);
+                                               $(".unfollow", this).toggleClass("hidden", false);
+                                       }
+                               });
+                       });
+                       return false;
+               });
+               $(".sone-post-menu .unfollow", postElement).click(function() {
+                       var unfollowElement = this;
+                       ajaxGet("unfollowSone.ajax", { "sone": soneId, "formPassword": getFormPassword() }, function() {
+                               $(unfollowElement).addClass("hidden");
+                               $(unfollowElement).parent().find(".follow").removeClass("hidden");
+                               $("#sone .sone-menu").each(function() {
+                                       if (getMenuSone(this) == soneId) {
+                                               $(".follow", this).toggleClass("hidden", false);
+                                               $(".unfollow", this).toggleClass("hidden", true);
+                                       }
+                               });
+                       });
+                       return false;
+               });
+       })(postElement);
 }
 
 /**
@@ -826,13 +919,55 @@ function ajaxifyReply(replyElement) {
                        });
                });
        })(replyElement);
+
+       /* ajaxify author links */
+       $(".reply-status-line .permalink a", replyElement).click(function() {
+               if (!$(".create-reply", getPostElement(replyElement)).hasClass("hidden")) {
+                       textArea = $("input.reply-input", getPostElement(replyElement)).focus().data("textarea");
+                       $(textArea).replaceSelection($(this).attr("href"));
+               }
+               return false;
+       });
+
        addCommentLink(getPostId(replyElement), getReplyAuthor(replyElement), replyElement, $(replyElement).find(".reply-status-line .permalink-author"));
 
        /* convert “show source” link into javascript function. */
        $(replyElement).find(".show-reply-source").each(function() {
                $("a", this).click(function() {
+                       reply = getReplyElement(this);
+                       rawReplyText = $(".reply-text.raw-text", reply);
+                       rawReplyText.toggleClass("hidden");
+                       if (rawReplyText.hasClass("hidden")) {
+                               $(".reply-text.short-text", reply).removeClass("hidden");
+                               $(".reply-text.text", reply).addClass("hidden");
+                               $(".expand-reply-text", reply).removeClass("hidden");
+                               $(".shrink-reply-text", reply).addClass("hidden");
+                       } else {
+                               $(".reply-text.short-text", reply).addClass("hidden");
+                               $(".reply-text.text", reply).addClass("hidden");
+                               $(".expand-reply-text", reply).addClass("hidden");
+                               $(".shrink-reply-text", reply).addClass("hidden");
+                       }
+                       return false;
+               });
+       });
+
+       /* convert “show more” link into javascript function. */
+       $(replyElement).find(".expand-reply-text").each(function() {
+               $(this).click(function() {
+                       $(".reply-text.text", getReplyElement(this)).toggleClass("hidden");
+                       $(".reply-text.short-text", getReplyElement(this)).toggleClass("hidden");
+                       $(".expand-reply-text", getReplyElement(this)).toggleClass("hidden");
+                       $(".shrink-reply-text", getReplyElement(this)).toggleClass("hidden");
+                       return false;
+               });
+       });
+       $(replyElement).find(".shrink-reply-text").each(function() {
+               $(this).click(function() {
                        $(".reply-text.text", getReplyElement(this)).toggleClass("hidden");
-                       $(".reply-text.raw-text", getReplyElement(this)).toggleClass("hidden");
+                       $(".reply-text.short-text", getReplyElement(this)).toggleClass("hidden");
+                       $(".expand-reply-text", getReplyElement(this)).toggleClass("hidden");
+                       $(".shrink-reply-text", getReplyElement(this)).toggleClass("hidden");
                        return false;
                });
        });
@@ -850,6 +985,46 @@ function ajaxifyReply(replyElement) {
                untrustSone(getReplyAuthor(this));
                return false;
        });
+
+       /* show Sone menu when hovering over the avatar. */
+       $(replyElement).find(".reply-avatar").mouseover(function() {
+               $(".sone-menu:visible").fadeOut();
+               $(".sone-reply-menu", replyElement).mouseleave(function() {
+                       $(this).fadeOut();
+               }).fadeIn();
+               return false;
+       });
+       (function(replyElement) {
+               var soneId = $(".sone-id", replyElement).text();
+               $(".sone-menu .follow", replyElement).click(function() {
+                       var followElement = this;
+                       ajaxGet("followSone.ajax", { "sone": soneId, "formPassword": getFormPassword() }, function() {
+                               $(followElement).addClass("hidden");
+                               $(followElement).parent().find(".unfollow").removeClass("hidden");
+                               $("#sone .sone-menu").each(function() {
+                                       if (getMenuSone(this) == soneId) {
+                                               $(".follow", this).toggleClass("hidden", true);
+                                               $(".unfollow", this).toggleClass("hidden", false);
+                                       }
+                               });
+                       });
+                       return false;
+               });
+               $(".sone-menu .unfollow", replyElement).click(function() {
+                       var unfollowElement = this;
+                       ajaxGet("unfollowSone.ajax", { "sone": soneId, "formPassword": getFormPassword() }, function() {
+                               $(unfollowElement).addClass("hidden");
+                               $(unfollowElement).parent().find(".follow").removeClass("hidden");
+                               $("#sone .sone-menu").each(function() {
+                                       if (getMenuSone(this) == soneId) {
+                                               $(".follow", this).toggleClass("hidden", false);
+                                               $(".unfollow", this).toggleClass("hidden", true);
+                                       }
+                               });
+                       });
+                       return false;
+               });
+       })(replyElement);
 }
 
 /**
@@ -966,7 +1141,7 @@ function checkForRemovedPosts(oldNotification, newNotification) {
  *            The new notification element
  */
 function checkForRemovedReplies(oldNotification, newNotification) {
-       if (getNotificationId(oldNotification) != "new-replies-notification") {
+       if (getNotificationId(oldNotification) != "new-reply-notification") {
                return;
        }
        oldIds = getElementIds(oldNotification, ".reply-id");
@@ -1010,7 +1185,7 @@ function getStatus() {
                                                        postId = $(this).text();
                                                        markPostAsKnown(getPost(postId), true);
                                                });
-                                       } else if (notificationId == "new-replies-notification") {
+                                       } else if (notificationId == "new-reply-notification") {
                                                $(".reply-id", this).each(function(index, element) {
                                                        replyId = $(this).text();
                                                        markReplyAsKnown(getReply(replyId), true);
@@ -1325,7 +1500,7 @@ function markSoneAsKnown(soneElement, skipRequest) {
 function markPostAsKnown(postElements, skipRequest) {
        $(postElements).each(function() {
                postElement = this;
-               if ($(postElement).hasClass("new") || ((typeof skipRequest != "undefined") && !skipRequest)) {
+               if ($(postElement).hasClass("new") || ((typeof skipRequest != "undefined"))) {
                        (function(postElement) {
                                $(postElement).removeClass("new");
                                if ((typeof skipRequest == "undefined") || !skipRequest) {
@@ -1341,7 +1516,7 @@ function markPostAsKnown(postElements, skipRequest) {
 function markReplyAsKnown(replyElements, skipRequest) {
        $(replyElements).each(function() {
                replyElement = this;
-               if ($(replyElement).hasClass("new") || ((typeof skipRequest != "undefined") && !skipRequest)) {
+               if ($(replyElement).hasClass("new") || ((typeof skipRequest != "undefined"))) {
                        (function(replyElement) {
                                $(replyElement).removeClass("new");
                                if ((typeof skipRequest == "undefined") || !skipRequest) {
@@ -1767,11 +1942,6 @@ $(document).ready(function() {
                ajaxifyNotification($(this));
        });
 
-       /* disable all permalinks. */
-       $(".permalink").click(function() {
-               return false;
-       });
-
        /* activate status polling. */
        setTimeout(getStatus, 5000);
 
index b37df26..cd9eddf 100644 (file)
@@ -6,6 +6,7 @@
 
        <script src="javascript/jquery-1.4.2.js" language="javascript"></script>
        <script src="javascript/jquery.url.js" language="javascript"></script>
+       <script src="javascript/jquery.fieldselection.js" language="javascript"></script>
        <script src="javascript/sone.js" language="javascript"></script>
 
        <div id="offline-marker"></div>
diff --git a/src/main/resources/templates/include/soneMenu.html b/src/main/resources/templates/include/soneMenu.html
new file mode 100644 (file)
index 0000000..49237ba
--- /dev/null
@@ -0,0 +1,17 @@
+<div class="sone-menu <% class|css|html>">
+       <div class="sone-id hidden"><%sone.id|html></div>
+       <img class="avatar" src="/WebOfTrust/GetIdenticon?identity=<%sone.id|html>&amp;width=64&height=64" width="64" height="64" alt="Avatar Image" />
+       <div class="inner-menu">
+               <div>
+                       <a class="author" href="viewSone.html?sone=<%sone.id|html>"><%sone.niceName|html></a>
+                       <span class="author-wot-link">(<a href="/WebOfTrust/ShowIdentity?id=<%sone.id|html>"><% =View.Post.WebOfTrustLink|l10n|html></a>)</span>
+               </div>
+               <div><%= View.Sone.Stats.Posts|l10n 0=sone.posts.size>, <%= View.Sone.Stats.Replies|l10n 0=sone.replies.size></div>
+               <%if !sone.local>
+                       <div>
+                               <a class="follow<%if sone.friend> hidden<%/if>"><%= View.Sone.Button.FollowSone|l10n|html></a>
+                               <a class="unfollow<%if !sone.friend> hidden<%/if>"><%= View.Sone.Button.UnfollowSone|l10n|html></a>
+                       </div>
+               <%/if>
+       </div>
+</div>
index 43d4d70..881aca4 100644 (file)
@@ -4,7 +4,7 @@
        <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>
+                       <%foreach localSones localSone|sort>
                                <option value="<% localSone.id|html>"<%if localSone.current> selected="selected"<%/if>><% localSone.niceName|html></option>
                        <%/foreach>
                </select>
index b451f2b..ec1c299 100644 (file)
@@ -3,7 +3,8 @@
        <div class="post-time hidden"><% post.time|html></div>
        <div class="post-author hidden"><% post.sone.id|html></div>
        <div class="post-author-local hidden"><% post.sone.local></div>
-       <div class="avatar">
+       <%include include/soneMenu.html class=="sone-post-menu" sone=post.sone>
+       <div class="avatar post-avatar" >
                <%if post.loaded>
                        <img src="/WebOfTrust/GetIdenticon?identity=<% post.sone.id|html>&amp;width=48&height=48" width="48" height="48" alt="Avatar Image" />
                <%else>
                        <%/if>
                        <% post.text|html|store key=originalText text=true>
                        <% post.text|parse sone=post.sone|store key=parsedText text=true>
+                       <% post.text|parse sone=post.sone length=core.preferences.charactersPerPost|store key=shortText text=true>
                        <div class="post-text raw-text<%if !raw> hidden<%/if>"><% originalText></div>
-                       <div class="post-text text<%if raw> hidden<%/if>"><% parsedText></div>
+                       <div class="post-text text<%if raw> hidden<%/if><%if !shortText|match key=parsedText> hidden<%/if>"><% parsedText></div>
+                       <div class="post-text short-text<%if raw> hidden<%/if><%if shortText|match key=parsedText> hidden<%/if>"><% shortText></div>
+                       <%if !shortText|match key=parsedText><%if !raw><a class="expand-post-text" href="viewPost.html?post=<% post.id|html>&amp;raw=true"><%= View.Post.ShowMore|l10n|html></a><%/if><%/if>
+                       <%if !shortText|match key=parsedText><%if !raw><a class="shrink-post-text hidden"><%= View.Post.ShowLess|l10n|html></a><%/if><%/if>
                </div>
                <div class="post-status-line status-line<%if !post.loaded> hidden<%/if>">
                        <div class="bookmarks">
index 2507917..abf3463 100644 (file)
@@ -3,7 +3,8 @@
        <div class="reply-time hidden"><% reply.time|html></div>
        <div class="reply-author hidden"><% reply.sone.id|html></div>
        <div class="reply-author-local hidden"><% reply.sone.local></div>
-       <div class="avatar">
+       <%include include/soneMenu.html class=="sone-reply-menu" sone=reply.sone>
+       <div class="avatar reply-avatar">
                <img src="/WebOfTrust/GetIdenticon?identity=<% reply.sone.id|html>&amp;width=36&height=36" width="36" height="36" alt="Avatar Image" />
        </div>
        <div class="inner-part">
                        <div class="author profile-link"><a href="viewSone.html?sone=<% reply.sone.id|html>"><% reply.sone.niceName|html></a></div>
                        <% reply.text|html|store key=originalText text=true>
                        <% reply.text|parse sone=reply.sone|store key=parsedText text=true>
+                       <% reply.text|parse sone=reply.sone length=core.preferences.charactersPerPost|store key=shortText text=true>
                        <div class="reply-text raw-text<%if !raw> hidden<%/if>"><% originalText></div>
-                       <div class="reply-text text<%if raw> hidden<%/if>"><% parsedText></div>
+                       <div class="reply-text text<%if raw> hidden<%/if><%if !shortText|match key=parsedText> hidden<%/if>"><% parsedText></div>
+                       <div class="reply-text short-text<%if raw> hidden<%/if><%if shortText|match key=parsedText> hidden<%/if>"><% shortText></div>
+                       <%if !shortText|match key=parsedText><%if !raw><a class="expand-reply-text" href="viewPost.html?post=<% reply.post.id|html>&amp;raw=true"><%= View.Post.ShowMore|l10n|html></a><%/if><%/if>
+                       <%if !shortText|match key=parsedText><%if !raw><a class="shrink-reply-text hidden"><%= View.Post.ShowLess|l10n|html></a><%/if><%/if>
                </div>
                <div class="reply-status-line status-line">
                        <div class="time"><% reply.time|date format="MMM d, yyyy, HH:mm:ss"></div>
index 91c921d..ba5c8e4 100644 (file)
@@ -6,7 +6,10 @@
        <div class="insert-marker" title="<%= View.Sone.Status.Inserting|l10n|html>">⬈</div>
        <div class="idle-marker" title="<%= View.Sone.Status.Idle|l10n|html>">✔</div>
        <div class="last-update"><%= View.Sone.Label.LastUpdate|l10n|html> <span class="time" title="<% sone.time|unknown|date format="MMM d, yyyy, HH:mm:ss">"><%sone.lastUpdatedText|html></span></div>
-       <div class="profile-link"><a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a></div>
+       <div>
+               <div class="profile-link"><a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a></div>
+               <div class="sone-stats">(<%= View.Sone.Stats.Posts|l10n 0=sone.posts.size>, <%= View.Sone.Stats.Replies|l10n 0=sone.replies.size>)</div>
+       </div>
        <div class="short-request-uri"><% sone.requestUri|substring start=4 length=43|html></div>
        <div class="hidden"><% sone.blacklisted></div>
        <%if sone.local>
index 33f0e27..260a321 100644 (file)
@@ -1,8 +1,74 @@
 <%include include/head.html>
 
        <div class="page-id hidden">known-sones</div>
+       
+       <script language="javascript">
+
+               $(document).ready(function() {
+                       $("select[name=sort]").change(function() {
+                               value = $(this).val();
+                               if ((value == "activity") || (value == "posts")) {
+                                       $("select[name=order]").val("desc");
+                               } else if (value == "name") {
+                                       $("select[name=order]").val("asc");
+                               } 
+                       });
+                       $("#sort-options select").change(function() {
+                               this.form.submit();
+                       });
+               });
+       
+       </script>
 
        <h1><%= Page.KnownSones.Page.Title|l10n|html></h1>
+       
+       <div id="sort-options">
+               <form action="knownSones.html" method="get">
+                       <div>
+                               Sort:
+                               <select name="sort">
+                                       <option value="name"<%if sort|match value="name"> selected="selected"<%/if>>Name</option>
+                                       <option value="activity"<%if sort|match value="activity"> selected="selected"<%/if>>Last activity</option>
+                                       <option value="posts"<%if sort|match value="posts"> selected="selected"<%/if>>Number of posts</option>
+                               </select>
+                               <select name="order">
+                                       <option value="asc"<%if order|match value="asc"> selected="selected"<%/if>>Ascending</option>
+                                       <option value="desc"<%if order|match value="desc"> selected="selected"<%/if>>Descending</option>
+                               </select>
+                       </div>
+                       <%ifnull !currentSone>
+                               <div>
+                                       Followed Sones:
+                                       <select name="followedSones">
+                                               <option value="none"></option>
+                                               <option value="show-only"<%if followedSones|match value="show-only"> selected="selected"<%/if>>Show only followed Sones</option>
+                                               <option value="hide"<%if followedSones|match value="hide"> selected="selected"<%/if>>Hide followed Sones</option>
+                                       </select>
+                               </div>
+                       <%/if>
+                       <div>
+                               <button type="submit">Apply</button>
+                       </div>
+               </form>
+       </div>
+
+       <div>
+               <form action="followSone.html" method="post">
+                       <input type="hidden" name="formPassword" value="<%formPassword|html>" />
+                       <input type="hidden" name="returnPage" value="<%request.uri|html>" />
+                       <input type="hidden" name="sone" value="<%foreach pagination.items sone><%if !sone.friend><%if !sone.current><%sone.id> <%/if><%/if><%/foreach>" />
+                       <button type="submit"><%= Page.KnownSones.Button.FollowAllSones|l10n|html></button>
+               </form>
+       </div>
+
+       <div>
+               <form action="unfollowSone.html" method="post">
+                       <input type="hidden" name="formPassword" value="<%formPassword|html>" />
+                       <input type="hidden" name="returnPage" value="<%request.uri|html>" />
+                       <input type="hidden" name="sone" value="<%foreach pagination.items sone><%if sone.friend><%sone.id> <%/if><%/foreach>" />
+                       <button type="submit"><%= Page.KnownSones.Button.UnfollowAllSones|l10n|html></button>
+               </form>
+       </div>
 
        <div id="known-sones">
                <%= page|store key=pageParameter>
index 087fc2e..10e9ad3 100644 (file)
@@ -4,7 +4,7 @@
 </div>
 <div class="text">
        <%= Notification.Mention.Text|l10n|html>
-       <%foreach posts post>
+       <%foreach posts post|unique>
                <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>
diff --git a/src/main/resources/templates/notify/rescuingSonesNotification.html b/src/main/resources/templates/notify/rescuingSonesNotification.html
deleted file mode 100644 (file)
index eef06ac..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-<div class="text">
-       <%= Notification.SoneIsBeingRescued.Text|l10n|html>
-       <%foreach sones sone>
-               <a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
-       <%/foreach>
-</div>
diff --git a/src/main/resources/templates/notify/soneInsertNotification.html b/src/main/resources/templates/notify/soneInsertNotification.html
new file mode 100644 (file)
index 0000000..27374ad
--- /dev/null
@@ -0,0 +1,7 @@
+<%if soneStatus|match value="inserting">
+       Your Sone <a href="viewSone.html?sone=<%insertSone.id|html>"><%insertSone.niceName|html></a> is now being inserted.
+<%elseif soneStatus|match value="inserted">
+       Your Sone <a href="viewSone.html?sone=<%insertSone.id|html>"><%insertSone.niceName|html></a> has been inserted in <%= Notification.SoneInsert.Duration|l10n 0=insertDuration>.
+<%elseif soneStatus|match value="insert-aborted">
+       Inserting your Sone <a href="viewSone.html?sone=<%insertSone.id|html>"><%insertSone.niceName|html></a> has failed.
+<%/if>
\ No newline at end of file
diff --git a/src/main/resources/templates/notify/sonesRescuedNotification.html b/src/main/resources/templates/notify/sonesRescuedNotification.html
deleted file mode 100644 (file)
index 84f015d..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-<div class="text">
-       <%= Notification.SoneRescued.Text|l10n|html>
-       <%foreach sones sone>
-               <a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
-       <%/foreach>
-       <%= Notification.SoneRescued.Text.RememberToUnlock|l10n|html>
-</div>
index 368c14e..39d579f 100644 (file)
@@ -8,6 +8,9 @@
                        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.CharactersPerPost", function(postsPerPageText) {
+                               registerInputTextareaSwap("#sone #options input[name=characters-per-post]", postsPerPageText, "characters-per-post", true, true);
+                       });
                        getTranslation("WebInterface.DefaultText.Option.PositiveTrust", function(positiveTrustText) {
                                registerInputTextareaSwap("#sone #options input[name=positive-trust]", positiveTrustText, "positive-trust", true, true);
                        });
                        <%= Page.Options.Option.AutoFollow.Description|l10n|html>
                </p>
 
+               <p>
+                       <input type="checkbox" name="enable-sone-insert-notifications"<%ifnull currentSone> disabled="disabled"<%/if><%if enable-sone-insert-notifications> checked="checked"<%/if> />
+                       <%= Page.Options.Option.EnableSoneInsertNotifications.Description|l10n|html>
+               </p>
+
                <h2><%= Page.Options.Section.RuntimeOptions.Title|l10n|html></h2>
 
                <p><%= Page.Options.Option.InsertionDelay.Description|l10n|html></p>
                <%/if>
                <p><input type="text" name="posts-per-page" value="<% posts-per-page|html>" /></p>
 
+               <p><%= Page.Options.Option.CharactersPerPost.Description|l10n|html></p>
+               <%if =characters-per-post|in collection=fieldErrors>
+                       <p class="warning"><%= Page.Options.Warnings.ValueNotChanged|l10n|html></p>
+               <%/if>
+               <p><input type="text" name="characters-per-post" value="<% characters-per-post|html>" /></p>
+
                <p>
                        <input type="checkbox" name="require-full-access"<%if require-full-access> checked="checked"<%/if> />
                        <%= Page.Options.Option.RequireFullAccess.Description|l10n|html></p>
                        </select>
                </p>
 
-               <h2><%= Page.Options.Section.RescueOptions.Title|l10n|html></h2>
-
-               <p><%= Page.Options.Option.SoneRescueMode.Description1|l10n|html></p>
-               <p><%= Page.Options.Option.SoneRescueMode.Description2|l10n|html></p>
-               <p><%= Page.Options.Option.SoneRescueMode.Description3|l10n|html></p>
-               <p><select name="sone-rescue-mode"><option disabled="disabled"><%= WebInterface.SelectBox.Choose|l10n|html></option><option value="true"<%if sone-rescue-mode> selected="selected"<%/if>><%= WebInterface.SelectBox.Yes|l10n|html></option><option value="false"<%if !sone-rescue-mode> selected="selected"<%/if>><%= WebInterface.SelectBox.No|l10n|html></option></select>
-
                <h2><%= Page.Options.Section.Cleaning.Title|l10n|html></h2>
 
                <p><%= Page.Options.Option.ClearOnNextRestart.Description|l10n|html|replace needle="{strong}" replacement="<strong>"|replace needle="{/strong}" replacement="</strong>"></p>
diff --git a/src/main/resources/templates/rescue.html b/src/main/resources/templates/rescue.html
new file mode 100644 (file)
index 0000000..e08edc3
--- /dev/null
@@ -0,0 +1,32 @@
+<%include include/head.html>
+
+<h1><%= Page.Rescue.Page.Title|l10n 0=currentSone.niceName|html></h1>
+
+<p><%= Page.Rescue.Text.Description|l10n|html></p>
+<p><%= Page.Rescue.Text.Procedure|l10n|html></p>
+
+<%if soneRescuer.fetching>
+       <p><%= Page.Rescue.Text.Fetching|l10n 0=soneRescuer.currentEdition|html></p>
+<%else>
+       <%if soneRescuer.hasNextEdition>
+               <%if soneRescuer.lastFetchSuccessful>
+                       <p><%= Page.Rescue.Text.Fetched|l10n 0=soneRescuer.currentEdition|html></p>
+               <%else>
+                       <p><%= Page.Rescue.Text.NotFetched|l10n 0=soneRescuer.currentEdition|html></p>
+               <%/if>
+               <form action="rescue.html" method="post">
+                       <input type="hidden" name="formPassword" value="<%formPassword|html>" />
+                       <label><%= Page.Rescue.Label.NextEdition|l10n|html></label>
+                       <input type="field" name="edition" value="<%soneRescuer.nextEdition>" />
+                       <button type="submit" name="fetch" value="true"><%= Page.Rescue.Button.Fetch|l10n|html></button>
+               </form>
+       <%else>
+               <%if soneRescuer.lastFetchSuccessful>
+                       <p><%= Page.Rescue.Text.Fetched|l10n 0=soneRescuer.currentEdition|html></p>
+               <%else>
+                       <p><%= Page.Rescue.Text.FetchedLast|l10n|html></p>
+               <%/if>
+       <%/if>
+<%/if>
+
+<%include include/tail.html>
index 5c8b7ff..55cc2a8 100644 (file)
                <h1><%= Page.ViewSone.Page.TitleWithoutSone|l10n|html></h1>
 
                <p><%= Page.ViewSone.UnknownSone.Description|l10n|html></p>
+               <p>
+                       <%= Page.ViewSone.UnknownSone.LinkToWebOfTrust|l10n|html>
+                       <a href="/WebOfTrust/ShowIdentity?id=<% sone.id|html>"><%= Page.ViewSone.Profile.Name.WoTLink|l10n|html></a>
+               </p>
 
        <%else>