<modelVersion>4.0.0</modelVersion>
<groupId>net.pterodactylus</groupId>
<artifactId>sone</artifactId>
- <version>0.6.5</version>
+ <version>0.6.6</version>
<dependencies>
<dependency>
<groupId>net.pterodactylus</groupId>
<artifactId>utils</artifactId>
- <version>0.9.6</version>
+ <version>0.10.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
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;
/**
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
-public class Core implements IdentityListener, UpdateListener, SoneProvider, PostProvider {
+public class Core extends AbstractService implements IdentityListener, UpdateListener, SoneProvider, PostProvider, SoneInsertListener {
/**
* Enumeration for the possible states of a {@link Sone}.
/** The FCP interface. */
private volatile FcpInterface fcpInterface;
- /** Whether the core has been stopped. */
- private volatile boolean stopped;
-
/** The Sones’ statuses. */
/* synchronize access on itself. */
private final Map<Sone, SoneStatus> soneStatuses = new HashMap<Sone, SoneStatus>();
/* synchronize access on this on localSones. */
private final Map<Sone, SoneInserter> soneInserters = new HashMap<Sone, SoneInserter>();
+ /** Sone rescuers. */
+ /* synchronize access on this on localSones. */
+ private final Map<Sone, SoneRescuer> soneRescuers = new HashMap<Sone, SoneRescuer>();
+
/** All local Sones. */
/* synchronize access on this on itself. */
private Map<String, Sone> localSones = new HashMap<String, Sone>();
/** 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.
*
* The identity manager
*/
public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager) {
+ super("Sone Core");
this.configuration = configuration;
this.freenetInterface = freenetInterface;
this.identityManager = identityManager;
*/
public void setConfiguration(Configuration configuration) {
this.configuration = configuration;
- saveConfiguration();
+ touchConfiguration();
}
/**
}
/**
+ * 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
/* TODO - load posts ’n stuff */
localSones.put(ownIdentity.getId(), sone);
final SoneInserter soneInserter = new SoneInserter(this, freenetInterface, sone);
+ soneInserter.addSoneInsertListener(this);
soneInserters.put(sone, soneInserter);
setSoneStatus(sone, SoneStatus.idle);
loadSone(sone);
- if (!preferences.isSoneRescueMode()) {
- soneInserter.start();
- }
- new Thread(new Runnable() {
-
- @Override
- @SuppressWarnings("synthetic-access")
- public void run() {
- if (!preferences.isSoneRescueMode()) {
- return;
- }
- logger.log(Level.INFO, "Trying to restore Sone from Freenet…");
- coreListenerManager.fireRescuingSone(sone);
- lockSone(sone);
- long edition = sone.getLatestEdition();
- /* find the latest edition the node knows about. */
- Pair<FreenetURI, FetchResult> currentUri = freenetInterface.fetchUri(sone.getRequestUri());
- if (currentUri != null) {
- long currentEdition = currentUri.getLeft().getEdition();
- if (currentEdition > edition) {
- edition = currentEdition;
- }
- }
- while (!stopped && (edition >= 0) && preferences.isSoneRescueMode()) {
- logger.log(Level.FINE, "Downloading edition " + edition + "…");
- soneDownloader.fetchSone(sone, sone.getRequestUri().setKeyType("SSK").setDocName("Sone-" + edition));
- --edition;
- }
- logger.log(Level.INFO, "Finished restoring Sone from Freenet, starting Inserter…");
- saveSone(sone);
- coreListenerManager.fireRescuedSone(sone);
- soneInserter.start();
- }
-
- }, "Sone Downloader").start();
+ soneInserter.start();
return sone;
}
}
Sone sone = addLocalSone(ownIdentity);
sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
sone.addFriend("nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI");
- saveSone(sone);
+ touchConfiguration();
return sone;
}
for (Sone localSone : getLocalSones()) {
if (localSone.getOptions().getBooleanOption("AutoFollow").get()) {
localSone.addFriend(sone.getId());
- saveSone(localSone);
+ touchConfiguration();
}
}
}
}
/**
- * 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 });
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");
if (newSones.remove(sone.getId())) {
knownSones.add(sone.getId());
coreListenerManager.fireMarkSoneKnown(sone);
- saveConfiguration();
+ touchConfiguration();
}
}
}
/* initialize options. */
sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
+ sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption<Boolean>(false));
/* load Sone. */
String sonePrefix = "Sone/" + sone.getId();
/* 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) {
}
/**
- * 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
coreListenerManager.fireNewPostFound(post);
}
sone.addPost(post);
- saveSone(sone);
+ touchConfiguration();
localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() {
/**
markPostKnown(post);
knownPosts.remove(post.getId());
}
- saveSone(post.getSone());
+ touchConfiguration();
}
/**
if (newPosts.remove(post.getId())) {
knownPosts.add(post.getId());
coreListenerManager.fireMarkPostKnown(post);
- saveConfiguration();
+ touchConfiguration();
}
}
}
coreListenerManager.fireNewReplyFound(reply);
}
sone.addReply(reply);
- saveSone(sone);
+ touchConfiguration();
localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() {
/**
knownReplies.remove(reply.getId());
}
sone.removeReply(reply);
- saveSone(sone);
+ touchConfiguration();
}
/**
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…");
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());
}
}
- //
- // PRIVATE METHODS
- //
-
/**
* Loads the configuration.
*/
}));
options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(10, new IntegerRangeValidator(1, Integer.MAX_VALUE)));
+ options.addIntegerOption("CharactersPerPost", new DefaultOption<Integer>(200, new OrValidator<Integer>(new IntegerRangeValidator(50, Integer.MAX_VALUE), new EqualityValidator<Integer>(-1))));
options.addBooleanOption("RequireFullAccess", new DefaultOption<Boolean>(false));
options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75, new IntegerRangeValidator(0, 100)));
options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-25, new IntegerRangeValidator(-100, 100)));
loadConfigurationValue("InsertionDelay");
loadConfigurationValue("PostsPerPage");
+ loadConfigurationValue("CharactersPerPost");
options.getBooleanOption("RequireFullAccess").set(configuration.getBooleanValue("Option/RequireFullAccess").getValue(null));
loadConfigurationValue("PositiveTrust");
loadConfigurationValue("NegativeTrust");
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.
}
/**
+ * Returns the number of characters per post, or <code>-1</code> if the
+ * posts should not be cut off.
+ *
+ * @return The numbers of characters per post
+ */
+ public int getCharactersPerPost() {
+ return options.getIntegerOption("CharactersPerPost").get();
+ }
+
+ /**
+ * Validates the number of characters per post.
+ *
+ * @param charactersPerPost
+ * The number of characters per post
+ * @return {@code true} if the number of characters per post was valid,
+ * {@code false} otherwise
+ */
+ public boolean validateCharactersPerPost(Integer charactersPerPost) {
+ return options.getIntegerOption("CharactersPerPost").validate(charactersPerPost);
+ }
+
+ /**
+ * Sets the number of characters per post.
+ *
+ * @param charactersPerPost
+ * The number of characters per post, or <code>-1</code> to
+ * not cut off the posts
+ * @return This preferences objects
+ */
+ public Preferences setCharactersPerPost(Integer charactersPerPost) {
+ options.getIntegerOption("CharactersPerPost").set(charactersPerPost);
+ return this;
+ }
+
+ /**
* Returns whether Sone requires full access to be even visible.
*
* @return {@code true} if Sone requires full access, {@code false}
}
/**
- * 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!
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
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
//
/**
- * Notifies all listeners that the given Sone is now being rescued.
- *
- * @see CoreListener#rescuingSone(Sone)
- * @param sone
- * The Sone that is being rescued
- */
- void fireRescuingSone(Sone sone) {
- for (CoreListener coreListener : getListeners()) {
- coreListener.rescuingSone(sone);
- }
- }
-
- /**
- * Notifies all listeners that the given Sone was rescued.
- *
- * @see CoreListener#rescuedSone(Sone)
- * @param sone
- * The Sone that was rescued
- */
- void fireRescuedSone(Sone sone) {
- for (CoreListener coreListener : getListeners()) {
- coreListener.rescuedSone(sone);
- }
- }
-
- /**
* Notifies all listeners that a new Sone has been discovered.
*
* @see CoreListener#newSoneFound(Sone)
}
/**
+ * Notifies all listeners that the insert of the given Sone has started.
+ *
+ * @see SoneInsertListener#insertStarted(Sone)
+ * @param sone
+ * The Sone being inserted
+ */
+ void fireSoneInserting(Sone sone) {
+ for (CoreListener coreListener : getListeners()) {
+ coreListener.soneInserting(sone);
+ }
+ }
+
+ /**
+ * Notifies all listeners that the insert of the given Sone has finished
+ * successfully.
+ *
+ * @see SoneInsertListener#insertFinished(Sone, long)
+ * @param sone
+ * The Sone that was inserted
+ * @param insertDuration
+ * The insert duration (in milliseconds)
+ */
+ void fireSoneInserted(Sone sone, long insertDuration) {
+ for (CoreListener coreListener : getListeners()) {
+ coreListener.soneInserted(sone, insertDuration);
+ }
+ }
+
+ /**
+ * Notifies all listeners that the insert of the given Sone was aborted.
+ *
+ * @see SoneInsertListener#insertStarted(Sone)
+ * @param sone
+ * The Sone being inserted
+ * @param cause
+ * The cause for the abortion (may be {@code null}
+ */
+ void fireSoneInsertAborted(Sone sone, Throwable cause) {
+ for (CoreListener coreListener : getListeners()) {
+ coreListener.soneInsertAborted(sone, cause);
+ }
+ }
+
+ /**
* Notifies all listeners that a new version was found.
*
* @see CoreListener#updateFound(Version, long, long)
import java.util.logging.Level;
import java.util.logging.Logger;
-import net.pterodactylus.sone.core.Core.Preferences;
import net.pterodactylus.sone.core.Core.SoneStatus;
import net.pterodactylus.sone.data.Client;
import net.pterodactylus.sone.data.Post;
/**
* Fetches the updated Sone. This method can be used to fetch a Sone from a
- * specific URI (which happens when {@link Preferences#isSoneRescueMode()
- * „Sone rescue mode“} is active).
+ * specific URI.
*
* @param sone
* The Sone to fetch
* The URI to fetch the Sone from
*/
public void fetchSone(Sone sone, FreenetURI soneUri) {
+ fetchSone(sone, soneUri, false);
+ }
+
+ /**
+ * Fetches the Sone from the given URI.
+ *
+ * @param sone
+ * The Sone to fetch
+ * @param soneUri
+ * The URI of the Sone to fetch
+ * @param fetchOnly
+ * {@code true} to only fetch and parse the Sone, {@code false}
+ * to {@link Core#updateSone(Sone) update} it in the core
+ * @return The downloaded Sone, or {@code null} if the Sone could not be
+ * downloaded
+ */
+ public Sone fetchSone(Sone sone, FreenetURI soneUri, boolean fetchOnly) {
logger.log(Level.FINE, "Starting fetch for Sone “%s” from %s…", new Object[] { sone, soneUri });
FreenetURI requestUri = soneUri.setMetaString(new String[] { "sone.xml" });
core.setSoneStatus(sone, SoneStatus.downloading);
Pair<FreenetURI, FetchResult> fetchResults = freenetInterface.fetchUri(requestUri);
if (fetchResults == null) {
/* TODO - mark Sone as bad. */
- return;
+ return null;
}
logger.log(Level.FINEST, "Got %d bytes back.", fetchResults.getRight().size());
Sone parsedSone = parseSone(sone, fetchResults.getRight(), fetchResults.getLeft());
if (parsedSone != null) {
- addSone(parsedSone);
- core.updateSone(parsedSone);
+ if (!fetchOnly) {
+ core.updateSone(parsedSone);
+ addSone(parsedSone);
+ }
}
+ return parsedSone;
} finally {
core.setSoneStatus(sone, (sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
}
--- /dev/null
+/*
+ * Sone - SoneInsertListener.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core;
+
+import java.util.EventListener;
+
+import net.pterodactylus.sone.data.Sone;
+
+/**
+ * Listener for Sone insert events.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface SoneInsertListener extends EventListener {
+
+ /**
+ * Notifies a listener that a Sone is now being inserted.
+ *
+ * @param sone
+ * The Sone being inserted
+ */
+ public void insertStarted(Sone sone);
+
+ /**
+ * Notifies a listener that a Sone has been successfully inserted.
+ *
+ * @param sone
+ * The Sone that was inserted
+ * @param insertDuration
+ * The duration of the insert (in milliseconds)
+ */
+ public void insertFinished(Sone sone, long insertDuration);
+
+ /**
+ * Notifies a listener that the insert of the given Sone was aborted.
+ *
+ * @param sone
+ * The Sone that was being inserted
+ * @param cause
+ * The cause of the abortion (may be {@code null})
+ */
+ public void insertAborted(Sone sone, Throwable cause);
+
+}
--- /dev/null
+/*
+ * Sone - SoneInsertListenerManager.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core;
+
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.event.AbstractListenerManager;
+
+/**
+ * Manager for {@link SoneInsertListener}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SoneInsertListenerManager extends AbstractListenerManager<Sone, SoneInsertListener> {
+
+ /**
+ * Creates a new Sone insert listener manager.
+ *
+ * @param sone
+ * The sone being inserted
+ */
+ public SoneInsertListenerManager(Sone sone) {
+ super(sone);
+ }
+
+ //
+ // ACTIONS
+ //
+
+ /**
+ * Notifies all listeners that the insert of the Sone has started.
+ *
+ * @see SoneInsertListener#insertStarted(Sone)
+ */
+ void fireInsertStarted() {
+ for (SoneInsertListener soneInsertListener : getListeners()) {
+ soneInsertListener.insertStarted(getSource());
+ }
+ }
+
+ /**
+ * Notifies all listeners that the insert of the Sone has finished
+ * successfully.
+ *
+ * @see SoneInsertListener#insertFinished(Sone, long)
+ * @param insertDuration
+ * The insert duration (in milliseconds)
+ */
+ void fireInsertFinished(long insertDuration) {
+ for (SoneInsertListener soneInsertListener : getListeners()) {
+ soneInsertListener.insertFinished(getSource(), insertDuration);
+ }
+ }
+
+ /**
+ * Notifies all listeners that the insert of the Sone was aborted.
+ *
+ * @see SoneInsertListener#insertAborted(Sone, Throwable)
+ * @param cause
+ * The cause of the abortion (may be {@code null}
+ */
+ void fireInsertAborted(Throwable cause) {
+ for (SoneInsertListener soneInsertListener : getListeners()) {
+ soneInsertListener.insertAborted(getSource(), cause);
+ }
+ }
+
+}
/** 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;
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);
}
//
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. */
}
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);
}
TemplateContext templateContext = templateContextFactory.createTemplateContext();
+ templateContext.set("core", core);
templateContext.set("currentSone", soneProperties);
templateContext.set("currentEdition", core.getUpdateChecker().getLatestEdition());
templateContext.set("version", SonePlugin.VERSION);
--- /dev/null
+/*
+ * Sone - SoneRescuer.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core;
+
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.service.AbstractService;
+import freenet.keys.FreenetURI;
+
+/**
+ * The Sone rescuer downloads older editions of a Sone and updates the currently
+ * stored Sone with it.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SoneRescuer extends AbstractService {
+
+ /** The core. */
+ private final Core core;
+
+ /** The Sone downloader. */
+ private final SoneDownloader soneDownloader;
+
+ /** The Sone being rescued. */
+ private final Sone sone;
+
+ /** Whether the rescuer is currently fetching a Sone. */
+ private volatile boolean fetching;
+
+ /** The currently tried edition. */
+ private volatile long currentEdition;
+
+ /** Whether the last fetch was successful. */
+ private volatile boolean lastFetchSuccessful = true;
+
+ /**
+ * Creates a new Sone rescuer.
+ *
+ * @param core
+ * The core
+ * @param soneDownloader
+ * The Sone downloader
+ * @param sone
+ * The Sone to rescue
+ */
+ public SoneRescuer(Core core, SoneDownloader soneDownloader, Sone sone) {
+ super("Sone Rescuer for " + sone.getName());
+ this.core = core;
+ this.soneDownloader = soneDownloader;
+ this.sone = sone;
+ currentEdition = sone.getRequestUri().getEdition();
+ }
+
+ //
+ // ACCESSORS
+ //
+
+ /**
+ * Returns whether the Sone rescuer is currently fetching a Sone.
+ *
+ * @return {@code true} if the Sone rescuer is currently fetching a Sone
+ */
+ public boolean isFetching() {
+ return fetching;
+ }
+
+ /**
+ * Returns the edition that is currently being downloaded.
+ *
+ * @return The edition that is currently being downloaded
+ */
+ public long getCurrentEdition() {
+ return currentEdition;
+ }
+
+ /**
+ * Returns whether the Sone rescuer can download a next edition.
+ *
+ * @return {@code true} if the Sone rescuer can download a next edition,
+ * {@code false} if the last edition was already tried
+ */
+ public boolean hasNextEdition() {
+ return currentEdition > 0;
+ }
+
+ /**
+ * Returns the next edition the Sone rescuer can download.
+ *
+ * @return The next edition the Sone rescuer can download
+ */
+ public long getNextEdition() {
+ return currentEdition - 1;
+ }
+
+ /**
+ * Sets the edition to rescue.
+ *
+ * @param edition
+ * The edition to rescue
+ * @return This Sone rescuer
+ */
+ public SoneRescuer setEdition(long edition) {
+ currentEdition = edition;
+ return this;
+ }
+
+ /**
+ * Sets whether the last fetch was successful.
+ *
+ * @return {@code true} if the last fetch was successful, {@code false}
+ * otherwise
+ */
+ public boolean isLastFetchSuccessful() {
+ return lastFetchSuccessful;
+ }
+
+ //
+ // ACTIONS
+ //
+
+ /**
+ * Starts the next fetch. If you want to fetch a different edition than “the
+ * next older one,” remember to call {@link #setEdition(long)} before
+ * calling this method.
+ */
+ public void startNextFetch() {
+ fetching = true;
+ notifySyncObject();
+ }
+
+ //
+ // SERVICE METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void serviceRun() {
+ while (!shouldStop()) {
+ while (!shouldStop() && !fetching) {
+ sleep();
+ }
+ if (fetching) {
+ core.lockSone(sone);
+ FreenetURI soneUri = sone.getRequestUri().setKeyType("SSK").setDocName("Sone-" + currentEdition).setMetaString(new String[] { "sone.xml" });
+ System.out.println("URI: " + soneUri);
+ Sone fetchedSone = soneDownloader.fetchSone(sone, soneUri, true);
+ System.out.println("Sone: " + fetchedSone);
+ lastFetchSuccessful = (fetchedSone != null);
+ if (lastFetchSuccessful) {
+ core.updateSone(fetchedSone, true);
+ }
+ fetching = false;
+ }
+ }
+ }
+
+}
};
+ /**
+ * Comparator that sorts Sones by last activity (least recent active first).
+ */
+ public static final Comparator<Sone> LAST_ACTIVITY_COMPARATOR = new Comparator<Sone>() {
+
+ @Override
+ public int compare(Sone firstSone, Sone secondSone) {
+ return (int) Math.min(Integer.MAX_VALUE, Math.max(Integer.MIN_VALUE, secondSone.getTime() - firstSone.getTime()));
+ }
+ };
+
+ /** Comparator that sorts Sones by numbers of posts (descending). */
+ public static final Comparator<Sone> POST_COUNT_COMPARATOR = new Comparator<Sone>() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int compare(Sone leftSone, Sone rightSone) {
+ return (leftSone.getPosts().size() != rightSone.getPosts().size()) ? (rightSone.getPosts().size() - leftSone.getPosts().size()) : (rightSone.getReplies().size() - leftSone.getReplies().size());
+ }
+ };
+
/** Filter to remove Sones that have not been downloaded. */
public static final Filter<Sone> EMPTY_SONE_FILTER = new Filter<Sone>() {
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;
*/
@Override
public String format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
- return l10n.getString(String.valueOf(data));
+ if (parameters.isEmpty()) {
+ return l10n.getString(String.valueOf(data));
+ }
+ List<Object> parameterValues = new ArrayList<Object>();
+ int parameterIndex = 0;
+ while (parameters.containsKey(String.valueOf(parameterIndex))) {
+ Object value = parameters.get(String.valueOf(parameterIndex));
+ if (((String) value).startsWith("=")) {
+ value = ((String) value).substring(1);
+ } else {
+ value = templateContext.get((String) value);
+ }
+ parameterValues.add(value);
+ ++parameterIndex;
+ }
+ return new MessageFormat(l10n.getString(String.valueOf(data)), new Locale(l10n.getSelectedLanguage().shortCode)).format(parameterValues.toArray());
}
-
}
}
/** 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);
List<Notification> filteredNotifications = new ArrayList<Notification>();
for (Notification notification : notifications) {
if (notification.getId().equals("new-post-notification")) {
- ListNotification<Post> filteredNotification = filterNewPostNotification((ListNotification<Post>) notification, currentSone);
+ ListNotification<Post> filteredNotification = filterNewPostNotification((ListNotification<Post>) notification, currentSone, true);
if (filteredNotification != null) {
filteredNotifications.add(filteredNotification);
}
if (filteredNotification != null) {
filteredNotifications.add(filteredNotification);
}
+ } else if (notification.getId().equals("mention-notification")) {
+ ListNotification<Post> filteredNotification = filterNewPostNotification((ListNotification<Post>) notification, null, false);
+ if (filteredNotification != null) {
+ filteredNotifications.add(filteredNotification);
+ }
} else {
filteredNotifications.add(notification);
}
/**
* Filters the new posts of the given notification. If {@code currentSone}
- * is {@code null}, {@code null} is returned and the notification is
- * subsequently removed. Otherwise only posts that are posted by friend
- * Sones of the given Sone are retained; all other posts are removed.
+ * is {@code null} and {@code soneRequired} is {@code true}, {@code null} is
+ * returned and the notification is subsequently removed. Otherwise only
+ * posts that are posted by friend Sones of the given Sone are retained; all
+ * other posts are removed.
*
* @param newPostNotification
* The new-post notification
* @param currentSone
* The current Sone, or {@code null} if not logged in
+ * @param soneRequired
+ * Whether a non-{@code null} Sone in {@code currentSone} is
+ * required
* @return The filtered new-post notification, or {@code null} if the
* notification should be removed
*/
- public static ListNotification<Post> filterNewPostNotification(ListNotification<Post> newPostNotification, Sone currentSone) {
- if (currentSone == null) {
+ public static ListNotification<Post> filterNewPostNotification(ListNotification<Post> newPostNotification, Sone currentSone, boolean soneRequired) {
+ if (soneRequired && (currentSone == null)) {
return null;
}
List<Post> newPosts = new ArrayList<Post>();
}
ListNotification<Post> filteredNotification = new ListNotification<Post>(newPostNotification);
filteredNotification.setElements(newPosts);
+ filteredNotification.setLastUpdateTime(newPostNotification.getLastUpdatedTime());
return filteredNotification;
}
}
ListNotification<Reply> filteredNotification = new ListNotification<Reply>(newReplyNotification);
filteredNotification.setElements(newReplies);
+ filteredNotification.setLastUpdateTime(newReplyNotification.getLastUpdatedTime());
return filteredNotification;
}
* considered visible if one of the following statements is true:
* <ul>
* <li>The post does not have a Sone.</li>
+ * <li>The post’s {@link Post#getTime() time} is in the future.</li>
+ * </ul>
+ * <p>
+ * If {@code post} is not {@code null} more checks are performed, and the
+ * post will be invisible if:
+ * </p>
+ * <ul>
* <li>The Sone of the post is not the given Sone, the given Sone does not
* follow the post’s Sone, and the given Sone is not the recipient of the
* post.</li>
* Sone.</li>
* <li>The given Sone has not explicitely assigned negative trust to the
* post’s Sone but the implicit trust is negative.</li>
- * <li>The post’s {@link Post#getTime() time} is in the future.</li>
* </ul>
* If none of these statements is true the post is considered visible.
*
* @param sone
- * The Sone that checks for a post’s visibility
+ * The Sone that checks for a post’s visibility (may be
+ * {@code null} to skip Sone-specific checks, such as trust)
* @param post
* The post to check for visibility
* @return {@code true} if the post is considered visible, {@code false}
* otherwise
*/
public static boolean isPostVisible(Sone sone, Post post) {
- Validation.begin().isNotNull("Sone", sone).isNotNull("Post", post).check().isNotNull("Sone’s Identity", sone.getIdentity()).check().isInstanceOf("Sone’s Identity", sone.getIdentity(), OwnIdentity.class).check();
+ Validation.begin().isNotNull("Post", post).check();
Sone postSone = post.getSone();
if (postSone == null) {
return false;
}
- Trust trust = postSone.getIdentity().getTrust((OwnIdentity) sone.getIdentity());
- if (trust != null) {
- if ((trust.getExplicit() != null) && (trust.getExplicit() < 0)) {
+ if (sone != null) {
+ Trust trust = postSone.getIdentity().getTrust((OwnIdentity) sone.getIdentity());
+ if (trust != null) {
+ if ((trust.getExplicit() != null) && (trust.getExplicit() < 0)) {
+ return false;
+ }
+ if ((trust.getExplicit() == null) && (trust.getImplicit() != null) && (trust.getImplicit() < 0)) {
+ return false;
+ }
+ } else {
return false;
}
- if ((trust.getExplicit() == null) && (trust.getImplicit() != null) && (trust.getImplicit() < 0)) {
+ if ((!postSone.equals(sone)) && !sone.hasFriend(postSone.getId()) && !sone.equals(post.getRecipient())) {
return false;
}
- } else {
- return false;
- }
- if ((!postSone.equals(sone)) && !sone.hasFriend(postSone.getId()) && !sone.equals(post.getRecipient())) {
- return false;
}
if (post.getTime() > System.currentTimeMillis()) {
return false;
* 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;
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;
@Override
public Object format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
String text = String.valueOf(data);
+ int length = -1;
+ try {
+ length = Integer.parseInt(parameters.get("length"));
+ } catch (NumberFormatException nfe1) {
+ /* ignore. */
+ }
+ if ((length == -1) && (parameters.get("length") != null)) {
+ try {
+ length = Integer.parseInt(String.valueOf(templateContext.get(parameters.get("length"))));
+ } catch (NumberFormatException nfe1) {
+ /* ignore. */
+ }
+ }
String soneKey = parameters.get("sone");
if (soneKey == null) {
soneKey = "sone";
SoneTextParserContext context = new SoneTextParserContext(request, sone);
StringWriter parsedTextWriter = new StringWriter();
try {
- render(parsedTextWriter, soneTextParser.parse(context, new StringReader(text)));
+ Iterable<Part> parts = soneTextParser.parse(context, new StringReader(text));
+ if (length > -1) {
+ List<Part> shortenedParts = new ArrayList<Part>();
+ for (Part part : parts) {
+ if (part instanceof PlainTextPart) {
+ String longText = ((PlainTextPart) part).getText();
+ if (length >= longText.length()) {
+ shortenedParts.add(part);
+ } else {
+ shortenedParts.add(new PlainTextPart(longText.substring(0, length) + "…"));
+ }
+ length -= longText.length();
+ } else if (part instanceof LinkPart) {
+ shortenedParts.add(part);
+ length -= ((LinkPart) part).getText().length();
+ } else {
+ shortenedParts.add(part);
+ }
+ if (length <= 0) {
+ break;
+ }
+ }
+ parts = shortenedParts;
+ }
+ render(parsedTextWriter, parts);
} catch (IOException ioe1) {
/* no exceptions in a StringReader or StringWriter, ignore. */
}
* @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;
}
}
} 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) {
--- /dev/null
+/*
+ * Sone - UniqueElementFilter.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.template;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import net.pterodactylus.util.template.Filter;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Filter that reduces a collection to a {@link Set}, removing duplicates.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UniqueElementFilter implements Filter {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Object format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
+ if (!(data instanceof Collection<?>)) {
+ return data;
+ }
+ return new HashSet<Object>((Collection<?>) data);
+ }
+
+}
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)));
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)));
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;
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);
}
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();
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);
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);
}
}
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;
@Override
protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
+ String sortField = request.getHttpRequest().getParam("sort");
+ String sortOrder = request.getHttpRequest().getParam("order");
+ String followedSones = request.getHttpRequest().getParam("followedSones");
+ templateContext.set("sort", (sortField != null) ? sortField : "name");
+ templateContext.set("order", (sortOrder != null) ? sortOrder : "asc");
+ templateContext.set("followedSones", followedSones);
+ final Sone currentSone = getCurrentSone(request.getToadletContext(), false);
List<Sone> knownSones = Filters.filteredList(new ArrayList<Sone>(webInterface.getCore().getSones()), Sone.EMPTY_SONE_FILTER);
- Collections.sort(knownSones, Sone.NICE_NAME_COMPARATOR);
+ if ((currentSone != null) && "show-only".equals(followedSones)) {
+ knownSones = Filters.filteredList(knownSones, new Filter<Sone>() {
+
+ @Override
+ public boolean filterObject(Sone sone) {
+ return currentSone.hasFriend(sone.getId());
+ }
+ });
+ } else if ((currentSone != null) && "hide".equals(followedSones)) {
+ knownSones = Filters.filteredList(knownSones, new Filter<Sone>() {
+
+ @Override
+ public boolean filterObject(Sone sone) {
+ return !currentSone.hasFriend(sone.getId());
+ }
+ });
+ }
+ if ("activity".equals(sortField)) {
+ if ("asc".equals(sortOrder)) {
+ Collections.sort(knownSones, new ReverseComparator<Sone>(Sone.LAST_ACTIVITY_COMPARATOR));
+ } else {
+ Collections.sort(knownSones, Sone.LAST_ACTIVITY_COMPARATOR);
+ }
+ } else if ("posts".equals(sortField)) {
+ if ("asc".equals(sortOrder)) {
+ Collections.sort(knownSones, new ReverseComparator<Sone>(Sone.POST_COUNT_COMPARATOR));
+ } else {
+ Collections.sort(knownSones, Sone.POST_COUNT_COMPARATOR);
+ }
+ } else {
+ if ("desc".equals(sortOrder)) {
+ Collections.sort(knownSones, new ReverseComparator<Sone>(Sone.NICE_NAME_COMPARATOR));
+ } else {
+ Collections.sort(knownSones, Sone.NICE_NAME_COMPARATOR);
+ }
+ }
Pagination<Sone> sonePagination = new Pagination<Sone>(knownSones, 25).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
templateContext.set("pagination", sonePagination);
templateContext.set("knownSones", sonePagination.getItems());
}
-
}
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)) {
} 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));
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());
}
}
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());
}
--- /dev/null
+/*
+ * Sone - RescuePage.java - Copyright © 2011 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import net.pterodactylus.sone.core.SoneRescuer;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.number.Numbers;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Page that lets the user control the rescue mode for a Sone.
+ *
+ * @see SoneRescuer
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class RescuePage extends SoneTemplatePage {
+
+ /**
+ * Creates a new rescue page.
+ *
+ * @param template
+ * The template to render
+ * @param webInterface
+ * The Sone web interface
+ */
+ public RescuePage(Template template, WebInterface webInterface) {
+ super("rescue.html", template, "Page.Rescue.Title", webInterface, true);
+ }
+
+ //
+ // SONETEMPLATEPAGE METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ super.processTemplate(request, templateContext);
+ Sone currentSone = getCurrentSone(request.getToadletContext(), false);
+ SoneRescuer soneRescuer = webInterface.getCore().getSoneRescuer(currentSone);
+ if (request.getMethod() == Method.POST) {
+ if ("true".equals(request.getHttpRequest().getPartAsStringFailsafe("fetch", 4))) {
+ long edition = Numbers.safeParseLong(request.getHttpRequest().getPartAsStringFailsafe("edition", 8), -1L);
+ if (edition > -1) {
+ soneRescuer.setEdition(edition);
+ }
+ soneRescuer.startNextFetch();
+ }
+ throw new RedirectException("rescue.html");
+ }
+ templateContext.set("soneRescuer", soneRescuer);
+ }
+
+}
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;
/** The logger. */
private static final Logger logger = Logging.getLogger(SearchPage.class);
+ /** Short-term cache. */
+ private final Cache<List<Phrase>, Set<Hit<Post>>> hitCache = new MemoryCache<List<Phrase>, Set<Hit<Post>>>(new ValueRetriever<List<Phrase>, Set<Hit<Post>>>() {
+
+ @SuppressWarnings("synthetic-access")
+ public CacheItem<Set<Hit<Post>>> retrieve(List<Phrase> phrases) throws CacheException {
+ Set<Post> posts = new HashSet<Post>();
+ for (Sone sone : webInterface.getCore().getSones()) {
+ posts.addAll(sone.getPosts());
+ }
+ return new DefaultCacheItem<Set<Hit<Post>>>(getHits(Filters.filteredSet(posts, Post.FUTURE_POSTS_FILTER), phrases, new PostStringGenerator()));
+ }
+
+ }, new TimedMap<List<Phrase>, CacheItem<Set<Hit<Post>>>>(300000));
+
/**
* Creates a new search page.
*
}
List<Phrase> phrases = parseSearchPhrases(query);
+ if (phrases.isEmpty()) {
+ throw new RedirectException("index.html");
+ }
Set<Sone> sones = webInterface.getCore().getSones();
Set<Hit<Sone>> soneHits = getHits(sones, phrases, SoneStringGenerator.COMPLETE_GENERATOR);
- Set<Post> posts = new HashSet<Post>();
- for (Sone sone : sones) {
- posts.addAll(sone.getPosts());
+ Set<Hit<Post>> postHits;
+ try {
+ postHits = hitCache.get(phrases);
+ } catch (CacheException ce1) {
+ /* should never happen. */
+ logger.log(Level.SEVERE, "Could not get search results from cache!", ce1);
+ postHits = Collections.emptySet();
}
- @SuppressWarnings("synthetic-access")
- Set<Hit<Post>> postHits = getHits(Filters.filteredSet(posts, Post.FUTURE_POSTS_FILTER), phrases, new PostStringGenerator());
/* now filter. */
soneHits = Filters.filteredSet(soneHits, Hit.POSITIVE_FILTER);
List<Phrase> phrases = new ArrayList<Phrase>();
for (String phrase : parsedPhrases) {
if (phrase.startsWith("+")) {
- phrases.add(new Phrase(phrase.substring(1), Phrase.Optionality.REQUIRED));
+ if (phrase.length() > 1) {
+ phrases.add(new Phrase(phrase.substring(1), Phrase.Optionality.REQUIRED));
+ } else {
+ phrases.add(new Phrase("+", Phrase.Optionality.OPTIONAL));
+ }
} else if (phrase.startsWith("-")) {
- phrases.add(new Phrase(phrase.substring(1), Phrase.Optionality.FORBIDDEN));
+ if (phrase.length() > 1) {
+ phrases.add(new Phrase(phrase.substring(1), Phrase.Optionality.FORBIDDEN));
+ } else {
+ phrases.add(new Phrase("-", Phrase.Optionality.OPTIONAL));
+ }
+ } else {
+ phrases.add(new Phrase(phrase, Phrase.Optionality.OPTIONAL));
}
- phrases.add(new Phrase(phrase, Phrase.Optionality.OPTIONAL));
}
return phrases;
}
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);
+ }
+
}
/**
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);
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);
}
}
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;
/** The “you have been mentioned” notification. */
private final ListNotification<Post> mentionNotification;
- /** The “rescuing Sone” notification. */
- private final ListNotification<Sone> rescuingSonesNotification;
-
- /** The “Sone rescued” notification. */
- private final ListNotification<Sone> sonesRescuedNotification;
+ /** Notifications for sone inserts. */
+ private final Map<Sone, TemplateNotification> soneInsertNotifications = new HashMap<Sone, TemplateNotification>();
/** Sone locked notification ticker objects. */
private final Map<Sone, Object> lockedSonesTickerObjects = Collections.synchronizedMap(new HashMap<Sone, Object>());
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);
Template mentionNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/mentionNotification.html"));
mentionNotification = new ListNotification<Post>("mention-notification", "posts", mentionNotificationTemplate, false);
- Template rescuingSonesTemplate = TemplateParser.parse(createReader("/templates/notify/rescuingSonesNotification.html"));
- rescuingSonesNotification = new ListNotification<Sone>("sones-being-rescued-notification", "sones", rescuingSonesTemplate);
-
- Template sonesRescuedTemplate = TemplateParser.parse(createReader("/templates/notify/sonesRescuedNotification.html"));
- sonesRescuedNotification = new ListNotification<Sone>("sones-rescued-notification", "sones", sonesRescuedTemplate);
-
Template lockedSonesTemplate = TemplateParser.parse(createReader("/templates/notify/lockedSonesNotification.html"));
lockedSonesNotification = new ListNotification<Sone>("sones-locked-notification", "sones", lockedSonesTemplate);
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"));
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)));
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}
*/
@Override
public void newReplyFound(Reply reply) {
- if (reply.getPost().getSone() == null) {
- return;
- }
boolean isLocal = getCore().isLocalSone(reply.getSone());
if (isLocal) {
localReplyNotification.add(reply);
* {@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);
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;
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());
}
}
profile.removeField(field);
currentSone.setProfile(profile);
- webInterface.getCore().saveSone(currentSone);
+ webInterface.getCore().touchConfiguration();
return createSuccessJsonObject().put("field", new JsonObject().put("id", field.getId()));
}
return createErrorJsonObject("auth-required");
}
currentSone.addFriend(soneId);
- webInterface.getCore().saveSone(currentSone);
+ webInterface.getCore().touchConfiguration();
return createSuccessJsonObject();
}
for (String notificationId : notificationIds) {
Notification notification = webInterface.getNotifications().getNotification(notificationId);
if ("new-post-notification".equals(notificationId)) {
- notification = ListNotificationFilters.filterNewPostNotification((ListNotification<Post>) notification, currentSone);
+ notification = ListNotificationFilters.filterNewPostNotification((ListNotification<Post>) notification, currentSone, false);
} else if ("new-reply-notification".equals(notificationId)) {
notification = ListNotificationFilters.filterNewReplyNotification((ListNotification<Reply>) notification, currentSone);
+ } else if ("mention-notification".equals(notificationId)) {
+ notification = ListNotificationFilters.filterNewPostNotification((ListNotification<Post>) notification, currentSone, false);
}
if (notification == null) {
// TODO - show error
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);
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);
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);
@Override
public boolean filterObject(Reply reply) {
- return reply.getPost() != null;
+ return (reply.getPost() != null) && (reply.getPost().getSone() != null);
}
});
JsonArray jsonReplies = new JsonArray();
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;
}
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())));
//
/**
- * 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);
}
//
//
/**
- * 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) {
}
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");
}
return createErrorJsonObject("not-possible");
}
currentSone.setProfile(profile);
- webInterface.getCore().saveSone(currentSone);
+ webInterface.getCore().touchConfiguration();
return createSuccessJsonObject();
}
return createErrorJsonObject("auth-required");
}
currentSone.removeFriend(soneId);
- webInterface.getCore().saveSone(currentSone);
+ webInterface.getCore().touchConfiguration();
return createSuccessJsonObject();
}
}
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");
}
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
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.
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.
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
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
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!
View.Sone.Label.LastUpdate=Last update:
View.Sone.Text.UnknownDate=unknown
+View.Sone.Stats.Posts={0,number} {0,choice,0#posts|1#post|1<posts}
+View.Sone.Stats.Replies={0,number} {0,choice,0#replies|1#reply|1<replies}
View.Sone.Button.UnlockSone=unlock
View.Sone.Button.UnlockSone.Tooltip=Allow this Sone to be inserted now
View.Sone.Button.LockSone=lock
View.Sone.Status.Inserting=This Sone is currently being inserted.
View.Post.UnknownAuthor=(unknown)
+View.Post.WebOfTrustLink=web of trust profile
View.Post.Permalink=link post
View.Post.PermalinkAuthor=link author
View.Post.Bookmarks.PostIsBookmarked=Post is bookmarked, click to remove from bookmarks
View.Post.UnlikeLink=Unlike
View.Post.ShowSource=Toggle Parser
View.Post.NotDownloaded=This post has not yet been downloaded, or it has been deleted.
+View.Post.ShowMore=show more
+View.Post.ShowLess=show less
View.UpdateStatus.Text.ChooseSenderIdentity=Choose the sender identity
WebInterface.DefaultText.FieldName=Field name
WebInterface.DefaultText.Option.InsertionDelay=Time to wait after a Sone is modified before insert (in seconds)
WebInterface.DefaultText.Option.PostsPerPage=Number of posts to show on a page
+WebInterface.DefaultText.Option.CharactersPerPost=Number of characters per post after which to cut the post off
WebInterface.DefaultText.Option.PositiveTrust=The positive trust to assign
WebInterface.DefaultText.Option.NegativeTrust=The negative trust to assign
WebInterface.DefaultText.Option.TrustComment=The comment to set in the web of trust
Notification.NewVersion.Text=Version {version} of the Sone plugin was found. Download it from USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/{edition}!
Notification.Mention.ShortText=You have been mentioned.
Notification.Mention.Text=You have been mentioned in the following posts:
+Notification.SoneInsert.Duration={0,number} {0,choice,0#seconds|1#second|1<seconds}
#sone #notification-area #local-post-notification, #sone #notification-area #local-reply-notification {
display: none;
-}
+}
#sone #plugin-warning {
border: solid 0.5em red;
padding: 1ex 0px;
border-bottom: solid 1px #ccc;
clear: both;
+ position: relative;
}
#sone .post.new {
border-bottom: none;
}
+#sone .post .sone-menu {
+ position: absolute;
+ top: 0;
+ left: -1ex;
+ padding: 1ex 1ex;
+ margin: -1px -1px;
+ display: none;
+ background-color: rgb(255, 255, 224);
+ border: solid 1px rgb(0, 0, 0);
+ z-index: 1;
+}
+
+#sone .post .sone-menu .avatar {
+ position: absolute;
+ margin-right: 1ex;
+}
+
+#sone .post .sone-menu .inner-menu {
+ margin-left: 64px;
+ padding-left: 1ex;
+ min-height: 64px;
+}
+
+#sone .sone-menu .follow, #sone .sone-menu .unfollow {
+ cursor: pointer;
+}
+
#sone .post > .avatar {
position: absolute;
}
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%;
}
#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;
}
}
#sone .sone .profile-link {
- display: block;
+ display: inline;
+}
+
+#sone .sone .sone-stats {
+ display: inline;
}
#sone .sone .short-request-uri {
color: red;
font-style: italic;
}
+
+#sone #sort-options {
+ margin-bottom: 1em;
+}
--- /dev/null
+/*
+ * jQuery plugin: fieldSelection - v0.1.0 - last change: 2006-12-16
+ * (c) 2006 Alex Brem <alex@0xab.cd> - http://blog.0xab.cd
+ */
+
+(function() {
+
+ var fieldSelection = {
+
+ getSelection: function() {
+
+ var e = this.jquery ? this[0] : this;
+
+ return (
+
+ /* mozilla / dom 3.0 */
+ ('selectionStart' in e && function() {
+ var l = e.selectionEnd - e.selectionStart;
+ return { start: e.selectionStart, end: e.selectionEnd, length: l, text: e.value.substr(e.selectionStart, l) };
+ }) ||
+
+ /* exploder */
+ (document.selection && function() {
+
+ e.focus();
+
+ var r = document.selection.createRange();
+ if (r == null) {
+ return { start: 0, end: e.value.length, length: 0 }
+ }
+
+ var re = e.createTextRange();
+ var rc = re.duplicate();
+ re.moveToBookmark(r.getBookmark());
+ rc.setEndPoint('EndToStart', re);
+
+ return { start: rc.text.length, end: rc.text.length + r.text.length, length: r.text.length, text: r.text };
+ }) ||
+
+ /* browser not supported */
+ function() {
+ return { start: 0, end: e.value.length, length: 0 };
+ }
+
+ )();
+
+ },
+
+ replaceSelection: function() {
+
+ var e = this.jquery ? this[0] : this;
+ var text = arguments[0] || '';
+
+ return (
+
+ /* mozilla / dom 3.0 */
+ ('selectionStart' in e && function() {
+ e.value = e.value.substr(0, e.selectionStart) + text + e.value.substr(e.selectionEnd, e.value.length);
+ return this;
+ }) ||
+
+ /* exploder */
+ (document.selection && function() {
+ e.focus();
+ document.selection.createRange().text = text;
+ return this;
+ }) ||
+
+ /* browser not supported */
+ function() {
+ e.value += text;
+ return this;
+ }
+
+ )();
+
+ }
+
+ };
+
+ jQuery.each(fieldSelection, function(i) { jQuery.fn[i] = this; });
+
+})();
}
/**
+ * 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”).
*
/* 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"));
/* 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);
}
/**
});
});
})(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;
});
});
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);
}
/**
* 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");
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);
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) {
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) {
ajaxifyNotification($(this));
});
- /* disable all permalinks. */
- $(".permalink").click(function() {
- return false;
- });
-
/* activate status polling. */
setTimeout(getStatus, 5000);
<script src="javascript/jquery-1.4.2.js" language="javascript"></script>
<script src="javascript/jquery.url.js" language="javascript"></script>
+ <script src="javascript/jquery.fieldselection.js" language="javascript"></script>
<script src="javascript/sone.js" language="javascript"></script>
<div id="offline-marker"></div>
--- /dev/null
+<div class="sone-menu <% class|css|html>">
+ <div class="sone-id hidden"><%sone.id|html></div>
+ <img class="avatar" src="/WebOfTrust/GetIdenticon?identity=<%sone.id|html>&width=64&height=64" width="64" height="64" alt="Avatar Image" />
+ <div class="inner-menu">
+ <div>
+ <a class="author" href="viewSone.html?sone=<%sone.id|html>"><%sone.niceName|html></a>
+ <span class="author-wot-link">(<a href="/WebOfTrust/ShowIdentity?id=<%sone.id|html>"><% =View.Post.WebOfTrustLink|l10n|html></a>)</span>
+ </div>
+ <div><%= View.Sone.Stats.Posts|l10n 0=sone.posts.size>, <%= View.Sone.Stats.Replies|l10n 0=sone.replies.size></div>
+ <%if !sone.local>
+ <div>
+ <a class="follow<%if sone.friend> hidden<%/if>"><%= View.Sone.Button.FollowSone|l10n|html></a>
+ <a class="unfollow<%if !sone.friend> hidden<%/if>"><%= View.Sone.Button.UnfollowSone|l10n|html></a>
+ </div>
+ <%/if>
+ </div>
+</div>
<label for="sender"><%= Page.Index.Label.Sender|l10n|html></label>
<div class="sender">
<select name="sender" title="<%= View.UpdateStatus.Text.ChooseSenderIdentity|l10n|html>">
- <%foreach localSones localSone>
+ <%foreach localSones localSone|sort>
<option value="<% localSone.id|html>"<%if localSone.current> selected="selected"<%/if>><% localSone.niceName|html></option>
<%/foreach>
</select>
<div class="post-time hidden"><% post.time|html></div>
<div class="post-author hidden"><% post.sone.id|html></div>
<div class="post-author-local hidden"><% post.sone.local></div>
- <div class="avatar">
+ <%include include/soneMenu.html class=="sone-post-menu" sone=post.sone>
+ <div class="avatar post-avatar" >
<%if post.loaded>
<img src="/WebOfTrust/GetIdenticon?identity=<% post.sone.id|html>&width=48&height=48" width="48" height="48" alt="Avatar Image" />
<%else>
<%/if>
<% post.text|html|store key=originalText text=true>
<% post.text|parse sone=post.sone|store key=parsedText text=true>
+ <% post.text|parse sone=post.sone length=core.preferences.charactersPerPost|store key=shortText text=true>
<div class="post-text raw-text<%if !raw> hidden<%/if>"><% originalText></div>
- <div class="post-text text<%if raw> hidden<%/if>"><% parsedText></div>
+ <div class="post-text text<%if raw> hidden<%/if><%if !shortText|match key=parsedText> hidden<%/if>"><% parsedText></div>
+ <div class="post-text short-text<%if raw> hidden<%/if><%if shortText|match key=parsedText> hidden<%/if>"><% shortText></div>
+ <%if !shortText|match key=parsedText><%if !raw><a class="expand-post-text" href="viewPost.html?post=<% post.id|html>&raw=true"><%= View.Post.ShowMore|l10n|html></a><%/if><%/if>
+ <%if !shortText|match key=parsedText><%if !raw><a class="shrink-post-text hidden"><%= View.Post.ShowLess|l10n|html></a><%/if><%/if>
</div>
<div class="post-status-line status-line<%if !post.loaded> hidden<%/if>">
<div class="bookmarks">
<div class="reply-time hidden"><% reply.time|html></div>
<div class="reply-author hidden"><% reply.sone.id|html></div>
<div class="reply-author-local hidden"><% reply.sone.local></div>
- <div class="avatar">
+ <%include include/soneMenu.html class=="sone-reply-menu" sone=reply.sone>
+ <div class="avatar reply-avatar">
<img src="/WebOfTrust/GetIdenticon?identity=<% reply.sone.id|html>&width=36&height=36" width="36" height="36" alt="Avatar Image" />
</div>
<div class="inner-part">
<div class="author profile-link"><a href="viewSone.html?sone=<% reply.sone.id|html>"><% reply.sone.niceName|html></a></div>
<% reply.text|html|store key=originalText text=true>
<% reply.text|parse sone=reply.sone|store key=parsedText text=true>
+ <% reply.text|parse sone=reply.sone length=core.preferences.charactersPerPost|store key=shortText text=true>
<div class="reply-text raw-text<%if !raw> hidden<%/if>"><% originalText></div>
- <div class="reply-text text<%if raw> hidden<%/if>"><% parsedText></div>
+ <div class="reply-text text<%if raw> hidden<%/if><%if !shortText|match key=parsedText> hidden<%/if>"><% parsedText></div>
+ <div class="reply-text short-text<%if raw> hidden<%/if><%if shortText|match key=parsedText> hidden<%/if>"><% shortText></div>
+ <%if !shortText|match key=parsedText><%if !raw><a class="expand-reply-text" href="viewPost.html?post=<% reply.post.id|html>&raw=true"><%= View.Post.ShowMore|l10n|html></a><%/if><%/if>
+ <%if !shortText|match key=parsedText><%if !raw><a class="shrink-reply-text hidden"><%= View.Post.ShowLess|l10n|html></a><%/if><%/if>
</div>
<div class="reply-status-line status-line">
<div class="time"><% reply.time|date format="MMM d, yyyy, HH:mm:ss"></div>
<div class="insert-marker" title="<%= View.Sone.Status.Inserting|l10n|html>">⬈</div>
<div class="idle-marker" title="<%= View.Sone.Status.Idle|l10n|html>">✔</div>
<div class="last-update"><%= View.Sone.Label.LastUpdate|l10n|html> <span class="time" title="<% sone.time|unknown|date format="MMM d, yyyy, HH:mm:ss">"><%sone.lastUpdatedText|html></span></div>
- <div class="profile-link"><a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a></div>
+ <div>
+ <div class="profile-link"><a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a></div>
+ <div class="sone-stats">(<%= View.Sone.Stats.Posts|l10n 0=sone.posts.size>, <%= View.Sone.Stats.Replies|l10n 0=sone.replies.size>)</div>
+ </div>
<div class="short-request-uri"><% sone.requestUri|substring start=4 length=43|html></div>
<div class="hidden"><% sone.blacklisted></div>
<%if sone.local>
<%include include/head.html>
<div class="page-id hidden">known-sones</div>
+
+ <script language="javascript">
+
+ $(document).ready(function() {
+ $("select[name=sort]").change(function() {
+ value = $(this).val();
+ if ((value == "activity") || (value == "posts")) {
+ $("select[name=order]").val("desc");
+ } else if (value == "name") {
+ $("select[name=order]").val("asc");
+ }
+ });
+ $("#sort-options select").change(function() {
+ this.form.submit();
+ });
+ });
+
+ </script>
<h1><%= Page.KnownSones.Page.Title|l10n|html></h1>
+
+ <div id="sort-options">
+ <form action="knownSones.html" method="get">
+ <div>
+ Sort:
+ <select name="sort">
+ <option value="name"<%if sort|match value="name"> selected="selected"<%/if>>Name</option>
+ <option value="activity"<%if sort|match value="activity"> selected="selected"<%/if>>Last activity</option>
+ <option value="posts"<%if sort|match value="posts"> selected="selected"<%/if>>Number of posts</option>
+ </select>
+ <select name="order">
+ <option value="asc"<%if order|match value="asc"> selected="selected"<%/if>>Ascending</option>
+ <option value="desc"<%if order|match value="desc"> selected="selected"<%/if>>Descending</option>
+ </select>
+ </div>
+ <%ifnull !currentSone>
+ <div>
+ Followed Sones:
+ <select name="followedSones">
+ <option value="none"></option>
+ <option value="show-only"<%if followedSones|match value="show-only"> selected="selected"<%/if>>Show only followed Sones</option>
+ <option value="hide"<%if followedSones|match value="hide"> selected="selected"<%/if>>Hide followed Sones</option>
+ </select>
+ </div>
+ <%/if>
+ <div>
+ <button type="submit">Apply</button>
+ </div>
+ </form>
+ </div>
+
+ <div>
+ <form action="followSone.html" method="post">
+ <input type="hidden" name="formPassword" value="<%formPassword|html>" />
+ <input type="hidden" name="returnPage" value="<%request.uri|html>" />
+ <input type="hidden" name="sone" value="<%foreach pagination.items sone><%if !sone.friend><%if !sone.current><%sone.id> <%/if><%/if><%/foreach>" />
+ <button type="submit"><%= Page.KnownSones.Button.FollowAllSones|l10n|html></button>
+ </form>
+ </div>
+
+ <div>
+ <form action="unfollowSone.html" method="post">
+ <input type="hidden" name="formPassword" value="<%formPassword|html>" />
+ <input type="hidden" name="returnPage" value="<%request.uri|html>" />
+ <input type="hidden" name="sone" value="<%foreach pagination.items sone><%if sone.friend><%sone.id> <%/if><%/foreach>" />
+ <button type="submit"><%= Page.KnownSones.Button.UnfollowAllSones|l10n|html></button>
+ </form>
+ </div>
<div id="known-sones">
<%= page|store key=pageParameter>
</div>
<div class="text">
<%= Notification.Mention.Text|l10n|html>
- <%foreach posts post>
+ <%foreach posts post|unique>
<div class="hidden post-id"><%post.id|html></div>
<a class="link-<% post.id|html>" href="viewPost.html?post=<% post.id|html>"><% post.sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
<%/foreach>
+++ /dev/null
-<div class="text">
- <%= Notification.SoneIsBeingRescued.Text|l10n|html>
- <%foreach sones sone>
- <a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
- <%/foreach>
-</div>
--- /dev/null
+<%if soneStatus|match value="inserting">
+ Your Sone <a href="viewSone.html?sone=<%insertSone.id|html>"><%insertSone.niceName|html></a> is now being inserted.
+<%elseif soneStatus|match value="inserted">
+ Your Sone <a href="viewSone.html?sone=<%insertSone.id|html>"><%insertSone.niceName|html></a> has been inserted in <%= Notification.SoneInsert.Duration|l10n 0=insertDuration>.
+<%elseif soneStatus|match value="insert-aborted">
+ Inserting your Sone <a href="viewSone.html?sone=<%insertSone.id|html>"><%insertSone.niceName|html></a> has failed.
+<%/if>
\ No newline at end of file
+++ /dev/null
-<div class="text">
- <%= Notification.SoneRescued.Text|l10n|html>
- <%foreach sones sone>
- <a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
- <%/foreach>
- <%= Notification.SoneRescued.Text.RememberToUnlock|l10n|html>
-</div>
getTranslation("WebInterface.DefaultText.Option.PostsPerPage", function(postsPerPageText) {
registerInputTextareaSwap("#sone #options input[name=posts-per-page]", postsPerPageText, "posts-per-page", true, true);
});
+ getTranslation("WebInterface.DefaultText.Option.CharactersPerPost", function(postsPerPageText) {
+ registerInputTextareaSwap("#sone #options input[name=characters-per-post]", postsPerPageText, "characters-per-post", true, true);
+ });
getTranslation("WebInterface.DefaultText.Option.PositiveTrust", function(positiveTrustText) {
registerInputTextareaSwap("#sone #options input[name=positive-trust]", positiveTrustText, "positive-trust", true, true);
});
<%= Page.Options.Option.AutoFollow.Description|l10n|html>
</p>
+ <p>
+ <input type="checkbox" name="enable-sone-insert-notifications"<%ifnull currentSone> disabled="disabled"<%/if><%if enable-sone-insert-notifications> checked="checked"<%/if> />
+ <%= Page.Options.Option.EnableSoneInsertNotifications.Description|l10n|html>
+ </p>
+
<h2><%= Page.Options.Section.RuntimeOptions.Title|l10n|html></h2>
<p><%= Page.Options.Option.InsertionDelay.Description|l10n|html></p>
<%/if>
<p><input type="text" name="posts-per-page" value="<% posts-per-page|html>" /></p>
+ <p><%= Page.Options.Option.CharactersPerPost.Description|l10n|html></p>
+ <%if =characters-per-post|in collection=fieldErrors>
+ <p class="warning"><%= Page.Options.Warnings.ValueNotChanged|l10n|html></p>
+ <%/if>
+ <p><input type="text" name="characters-per-post" value="<% characters-per-post|html>" /></p>
+
<p>
<input type="checkbox" name="require-full-access"<%if require-full-access> checked="checked"<%/if> />
<%= Page.Options.Option.RequireFullAccess.Description|l10n|html></p>
</select>
</p>
- <h2><%= Page.Options.Section.RescueOptions.Title|l10n|html></h2>
-
- <p><%= Page.Options.Option.SoneRescueMode.Description1|l10n|html></p>
- <p><%= Page.Options.Option.SoneRescueMode.Description2|l10n|html></p>
- <p><%= Page.Options.Option.SoneRescueMode.Description3|l10n|html></p>
- <p><select name="sone-rescue-mode"><option disabled="disabled"><%= WebInterface.SelectBox.Choose|l10n|html></option><option value="true"<%if sone-rescue-mode> selected="selected"<%/if>><%= WebInterface.SelectBox.Yes|l10n|html></option><option value="false"<%if !sone-rescue-mode> selected="selected"<%/if>><%= WebInterface.SelectBox.No|l10n|html></option></select>
-
<h2><%= Page.Options.Section.Cleaning.Title|l10n|html></h2>
<p><%= Page.Options.Option.ClearOnNextRestart.Description|l10n|html|replace needle="{strong}" replacement="<strong>"|replace needle="{/strong}" replacement="</strong>"></p>
--- /dev/null
+<%include include/head.html>
+
+<h1><%= Page.Rescue.Page.Title|l10n 0=currentSone.niceName|html></h1>
+
+<p><%= Page.Rescue.Text.Description|l10n|html></p>
+<p><%= Page.Rescue.Text.Procedure|l10n|html></p>
+
+<%if soneRescuer.fetching>
+ <p><%= Page.Rescue.Text.Fetching|l10n 0=soneRescuer.currentEdition|html></p>
+<%else>
+ <%if soneRescuer.hasNextEdition>
+ <%if soneRescuer.lastFetchSuccessful>
+ <p><%= Page.Rescue.Text.Fetched|l10n 0=soneRescuer.currentEdition|html></p>
+ <%else>
+ <p><%= Page.Rescue.Text.NotFetched|l10n 0=soneRescuer.currentEdition|html></p>
+ <%/if>
+ <form action="rescue.html" method="post">
+ <input type="hidden" name="formPassword" value="<%formPassword|html>" />
+ <label><%= Page.Rescue.Label.NextEdition|l10n|html></label>
+ <input type="field" name="edition" value="<%soneRescuer.nextEdition>" />
+ <button type="submit" name="fetch" value="true"><%= Page.Rescue.Button.Fetch|l10n|html></button>
+ </form>
+ <%else>
+ <%if soneRescuer.lastFetchSuccessful>
+ <p><%= Page.Rescue.Text.Fetched|l10n 0=soneRescuer.currentEdition|html></p>
+ <%else>
+ <p><%= Page.Rescue.Text.FetchedLast|l10n|html></p>
+ <%/if>
+ <%/if>
+<%/if>
+
+<%include include/tail.html>
<h1><%= Page.ViewSone.Page.TitleWithoutSone|l10n|html></h1>
<p><%= Page.ViewSone.UnknownSone.Description|l10n|html></p>
+ <p>
+ <%= Page.ViewSone.UnknownSone.LinkToWebOfTrust|l10n|html>
+ <a href="/WebOfTrust/ShowIdentity?id=<% sone.id|html>"><%= Page.ViewSone.Profile.Name.WoTLink|l10n|html></a>
+ </p>
<%else>