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