🚧 Use handler for new-reply notifications
[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.HashMap;
25 import java.util.Map;
26 import java.util.Set;
27 import java.util.TimeZone;
28 import java.util.UUID;
29 import java.util.logging.Logger;
30 import javax.annotation.Nonnull;
31 import javax.annotation.Nullable;
32 import javax.inject.Named;
33
34 import net.pterodactylus.sone.core.Core;
35 import net.pterodactylus.sone.core.ElementLoader;
36 import net.pterodactylus.sone.core.event.*;
37 import net.pterodactylus.sone.data.Post;
38 import net.pterodactylus.sone.data.PostReply;
39 import net.pterodactylus.sone.data.Sone;
40 import net.pterodactylus.sone.freenet.L10nFilter;
41 import net.pterodactylus.sone.freenet.Translation;
42 import net.pterodactylus.sone.main.Loaders;
43 import net.pterodactylus.sone.main.PluginHomepage;
44 import net.pterodactylus.sone.main.PluginVersion;
45 import net.pterodactylus.sone.main.PluginYear;
46 import net.pterodactylus.sone.main.SonePlugin;
47 import net.pterodactylus.sone.notify.ListNotification;
48 import net.pterodactylus.sone.notify.ListNotificationFilter;
49 import net.pterodactylus.sone.notify.PostVisibilityFilter;
50 import net.pterodactylus.sone.notify.ReplyVisibilityFilter;
51 import net.pterodactylus.sone.template.LinkedElementRenderFilter;
52 import net.pterodactylus.sone.template.ParserFilter;
53 import net.pterodactylus.sone.template.RenderFilter;
54 import net.pterodactylus.sone.template.ShortenFilter;
55 import net.pterodactylus.sone.text.TimeTextConverter;
56 import net.pterodactylus.sone.web.ajax.BookmarkAjaxPage;
57 import net.pterodactylus.sone.web.ajax.CreatePostAjaxPage;
58 import net.pterodactylus.sone.web.ajax.CreateReplyAjaxPage;
59 import net.pterodactylus.sone.web.ajax.DeletePostAjaxPage;
60 import net.pterodactylus.sone.web.ajax.DeleteProfileFieldAjaxPage;
61 import net.pterodactylus.sone.web.ajax.DeleteReplyAjaxPage;
62 import net.pterodactylus.sone.web.ajax.DismissNotificationAjaxPage;
63 import net.pterodactylus.sone.web.ajax.EditAlbumAjaxPage;
64 import net.pterodactylus.sone.web.ajax.EditImageAjaxPage;
65 import net.pterodactylus.sone.web.ajax.EditProfileFieldAjaxPage;
66 import net.pterodactylus.sone.web.ajax.FollowSoneAjaxPage;
67 import net.pterodactylus.sone.web.ajax.GetLikesAjaxPage;
68 import net.pterodactylus.sone.web.ajax.GetLinkedElementAjaxPage;
69 import net.pterodactylus.sone.web.ajax.GetNotificationsAjaxPage;
70 import net.pterodactylus.sone.web.ajax.GetPostAjaxPage;
71 import net.pterodactylus.sone.web.ajax.GetReplyAjaxPage;
72 import net.pterodactylus.sone.web.ajax.GetStatusAjaxPage;
73 import net.pterodactylus.sone.web.ajax.GetTimesAjaxPage;
74 import net.pterodactylus.sone.web.ajax.GetTranslationAjaxPage;
75 import net.pterodactylus.sone.web.ajax.LikeAjaxPage;
76 import net.pterodactylus.sone.web.ajax.LockSoneAjaxPage;
77 import net.pterodactylus.sone.web.ajax.MarkAsKnownAjaxPage;
78 import net.pterodactylus.sone.web.ajax.MoveProfileFieldAjaxPage;
79 import net.pterodactylus.sone.web.ajax.UnbookmarkAjaxPage;
80 import net.pterodactylus.sone.web.ajax.UnfollowSoneAjaxPage;
81 import net.pterodactylus.sone.web.ajax.UnlikeAjaxPage;
82 import net.pterodactylus.sone.web.ajax.UnlockSoneAjaxPage;
83 import net.pterodactylus.sone.web.page.FreenetRequest;
84 import net.pterodactylus.sone.web.page.TemplateRenderer;
85 import net.pterodactylus.sone.web.pages.*;
86 import net.pterodactylus.util.notify.Notification;
87 import net.pterodactylus.util.notify.NotificationManager;
88 import net.pterodactylus.util.notify.TemplateNotification;
89 import net.pterodactylus.util.template.Template;
90 import net.pterodactylus.util.template.TemplateContextFactory;
91 import net.pterodactylus.util.web.RedirectPage;
92 import net.pterodactylus.util.web.TemplatePage;
93
94 import freenet.clients.http.SessionManager;
95 import freenet.clients.http.SessionManager.Session;
96 import freenet.clients.http.ToadletContext;
97
98 import com.codahale.metrics.*;
99 import com.google.common.base.Optional;
100 import com.google.common.collect.ImmutableSet;
101 import com.google.common.eventbus.Subscribe;
102 import com.google.inject.Inject;
103
104 /**
105  * Bundles functionality that a web interface of a Freenet plugin needs, e.g.
106  * references to l10n helpers.
107  */
108 public class WebInterface implements SessionProvider {
109
110         /** The logger. */
111         private static final Logger logger = getLogger(WebInterface.class.getName());
112
113         /** The loaders for templates, pages, and classpath providers. */
114         private final Loaders loaders;
115
116         /** The notification manager. */
117         private final NotificationManager notificationManager;
118
119         /** The Sone plugin. */
120         private final SonePlugin sonePlugin;
121
122         /** The form password. */
123         private final String formPassword;
124
125         /** The template context factory. */
126         private final TemplateContextFactory templateContextFactory;
127         private final TemplateRenderer templateRenderer;
128
129         /** The parser filter. */
130         private final ParserFilter parserFilter;
131         private final ShortenFilter shortenFilter;
132         private final RenderFilter renderFilter;
133
134         private final ListNotificationFilter listNotificationFilter;
135         private final PostVisibilityFilter postVisibilityFilter;
136         private final ReplyVisibilityFilter replyVisibilityFilter;
137
138         private final ElementLoader elementLoader;
139         private final LinkedElementRenderFilter linkedElementRenderFilter;
140         private final TimeTextConverter timeTextConverter = new TimeTextConverter();
141         private final L10nFilter l10nFilter;
142
143         private final PageToadletRegistry pageToadletRegistry;
144         private final MetricRegistry metricRegistry;
145         private final Translation translation;
146
147         /** The â€śnew post” notification. */
148         private final ListNotification<Post> newPostNotification;
149
150         /** The â€śnew reply” notification. */
151         private final ListNotification<PostReply> newReplyNotification;
152
153         /** The invisible â€ślocal post” notification. */
154         private final ListNotification<Post> localPostNotification;
155
156         /** The invisible â€ślocal reply” notification. */
157         private final ListNotification<PostReply> localReplyNotification;
158
159         /** Notifications for sone inserts. */
160         private final Map<Sone, TemplateNotification> soneInsertNotifications = new HashMap<>();
161
162         @Inject
163         public WebInterface(SonePlugin sonePlugin, Loaders loaders, ListNotificationFilter listNotificationFilter,
164                         PostVisibilityFilter postVisibilityFilter, ReplyVisibilityFilter replyVisibilityFilter,
165                         ElementLoader elementLoader, TemplateContextFactory templateContextFactory,
166                         TemplateRenderer templateRenderer,
167                         ParserFilter parserFilter, ShortenFilter shortenFilter,
168                         RenderFilter renderFilter,
169                         LinkedElementRenderFilter linkedElementRenderFilter,
170                         PageToadletRegistry pageToadletRegistry, MetricRegistry metricRegistry, Translation translation, L10nFilter l10nFilter,
171                         NotificationManager notificationManager, @Named("newRemotePost") ListNotification<Post> newPostNotification,
172                         @Named("newRemotePostReply") ListNotification<PostReply> newReplyNotification,
173                         @Named("localPost") ListNotification<Post> localPostNotification) {
174                 this.sonePlugin = sonePlugin;
175                 this.loaders = loaders;
176                 this.listNotificationFilter = listNotificationFilter;
177                 this.postVisibilityFilter = postVisibilityFilter;
178                 this.replyVisibilityFilter = replyVisibilityFilter;
179                 this.elementLoader = elementLoader;
180                 this.templateRenderer = templateRenderer;
181                 this.parserFilter = parserFilter;
182                 this.shortenFilter = shortenFilter;
183                 this.renderFilter = renderFilter;
184                 this.linkedElementRenderFilter = linkedElementRenderFilter;
185                 this.pageToadletRegistry = pageToadletRegistry;
186                 this.metricRegistry = metricRegistry;
187                 this.l10nFilter = l10nFilter;
188                 this.translation = translation;
189                 this.notificationManager = notificationManager;
190                 this.newPostNotification = newPostNotification;
191                 this.newReplyNotification = newReplyNotification;
192                 this.localPostNotification = localPostNotification;
193                 formPassword = sonePlugin.pluginRespirator().getToadletContainer().getFormPassword();
194
195                 this.templateContextFactory = templateContextFactory;
196                 templateContextFactory.addTemplateObject("webInterface", this);
197                 templateContextFactory.addTemplateObject("formPassword", formPassword);
198
199                 /* create notifications. */
200                 Template localReplyNotificationTemplate = loaders.loadTemplate("/templates/notify/newReplyNotification.html");
201                 localReplyNotification = new ListNotification<>("local-reply-notification", "replies", localReplyNotificationTemplate, false);
202         }
203
204         //
205         // ACCESSORS
206         //
207
208         /**
209          * Returns the Sone core used by the Sone plugin.
210          *
211          * @return The Sone core
212          */
213         @Nonnull
214         public Core getCore() {
215                 return sonePlugin.core();
216         }
217
218         /**
219          * Returns the template context factory of the web interface.
220          *
221          * @return The template context factory
222          */
223         public TemplateContextFactory getTemplateContextFactory() {
224                 return templateContextFactory;
225         }
226
227         private Session getCurrentSessionWithoutCreation(ToadletContext toadletContenxt) {
228                 return getSessionManager().useSession(toadletContenxt);
229         }
230
231         private Session getOrCreateCurrentSession(ToadletContext toadletContenxt) {
232                 Session session = getCurrentSessionWithoutCreation(toadletContenxt);
233                 if (session == null) {
234                         session = getSessionManager().createSession(UUID.randomUUID().toString(), toadletContenxt);
235                 }
236                 return session;
237         }
238
239         public Sone getCurrentSoneCreatingSession(ToadletContext toadletContext) {
240                 Collection<Sone> localSones = getCore().getLocalSones();
241                 if (localSones.size() == 1) {
242                         return localSones.iterator().next();
243                 }
244                 return getCurrentSone(getOrCreateCurrentSession(toadletContext));
245         }
246
247         public Sone getCurrentSoneWithoutCreatingSession(ToadletContext toadletContext) {
248                 Collection<Sone> localSones = getCore().getLocalSones();
249                 if (localSones.size() == 1) {
250                         return localSones.iterator().next();
251                 }
252                 return getCurrentSone(getCurrentSessionWithoutCreation(toadletContext));
253         }
254
255         /**
256          * Returns the currently logged in Sone.
257          *
258          * @param session
259          *            The session
260          * @return The currently logged in Sone, or {@code null} if no Sone is
261          *         currently logged in
262          */
263         private Sone getCurrentSone(Session session) {
264                 if (session == null) {
265                         return null;
266                 }
267                 String soneId = (String) session.getAttribute("Sone.CurrentSone");
268                 if (soneId == null) {
269                         return null;
270                 }
271                 return getCore().getLocalSone(soneId);
272         }
273
274         @Override
275         @Nullable
276         public Sone getCurrentSone(@Nonnull ToadletContext toadletContext, boolean createSession) {
277                 return createSession ? getCurrentSoneCreatingSession(toadletContext) : getCurrentSoneWithoutCreatingSession(toadletContext);
278         }
279
280         /**
281          * Sets the currently logged in Sone.
282          *
283          * @param toadletContext
284          *            The toadlet context
285          * @param sone
286          *            The Sone to set as currently logged in
287          */
288         @Override
289         public void setCurrentSone(@Nonnull ToadletContext toadletContext, @Nullable Sone sone) {
290                 Session session = getOrCreateCurrentSession(toadletContext);
291                 if (sone == null) {
292                         session.removeAttribute("Sone.CurrentSone");
293                 } else {
294                         session.setAttribute("Sone.CurrentSone", sone.getId());
295                 }
296         }
297
298         /**
299          * Returns the notification manager.
300          *
301          * @return The notification manager
302          */
303         public NotificationManager getNotifications() {
304                 return notificationManager;
305         }
306
307         @Nonnull
308         public Optional<Notification> getNotification(@Nonnull String notificationId) {
309                 return Optional.fromNullable(notificationManager.getNotification(notificationId));
310         }
311
312         @Nonnull
313         public Collection<Notification> getNotifications(@Nullable Sone currentSone) {
314                 return listNotificationFilter.filterNotifications(notificationManager.getNotifications(), currentSone);
315         }
316
317         public Translation getTranslation() {
318                 return translation;
319         }
320
321         /**
322          * Returns the session manager of the node.
323          *
324          * @return The node’s session manager
325          */
326         public SessionManager getSessionManager() {
327                 return sonePlugin.pluginRespirator().getSessionManager("Sone");
328         }
329
330         /**
331          * Returns the node’s form password.
332          *
333          * @return The form password
334          */
335         public String getFormPassword() {
336                 return formPassword;
337         }
338
339         @Nonnull
340         public Collection<Post> getNewPosts(@Nullable Sone currentSone) {
341                 Set<Post> allNewPosts = ImmutableSet.<Post> builder()
342                                 .addAll(newPostNotification.getElements())
343                                 .addAll(localPostNotification.getElements())
344                                 .build();
345                 return from(allNewPosts).filter(postVisibilityFilter.isVisible(currentSone)).toSet();
346         }
347
348         /**
349          * Returns the replies that have been announced as new in the
350          * {@link #newReplyNotification}.
351          *
352          * @return The new replies
353          */
354         public Set<PostReply> getNewReplies() {
355                 return ImmutableSet.<PostReply> builder().addAll(newReplyNotification.getElements()).addAll(localReplyNotification.getElements()).build();
356         }
357
358         @Nonnull
359         public Collection<PostReply> getNewReplies(@Nullable Sone currentSone) {
360                 Set<PostReply> allNewReplies = ImmutableSet.<PostReply>builder()
361                                 .addAll(newReplyNotification.getElements())
362                                 .addAll(localReplyNotification.getElements())
363                                 .build();
364                 return from(allNewReplies).filter(replyVisibilityFilter.isVisible(currentSone)).toSet();
365         }
366
367         //
368         // PRIVATE ACCESSORS
369         //
370
371         /**
372          * Returns whether the first start notification is currently displayed.
373          *
374          * @return {@code true} if the first-start notification is currently
375          *         displayed, {@code false} otherwise
376          */
377         private boolean hasFirstStartNotification() {
378                 return notificationManager.getNotification("first-start-notification") != null;
379         }
380
381         //
382         // ACTIONS
383         //
384
385         /**
386          * Starts the web interface and registers all toadlets.
387          */
388         public void start() {
389                 registerToadlets();
390         }
391
392         /**
393          * Stops the web interface and unregisters all toadlets.
394          */
395         public void stop() {
396                 pageToadletRegistry.unregisterToadlets();
397         }
398
399         //
400         // PRIVATE METHODS
401         //
402
403         /**
404          * Register all toadlets.
405          */
406         private void registerToadlets() {
407                 Template postTemplate = loaders.loadTemplate("/templates/include/viewPost.html");
408                 Template replyTemplate = loaders.loadTemplate("/templates/include/viewReply.html");
409                 Template openSearchTemplate = loaders.loadTemplate("/templates/xml/OpenSearch.xml");
410
411                 pageToadletRegistry.addPage(new RedirectPage<FreenetRequest>("", "index.html"));
412                 pageToadletRegistry.addPage(new IndexPage(this, loaders, templateRenderer, postVisibilityFilter));
413                 pageToadletRegistry.addPage(new NewPage(this, loaders, templateRenderer));
414                 pageToadletRegistry.addPage(new CreateSonePage(this, loaders, templateRenderer));
415                 pageToadletRegistry.addPage(new KnownSonesPage(this, loaders, templateRenderer));
416                 pageToadletRegistry.addPage(new EditProfilePage(this, loaders, templateRenderer));
417                 pageToadletRegistry.addPage(new EditProfileFieldPage(this, loaders, templateRenderer));
418                 pageToadletRegistry.addPage(new DeleteProfileFieldPage(this, loaders, templateRenderer));
419                 pageToadletRegistry.addPage(new CreatePostPage(this, loaders, templateRenderer));
420                 pageToadletRegistry.addPage(new CreateReplyPage(this, loaders, templateRenderer));
421                 pageToadletRegistry.addPage(new ViewSonePage(this, loaders, templateRenderer));
422                 pageToadletRegistry.addPage(new ViewPostPage(this, loaders, templateRenderer));
423                 pageToadletRegistry.addPage(new LikePage(this, loaders, templateRenderer));
424                 pageToadletRegistry.addPage(new UnlikePage(this, loaders, templateRenderer));
425                 pageToadletRegistry.addPage(new DeletePostPage(this, loaders, templateRenderer));
426                 pageToadletRegistry.addPage(new DeleteReplyPage(this, loaders, templateRenderer));
427                 pageToadletRegistry.addPage(new LockSonePage(this, loaders, templateRenderer));
428                 pageToadletRegistry.addPage(new UnlockSonePage(this, loaders, templateRenderer));
429                 pageToadletRegistry.addPage(new FollowSonePage(this, loaders, templateRenderer));
430                 pageToadletRegistry.addPage(new UnfollowSonePage(this, loaders, templateRenderer));
431                 pageToadletRegistry.addPage(new ImageBrowserPage(this, loaders, templateRenderer));
432                 pageToadletRegistry.addPage(new CreateAlbumPage(this, loaders, templateRenderer));
433                 pageToadletRegistry.addPage(new EditAlbumPage(this, loaders, templateRenderer));
434                 pageToadletRegistry.addPage(new DeleteAlbumPage(this, loaders, templateRenderer));
435                 pageToadletRegistry.addPage(new UploadImagePage(this, loaders, templateRenderer));
436                 pageToadletRegistry.addPage(new EditImagePage(this, loaders, templateRenderer));
437                 pageToadletRegistry.addPage(new DeleteImagePage(this, loaders, templateRenderer));
438                 pageToadletRegistry.addPage(new MarkAsKnownPage(this, loaders, templateRenderer));
439                 pageToadletRegistry.addPage(new BookmarkPage(this, loaders, templateRenderer));
440                 pageToadletRegistry.addPage(new UnbookmarkPage(this, loaders, templateRenderer));
441                 pageToadletRegistry.addPage(new BookmarksPage(this, loaders, templateRenderer));
442                 pageToadletRegistry.addPage(new SearchPage(this, loaders, templateRenderer));
443                 pageToadletRegistry.addPage(new DeleteSonePage(this, loaders, templateRenderer));
444                 pageToadletRegistry.addPage(new LoginPage(this, loaders, templateRenderer));
445                 pageToadletRegistry.addPage(new LogoutPage(this, loaders, templateRenderer));
446                 pageToadletRegistry.addPage(new OptionsPage(this, loaders, templateRenderer));
447                 pageToadletRegistry.addPage(new RescuePage(this, loaders, templateRenderer));
448                 pageToadletRegistry.addPage(new AboutPage(this, loaders, templateRenderer, new PluginVersion(SonePlugin.getPluginVersion()), new PluginYear(sonePlugin.getYear()), new PluginHomepage(sonePlugin.getHomepage())));
449                 pageToadletRegistry.addPage(new InvalidPage(this, loaders, templateRenderer));
450                 pageToadletRegistry.addPage(new NoPermissionPage(this, loaders, templateRenderer));
451                 pageToadletRegistry.addPage(new EmptyImageTitlePage(this, loaders, templateRenderer));
452                 pageToadletRegistry.addPage(new EmptyAlbumTitlePage(this, loaders, templateRenderer));
453                 pageToadletRegistry.addPage(new DismissNotificationPage(this, loaders, templateRenderer));
454                 pageToadletRegistry.addPage(new DebugPage(this, loaders, templateRenderer));
455                 pageToadletRegistry.addDebugPage(new MetricsPage(this, loaders, templateRenderer, metricRegistry));
456                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("css/", "/static/css/", "text/css"));
457                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("javascript/", "/static/javascript/", "text/javascript"));
458                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("images/", "/static/images/", "image/png"));
459                 pageToadletRegistry.addPage(new TemplatePage<FreenetRequest>("OpenSearch.xml", "application/opensearchdescription+xml", templateContextFactory, openSearchTemplate));
460                 pageToadletRegistry.addPage(new GetImagePage(this));
461                 pageToadletRegistry.addPage(new GetTranslationAjaxPage(this));
462                 pageToadletRegistry.addPage(new GetStatusAjaxPage(this, elementLoader, timeTextConverter, l10nFilter, TimeZone.getDefault()));
463                 pageToadletRegistry.addPage(new GetNotificationsAjaxPage(this));
464                 pageToadletRegistry.addPage(new DismissNotificationAjaxPage(this));
465                 pageToadletRegistry.addPage(new CreatePostAjaxPage(this));
466                 pageToadletRegistry.addPage(new CreateReplyAjaxPage(this));
467                 pageToadletRegistry.addPage(new GetReplyAjaxPage(this, replyTemplate));
468                 pageToadletRegistry.addPage(new GetPostAjaxPage(this, postTemplate));
469                 pageToadletRegistry.addPage(new GetLinkedElementAjaxPage(this, elementLoader, linkedElementRenderFilter));
470                 pageToadletRegistry.addPage(new GetTimesAjaxPage(this, timeTextConverter, l10nFilter, TimeZone.getDefault()));
471                 pageToadletRegistry.addPage(new MarkAsKnownAjaxPage(this));
472                 pageToadletRegistry.addPage(new DeletePostAjaxPage(this));
473                 pageToadletRegistry.addPage(new DeleteReplyAjaxPage(this));
474                 pageToadletRegistry.addPage(new LockSoneAjaxPage(this));
475                 pageToadletRegistry.addPage(new UnlockSoneAjaxPage(this));
476                 pageToadletRegistry.addPage(new FollowSoneAjaxPage(this));
477                 pageToadletRegistry.addPage(new UnfollowSoneAjaxPage(this));
478                 pageToadletRegistry.addPage(new EditAlbumAjaxPage(this));
479                 pageToadletRegistry.addPage(new EditImageAjaxPage(this, parserFilter, shortenFilter, renderFilter));
480                 pageToadletRegistry.addPage(new LikeAjaxPage(this));
481                 pageToadletRegistry.addPage(new UnlikeAjaxPage(this));
482                 pageToadletRegistry.addPage(new GetLikesAjaxPage(this));
483                 pageToadletRegistry.addPage(new BookmarkAjaxPage(this));
484                 pageToadletRegistry.addPage(new UnbookmarkAjaxPage(this));
485                 pageToadletRegistry.addPage(new EditProfileFieldAjaxPage(this));
486                 pageToadletRegistry.addPage(new DeleteProfileFieldAjaxPage(this));
487                 pageToadletRegistry.addPage(new MoveProfileFieldAjaxPage(this));
488
489                 pageToadletRegistry.registerToadlets();
490         }
491
492         /**
493          * Returns the Sone insert notification for the given Sone. If no
494          * notification for the given Sone exists, a new notification is created and
495          * cached.
496          *
497          * @param sone
498          *            The Sone to get the insert notification for
499          * @return The Sone insert notification
500          */
501         private TemplateNotification getSoneInsertNotification(Sone sone) {
502                 synchronized (soneInsertNotifications) {
503                         TemplateNotification templateNotification = soneInsertNotifications.get(sone);
504                         if (templateNotification == null) {
505                                 templateNotification = new TemplateNotification(loaders.loadTemplate("/templates/notify/soneInsertNotification.html"));
506                                 templateNotification.set("insertSone", sone);
507                                 soneInsertNotifications.put(sone, templateNotification);
508                         }
509                         return templateNotification;
510                 }
511         }
512
513         //
514         // EVENT HANDLERS
515         //
516
517         /**
518          * Notifies the web interface that a new {@link PostReply} was found.
519          *
520          * @param newPostReplyFoundEvent
521          *            The event
522          */
523         @Subscribe
524         public void newReplyFound(NewPostReplyFoundEvent newPostReplyFoundEvent) {
525                 PostReply reply = newPostReplyFoundEvent.getPostReply();
526                 boolean isLocal = reply.getSone().isLocal();
527                 if (isLocal) {
528                         localReplyNotification.add(reply);
529                         if (!hasFirstStartNotification()) {
530                                 notificationManager.addNotification(localReplyNotification);
531                         }
532                 }
533         }
534
535         @Subscribe
536         public void markPostKnown(MarkPostKnownEvent markPostKnownEvent) {
537                 removePost(markPostKnownEvent.getPost());
538         }
539
540         @Subscribe
541         public void markReplyKnown(MarkPostReplyKnownEvent markPostReplyKnownEvent) {
542                 removeReply(markPostReplyKnownEvent.getPostReply());
543         }
544
545         @Subscribe
546         public void postRemoved(PostRemovedEvent postRemovedEvent) {
547                 removePost(postRemovedEvent.getPost());
548         }
549
550         private void removePost(Post post) {
551                 newPostNotification.remove(post);
552         }
553
554         @Subscribe
555         public void replyRemoved(PostReplyRemovedEvent postReplyRemovedEvent) {
556                 removeReply(postReplyRemovedEvent.getPostReply());
557         }
558
559         private void removeReply(PostReply reply) {
560                 localReplyNotification.remove(reply);
561         }
562
563         /**
564          * Notifies the web interface that a {@link Sone} is being inserted.
565          *
566          * @param soneInsertingEvent
567          *            The event
568          */
569         @Subscribe
570         public void soneInserting(SoneInsertingEvent soneInsertingEvent) {
571                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertingEvent.getSone());
572                 soneInsertNotification.set("soneStatus", "inserting");
573                 if (soneInsertingEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
574                         notificationManager.addNotification(soneInsertNotification);
575                 }
576         }
577
578         /**
579          * Notifies the web interface that a {@link Sone} was inserted.
580          *
581          * @param soneInsertedEvent
582          *            The event
583          */
584         @Subscribe
585         public void soneInserted(SoneInsertedEvent soneInsertedEvent) {
586                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertedEvent.getSone());
587                 soneInsertNotification.set("soneStatus", "inserted");
588                 soneInsertNotification.set("insertDuration", soneInsertedEvent.getInsertDuration() / 1000);
589                 if (soneInsertedEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
590                         notificationManager.addNotification(soneInsertNotification);
591                 }
592         }
593
594         /**
595          * Notifies the web interface that a {@link Sone} insert was aborted.
596          *
597          * @param soneInsertAbortedEvent
598          *            The event
599          */
600         @Subscribe
601         public void soneInsertAborted(SoneInsertAbortedEvent soneInsertAbortedEvent) {
602                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertAbortedEvent.getSone());
603                 soneInsertNotification.set("soneStatus", "insert-aborted");
604                 soneInsertNotification.set("insert-error", soneInsertAbortedEvent.getCause());
605                 if (soneInsertAbortedEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
606                         notificationManager.addNotification(soneInsertNotification);
607                 }
608         }
609
610         @Subscribe
611         public void debugActivated(@Nonnull DebugActivatedEvent debugActivatedEvent) {
612                 pageToadletRegistry.activateDebugMode();
613         }
614
615 }