]> BookStack Code Mirror - bookstack/commitdiff
Notifications: Got core notification logic working for new pages
authorDan Brown <redacted>
Fri, 4 Aug 2023 15:51:29 +0000 (16:51 +0100)
committerDan Brown <redacted>
Fri, 4 Aug 2023 15:51:29 +0000 (16:51 +0100)
Also rolled out watch UI to chapter and page views

app/Activity/Notifications/Handlers/PageCreationNotificationHandler.php
app/Activity/Tools/EntityWatchers.php
app/Entities/Controllers/ChapterController.php
app/Entities/Controllers/PageController.php
resources/views/books/show.blade.php
resources/views/chapters/show.blade.php
resources/views/entities/watch-controls.blade.php
resources/views/pages/show.blade.php

index a61df48ae4444c5e3d7ae374aa93247e9f1cb5a9..8f19b3558a0fa18fff6a06398addb72528764735 100644 (file)
@@ -3,17 +3,20 @@
 namespace BookStack\Activity\Notifications\Handlers;
 
 use BookStack\Activity\Models\Loggable;
-use BookStack\Activity\Models\Watch;
+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
 {
     public function handle(string $activityType, Loggable|string $detail, User $user): void
     {
-        // TODO
-
+        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
@@ -27,8 +30,15 @@ class PageCreationNotificationHandler implements NotificationHandler
 
         // Get all relevant watchers
         $watchers = new EntityWatchers($detail, WatchLevels::NEW);
+        $users = User::query()->whereIn('id', $watchers->getWatcherUserIds())->get();
 
-        // TODO - need to check entity visibility and receive-notifications permissions.
-        //   Maybe abstract this to a generic late-stage filter?
+        // 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));
+            }
+        }
     }
 }
index 20375ef454d9bb108e114d9e2f016c2a5f348a84..38ba8c591e444ac05162a09219e736ed7d808682 100644 (file)
@@ -10,7 +10,14 @@ use Illuminate\Database\Eloquent\Builder;
 
 class EntityWatchers
 {
+    /**
+     * @var int[]
+     */
     protected array $watchers = [];
+
+    /**
+     * @var int[]
+     */
     protected array $ignorers = [];
 
     public function __construct(
@@ -20,16 +27,35 @@ class EntityWatchers
         $this->build();
     }
 
+    public function getWatcherUserIds(): array
+    {
+        return $this->watchers;
+    }
+
     protected function build(): void
     {
         $watches = $this->getRelevantWatches();
 
-        // TODO - De-dupe down watches per-user across entity types
-        // so we end up with [user_id => status] values
-        // then filter to current watch level, considering ignores,
-        // then populate the class watchers/ignores with ids.
+        // Sort before de-duping, so that the order looped below follows book -> chapter -> page ordering
+        usort($watches, function (Watch $watchA, Watch $watchB) {
+            $entityTypeDiff = $watchA->watchable_type <=> $watchB->watchable_type;
+            return $entityTypeDiff === 0 ? ($watchA->user_id <=> $watchB->user_id) : $entityTypeDiff;
+        });
+
+        // De-dupe by user id to get their most relevant level
+        $levelByUserId = [];
+        foreach ($watches as $watch) {
+            $levelByUserId[$watch->user_id] = $watch->level;
+        }
+
+        // Populate the class arrays
+        $this->watchers = array_keys(array_filter($levelByUserId, fn(int $level) => $level >= $this->watchLevel));
+        $this->ignorers = array_keys(array_filter($levelByUserId, fn(int $level) => $level === 0));
     }
 
+    /**
+     * @return Watch[]
+     */
     protected function getRelevantWatches(): array
     {
         /** @var Entity[] $entitiesInvolved */
@@ -49,7 +75,7 @@ class EntityWatchers
         });
 
         return $query->get([
-           'level', 'watchable_id', 'watchable_type', 'user_id'
+            'level', 'watchable_id', 'watchable_type', 'user_id'
         ])->all();
     }
 }
index 7dcb669038bca6d770c59b1da206a4f3a74c8afe..c28db38004d623f72fa823d39ba220f5c6344cfd 100644 (file)
@@ -3,6 +3,7 @@
 namespace BookStack\Entities\Controllers;
 
 use BookStack\Activity\Models\View;
+use BookStack\Activity\Tools\UserWatchOptions;
 use BookStack\Entities\Models\Book;
 use BookStack\Entities\Repos\ChapterRepo;
 use BookStack\Entities\Tools\BookContents;
@@ -81,6 +82,7 @@ class ChapterController extends Controller
             'chapter'        => $chapter,
             'current'        => $chapter,
             'sidebarTree'    => $sidebarTree,
+            'watchOptions'   => new UserWatchOptions(user()),
             'pages'          => $pages,
             'next'           => $nextPreviousLocator->getNext(),
             'previous'       => $nextPreviousLocator->getPrevious(),
index e96d41bb1b445bd290c0f13b04f6b09ccd638b95..dad05d034a8bf2f754d4a6bfb9e51a6f425d84db 100644 (file)
@@ -4,6 +4,7 @@ namespace BookStack\Entities\Controllers;
 
 use BookStack\Activity\Models\View;
 use BookStack\Activity\Tools\CommentTree;
+use BookStack\Activity\Tools\UserWatchOptions;
 use BookStack\Entities\Models\Page;
 use BookStack\Entities\Repos\PageRepo;
 use BookStack\Entities\Tools\BookContents;
@@ -151,6 +152,7 @@ class PageController extends Controller
             'sidebarTree'     => $sidebarTree,
             'commentTree'     => $commentTree,
             'pageNav'         => $pageNav,
+            'watchOptions'    => new UserWatchOptions(user()),
             'next'            => $nextPreviousLocator->getNext(),
             'previous'        => $nextPreviousLocator->getPrevious(),
             'referenceCount'  => $this->referenceFetcher->getPageReferenceCountToEntity($page),
index 5c8b0a772831fa8ca8a612c35bc6d8089ded0175..b52a103a4b9331dd7b794068e27e5f9d0a7ee32a 100644 (file)
 
             <hr class="primary-background">
 
-            @if(signedInUser())
-                @include('entities.favourite-action', ['entity' => $book])
-            @endif
             @if($watchOptions->canWatch() && !$watchOptions->isWatching($book))
                 @include('entities.watch-action', ['entity' => $book])
             @endif
+            @if(signedInUser())
+                @include('entities.favourite-action', ['entity' => $book])
+            @endif
             @if(userCan('content-export'))
                 @include('entities.export-menu', ['entity' => $book])
             @endif
index de52888dda2f63eeee4b77fd4d61644d532cb912..3b1cfb07c4b3c62ab56a451735a7e075d0efae7b 100644 (file)
 
             <hr class="primary-background"/>
 
+            @if($watchOptions->canWatch() && !$watchOptions->isWatching($chapter))
+                @include('entities.watch-action', ['entity' => $chapter])
+            @endif
             @if(signedInUser())
                 @include('entities.favourite-action', ['entity' => $chapter])
             @endif
index adde98998ff658535dbbd37463735d9adf969c7f..5ad6108a032fd3c80bdcc7f9c94fc7f0d0d9c88b 100644 (file)
@@ -5,7 +5,7 @@
     <input type="hidden" name="id" value="{{ $entity->id }}">
 
     <ul refs="dropdown@menu" class="dropdown-menu xl-limited anchor-left pb-none">
-        @foreach(\BookStack\Activity\WatchLevels::all() as $option)
+        @foreach(\BookStack\Activity\WatchLevels::all() as $option => $value)
             <li>
                 <button name="level" value="{{ $option }}" class="icon-item">
                     @if($watchLevel === $option)
index ae6b273fe4fe1d208a2d0eade98e0933d67f7392..aefcb96e1105e810a41dd1da407b302409ab56ca 100644 (file)
 
             <hr class="primary-background"/>
 
+            @if($watchOptions->canWatch() && !$watchOptions->isWatching($page))
+                @include('entities.watch-action', ['entity' => $page])
+            @endif
             @if(signedInUser())
                 @include('entities.favourite-action', ['entity' => $page])
             @endif