From: David ‘Bombe’ Roden Date: Mon, 12 Sep 2011 11:40:37 +0000 (+0200) Subject: Merge branch 'release-0.6.6' X-Git-Tag: 0.6.6^0 X-Git-Url: https://git.pterodactylus.net/?p=Sone.git;a=commitdiff_plain;h=1bc78b582ac59f2438002997f5780db4dcee0a2a;hp=47ed7eaf00c35889781831d33d04e9f91c9ad266 Merge branch 'release-0.6.6' --- diff --git a/pom.xml b/pom.xml index 36a51ff..a087474 100644 --- a/pom.xml +++ b/pom.xml @@ -2,12 +2,12 @@ 4.0.0 net.pterodactylus sone - 0.6.5 + 0.6.6 net.pterodactylus utils - 0.9.6 + 0.10.0 junit diff --git a/src/main/java/net/pterodactylus/sone/core/Core.java b/src/main/java/net/pterodactylus/sone/core/Core.java index 3c99eef..c8d7588 100644 --- a/src/main/java/net/pterodactylus/sone/core/Core.java +++ b/src/main/java/net/pterodactylus/sone/core/Core.java @@ -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 David ‘Bombe’ Roden */ -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 soneStatuses = new HashMap(); @@ -139,6 +137,10 @@ public class Core implements IdentityListener, UpdateListener, SoneProvider, Pos /* synchronize access on this on localSones. */ private final Map soneInserters = new HashMap(); + /** Sone rescuers. */ + /* synchronize access on this on localSones. */ + private final Map soneRescuers = new HashMap(); + /** All local Sones. */ /* synchronize access on this on itself. */ private Map localSones = new HashMap(); @@ -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 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(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(false)); + sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption(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(10, new IntegerRangeValidator(1, Integer.MAX_VALUE))); + options.addIntegerOption("CharactersPerPost", new DefaultOption(200, new OrValidator(new IntegerRangeValidator(50, Integer.MAX_VALUE), new EqualityValidator(-1)))); options.addBooleanOption("RequireFullAccess", new DefaultOption(false)); options.addIntegerOption("PositiveTrust", new DefaultOption(75, new IntegerRangeValidator(0, 100))); options.addIntegerOption("NegativeTrust", new DefaultOption(-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 -1 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 -1 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! diff --git a/src/main/java/net/pterodactylus/sone/core/CoreListener.java b/src/main/java/net/pterodactylus/sone/core/CoreListener.java index d34ac36..d5120ac 100644 --- a/src/main/java/net/pterodactylus/sone/core/CoreListener.java +++ b/src/main/java/net/pterodactylus/sone/core/CoreListener.java @@ -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 diff --git a/src/main/java/net/pterodactylus/sone/core/CoreListenerManager.java b/src/main/java/net/pterodactylus/sone/core/CoreListenerManager.java index 786cfb8..875a2b6 100644 --- a/src/main/java/net/pterodactylus/sone/core/CoreListenerManager.java +++ b/src/main/java/net/pterodactylus/sone/core/CoreListenerManager.java @@ -45,32 +45,6 @@ public class CoreListenerManager extends AbstractListenerManager 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 index 0000000..6391d8a --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/core/SoneInsertListener.java @@ -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 . + */ + +package net.pterodactylus.sone.core; + +import java.util.EventListener; + +import net.pterodactylus.sone.data.Sone; + +/** + * Listener for Sone insert events. + * + * @author David ‘Bombe’ Roden + */ +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 index 0000000..9e4ea2a --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/core/SoneInsertListenerManager.java @@ -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 . + */ + +package net.pterodactylus.sone.core; + +import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.util.event.AbstractListenerManager; + +/** + * Manager for {@link SoneInsertListener}s. + * + * @author David ‘Bombe’ Roden + */ +public class SoneInsertListenerManager extends AbstractListenerManager { + + /** + * 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); + } + } + +} diff --git a/src/main/java/net/pterodactylus/sone/core/SoneInserter.java b/src/main/java/net/pterodactylus/sone/core/SoneInserter.java index cc5f689..f654d09 100644 --- a/src/main/java/net/pterodactylus/sone/core/SoneInserter.java +++ b/src/main/java/net/pterodactylus/sone/core/SoneInserter.java @@ -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 index 0000000..542ec4a --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/core/SoneRescuer.java @@ -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 . + */ + +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 David ‘Bombe’ Roden + */ +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; + } + } + } + +} diff --git a/src/main/java/net/pterodactylus/sone/data/Sone.java b/src/main/java/net/pterodactylus/sone/data/Sone.java index e1c0391..3477346 100644 --- a/src/main/java/net/pterodactylus/sone/data/Sone.java +++ b/src/main/java/net/pterodactylus/sone/data/Sone.java @@ -60,6 +60,29 @@ public class Sone implements Fingerprintable, Comparable { }; + /** + * Comparator that sorts Sones by last activity (least recent active first). + */ + public static final Comparator LAST_ACTIVITY_COMPARATOR = new Comparator() { + + @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 POST_COUNT_COMPARATOR = new Comparator() { + + /** + * {@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 EMPTY_SONE_FILTER = new Filter() { diff --git a/src/main/java/net/pterodactylus/sone/freenet/L10nFilter.java b/src/main/java/net/pterodactylus/sone/freenet/L10nFilter.java index fa7385c..910e0d6 100644 --- a/src/main/java/net/pterodactylus/sone/freenet/L10nFilter.java +++ b/src/main/java/net/pterodactylus/sone/freenet/L10nFilter.java @@ -17,6 +17,10 @@ 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 parameters) { - return l10n.getString(String.valueOf(data)); + if (parameters.isEmpty()) { + return l10n.getString(String.valueOf(data)); + } + List parameterValues = new ArrayList(); + 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()); } - } diff --git a/src/main/java/net/pterodactylus/sone/main/SonePlugin.java b/src/main/java/net/pterodactylus/sone/main/SonePlugin.java index b5a7fcc..738c0d1 100644 --- a/src/main/java/net/pterodactylus/sone/main/SonePlugin.java +++ b/src/main/java/net/pterodactylus/sone/main/SonePlugin.java @@ -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); diff --git a/src/main/java/net/pterodactylus/sone/notify/ListNotificationFilters.java b/src/main/java/net/pterodactylus/sone/notify/ListNotificationFilters.java index 37cff6f..3db8912 100644 --- a/src/main/java/net/pterodactylus/sone/notify/ListNotificationFilters.java +++ b/src/main/java/net/pterodactylus/sone/notify/ListNotificationFilters.java @@ -55,7 +55,7 @@ public class ListNotificationFilters { List filteredNotifications = new ArrayList(); for (Notification notification : notifications) { if (notification.getId().equals("new-post-notification")) { - ListNotification filteredNotification = filterNewPostNotification((ListNotification) notification, currentSone); + ListNotification filteredNotification = filterNewPostNotification((ListNotification) 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 filteredNotification = filterNewPostNotification((ListNotification) 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 filterNewPostNotification(ListNotification newPostNotification, Sone currentSone) { - if (currentSone == null) { + public static ListNotification filterNewPostNotification(ListNotification newPostNotification, Sone currentSone, boolean soneRequired) { + if (soneRequired && (currentSone == null)) { return null; } List newPosts = new ArrayList(); @@ -102,6 +111,7 @@ public class ListNotificationFilters { } ListNotification filteredNotification = new ListNotification(newPostNotification); filteredNotification.setElements(newPosts); + filteredNotification.setLastUpdateTime(newPostNotification.getLastUpdatedTime()); return filteredNotification; } @@ -137,6 +147,7 @@ public class ListNotificationFilters { } ListNotification filteredNotification = new ListNotification(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: *
    *
  • The post does not have a Sone.
  • + *
  • The post’s {@link Post#getTime() time} is in the future.
  • + *
+ *

+ * If {@code post} is not {@code null} more checks are performed, and the + * post will be invisible if: + *

+ *
    *
  • 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.
  • @@ -153,36 +171,38 @@ public class ListNotificationFilters { * Sone. *
  • The given Sone has not explicitely assigned negative trust to the * post’s Sone but the implicit trust is negative.
  • - *
  • The post’s {@link Post#getTime() time} is in the future.
  • *
* 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; diff --git a/src/main/java/net/pterodactylus/sone/template/ParserFilter.java b/src/main/java/net/pterodactylus/sone/template/ParserFilter.java index 26afe2e..332f504 100644 --- a/src/main/java/net/pterodactylus/sone/template/ParserFilter.java +++ b/src/main/java/net/pterodactylus/sone/template/ParserFilter.java @@ -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 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 parts = soneTextParser.parse(context, new StringReader(text)); + if (length > -1) { + List shortenedParts = new ArrayList(); + 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; } } diff --git a/src/main/java/net/pterodactylus/sone/template/SoneAccessor.java b/src/main/java/net/pterodactylus/sone/template/SoneAccessor.java index 76fdcc1..ead6066 100644 --- a/src/main/java/net/pterodactylus/sone/template/SoneAccessor.java +++ b/src/main/java/net/pterodactylus/sone/template/SoneAccessor.java @@ -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 index 0000000..70dae70 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/template/UniqueElementFilter.java @@ -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 . + */ + +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 David ‘Bombe’ Roden + */ +public class UniqueElementFilter implements Filter { + + /** + * {@inheritDoc} + */ + @Override + public Object format(TemplateContext templateContext, Object data, Map parameters) { + if (!(data instanceof Collection)) { + return data; + } + return new HashSet((Collection) data); + } + +} diff --git a/src/main/java/net/pterodactylus/sone/text/SoneTextParser.java b/src/main/java/net/pterodactylus/sone/text/SoneTextParser.java index 94c3db4..4195516 100644 --- a/src/main/java/net/pterodactylus/sone/text/SoneTextParser.java +++ b/src/main/java/net/pterodactylus/sone/text/SoneTextParser.java @@ -182,7 +182,7 @@ public class SoneTextParser implements Parser { 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 { 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))); diff --git a/src/main/java/net/pterodactylus/sone/web/CreateReplyPage.java b/src/main/java/net/pterodactylus/sone/web/CreateReplyPage.java index aae5e83..2b12352 100644 --- a/src/main/java/net/pterodactylus/sone/web/CreateReplyPage.java +++ b/src/main/java/net/pterodactylus/sone/web/CreateReplyPage.java @@ -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); } diff --git a/src/main/java/net/pterodactylus/sone/web/EditProfilePage.java b/src/main/java/net/pterodactylus/sone/web/EditProfilePage.java index 52468a3..43dd15b 100644 --- a/src/main/java/net/pterodactylus/sone/web/EditProfilePage.java +++ b/src/main/java/net/pterodactylus/sone/web/EditProfilePage.java @@ -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); diff --git a/src/main/java/net/pterodactylus/sone/web/FollowSonePage.java b/src/main/java/net/pterodactylus/sone/web/FollowSonePage.java index 9cd1f2a..81225e9 100644 --- a/src/main/java/net/pterodactylus/sone/web/FollowSonePage.java +++ b/src/main/java/net/pterodactylus/sone/web/FollowSonePage.java @@ -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); } } diff --git a/src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java b/src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java index 7c8f9ad..ad63791 100644 --- a/src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java +++ b/src/main/java/net/pterodactylus/sone/web/KnownSonesPage.java @@ -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 knownSones = Filters.filteredList(new ArrayList(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() { + + @Override + public boolean filterObject(Sone sone) { + return currentSone.hasFriend(sone.getId()); + } + }); + } else if ((currentSone != null) && "hide".equals(followedSones)) { + knownSones = Filters.filteredList(knownSones, new Filter() { + + @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.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.POST_COUNT_COMPARATOR)); + } else { + Collections.sort(knownSones, Sone.POST_COUNT_COMPARATOR); + } + } else { + if ("desc".equals(sortOrder)) { + Collections.sort(knownSones, new ReverseComparator(Sone.NICE_NAME_COMPARATOR)); + } else { + Collections.sort(knownSones, Sone.NICE_NAME_COMPARATOR); + } + } Pagination sonePagination = new Pagination(knownSones, 25).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0)); templateContext.set("pagination", sonePagination); templateContext.set("knownSones", sonePagination.getItems()); } - } diff --git a/src/main/java/net/pterodactylus/sone/web/OptionsPage.java b/src/main/java/net/pterodactylus/sone/web/OptionsPage.java index 6785e81..fda6afd 100644 --- a/src/main/java/net/pterodactylus/sone/web/OptionsPage.java +++ b/src/main/java/net/pterodactylus/sone/web/OptionsPage.java @@ -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 index 0000000..40b03f1 --- /dev/null +++ b/src/main/java/net/pterodactylus/sone/web/RescuePage.java @@ -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 . + */ + +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 David ‘Bombe’ Roden + */ +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); + } + +} diff --git a/src/main/java/net/pterodactylus/sone/web/SearchPage.java b/src/main/java/net/pterodactylus/sone/web/SearchPage.java index 6f5e001..fab7a57 100644 --- a/src/main/java/net/pterodactylus/sone/web/SearchPage.java +++ b/src/main/java/net/pterodactylus/sone/web/SearchPage.java @@ -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, Set>> hitCache = new MemoryCache, Set>>(new ValueRetriever, Set>>() { + + @SuppressWarnings("synthetic-access") + public CacheItem>> retrieve(List phrases) throws CacheException { + Set posts = new HashSet(); + for (Sone sone : webInterface.getCore().getSones()) { + posts.addAll(sone.getPosts()); + } + return new DefaultCacheItem>>(getHits(Filters.filteredSet(posts, Post.FUTURE_POSTS_FILTER), phrases, new PostStringGenerator())); + } + + }, new TimedMap, CacheItem>>>(300000)); + /** * Creates a new search page. * @@ -83,16 +104,21 @@ public class SearchPage extends SoneTemplatePage { } List phrases = parseSearchPhrases(query); + if (phrases.isEmpty()) { + throw new RedirectException("index.html"); + } Set sones = webInterface.getCore().getSones(); Set> soneHits = getHits(sones, phrases, SoneStringGenerator.COMPLETE_GENERATOR); - Set posts = new HashSet(); - for (Sone sone : sones) { - posts.addAll(sone.getPosts()); + Set> 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> 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 phrases = new ArrayList(); 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); + } + } /** diff --git a/src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java b/src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java index a0174a9..b7e36f4 100644 --- a/src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java +++ b/src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java @@ -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); diff --git a/src/main/java/net/pterodactylus/sone/web/UnfollowSonePage.java b/src/main/java/net/pterodactylus/sone/web/UnfollowSonePage.java index a7836f5..33b88a1 100644 --- a/src/main/java/net/pterodactylus/sone/web/UnfollowSonePage.java +++ b/src/main/java/net/pterodactylus/sone/web/UnfollowSonePage.java @@ -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); } } diff --git a/src/main/java/net/pterodactylus/sone/web/WebInterface.java b/src/main/java/net/pterodactylus/sone/web/WebInterface.java index 73436c7..08202d4 100644 --- a/src/main/java/net/pterodactylus/sone/web/WebInterface.java +++ b/src/main/java/net/pterodactylus/sone/web/WebInterface.java @@ -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 mentionNotification; - /** The “rescuing Sone” notification. */ - private final ListNotification rescuingSonesNotification; - - /** The “Sone rescued” notification. */ - private final ListNotification sonesRescuedNotification; + /** Notifications for sone inserts. */ + private final Map soneInsertNotifications = new HashMap(); /** Sone locked notification ticker objects. */ private final Map lockedSonesTickerObjects = Collections.synchronizedMap(new HashMap()); @@ -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("mention-notification", "posts", mentionNotificationTemplate, false); - Template rescuingSonesTemplate = TemplateParser.parse(createReader("/templates/notify/rescuingSonesNotification.html")); - rescuingSonesNotification = new ListNotification("sones-being-rescued-notification", "sones", rescuingSonesTemplate); - - Template sonesRescuedTemplate = TemplateParser.parse(createReader("/templates/notify/sonesRescuedNotification.html")); - sonesRescuedNotification = new ListNotification("sones-rescued-notification", "sones", sonesRescuedTemplate); - Template lockedSonesTemplate = TemplateParser.parse(createReader("/templates/notify/lockedSonesNotification.html")); lockedSonesNotification = new ListNotification("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); diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/CreateReplyAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/CreateReplyAjaxPage.java index 9ed960f..ce78f58 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/CreateReplyAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/CreateReplyAjaxPage.java @@ -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()); } diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPage.java index 383d2d4..4625c13 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPage.java @@ -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())); } diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/FollowSoneAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/FollowSoneAjaxPage.java index 1269b41..d82ab1d 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/FollowSoneAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/FollowSoneAjaxPage.java @@ -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(); } diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationAjaxPage.java index 1406c3a..8bfe472 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationAjaxPage.java @@ -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) notification, currentSone); + notification = ListNotificationFilters.filterNewPostNotification((ListNotification) notification, currentSone, false); } else if ("new-reply-notification".equals(notificationId)) { notification = ListNotificationFilters.filterNewReplyNotification((ListNotification) notification, currentSone); + } else if ("mention-notification".equals(notificationId)) { + notification = ListNotificationFilters.filterNewPostNotification((ListNotification) 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); diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/GetPostAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/GetPostAjaxPage.java index f46ec6b..f56c5b7 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/GetPostAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/GetPostAjaxPage.java @@ -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); diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/GetReplyAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/GetReplyAjaxPage.java index 24bbbe2..6cc7d47 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/GetReplyAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/GetReplyAjaxPage.java @@ -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); diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java index 246cbed..f704905 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java @@ -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; } diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/GetTimesAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/GetTimesAjaxPage.java index 0be0c46..31ec399 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/GetTimesAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/GetTimesAjaxPage.java @@ -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) { diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/LikeAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/LikeAjaxPage.java index efd1439..bf87ead 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/LikeAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/LikeAjaxPage.java @@ -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"); } diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPage.java index 780e4d1..092c1f7 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPage.java @@ -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(); } diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/UnfollowSoneAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/UnfollowSoneAjaxPage.java index c2379e8..7f719b7 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/UnfollowSoneAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/UnfollowSoneAjaxPage.java @@ -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(); } diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/UnlikeAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/UnlikeAjaxPage.java index e5c933d..84b0593 100644 --- a/src/main/java/net/pterodactylus/sone/web/ajax/UnlikeAjaxPage.java +++ b/src/main/java/net/pterodactylus/sone/web/ajax/UnlikeAjaxPage.java @@ -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"); } diff --git a/src/main/resources/i18n/sone.en.properties b/src/main/resources/i18n/sone.en.properties index 3610afc..739c615 100644 --- a/src/main/resources/i18n/sone.en.properties +++ b/src/main/resources/i18n/sone.en.properties @@ -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 .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 index 0000000..47f25a9 --- /dev/null +++ b/src/main/resources/static/javascript/jquery.fieldselection.js @@ -0,0 +1,83 @@ +/* + * jQuery plugin: fieldSelection - v0.1.0 - last change: 2006-12-16 + * (c) 2006 Alex Brem - 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; }); + +})(); diff --git a/src/main/resources/static/javascript/sone.js b/src/main/resources/static/javascript/sone.js index d6c83ae..ca80b48 100644 --- a/src/main/resources/static/javascript/sone.js +++ b/src/main/resources/static/javascript/sone.js @@ -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); diff --git a/src/main/resources/templates/include/head.html b/src/main/resources/templates/include/head.html index b37df26..cd9eddf 100644 --- a/src/main/resources/templates/include/head.html +++ b/src/main/resources/templates/include/head.html @@ -6,6 +6,7 @@ +
diff --git a/src/main/resources/templates/include/soneMenu.html b/src/main/resources/templates/include/soneMenu.html new file mode 100644 index 0000000..49237ba --- /dev/null +++ b/src/main/resources/templates/include/soneMenu.html @@ -0,0 +1,17 @@ +
+ + Avatar Image +
+ +
<%= View.Sone.Stats.Posts|l10n 0=sone.posts.size>, <%= View.Sone.Stats.Replies|l10n 0=sone.replies.size>
+ <%if !sone.local> + + <%/if> +
+
diff --git a/src/main/resources/templates/include/updateStatus.html b/src/main/resources/templates/include/updateStatus.html index 43d4d70..881aca4 100644 --- a/src/main/resources/templates/include/updateStatus.html +++ b/src/main/resources/templates/include/updateStatus.html @@ -4,7 +4,7 @@
diff --git a/src/main/resources/templates/include/viewPost.html b/src/main/resources/templates/include/viewPost.html index b451f2b..ec1c299 100644 --- a/src/main/resources/templates/include/viewPost.html +++ b/src/main/resources/templates/include/viewPost.html @@ -3,7 +3,8 @@ -
+ <%include include/soneMenu.html class=="sone-post-menu" sone=post.sone> +
<%if post.loaded> Avatar Image <%else> @@ -23,8 +24,12 @@ <%/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>
<% originalText>
-
<% parsedText>
+
<% parsedText>
+
<% shortText>
+ <%if !shortText|match key=parsedText><%if !raw><%= View.Post.ShowMore|l10n|html><%/if><%/if> + <%if !shortText|match key=parsedText><%if !raw><%/if><%/if>
diff --git a/src/main/resources/templates/include/viewReply.html b/src/main/resources/templates/include/viewReply.html index 2507917..abf3463 100644 --- a/src/main/resources/templates/include/viewReply.html +++ b/src/main/resources/templates/include/viewReply.html @@ -3,7 +3,8 @@ -
+ <%include include/soneMenu.html class=="sone-reply-menu" sone=reply.sone> +
Avatar Image
@@ -11,8 +12,12 @@ <% 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>
<% originalText>
-
<% parsedText>
+
<% parsedText>
+
<% shortText>
+ <%if !shortText|match key=parsedText><%if !raw><%= View.Post.ShowMore|l10n|html><%/if><%/if> + <%if !shortText|match key=parsedText><%if !raw><%/if><%/if>
<% reply.time|date format="MMM d, yyyy, HH:mm:ss">
diff --git a/src/main/resources/templates/include/viewSone.html b/src/main/resources/templates/include/viewSone.html index 91c921d..ba5c8e4 100644 --- a/src/main/resources/templates/include/viewSone.html +++ b/src/main/resources/templates/include/viewSone.html @@ -6,7 +6,10 @@
⬈
✔
<%= View.Sone.Label.LastUpdate|l10n|html> "><%sone.lastUpdatedText|html>
- +
+ +
(<%= View.Sone.Stats.Posts|l10n 0=sone.posts.size>, <%= View.Sone.Stats.Replies|l10n 0=sone.replies.size>)
+
<% sone.requestUri|substring start=4 length=43|html>
<%if sone.local> diff --git a/src/main/resources/templates/knownSones.html b/src/main/resources/templates/knownSones.html index 33f0e27..260a321 100644 --- a/src/main/resources/templates/knownSones.html +++ b/src/main/resources/templates/knownSones.html @@ -1,8 +1,74 @@ <%include include/head.html> + +

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

+ +
+
+
+ Sort: + + +
+ <%ifnull !currentSone> +
+ Followed Sones: + +
+ <%/if> +
+ +
+
+
+ +
+
+ + + + +
+
+ +
+
+ + + + +
+
<%= page|store key=pageParameter> diff --git a/src/main/resources/templates/notify/mentionNotification.html b/src/main/resources/templates/notify/mentionNotification.html index 087fc2e..10e9ad3 100644 --- a/src/main/resources/templates/notify/mentionNotification.html +++ b/src/main/resources/templates/notify/mentionNotification.html @@ -4,7 +4,7 @@
<%= Notification.Mention.Text|l10n|html> - <%foreach posts post> + <%foreach posts post|unique> <% post.sone.niceName|html><%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 index eef06ac..0000000 --- a/src/main/resources/templates/notify/rescuingSonesNotification.html +++ /dev/null @@ -1,6 +0,0 @@ -
- <%= Notification.SoneIsBeingRescued.Text|l10n|html> - <%foreach sones sone> - <% sone.niceName|html><%notlast>,<%/notlast><%last>.<%/last> - <%/foreach> -
diff --git a/src/main/resources/templates/notify/soneInsertNotification.html b/src/main/resources/templates/notify/soneInsertNotification.html new file mode 100644 index 0000000..27374ad --- /dev/null +++ b/src/main/resources/templates/notify/soneInsertNotification.html @@ -0,0 +1,7 @@ +<%if soneStatus|match value="inserting"> + Your Sone <%insertSone.niceName|html> is now being inserted. +<%elseif soneStatus|match value="inserted"> + Your Sone <%insertSone.niceName|html> has been inserted in <%= Notification.SoneInsert.Duration|l10n 0=insertDuration>. +<%elseif soneStatus|match value="insert-aborted"> + Inserting your Sone <%insertSone.niceName|html> 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 index 84f015d..0000000 --- a/src/main/resources/templates/notify/sonesRescuedNotification.html +++ /dev/null @@ -1,7 +0,0 @@ -
- <%= Notification.SoneRescued.Text|l10n|html> - <%foreach sones sone> - <% sone.niceName|html><%notlast>,<%/notlast><%last>.<%/last> - <%/foreach> - <%= Notification.SoneRescued.Text.RememberToUnlock|l10n|html> -
diff --git a/src/main/resources/templates/options.html b/src/main/resources/templates/options.html index 368c14e..39d579f 100644 --- a/src/main/resources/templates/options.html +++ b/src/main/resources/templates/options.html @@ -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); }); @@ -40,6 +43,11 @@ <%= Page.Options.Option.AutoFollow.Description|l10n|html>

+

+ disabled="disabled"<%/if><%if enable-sone-insert-notifications> checked="checked"<%/if> /> + <%= Page.Options.Option.EnableSoneInsertNotifications.Description|l10n|html> +

+

<%= Page.Options.Section.RuntimeOptions.Title|l10n|html>

<%= Page.Options.Option.InsertionDelay.Description|l10n|html>

@@ -54,6 +62,12 @@ <%/if>

+

<%= Page.Options.Option.CharactersPerPost.Description|l10n|html>

+ <%if =characters-per-post|in collection=fieldErrors> +

<%= Page.Options.Warnings.ValueNotChanged|l10n|html>

+ <%/if> +

+

checked="checked"<%/if> /> <%= Page.Options.Option.RequireFullAccess.Description|l10n|html>

@@ -89,13 +103,6 @@

-

<%= Page.Options.Section.RescueOptions.Title|l10n|html>

- -

<%= Page.Options.Option.SoneRescueMode.Description1|l10n|html>

-

<%= Page.Options.Option.SoneRescueMode.Description2|l10n|html>

-

<%= Page.Options.Option.SoneRescueMode.Description3|l10n|html>

-

-

<%= Page.Options.Section.Cleaning.Title|l10n|html>

<%= Page.Options.Option.ClearOnNextRestart.Description|l10n|html|replace needle="{strong}" replacement=""|replace needle="{/strong}" replacement="">

diff --git a/src/main/resources/templates/rescue.html b/src/main/resources/templates/rescue.html new file mode 100644 index 0000000..e08edc3 --- /dev/null +++ b/src/main/resources/templates/rescue.html @@ -0,0 +1,32 @@ +<%include include/head.html> + +

<%= Page.Rescue.Page.Title|l10n 0=currentSone.niceName|html>

+ +

<%= Page.Rescue.Text.Description|l10n|html>

+

<%= Page.Rescue.Text.Procedure|l10n|html>

+ +<%if soneRescuer.fetching> +

<%= Page.Rescue.Text.Fetching|l10n 0=soneRescuer.currentEdition|html>

+<%else> + <%if soneRescuer.hasNextEdition> + <%if soneRescuer.lastFetchSuccessful> +

<%= Page.Rescue.Text.Fetched|l10n 0=soneRescuer.currentEdition|html>

+ <%else> +

<%= Page.Rescue.Text.NotFetched|l10n 0=soneRescuer.currentEdition|html>

+ <%/if> +
+ + + + +
+ <%else> + <%if soneRescuer.lastFetchSuccessful> +

<%= Page.Rescue.Text.Fetched|l10n 0=soneRescuer.currentEdition|html>

+ <%else> +

<%= Page.Rescue.Text.FetchedLast|l10n|html>

+ <%/if> + <%/if> +<%/if> + +<%include include/tail.html> diff --git a/src/main/resources/templates/viewSone.html b/src/main/resources/templates/viewSone.html index 5c8b7ff..55cc2a8 100644 --- a/src/main/resources/templates/viewSone.html +++ b/src/main/resources/templates/viewSone.html @@ -14,6 +14,10 @@

<%= Page.ViewSone.Page.TitleWithoutSone|l10n|html>

<%= Page.ViewSone.UnknownSone.Description|l10n|html>

+

+ <%= Page.ViewSone.UnknownSone.LinkToWebOfTrust|l10n|html> + <%= Page.ViewSone.Profile.Name.WoTLink|l10n|html> +

<%else>