🐛 Use new new-post notification in web interface
[Sone.git] / src / main / java / net / pterodactylus / sone / web / WebInterface.java
1 /*
2  * Sone - WebInterface.java - Copyright Â© 2010–2019 David Roden
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17
18 package net.pterodactylus.sone.web;
19
20 import static com.google.common.collect.FluentIterable.from;
21 import static java.util.logging.Logger.getLogger;
22
23 import java.util.Collection;
24 import java.util.Collections;
25 import java.util.HashMap;
26 import java.util.HashSet;
27 import java.util.Map;
28 import java.util.Set;
29 import java.util.TimeZone;
30 import java.util.UUID;
31 import java.util.concurrent.Executors;
32 import java.util.concurrent.ScheduledExecutorService;
33 import java.util.concurrent.ScheduledFuture;
34 import java.util.concurrent.TimeUnit;
35 import java.util.logging.Logger;
36 import javax.annotation.Nonnull;
37 import javax.annotation.Nullable;
38 import javax.inject.Named;
39
40 import net.pterodactylus.sone.core.Core;
41 import net.pterodactylus.sone.core.ElementLoader;
42 import net.pterodactylus.sone.core.event.*;
43 import net.pterodactylus.sone.data.Image;
44 import net.pterodactylus.sone.data.Post;
45 import net.pterodactylus.sone.data.PostReply;
46 import net.pterodactylus.sone.data.Sone;
47 import net.pterodactylus.sone.freenet.L10nFilter;
48 import net.pterodactylus.sone.freenet.Translation;
49 import net.pterodactylus.sone.main.Loaders;
50 import net.pterodactylus.sone.main.PluginHomepage;
51 import net.pterodactylus.sone.main.PluginVersion;
52 import net.pterodactylus.sone.main.PluginYear;
53 import net.pterodactylus.sone.main.SonePlugin;
54 import net.pterodactylus.sone.notify.ListNotification;
55 import net.pterodactylus.sone.notify.ListNotificationFilter;
56 import net.pterodactylus.sone.notify.PostVisibilityFilter;
57 import net.pterodactylus.sone.notify.ReplyVisibilityFilter;
58 import net.pterodactylus.sone.template.LinkedElementRenderFilter;
59 import net.pterodactylus.sone.template.ParserFilter;
60 import net.pterodactylus.sone.template.RenderFilter;
61 import net.pterodactylus.sone.template.ShortenFilter;
62 import net.pterodactylus.sone.text.Part;
63 import net.pterodactylus.sone.text.SonePart;
64 import net.pterodactylus.sone.text.SoneTextParser;
65 import net.pterodactylus.sone.text.TimeTextConverter;
66 import net.pterodactylus.sone.web.ajax.BookmarkAjaxPage;
67 import net.pterodactylus.sone.web.ajax.CreatePostAjaxPage;
68 import net.pterodactylus.sone.web.ajax.CreateReplyAjaxPage;
69 import net.pterodactylus.sone.web.ajax.DeletePostAjaxPage;
70 import net.pterodactylus.sone.web.ajax.DeleteProfileFieldAjaxPage;
71 import net.pterodactylus.sone.web.ajax.DeleteReplyAjaxPage;
72 import net.pterodactylus.sone.web.ajax.DismissNotificationAjaxPage;
73 import net.pterodactylus.sone.web.ajax.EditAlbumAjaxPage;
74 import net.pterodactylus.sone.web.ajax.EditImageAjaxPage;
75 import net.pterodactylus.sone.web.ajax.EditProfileFieldAjaxPage;
76 import net.pterodactylus.sone.web.ajax.FollowSoneAjaxPage;
77 import net.pterodactylus.sone.web.ajax.GetLikesAjaxPage;
78 import net.pterodactylus.sone.web.ajax.GetLinkedElementAjaxPage;
79 import net.pterodactylus.sone.web.ajax.GetNotificationsAjaxPage;
80 import net.pterodactylus.sone.web.ajax.GetPostAjaxPage;
81 import net.pterodactylus.sone.web.ajax.GetReplyAjaxPage;
82 import net.pterodactylus.sone.web.ajax.GetStatusAjaxPage;
83 import net.pterodactylus.sone.web.ajax.GetTimesAjaxPage;
84 import net.pterodactylus.sone.web.ajax.GetTranslationAjaxPage;
85 import net.pterodactylus.sone.web.ajax.LikeAjaxPage;
86 import net.pterodactylus.sone.web.ajax.LockSoneAjaxPage;
87 import net.pterodactylus.sone.web.ajax.MarkAsKnownAjaxPage;
88 import net.pterodactylus.sone.web.ajax.MoveProfileFieldAjaxPage;
89 import net.pterodactylus.sone.web.ajax.UnbookmarkAjaxPage;
90 import net.pterodactylus.sone.web.ajax.UnfollowSoneAjaxPage;
91 import net.pterodactylus.sone.web.ajax.UnlikeAjaxPage;
92 import net.pterodactylus.sone.web.ajax.UnlockSoneAjaxPage;
93 import net.pterodactylus.sone.web.page.FreenetRequest;
94 import net.pterodactylus.sone.web.page.TemplateRenderer;
95 import net.pterodactylus.sone.web.pages.*;
96 import net.pterodactylus.util.notify.Notification;
97 import net.pterodactylus.util.notify.NotificationManager;
98 import net.pterodactylus.util.notify.TemplateNotification;
99 import net.pterodactylus.util.template.Template;
100 import net.pterodactylus.util.template.TemplateContextFactory;
101 import net.pterodactylus.util.web.RedirectPage;
102 import net.pterodactylus.util.web.TemplatePage;
103
104 import freenet.clients.http.SessionManager;
105 import freenet.clients.http.SessionManager.Session;
106 import freenet.clients.http.ToadletContext;
107
108 import com.codahale.metrics.*;
109 import com.google.common.base.Optional;
110 import com.google.common.collect.Collections2;
111 import com.google.common.collect.ImmutableSet;
112 import com.google.common.eventbus.Subscribe;
113 import com.google.inject.Inject;
114
115 /**
116  * Bundles functionality that a web interface of a Freenet plugin needs, e.g.
117  * references to l10n helpers.
118  */
119 public class WebInterface implements SessionProvider {
120
121         /** The logger. */
122         private static final Logger logger = getLogger(WebInterface.class.getName());
123
124         /** The loaders for templates, pages, and classpath providers. */
125         private final Loaders loaders;
126
127         /** The notification manager. */
128         private final NotificationManager notificationManager;
129
130         /** The Sone plugin. */
131         private final SonePlugin sonePlugin;
132
133         /** The form password. */
134         private final String formPassword;
135
136         /** The template context factory. */
137         private final TemplateContextFactory templateContextFactory;
138         private final TemplateRenderer templateRenderer;
139
140         /** The Sone text parser. */
141         private final SoneTextParser soneTextParser;
142
143         /** The parser filter. */
144         private final ParserFilter parserFilter;
145         private final ShortenFilter shortenFilter;
146         private final RenderFilter renderFilter;
147
148         private final ListNotificationFilter listNotificationFilter;
149         private final PostVisibilityFilter postVisibilityFilter;
150         private final ReplyVisibilityFilter replyVisibilityFilter;
151
152         private final ElementLoader elementLoader;
153         private final LinkedElementRenderFilter linkedElementRenderFilter;
154         private final TimeTextConverter timeTextConverter = new TimeTextConverter();
155         private final L10nFilter l10nFilter;
156
157         private final PageToadletRegistry pageToadletRegistry;
158         private final MetricRegistry metricRegistry;
159         private final Translation translation;
160
161         /** The â€œnew post” notification. */
162         private final ListNotification<Post> newPostNotification;
163
164         /** The â€œnew reply” notification. */
165         private final ListNotification<PostReply> newReplyNotification;
166
167         /** The invisible â€œlocal post” notification. */
168         private final ListNotification<Post> localPostNotification;
169
170         /** The invisible â€œlocal reply” notification. */
171         private final ListNotification<PostReply> localReplyNotification;
172
173         /** The â€œyou have been mentioned” notification. */
174         private final ListNotification<Post> mentionNotification;
175
176         /** Notifications for sone inserts. */
177         private final Map<Sone, TemplateNotification> soneInsertNotifications = new HashMap<>();
178
179         /** Sone locked notification ticker objects. */
180         private final Map<Sone, ScheduledFuture<?>> lockedSonesTickerObjects = Collections.synchronizedMap(new HashMap<Sone, ScheduledFuture<?>>());
181
182         /** The â€œSone locked” notification. */
183         private final ListNotification<Sone> lockedSonesNotification;
184
185         /** The â€œnew version” notification. */
186         private final TemplateNotification newVersionNotification;
187
188         /** The â€œinserting images” notification. */
189         private final ListNotification<Image> insertingImagesNotification;
190
191         /** The â€œinserted images” notification. */
192         private final ListNotification<Image> insertedImagesNotification;
193
194         /** The â€œimage insert failed” notification. */
195         private final ListNotification<Image> imageInsertFailedNotification;
196
197         /** Scheduled executor for time-based notifications. */
198         private final ScheduledExecutorService ticker = Executors.newScheduledThreadPool(1);
199
200         @Inject
201         public WebInterface(SonePlugin sonePlugin, Loaders loaders, ListNotificationFilter listNotificationFilter,
202                         PostVisibilityFilter postVisibilityFilter, ReplyVisibilityFilter replyVisibilityFilter,
203                         ElementLoader elementLoader, TemplateContextFactory templateContextFactory,
204                         TemplateRenderer templateRenderer,
205                         ParserFilter parserFilter, ShortenFilter shortenFilter,
206                         RenderFilter renderFilter,
207                         LinkedElementRenderFilter linkedElementRenderFilter,
208                         PageToadletRegistry pageToadletRegistry, MetricRegistry metricRegistry, Translation translation, L10nFilter l10nFilter,
209                         NotificationManager notificationManager, @Named("newRemotePost") ListNotification<Post> newPostNotification) {
210                 this.sonePlugin = sonePlugin;
211                 this.loaders = loaders;
212                 this.listNotificationFilter = listNotificationFilter;
213                 this.postVisibilityFilter = postVisibilityFilter;
214                 this.replyVisibilityFilter = replyVisibilityFilter;
215                 this.elementLoader = elementLoader;
216                 this.templateRenderer = templateRenderer;
217                 this.parserFilter = parserFilter;
218                 this.shortenFilter = shortenFilter;
219                 this.renderFilter = renderFilter;
220                 this.linkedElementRenderFilter = linkedElementRenderFilter;
221                 this.pageToadletRegistry = pageToadletRegistry;
222                 this.metricRegistry = metricRegistry;
223                 this.l10nFilter = l10nFilter;
224                 this.translation = translation;
225                 this.notificationManager = notificationManager;
226                 this.newPostNotification = newPostNotification;
227                 formPassword = sonePlugin.pluginRespirator().getToadletContainer().getFormPassword();
228                 soneTextParser = new SoneTextParser(getCore(), getCore());
229
230                 this.templateContextFactory = templateContextFactory;
231                 templateContextFactory.addTemplateObject("webInterface", this);
232                 templateContextFactory.addTemplateObject("formPassword", formPassword);
233
234                 /* create notifications. */
235                 Template localPostNotificationTemplate = loaders.loadTemplate("/templates/notify/newPostNotification.html");
236                 localPostNotification = new ListNotification<>("local-post-notification", "posts", localPostNotificationTemplate, false);
237
238                 Template newReplyNotificationTemplate = loaders.loadTemplate("/templates/notify/newReplyNotification.html");
239                 newReplyNotification = new ListNotification<>("new-reply-notification", "replies", newReplyNotificationTemplate, false);
240
241                 Template localReplyNotificationTemplate = loaders.loadTemplate("/templates/notify/newReplyNotification.html");
242                 localReplyNotification = new ListNotification<>("local-reply-notification", "replies", localReplyNotificationTemplate, false);
243
244                 Template mentionNotificationTemplate = loaders.loadTemplate("/templates/notify/mentionNotification.html");
245                 mentionNotification = new ListNotification<>("mention-notification", "posts", mentionNotificationTemplate, false);
246
247                 Template lockedSonesTemplate = loaders.loadTemplate("/templates/notify/lockedSonesNotification.html");
248                 lockedSonesNotification = new ListNotification<>("sones-locked-notification", "sones", lockedSonesTemplate);
249
250                 Template newVersionTemplate = loaders.loadTemplate("/templates/notify/newVersionNotification.html");
251                 newVersionNotification = new TemplateNotification("new-version-notification", newVersionTemplate);
252
253                 Template insertingImagesTemplate = loaders.loadTemplate("/templates/notify/inserting-images-notification.html");
254                 insertingImagesNotification = new ListNotification<>("inserting-images-notification", "images", insertingImagesTemplate);
255
256                 Template insertedImagesTemplate = loaders.loadTemplate("/templates/notify/inserted-images-notification.html");
257                 insertedImagesNotification = new ListNotification<>("inserted-images-notification", "images", insertedImagesTemplate);
258
259                 Template imageInsertFailedTemplate = loaders.loadTemplate("/templates/notify/image-insert-failed-notification.html");
260                 imageInsertFailedNotification = new ListNotification<>("image-insert-failed-notification", "images", imageInsertFailedTemplate);
261         }
262
263         //
264         // ACCESSORS
265         //
266
267         /**
268          * Returns the Sone core used by the Sone plugin.
269          *
270          * @return The Sone core
271          */
272         @Nonnull
273         public Core getCore() {
274                 return sonePlugin.core();
275         }
276
277         /**
278          * Returns the template context factory of the web interface.
279          *
280          * @return The template context factory
281          */
282         public TemplateContextFactory getTemplateContextFactory() {
283                 return templateContextFactory;
284         }
285
286         private Session getCurrentSessionWithoutCreation(ToadletContext toadletContenxt) {
287                 return getSessionManager().useSession(toadletContenxt);
288         }
289
290         private Session getOrCreateCurrentSession(ToadletContext toadletContenxt) {
291                 Session session = getCurrentSessionWithoutCreation(toadletContenxt);
292                 if (session == null) {
293                         session = getSessionManager().createSession(UUID.randomUUID().toString(), toadletContenxt);
294                 }
295                 return session;
296         }
297
298         public Sone getCurrentSoneCreatingSession(ToadletContext toadletContext) {
299                 Collection<Sone> localSones = getCore().getLocalSones();
300                 if (localSones.size() == 1) {
301                         return localSones.iterator().next();
302                 }
303                 return getCurrentSone(getOrCreateCurrentSession(toadletContext));
304         }
305
306         public Sone getCurrentSoneWithoutCreatingSession(ToadletContext toadletContext) {
307                 Collection<Sone> localSones = getCore().getLocalSones();
308                 if (localSones.size() == 1) {
309                         return localSones.iterator().next();
310                 }
311                 return getCurrentSone(getCurrentSessionWithoutCreation(toadletContext));
312         }
313
314         /**
315          * Returns the currently logged in Sone.
316          *
317          * @param session
318          *            The session
319          * @return The currently logged in Sone, or {@code null} if no Sone is
320          *         currently logged in
321          */
322         private Sone getCurrentSone(Session session) {
323                 if (session == null) {
324                         return null;
325                 }
326                 String soneId = (String) session.getAttribute("Sone.CurrentSone");
327                 if (soneId == null) {
328                         return null;
329                 }
330                 return getCore().getLocalSone(soneId);
331         }
332
333         @Override
334         @Nullable
335         public Sone getCurrentSone(@Nonnull ToadletContext toadletContext, boolean createSession) {
336                 return createSession ? getCurrentSoneCreatingSession(toadletContext) : getCurrentSoneWithoutCreatingSession(toadletContext);
337         }
338
339         /**
340          * Sets the currently logged in Sone.
341          *
342          * @param toadletContext
343          *            The toadlet context
344          * @param sone
345          *            The Sone to set as currently logged in
346          */
347         @Override
348         public void setCurrentSone(@Nonnull ToadletContext toadletContext, @Nullable Sone sone) {
349                 Session session = getOrCreateCurrentSession(toadletContext);
350                 if (sone == null) {
351                         session.removeAttribute("Sone.CurrentSone");
352                 } else {
353                         session.setAttribute("Sone.CurrentSone", sone.getId());
354                 }
355         }
356
357         /**
358          * Returns the notification manager.
359          *
360          * @return The notification manager
361          */
362         public NotificationManager getNotifications() {
363                 return notificationManager;
364         }
365
366         @Nonnull
367         public Optional<Notification> getNotification(@Nonnull String notificationId) {
368                 return Optional.fromNullable(notificationManager.getNotification(notificationId));
369         }
370
371         @Nonnull
372         public Collection<Notification> getNotifications(@Nullable Sone currentSone) {
373                 return listNotificationFilter.filterNotifications(notificationManager.getNotifications(), currentSone);
374         }
375
376         public Translation getTranslation() {
377                 return translation;
378         }
379
380         /**
381          * Returns the session manager of the node.
382          *
383          * @return The node’s session manager
384          */
385         public SessionManager getSessionManager() {
386                 return sonePlugin.pluginRespirator().getSessionManager("Sone");
387         }
388
389         /**
390          * Returns the node’s form password.
391          *
392          * @return The form password
393          */
394         public String getFormPassword() {
395                 return formPassword;
396         }
397
398         @Nonnull
399         public Collection<Post> getNewPosts(@Nullable Sone currentSone) {
400                 Set<Post> allNewPosts = ImmutableSet.<Post> builder()
401                                 .addAll(newPostNotification.getElements())
402                                 .addAll(localPostNotification.getElements())
403                                 .build();
404                 return from(allNewPosts).filter(postVisibilityFilter.isVisible(currentSone)).toSet();
405         }
406
407         /**
408          * Returns the replies that have been announced as new in the
409          * {@link #newReplyNotification}.
410          *
411          * @return The new replies
412          */
413         public Set<PostReply> getNewReplies() {
414                 return ImmutableSet.<PostReply> builder().addAll(newReplyNotification.getElements()).addAll(localReplyNotification.getElements()).build();
415         }
416
417         @Nonnull
418         public Collection<PostReply> getNewReplies(@Nullable Sone currentSone) {
419                 Set<PostReply> allNewReplies = ImmutableSet.<PostReply>builder()
420                                 .addAll(newReplyNotification.getElements())
421                                 .addAll(localReplyNotification.getElements())
422                                 .build();
423                 return from(allNewReplies).filter(replyVisibilityFilter.isVisible(currentSone)).toSet();
424         }
425
426         /**
427          * Sets whether the current start of the plugin is the first start. It is
428          * considered a first start if the configuration file does not exist.
429          *
430          * @param firstStart
431          *            {@code true} if no configuration file existed when Sone was
432          *            loaded, {@code false} otherwise
433          */
434         public void setFirstStart(boolean firstStart) {
435                 if (firstStart) {
436                         Template firstStartNotificationTemplate = loaders.loadTemplate("/templates/notify/firstStartNotification.html");
437                         Notification firstStartNotification = new TemplateNotification("first-start-notification", firstStartNotificationTemplate);
438                         notificationManager.addNotification(firstStartNotification);
439                 }
440         }
441
442         /**
443          * Sets whether Sone was started with a fresh configuration file.
444          *
445          * @param newConfig
446          *            {@code true} if Sone was started with a fresh configuration,
447          *            {@code false} if the existing configuration could be read
448          */
449         public void setNewConfig(boolean newConfig) {
450                 if (newConfig && !hasFirstStartNotification()) {
451                         Template configNotReadNotificationTemplate = loaders.loadTemplate("/templates/notify/configNotReadNotification.html");
452                         Notification configNotReadNotification = new TemplateNotification("config-not-read-notification", configNotReadNotificationTemplate);
453                         notificationManager.addNotification(configNotReadNotification);
454                 }
455         }
456
457         //
458         // PRIVATE ACCESSORS
459         //
460
461         /**
462          * Returns whether the first start notification is currently displayed.
463          *
464          * @return {@code true} if the first-start notification is currently
465          *         displayed, {@code false} otherwise
466          */
467         private boolean hasFirstStartNotification() {
468                 return notificationManager.getNotification("first-start-notification") != null;
469         }
470
471         //
472         // ACTIONS
473         //
474
475         /**
476          * Starts the web interface and registers all toadlets.
477          */
478         public void start() {
479                 registerToadlets();
480
481                 /* notification templates. */
482                 Template startupNotificationTemplate = loaders.loadTemplate("/templates/notify/startupNotification.html");
483
484                 final TemplateNotification startupNotification = new TemplateNotification("startup-notification", startupNotificationTemplate);
485                 notificationManager.addNotification(startupNotification);
486
487                 ticker.schedule(new Runnable() {
488
489                         @Override
490                         public void run() {
491                                 startupNotification.dismiss();
492                         }
493                 }, 2, TimeUnit.MINUTES);
494
495                 Template wotMissingNotificationTemplate = loaders.loadTemplate("/templates/notify/wotMissingNotification.html");
496                 final TemplateNotification wotMissingNotification = new TemplateNotification("wot-missing-notification", wotMissingNotificationTemplate);
497                 ticker.scheduleAtFixedRate(new Runnable() {
498
499                         @Override
500                         @SuppressWarnings("synthetic-access")
501                         public void run() {
502                                 if (getCore().getIdentityManager().isConnected()) {
503                                         wotMissingNotification.dismiss();
504                                 } else {
505                                         notificationManager.addNotification(wotMissingNotification);
506                                 }
507                         }
508
509                 }, 15, 15, TimeUnit.SECONDS);
510         }
511
512         /**
513          * Stops the web interface and unregisters all toadlets.
514          */
515         public void stop() {
516                 pageToadletRegistry.unregisterToadlets();
517                 ticker.shutdownNow();
518         }
519
520         //
521         // PRIVATE METHODS
522         //
523
524         /**
525          * Register all toadlets.
526          */
527         private void registerToadlets() {
528                 Template postTemplate = loaders.loadTemplate("/templates/include/viewPost.html");
529                 Template replyTemplate = loaders.loadTemplate("/templates/include/viewReply.html");
530                 Template openSearchTemplate = loaders.loadTemplate("/templates/xml/OpenSearch.xml");
531
532                 pageToadletRegistry.addPage(new RedirectPage<FreenetRequest>("", "index.html"));
533                 pageToadletRegistry.addPage(new IndexPage(this, loaders, templateRenderer, postVisibilityFilter));
534                 pageToadletRegistry.addPage(new NewPage(this, loaders, templateRenderer));
535                 pageToadletRegistry.addPage(new CreateSonePage(this, loaders, templateRenderer));
536                 pageToadletRegistry.addPage(new KnownSonesPage(this, loaders, templateRenderer));
537                 pageToadletRegistry.addPage(new EditProfilePage(this, loaders, templateRenderer));
538                 pageToadletRegistry.addPage(new EditProfileFieldPage(this, loaders, templateRenderer));
539                 pageToadletRegistry.addPage(new DeleteProfileFieldPage(this, loaders, templateRenderer));
540                 pageToadletRegistry.addPage(new CreatePostPage(this, loaders, templateRenderer));
541                 pageToadletRegistry.addPage(new CreateReplyPage(this, loaders, templateRenderer));
542                 pageToadletRegistry.addPage(new ViewSonePage(this, loaders, templateRenderer));
543                 pageToadletRegistry.addPage(new ViewPostPage(this, loaders, templateRenderer));
544                 pageToadletRegistry.addPage(new LikePage(this, loaders, templateRenderer));
545                 pageToadletRegistry.addPage(new UnlikePage(this, loaders, templateRenderer));
546                 pageToadletRegistry.addPage(new DeletePostPage(this, loaders, templateRenderer));
547                 pageToadletRegistry.addPage(new DeleteReplyPage(this, loaders, templateRenderer));
548                 pageToadletRegistry.addPage(new LockSonePage(this, loaders, templateRenderer));
549                 pageToadletRegistry.addPage(new UnlockSonePage(this, loaders, templateRenderer));
550                 pageToadletRegistry.addPage(new FollowSonePage(this, loaders, templateRenderer));
551                 pageToadletRegistry.addPage(new UnfollowSonePage(this, loaders, templateRenderer));
552                 pageToadletRegistry.addPage(new ImageBrowserPage(this, loaders, templateRenderer));
553                 pageToadletRegistry.addPage(new CreateAlbumPage(this, loaders, templateRenderer));
554                 pageToadletRegistry.addPage(new EditAlbumPage(this, loaders, templateRenderer));
555                 pageToadletRegistry.addPage(new DeleteAlbumPage(this, loaders, templateRenderer));
556                 pageToadletRegistry.addPage(new UploadImagePage(this, loaders, templateRenderer));
557                 pageToadletRegistry.addPage(new EditImagePage(this, loaders, templateRenderer));
558                 pageToadletRegistry.addPage(new DeleteImagePage(this, loaders, templateRenderer));
559                 pageToadletRegistry.addPage(new MarkAsKnownPage(this, loaders, templateRenderer));
560                 pageToadletRegistry.addPage(new BookmarkPage(this, loaders, templateRenderer));
561                 pageToadletRegistry.addPage(new UnbookmarkPage(this, loaders, templateRenderer));
562                 pageToadletRegistry.addPage(new BookmarksPage(this, loaders, templateRenderer));
563                 pageToadletRegistry.addPage(new SearchPage(this, loaders, templateRenderer));
564                 pageToadletRegistry.addPage(new DeleteSonePage(this, loaders, templateRenderer));
565                 pageToadletRegistry.addPage(new LoginPage(this, loaders, templateRenderer));
566                 pageToadletRegistry.addPage(new LogoutPage(this, loaders, templateRenderer));
567                 pageToadletRegistry.addPage(new OptionsPage(this, loaders, templateRenderer));
568                 pageToadletRegistry.addPage(new RescuePage(this, loaders, templateRenderer));
569                 pageToadletRegistry.addPage(new AboutPage(this, loaders, templateRenderer, new PluginVersion(SonePlugin.getPluginVersion()), new PluginYear(sonePlugin.getYear()), new PluginHomepage(sonePlugin.getHomepage())));
570                 pageToadletRegistry.addPage(new InvalidPage(this, loaders, templateRenderer));
571                 pageToadletRegistry.addPage(new NoPermissionPage(this, loaders, templateRenderer));
572                 pageToadletRegistry.addPage(new EmptyImageTitlePage(this, loaders, templateRenderer));
573                 pageToadletRegistry.addPage(new EmptyAlbumTitlePage(this, loaders, templateRenderer));
574                 pageToadletRegistry.addPage(new DismissNotificationPage(this, loaders, templateRenderer));
575                 pageToadletRegistry.addPage(new DebugPage(this, loaders, templateRenderer));
576                 pageToadletRegistry.addDebugPage(new MetricsPage(this, loaders, templateRenderer, metricRegistry));
577                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("css/", "/static/css/", "text/css"));
578                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("javascript/", "/static/javascript/", "text/javascript"));
579                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("images/", "/static/images/", "image/png"));
580                 pageToadletRegistry.addPage(new TemplatePage<FreenetRequest>("OpenSearch.xml", "application/opensearchdescription+xml", templateContextFactory, openSearchTemplate));
581                 pageToadletRegistry.addPage(new GetImagePage(this));
582                 pageToadletRegistry.addPage(new GetTranslationAjaxPage(this));
583                 pageToadletRegistry.addPage(new GetStatusAjaxPage(this, elementLoader, timeTextConverter, l10nFilter, TimeZone.getDefault()));
584                 pageToadletRegistry.addPage(new GetNotificationsAjaxPage(this));
585                 pageToadletRegistry.addPage(new DismissNotificationAjaxPage(this));
586                 pageToadletRegistry.addPage(new CreatePostAjaxPage(this));
587                 pageToadletRegistry.addPage(new CreateReplyAjaxPage(this));
588                 pageToadletRegistry.addPage(new GetReplyAjaxPage(this, replyTemplate));
589                 pageToadletRegistry.addPage(new GetPostAjaxPage(this, postTemplate));
590                 pageToadletRegistry.addPage(new GetLinkedElementAjaxPage(this, elementLoader, linkedElementRenderFilter));
591                 pageToadletRegistry.addPage(new GetTimesAjaxPage(this, timeTextConverter, l10nFilter, TimeZone.getDefault()));
592                 pageToadletRegistry.addPage(new MarkAsKnownAjaxPage(this));
593                 pageToadletRegistry.addPage(new DeletePostAjaxPage(this));
594                 pageToadletRegistry.addPage(new DeleteReplyAjaxPage(this));
595                 pageToadletRegistry.addPage(new LockSoneAjaxPage(this));
596                 pageToadletRegistry.addPage(new UnlockSoneAjaxPage(this));
597                 pageToadletRegistry.addPage(new FollowSoneAjaxPage(this));
598                 pageToadletRegistry.addPage(new UnfollowSoneAjaxPage(this));
599                 pageToadletRegistry.addPage(new EditAlbumAjaxPage(this));
600                 pageToadletRegistry.addPage(new EditImageAjaxPage(this, parserFilter, shortenFilter, renderFilter));
601                 pageToadletRegistry.addPage(new LikeAjaxPage(this));
602                 pageToadletRegistry.addPage(new UnlikeAjaxPage(this));
603                 pageToadletRegistry.addPage(new GetLikesAjaxPage(this));
604                 pageToadletRegistry.addPage(new BookmarkAjaxPage(this));
605                 pageToadletRegistry.addPage(new UnbookmarkAjaxPage(this));
606                 pageToadletRegistry.addPage(new EditProfileFieldAjaxPage(this));
607                 pageToadletRegistry.addPage(new DeleteProfileFieldAjaxPage(this));
608                 pageToadletRegistry.addPage(new MoveProfileFieldAjaxPage(this));
609
610                 pageToadletRegistry.registerToadlets();
611         }
612
613         /**
614          * Returns all {@link Sone#isLocal() local Sone}s that are referenced by
615          * {@link SonePart}s in the given text (after parsing it using
616          * {@link SoneTextParser}).
617          *
618          * @param text
619          *            The text to parse
620          * @return All mentioned local Sones
621          */
622         private Collection<Sone> getMentionedSones(String text) {
623                 /* we need no context to find mentioned Sones. */
624                 Set<Sone> mentionedSones = new HashSet<>();
625                 for (Part part : soneTextParser.parse(text, null)) {
626                         if (part instanceof SonePart) {
627                                 mentionedSones.add(((SonePart) part).getSone());
628                         }
629                 }
630                 return Collections2.filter(mentionedSones, Sone.LOCAL_SONE_FILTER);
631         }
632
633         /**
634          * Returns the Sone insert notification for the given Sone. If no
635          * notification for the given Sone exists, a new notification is created and
636          * cached.
637          *
638          * @param sone
639          *            The Sone to get the insert notification for
640          * @return The Sone insert notification
641          */
642         private TemplateNotification getSoneInsertNotification(Sone sone) {
643                 synchronized (soneInsertNotifications) {
644                         TemplateNotification templateNotification = soneInsertNotifications.get(sone);
645                         if (templateNotification == null) {
646                                 templateNotification = new TemplateNotification(loaders.loadTemplate("/templates/notify/soneInsertNotification.html"));
647                                 templateNotification.set("insertSone", sone);
648                                 soneInsertNotifications.put(sone, templateNotification);
649                         }
650                         return templateNotification;
651                 }
652         }
653
654         private boolean localSoneMentionedInNewPostOrReply(Post post) {
655                 if (!post.getSone().isLocal()) {
656                         if (!getMentionedSones(post.getText()).isEmpty() && !post.isKnown()) {
657                                 return true;
658                         }
659                 }
660                 for (PostReply postReply : getCore().getReplies(post.getId())) {
661                         if (postReply.getSone().isLocal()) {
662                                 continue;
663                         }
664                         if (!getMentionedSones(postReply.getText()).isEmpty() && !postReply.isKnown()) {
665                                 return true;
666                         }
667                 }
668                 return false;
669         }
670
671         //
672         // EVENT HANDLERS
673         //
674
675         /**
676          * Notifies the web interface that a new {@link Post} was found.
677          *
678          * @param newPostFoundEvent
679          *            The event
680          */
681         @Subscribe
682         public void newPostFound(NewPostFoundEvent newPostFoundEvent) {
683                 Post post = newPostFoundEvent.getPost();
684                 boolean isLocal = post.getSone().isLocal();
685                 if (isLocal) {
686                         localPostNotification.add(post);
687                 }
688                 if (!hasFirstStartNotification()) {
689                         if (isLocal) {
690                                 notificationManager.addNotification(localPostNotification);
691                         }
692                         if (!getMentionedSones(post.getText()).isEmpty() && !isLocal) {
693                                 mentionNotification.add(post);
694                                 notificationManager.addNotification(mentionNotification);
695                         }
696                 }
697         }
698
699         /**
700          * Notifies the web interface that a new {@link PostReply} was found.
701          *
702          * @param newPostReplyFoundEvent
703          *            The event
704          */
705         @Subscribe
706         public void newReplyFound(NewPostReplyFoundEvent newPostReplyFoundEvent) {
707                 PostReply reply = newPostReplyFoundEvent.getPostReply();
708                 boolean isLocal = reply.getSone().isLocal();
709                 if (isLocal) {
710                         localReplyNotification.add(reply);
711                 } else {
712                         newReplyNotification.add(reply);
713                 }
714                 if (!hasFirstStartNotification()) {
715                         notificationManager.addNotification(isLocal ? localReplyNotification : newReplyNotification);
716                         if (reply.getPost().isPresent() && localSoneMentionedInNewPostOrReply(reply.getPost().get())) {
717                                 mentionNotification.add(reply.getPost().get());
718                                 notificationManager.addNotification(mentionNotification);
719                         }
720                 } else {
721                         getCore().markReplyKnown(reply);
722                 }
723         }
724
725         @Subscribe
726         public void markPostKnown(MarkPostKnownEvent markPostKnownEvent) {
727                 removePost(markPostKnownEvent.getPost());
728         }
729
730         @Subscribe
731         public void markReplyKnown(MarkPostReplyKnownEvent markPostReplyKnownEvent) {
732                 removeReply(markPostReplyKnownEvent.getPostReply());
733         }
734
735         @Subscribe
736         public void postRemoved(PostRemovedEvent postRemovedEvent) {
737                 removePost(postRemovedEvent.getPost());
738         }
739
740         private void removePost(Post post) {
741                 newPostNotification.remove(post);
742                 localPostNotification.remove(post);
743                 if (!localSoneMentionedInNewPostOrReply(post)) {
744                         mentionNotification.remove(post);
745                 }
746         }
747
748         @Subscribe
749         public void replyRemoved(PostReplyRemovedEvent postReplyRemovedEvent) {
750                 removeReply(postReplyRemovedEvent.getPostReply());
751         }
752
753         private void removeReply(PostReply reply) {
754                 newReplyNotification.remove(reply);
755                 localReplyNotification.remove(reply);
756                 if (reply.getPost().isPresent() && !localSoneMentionedInNewPostOrReply(reply.getPost().get())) {
757                         mentionNotification.remove(reply.getPost().get());
758                 }
759         }
760
761         /**
762          * Notifies the web interface that a Sone was locked.
763          *
764          * @param soneLockedEvent
765          *            The event
766          */
767         @Subscribe
768         public void soneLocked(SoneLockedEvent soneLockedEvent) {
769                 final Sone sone = soneLockedEvent.getSone();
770                 ScheduledFuture<?> tickerObject = ticker.schedule(new Runnable() {
771
772                         @Override
773                         @SuppressWarnings("synthetic-access")
774                         public void run() {
775                                 lockedSonesNotification.add(sone);
776                                 notificationManager.addNotification(lockedSonesNotification);
777                         }
778                 }, 5, TimeUnit.MINUTES);
779                 lockedSonesTickerObjects.put(sone, tickerObject);
780         }
781
782         /**
783          * Notifies the web interface that a Sone was unlocked.
784          *
785          * @param soneUnlockedEvent
786          *            The event
787          */
788         @Subscribe
789         public void soneUnlocked(SoneUnlockedEvent soneUnlockedEvent) {
790                 lockedSonesNotification.remove(soneUnlockedEvent.getSone());
791                 lockedSonesTickerObjects.remove(soneUnlockedEvent.getSone()).cancel(false);
792         }
793
794         /**
795          * Notifies the web interface that a {@link Sone} is being inserted.
796          *
797          * @param soneInsertingEvent
798          *            The event
799          */
800         @Subscribe
801         public void soneInserting(SoneInsertingEvent soneInsertingEvent) {
802                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertingEvent.getSone());
803                 soneInsertNotification.set("soneStatus", "inserting");
804                 if (soneInsertingEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
805                         notificationManager.addNotification(soneInsertNotification);
806                 }
807         }
808
809         /**
810          * Notifies the web interface that a {@link Sone} was inserted.
811          *
812          * @param soneInsertedEvent
813          *            The event
814          */
815         @Subscribe
816         public void soneInserted(SoneInsertedEvent soneInsertedEvent) {
817                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertedEvent.getSone());
818                 soneInsertNotification.set("soneStatus", "inserted");
819                 soneInsertNotification.set("insertDuration", soneInsertedEvent.getInsertDuration() / 1000);
820                 if (soneInsertedEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
821                         notificationManager.addNotification(soneInsertNotification);
822                 }
823         }
824
825         /**
826          * Notifies the web interface that a {@link Sone} insert was aborted.
827          *
828          * @param soneInsertAbortedEvent
829          *            The event
830          */
831         @Subscribe
832         public void soneInsertAborted(SoneInsertAbortedEvent soneInsertAbortedEvent) {
833                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertAbortedEvent.getSone());
834                 soneInsertNotification.set("soneStatus", "insert-aborted");
835                 soneInsertNotification.set("insert-error", soneInsertAbortedEvent.getCause());
836                 if (soneInsertAbortedEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
837                         notificationManager.addNotification(soneInsertNotification);
838                 }
839         }
840
841         /**
842          * Notifies the web interface that a new Sone version was found.
843          *
844          * @param updateFoundEvent
845          *            The event
846          */
847         @Subscribe
848         public void updateFound(UpdateFoundEvent updateFoundEvent) {
849                 newVersionNotification.set("latestVersion", updateFoundEvent.getVersion());
850                 newVersionNotification.set("latestEdition", updateFoundEvent.getLatestEdition());
851                 newVersionNotification.set("releaseTime", updateFoundEvent.getReleaseTime());
852                 newVersionNotification.set("disruptive", updateFoundEvent.isDisruptive());
853                 notificationManager.addNotification(newVersionNotification);
854         }
855
856         /**
857          * Notifies the web interface that an image insert was started
858          *
859          * @param imageInsertStartedEvent
860          *            The event
861          */
862         @Subscribe
863         public void imageInsertStarted(ImageInsertStartedEvent imageInsertStartedEvent) {
864                 insertingImagesNotification.add(imageInsertStartedEvent.getImage());
865                 notificationManager.addNotification(insertingImagesNotification);
866         }
867
868         /**
869          * Notifies the web interface that an {@link Image} insert was aborted.
870          *
871          * @param imageInsertAbortedEvent
872          *            The event
873          */
874         @Subscribe
875         public void imageInsertAborted(ImageInsertAbortedEvent imageInsertAbortedEvent) {
876                 insertingImagesNotification.remove(imageInsertAbortedEvent.getImage());
877         }
878
879         /**
880          * Notifies the web interface that an {@link Image} insert is finished.
881          *
882          * @param imageInsertFinishedEvent
883          *            The event
884          */
885         @Subscribe
886         public void imageInsertFinished(ImageInsertFinishedEvent imageInsertFinishedEvent) {
887                 insertingImagesNotification.remove(imageInsertFinishedEvent.getImage());
888                 insertedImagesNotification.add(imageInsertFinishedEvent.getImage());
889                 notificationManager.addNotification(insertedImagesNotification);
890         }
891
892         /**
893          * Notifies the web interface that an {@link Image} insert has failed.
894          *
895          * @param imageInsertFailedEvent
896          *            The event
897          */
898         @Subscribe
899         public void imageInsertFailed(ImageInsertFailedEvent imageInsertFailedEvent) {
900                 insertingImagesNotification.remove(imageInsertFailedEvent.getImage());
901                 imageInsertFailedNotification.add(imageInsertFailedEvent.getImage());
902                 notificationManager.addNotification(imageInsertFailedNotification);
903         }
904
905         @Subscribe
906         public void debugActivated(@Nonnull DebugActivatedEvent debugActivatedEvent) {
907                 pageToadletRegistry.activateDebugMode();
908         }
909
910 }