]> BookStack Code Mirror - bookstack/commitdiff
Notifications: Added logic and classes for remaining notification types
authorDan Brown <redacted>
Sat, 5 Aug 2023 13:19:23 +0000 (14:19 +0100)
committerDan Brown <redacted>
Sat, 5 Aug 2023 13:19:23 +0000 (14:19 +0100)
app/Activity/Models/Comment.php
app/Activity/Notifications/Handlers/BaseNotificationHandler.php [new file with mode: 0644]
app/Activity/Notifications/Handlers/CommentCreationNotificationHandler.php
app/Activity/Notifications/Handlers/PageCreationNotificationHandler.php
app/Activity/Notifications/Handlers/PageUpdateNotificationHandler.php
app/Activity/Notifications/Messages/CommentCreationNotification.php [new file with mode: 0644]
app/Activity/Notifications/Messages/PageUpdateNotification.php [new file with mode: 0644]
app/Activity/Tools/EntityWatchers.php

index 7aea6124ad6f7fae602a9c970a60791fbb4991e5..72098a3c3c34d3974d3bfb09bd6a52d3f9629c53 100644 (file)
@@ -32,6 +32,14 @@ class Comment extends Model implements Loggable
         return $this->morphTo('entity');
     }
 
+    /**
+     * Get the parent comment this is in reply to (if existing).
+     */
+    public function parent()
+    {
+        return $this->belongsTo(Comment::class);
+    }
+
     /**
      * Check if a comment has been updated since creation.
      */
diff --git a/app/Activity/Notifications/Handlers/BaseNotificationHandler.php b/app/Activity/Notifications/Handlers/BaseNotificationHandler.php
new file mode 100644 (file)
index 0000000..e0b3f3c
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+
+namespace BookStack\Activity\Notifications\Handlers;
+
+use BookStack\Activity\Notifications\Messages\BaseActivityNotification;
+use BookStack\Entities\Models\Entity;
+use BookStack\Permissions\PermissionApplicator;
+use BookStack\Users\Models\User;
+
+abstract class BaseNotificationHandler implements NotificationHandler
+{
+    public function __construct(
+        protected PermissionApplicator $permissionApplicator
+    ) {
+    }
+
+    /**
+     * @param class-string<BaseActivityNotification> $notification
+     * @param int[] $userIds
+     */
+    protected function sendNotificationToUserIds(string $notification, array $userIds, User $initiator, Entity $relatedModel): void
+    {
+        $users = User::query()->whereIn('id', array_unique($userIds))->get();
+
+        foreach ($users as $user) {
+            // Prevent sending to the user that initiated the activity
+            if ($user->id === $initiator->id) {
+                continue;
+            }
+
+            // Prevent sending of the user does not have notification permissions
+            if (!$user->can('receive-notifications')) {
+                continue;
+            }
+
+            // Prevent sending if the user does not have access to the related content
+            if (!$this->permissionApplicator->checkOwnableUserAccess($relatedModel, 'view')) {
+                continue;
+            }
+
+            // Send the notification
+            $user->notify(new $notification($relatedModel, $initiator));
+        }
+    }
+}
index 67c30433903a0cf6e8eba10c58c934245323fcf4..27d61307a2e215400672bb52225438b7b3baa521 100644 (file)
@@ -2,13 +2,44 @@
 
 namespace BookStack\Activity\Notifications\Handlers;
 
+use BookStack\Activity\Models\Comment;
 use BookStack\Activity\Models\Loggable;
+use BookStack\Activity\Notifications\Messages\CommentCreationNotification;
+use BookStack\Activity\Tools\EntityWatchers;
+use BookStack\Activity\WatchLevels;
+use BookStack\Settings\UserNotificationPreferences;
 use BookStack\Users\Models\User;
 
-class CommentCreationNotificationHandler implements NotificationHandler
+class CommentCreationNotificationHandler extends BaseNotificationHandler
 {
     public function handle(string $activityType, Loggable|string $detail, User $user): void
     {
-        // TODO
+        if (!($detail instanceof Comment)) {
+            throw new \InvalidArgumentException("Detail for comment creation notifications must be a comment");
+        }
+
+        // Main watchers
+        $page = $detail->entity;
+        $watchers = new EntityWatchers($page, WatchLevels::COMMENTS);
+        $watcherIds = $watchers->getWatcherUserIds();
+
+        // Page owner if user preferences allow
+        if (!$watchers->isUserIgnoring($detail->owned_by) && $detail->ownedBy) {
+            $userNotificationPrefs = new UserNotificationPreferences($detail->ownedBy);
+            if ($userNotificationPrefs->notifyOnOwnPageComments()) {
+                $watcherIds[] = $detail->owned_by;
+            }
+        }
+
+        // Parent comment creator if preferences allow
+        $parentComment = $detail->parent()->first();
+        if ($parentComment && !$watchers->isUserIgnoring($parentComment->created_by) && $parentComment->createdBy) {
+            $parentCommenterNotificationsPrefs = new UserNotificationPreferences($parentComment->createdBy);
+            if ($parentCommenterNotificationsPrefs->notifyOnCommentReplies()) {
+                $watcherIds[] = $parentComment->created_by;
+            }
+        }
+
+        $this->sendNotificationToUserIds(CommentCreationNotification::class, $watcherIds, $user, $page);
     }
 }
index 8f19b3558a0fa18fff6a06398addb72528764735..e9aca2f231fae7509275db3b5ad0935149fc5d5d 100644 (file)
@@ -7,38 +7,17 @@ use BookStack\Activity\Notifications\Messages\PageCreationNotification;
 use BookStack\Activity\Tools\EntityWatchers;
 use BookStack\Activity\WatchLevels;
 use BookStack\Entities\Models\Page;
-use BookStack\Permissions\PermissionApplicator;
 use BookStack\Users\Models\User;
 
-class PageCreationNotificationHandler implements NotificationHandler
+class PageCreationNotificationHandler extends BaseNotificationHandler
 {
     public function handle(string $activityType, Loggable|string $detail, User $user): void
     {
         if (!($detail instanceof Page)) {
             throw new \InvalidArgumentException("Detail for page create notifications must be a page");
         }
-        // No user-level preferences to care about here.
-        // Possible Scenarios:
-        // ✅ User watching parent chapter
-        // ✅ User watching parent book
-        // ❌ User ignoring parent book
-        // ❌ User ignoring parent chapter
-        // ❌ User watching parent book, ignoring chapter
-        // ✅ User watching parent book, watching chapter
-        // ❌ User ignoring parent book, ignoring chapter
-        // ✅ User ignoring parent book, watching chapter
 
-        // Get all relevant watchers
         $watchers = new EntityWatchers($detail, WatchLevels::NEW);
-        $users = User::query()->whereIn('id', $watchers->getWatcherUserIds())->get();
-
-        // TODO - Clean this up, likely abstract to base class
-        // TODO - Prevent sending to current user
-        $permissions = app()->make(PermissionApplicator::class);
-        foreach ($users as $user) {
-            if ($user->can('receive-notifications') && $permissions->checkOwnableUserAccess($detail, 'view')) {
-                $user->notify(new PageCreationNotification($detail, $user));
-            }
-        }
+        $this->sendNotificationToUserIds(PageCreationNotification::class, $watchers->getWatcherUserIds(), $user, $detail);
     }
 }
index bbd189d52b19bc07e0facd4ae04ca3f8d4197726..5a2bf4e9cddfe89cf86d9b229c02a39155c68715 100644 (file)
@@ -3,12 +3,31 @@
 namespace BookStack\Activity\Notifications\Handlers;
 
 use BookStack\Activity\Models\Loggable;
+use BookStack\Activity\Notifications\Messages\PageUpdateNotification;
+use BookStack\Activity\Tools\EntityWatchers;
+use BookStack\Activity\WatchLevels;
+use BookStack\Entities\Models\Page;
+use BookStack\Settings\UserNotificationPreferences;
 use BookStack\Users\Models\User;
 
-class PageUpdateNotificationHandler implements NotificationHandler
+class PageUpdateNotificationHandler extends BaseNotificationHandler
 {
     public function handle(string $activityType, Loggable|string $detail, User $user): void
     {
-        // TODO
+        if (!($detail instanceof Page)) {
+            throw new \InvalidArgumentException("Detail for page update notifications must be a page");
+        }
+
+        $watchers = new EntityWatchers($detail, WatchLevels::UPDATES);
+        $watcherIds = $watchers->getWatcherUserIds();
+
+        if (!$watchers->isUserIgnoring($detail->owned_by) && $detail->ownedBy) {
+            $userNotificationPrefs = new UserNotificationPreferences($detail->ownedBy);
+            if ($userNotificationPrefs->notifyOnOwnPageChanges()) {
+                $watcherIds[] = $detail->owned_by;
+            }
+        }
+
+        $this->sendNotificationToUserIds(PageUpdateNotification::class, $watcherIds, $user, $detail);
     }
 }
diff --git a/app/Activity/Notifications/Messages/CommentCreationNotification.php b/app/Activity/Notifications/Messages/CommentCreationNotification.php
new file mode 100644 (file)
index 0000000..817eb7b
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+namespace BookStack\Activity\Notifications\Messages;
+
+use BookStack\Activity\Models\Comment;
+use BookStack\Activity\Notifications\LinkedMailMessageLine;
+use BookStack\Entities\Models\Page;
+use Illuminate\Notifications\Messages\MailMessage;
+
+class CommentCreationNotification extends BaseActivityNotification
+{
+    public function toMail(mixed $notifiable): MailMessage
+    {
+        /** @var Comment $comment */
+        $comment = $this->detail;
+        /** @var Page $page */
+        $page = $comment->entity;
+
+        return (new MailMessage())
+            ->subject("New Comment on Page: " . $page->getShortName())
+            ->line("A user has commented on a page in " . setting('app-name') . ':')
+            ->line("Page Name: " . $page->name)
+            ->line("Commenter: " . $this->user->name)
+            ->line("Comment: " . strip_tags($comment->html))
+            ->action('View Comment', $page->getUrl('#comment' . $comment->local_id))
+            ->line(new LinkedMailMessageLine(
+                url('/preferences/notifications'),
+                'This notification was sent to you because :link cover this type of activity for this item.',
+                'your notification preferences',
+            ));
+    }
+}
diff --git a/app/Activity/Notifications/Messages/PageUpdateNotification.php b/app/Activity/Notifications/Messages/PageUpdateNotification.php
new file mode 100644 (file)
index 0000000..f29f50d
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+
+namespace BookStack\Activity\Notifications\Messages;
+
+use BookStack\Activity\Notifications\LinkedMailMessageLine;
+use BookStack\Entities\Models\Page;
+use Illuminate\Notifications\Messages\MailMessage;
+
+class PageUpdateNotification extends BaseActivityNotification
+{
+    public function toMail(mixed $notifiable): MailMessage
+    {
+        /** @var Page $page */
+        $page = $this->detail;
+
+        return (new MailMessage())
+            ->subject("Updated Page: " . $page->getShortName())
+            ->line("A page has been updated in " . setting('app-name') . ':')
+            ->line("Page Name: " . $page->name)
+            ->line("Updated By: " . $this->user->name)
+            ->line("To prevent a mass of notifications, for a while you won't be sent notifications for further edits to this page by the same editor.")
+            ->action('View Page', $page->getUrl())
+            ->line(new LinkedMailMessageLine(
+                url('/preferences/notifications'),
+                'This notification was sent to you because :link cover this type of activity for this item.',
+                'your notification preferences',
+            ));
+    }
+}
index 38ba8c591e444ac05162a09219e736ed7d808682..1ab53cb1ce0ba1ac13852ebb705b3febe049bd78 100644 (file)
@@ -32,6 +32,11 @@ class EntityWatchers
         return $this->watchers;
     }
 
+    public function isUserIgnoring(int $userId): bool
+    {
+        return in_array($userId, $this->ignorers);
+    }
+
     protected function build(): void
     {
         $watches = $this->getRelevantWatches();