♻️ Move image-insert notifications 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          * Sets whether the current start of the plugin is the first start. It is
391          * considered a first start if the configuration file does not exist.
392          *
393          * @param firstStart
394          *            {@code true} if no configuration file existed when Sone was
395          *            loaded, {@code false} otherwise
396          */
397         public void setFirstStart(boolean firstStart) {
398                 if (firstStart) {
399                         Template firstStartNotificationTemplate = loaders.loadTemplate("/templates/notify/firstStartNotification.html");
400                         Notification firstStartNotification = new TemplateNotification("first-start-notification", firstStartNotificationTemplate);
401                         notificationManager.addNotification(firstStartNotification);
402                 }
403         }
404
405         /**
406          * Sets whether Sone was started with a fresh configuration file.
407          *
408          * @param newConfig
409          *            {@code true} if Sone was started with a fresh configuration,
410          *            {@code false} if the existing configuration could be read
411          */
412         public void setNewConfig(boolean newConfig) {
413                 if (newConfig && !hasFirstStartNotification()) {
414                         Template configNotReadNotificationTemplate = loaders.loadTemplate("/templates/notify/configNotReadNotification.html");
415                         Notification configNotReadNotification = new TemplateNotification("config-not-read-notification", configNotReadNotificationTemplate);
416                         notificationManager.addNotification(configNotReadNotification);
417                 }
418         }
419
420         //
421         // PRIVATE ACCESSORS
422         //
423
424         /**
425          * Returns whether the first start notification is currently displayed.
426          *
427          * @return {@code true} if the first-start notification is currently
428          *         displayed, {@code false} otherwise
429          */
430         private boolean hasFirstStartNotification() {
431                 return notificationManager.getNotification("first-start-notification") != null;
432         }
433
434         //
435         // ACTIONS
436         //
437
438         /**
439          * Starts the web interface and registers all toadlets.
440          */
441         public void start() {
442                 registerToadlets();
443
444                 /* notification templates. */
445                 Template startupNotificationTemplate = loaders.loadTemplate("/templates/notify/startupNotification.html");
446
447                 final TemplateNotification startupNotification = new TemplateNotification("startup-notification", startupNotificationTemplate);
448                 notificationManager.addNotification(startupNotification);
449
450                 ticker.schedule(new Runnable() {
451
452                         @Override
453                         public void run() {
454                                 startupNotification.dismiss();
455                         }
456                 }, 2, TimeUnit.MINUTES);
457
458                 Template wotMissingNotificationTemplate = loaders.loadTemplate("/templates/notify/wotMissingNotification.html");
459                 final TemplateNotification wotMissingNotification = new TemplateNotification("wot-missing-notification", wotMissingNotificationTemplate);
460                 ticker.scheduleAtFixedRate(new Runnable() {
461
462                         @Override
463                         @SuppressWarnings("synthetic-access")
464                         public void run() {
465                                 if (getCore().getIdentityManager().isConnected()) {
466                                         wotMissingNotification.dismiss();
467                                 } else {
468                                         notificationManager.addNotification(wotMissingNotification);
469                                 }
470                         }
471
472                 }, 15, 15, TimeUnit.SECONDS);
473         }
474
475         /**
476          * Stops the web interface and unregisters all toadlets.
477          */
478         public void stop() {
479                 pageToadletRegistry.unregisterToadlets();
480                 ticker.shutdownNow();
481         }
482
483         //
484         // PRIVATE METHODS
485         //
486
487         /**
488          * Register all toadlets.
489          */
490         private void registerToadlets() {
491                 Template postTemplate = loaders.loadTemplate("/templates/include/viewPost.html");
492                 Template replyTemplate = loaders.loadTemplate("/templates/include/viewReply.html");
493                 Template openSearchTemplate = loaders.loadTemplate("/templates/xml/OpenSearch.xml");
494
495                 pageToadletRegistry.addPage(new RedirectPage<FreenetRequest>("", "index.html"));
496                 pageToadletRegistry.addPage(new IndexPage(this, loaders, templateRenderer, postVisibilityFilter));
497                 pageToadletRegistry.addPage(new NewPage(this, loaders, templateRenderer));
498                 pageToadletRegistry.addPage(new CreateSonePage(this, loaders, templateRenderer));
499                 pageToadletRegistry.addPage(new KnownSonesPage(this, loaders, templateRenderer));
500                 pageToadletRegistry.addPage(new EditProfilePage(this, loaders, templateRenderer));
501                 pageToadletRegistry.addPage(new EditProfileFieldPage(this, loaders, templateRenderer));
502                 pageToadletRegistry.addPage(new DeleteProfileFieldPage(this, loaders, templateRenderer));
503                 pageToadletRegistry.addPage(new CreatePostPage(this, loaders, templateRenderer));
504                 pageToadletRegistry.addPage(new CreateReplyPage(this, loaders, templateRenderer));
505                 pageToadletRegistry.addPage(new ViewSonePage(this, loaders, templateRenderer));
506                 pageToadletRegistry.addPage(new ViewPostPage(this, loaders, templateRenderer));
507                 pageToadletRegistry.addPage(new LikePage(this, loaders, templateRenderer));
508                 pageToadletRegistry.addPage(new UnlikePage(this, loaders, templateRenderer));
509                 pageToadletRegistry.addPage(new DeletePostPage(this, loaders, templateRenderer));
510                 pageToadletRegistry.addPage(new DeleteReplyPage(this, loaders, templateRenderer));
511                 pageToadletRegistry.addPage(new LockSonePage(this, loaders, templateRenderer));
512                 pageToadletRegistry.addPage(new UnlockSonePage(this, loaders, templateRenderer));
513                 pageToadletRegistry.addPage(new FollowSonePage(this, loaders, templateRenderer));
514                 pageToadletRegistry.addPage(new UnfollowSonePage(this, loaders, templateRenderer));
515                 pageToadletRegistry.addPage(new ImageBrowserPage(this, loaders, templateRenderer));
516                 pageToadletRegistry.addPage(new CreateAlbumPage(this, loaders, templateRenderer));
517                 pageToadletRegistry.addPage(new EditAlbumPage(this, loaders, templateRenderer));
518                 pageToadletRegistry.addPage(new DeleteAlbumPage(this, loaders, templateRenderer));
519                 pageToadletRegistry.addPage(new UploadImagePage(this, loaders, templateRenderer));
520                 pageToadletRegistry.addPage(new EditImagePage(this, loaders, templateRenderer));
521                 pageToadletRegistry.addPage(new DeleteImagePage(this, loaders, templateRenderer));
522                 pageToadletRegistry.addPage(new MarkAsKnownPage(this, loaders, templateRenderer));
523                 pageToadletRegistry.addPage(new BookmarkPage(this, loaders, templateRenderer));
524                 pageToadletRegistry.addPage(new UnbookmarkPage(this, loaders, templateRenderer));
525                 pageToadletRegistry.addPage(new BookmarksPage(this, loaders, templateRenderer));
526                 pageToadletRegistry.addPage(new SearchPage(this, loaders, templateRenderer));
527                 pageToadletRegistry.addPage(new DeleteSonePage(this, loaders, templateRenderer));
528                 pageToadletRegistry.addPage(new LoginPage(this, loaders, templateRenderer));
529                 pageToadletRegistry.addPage(new LogoutPage(this, loaders, templateRenderer));
530                 pageToadletRegistry.addPage(new OptionsPage(this, loaders, templateRenderer));
531                 pageToadletRegistry.addPage(new RescuePage(this, loaders, templateRenderer));
532                 pageToadletRegistry.addPage(new AboutPage(this, loaders, templateRenderer, new PluginVersion(SonePlugin.getPluginVersion()), new PluginYear(sonePlugin.getYear()), new PluginHomepage(sonePlugin.getHomepage())));
533                 pageToadletRegistry.addPage(new InvalidPage(this, loaders, templateRenderer));
534                 pageToadletRegistry.addPage(new NoPermissionPage(this, loaders, templateRenderer));
535                 pageToadletRegistry.addPage(new EmptyImageTitlePage(this, loaders, templateRenderer));
536                 pageToadletRegistry.addPage(new EmptyAlbumTitlePage(this, loaders, templateRenderer));
537                 pageToadletRegistry.addPage(new DismissNotificationPage(this, loaders, templateRenderer));
538                 pageToadletRegistry.addPage(new DebugPage(this, loaders, templateRenderer));
539                 pageToadletRegistry.addDebugPage(new MetricsPage(this, loaders, templateRenderer, metricRegistry));
540                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("css/", "/static/css/", "text/css"));
541                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("javascript/", "/static/javascript/", "text/javascript"));
542                 pageToadletRegistry.addPage(loaders.<FreenetRequest>loadStaticPage("images/", "/static/images/", "image/png"));
543                 pageToadletRegistry.addPage(new TemplatePage<FreenetRequest>("OpenSearch.xml", "application/opensearchdescription+xml", templateContextFactory, openSearchTemplate));
544                 pageToadletRegistry.addPage(new GetImagePage(this));
545                 pageToadletRegistry.addPage(new GetTranslationAjaxPage(this));
546                 pageToadletRegistry.addPage(new GetStatusAjaxPage(this, elementLoader, timeTextConverter, l10nFilter, TimeZone.getDefault()));
547                 pageToadletRegistry.addPage(new GetNotificationsAjaxPage(this));
548                 pageToadletRegistry.addPage(new DismissNotificationAjaxPage(this));
549                 pageToadletRegistry.addPage(new CreatePostAjaxPage(this));
550                 pageToadletRegistry.addPage(new CreateReplyAjaxPage(this));
551                 pageToadletRegistry.addPage(new GetReplyAjaxPage(this, replyTemplate));
552                 pageToadletRegistry.addPage(new GetPostAjaxPage(this, postTemplate));
553                 pageToadletRegistry.addPage(new GetLinkedElementAjaxPage(this, elementLoader, linkedElementRenderFilter));
554                 pageToadletRegistry.addPage(new GetTimesAjaxPage(this, timeTextConverter, l10nFilter, TimeZone.getDefault()));
555                 pageToadletRegistry.addPage(new MarkAsKnownAjaxPage(this));
556                 pageToadletRegistry.addPage(new DeletePostAjaxPage(this));
557                 pageToadletRegistry.addPage(new DeleteReplyAjaxPage(this));
558                 pageToadletRegistry.addPage(new LockSoneAjaxPage(this));
559                 pageToadletRegistry.addPage(new UnlockSoneAjaxPage(this));
560                 pageToadletRegistry.addPage(new FollowSoneAjaxPage(this));
561                 pageToadletRegistry.addPage(new UnfollowSoneAjaxPage(this));
562                 pageToadletRegistry.addPage(new EditAlbumAjaxPage(this));
563                 pageToadletRegistry.addPage(new EditImageAjaxPage(this, parserFilter, shortenFilter, renderFilter));
564                 pageToadletRegistry.addPage(new LikeAjaxPage(this));
565                 pageToadletRegistry.addPage(new UnlikeAjaxPage(this));
566                 pageToadletRegistry.addPage(new GetLikesAjaxPage(this));
567                 pageToadletRegistry.addPage(new BookmarkAjaxPage(this));
568                 pageToadletRegistry.addPage(new UnbookmarkAjaxPage(this));
569                 pageToadletRegistry.addPage(new EditProfileFieldAjaxPage(this));
570                 pageToadletRegistry.addPage(new DeleteProfileFieldAjaxPage(this));
571                 pageToadletRegistry.addPage(new MoveProfileFieldAjaxPage(this));
572
573                 pageToadletRegistry.registerToadlets();
574         }
575
576         /**
577          * Returns all {@link Sone#isLocal() local Sone}s that are referenced by
578          * {@link SonePart}s in the given text (after parsing it using
579          * {@link SoneTextParser}).
580          *
581          * @param text
582          *            The text to parse
583          * @return All mentioned local Sones
584          */
585         private Collection<Sone> getMentionedSones(String text) {
586                 /* we need no context to find mentioned Sones. */
587                 Set<Sone> mentionedSones = new HashSet<>();
588                 for (Part part : soneTextParser.parse(text, null)) {
589                         if (part instanceof SonePart) {
590                                 mentionedSones.add(((SonePart) part).getSone());
591                         }
592                 }
593                 return Collections2.filter(mentionedSones, Sone.LOCAL_SONE_FILTER);
594         }
595
596         /**
597          * Returns the Sone insert notification for the given Sone. If no
598          * notification for the given Sone exists, a new notification is created and
599          * cached.
600          *
601          * @param sone
602          *            The Sone to get the insert notification for
603          * @return The Sone insert notification
604          */
605         private TemplateNotification getSoneInsertNotification(Sone sone) {
606                 synchronized (soneInsertNotifications) {
607                         TemplateNotification templateNotification = soneInsertNotifications.get(sone);
608                         if (templateNotification == null) {
609                                 templateNotification = new TemplateNotification(loaders.loadTemplate("/templates/notify/soneInsertNotification.html"));
610                                 templateNotification.set("insertSone", sone);
611                                 soneInsertNotifications.put(sone, templateNotification);
612                         }
613                         return templateNotification;
614                 }
615         }
616
617         private boolean localSoneMentionedInNewPostOrReply(Post post) {
618                 if (!post.getSone().isLocal()) {
619                         if (!getMentionedSones(post.getText()).isEmpty() && !post.isKnown()) {
620                                 return true;
621                         }
622                 }
623                 for (PostReply postReply : getCore().getReplies(post.getId())) {
624                         if (postReply.getSone().isLocal()) {
625                                 continue;
626                         }
627                         if (!getMentionedSones(postReply.getText()).isEmpty() && !postReply.isKnown()) {
628                                 return true;
629                         }
630                 }
631                 return false;
632         }
633
634         //
635         // EVENT HANDLERS
636         //
637
638         /**
639          * Notifies the web interface that a new {@link Post} was found.
640          *
641          * @param newPostFoundEvent
642          *            The event
643          */
644         @Subscribe
645         public void newPostFound(NewPostFoundEvent newPostFoundEvent) {
646                 Post post = newPostFoundEvent.getPost();
647                 boolean isLocal = post.getSone().isLocal();
648                 if (!hasFirstStartNotification()) {
649                         if (!getMentionedSones(post.getText()).isEmpty() && !isLocal) {
650                                 mentionNotification.add(post);
651                                 notificationManager.addNotification(mentionNotification);
652                         }
653                 }
654         }
655
656         /**
657          * Notifies the web interface that a new {@link PostReply} was found.
658          *
659          * @param newPostReplyFoundEvent
660          *            The event
661          */
662         @Subscribe
663         public void newReplyFound(NewPostReplyFoundEvent newPostReplyFoundEvent) {
664                 PostReply reply = newPostReplyFoundEvent.getPostReply();
665                 boolean isLocal = reply.getSone().isLocal();
666                 if (isLocal) {
667                         localReplyNotification.add(reply);
668                 } else {
669                         newReplyNotification.add(reply);
670                 }
671                 if (!hasFirstStartNotification()) {
672                         notificationManager.addNotification(isLocal ? localReplyNotification : newReplyNotification);
673                         if (reply.getPost().isPresent() && localSoneMentionedInNewPostOrReply(reply.getPost().get())) {
674                                 mentionNotification.add(reply.getPost().get());
675                                 notificationManager.addNotification(mentionNotification);
676                         }
677                 } else {
678                         getCore().markReplyKnown(reply);
679                 }
680         }
681
682         @Subscribe
683         public void markPostKnown(MarkPostKnownEvent markPostKnownEvent) {
684                 removePost(markPostKnownEvent.getPost());
685         }
686
687         @Subscribe
688         public void markReplyKnown(MarkPostReplyKnownEvent markPostReplyKnownEvent) {
689                 removeReply(markPostReplyKnownEvent.getPostReply());
690         }
691
692         @Subscribe
693         public void postRemoved(PostRemovedEvent postRemovedEvent) {
694                 removePost(postRemovedEvent.getPost());
695         }
696
697         private void removePost(Post post) {
698                 newPostNotification.remove(post);
699                 if (!localSoneMentionedInNewPostOrReply(post)) {
700                         mentionNotification.remove(post);
701                 }
702         }
703
704         @Subscribe
705         public void replyRemoved(PostReplyRemovedEvent postReplyRemovedEvent) {
706                 removeReply(postReplyRemovedEvent.getPostReply());
707         }
708
709         private void removeReply(PostReply reply) {
710                 newReplyNotification.remove(reply);
711                 localReplyNotification.remove(reply);
712                 if (reply.getPost().isPresent() && !localSoneMentionedInNewPostOrReply(reply.getPost().get())) {
713                         mentionNotification.remove(reply.getPost().get());
714                 }
715         }
716
717         /**
718          * Notifies the web interface that a {@link Sone} is being inserted.
719          *
720          * @param soneInsertingEvent
721          *            The event
722          */
723         @Subscribe
724         public void soneInserting(SoneInsertingEvent soneInsertingEvent) {
725                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertingEvent.getSone());
726                 soneInsertNotification.set("soneStatus", "inserting");
727                 if (soneInsertingEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
728                         notificationManager.addNotification(soneInsertNotification);
729                 }
730         }
731
732         /**
733          * Notifies the web interface that a {@link Sone} was inserted.
734          *
735          * @param soneInsertedEvent
736          *            The event
737          */
738         @Subscribe
739         public void soneInserted(SoneInsertedEvent soneInsertedEvent) {
740                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertedEvent.getSone());
741                 soneInsertNotification.set("soneStatus", "inserted");
742                 soneInsertNotification.set("insertDuration", soneInsertedEvent.getInsertDuration() / 1000);
743                 if (soneInsertedEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
744                         notificationManager.addNotification(soneInsertNotification);
745                 }
746         }
747
748         /**
749          * Notifies the web interface that a {@link Sone} insert was aborted.
750          *
751          * @param soneInsertAbortedEvent
752          *            The event
753          */
754         @Subscribe
755         public void soneInsertAborted(SoneInsertAbortedEvent soneInsertAbortedEvent) {
756                 TemplateNotification soneInsertNotification = getSoneInsertNotification(soneInsertAbortedEvent.getSone());
757                 soneInsertNotification.set("soneStatus", "insert-aborted");
758                 soneInsertNotification.set("insert-error", soneInsertAbortedEvent.getCause());
759                 if (soneInsertAbortedEvent.getSone().getOptions().isSoneInsertNotificationEnabled()) {
760                         notificationManager.addNotification(soneInsertNotification);
761                 }
762         }
763
764         @Subscribe
765         public void debugActivated(@Nonnull DebugActivatedEvent debugActivatedEvent) {
766                 pageToadletRegistry.activateDebugMode();
767         }
768
769 }