diff --git a/app/Activity/Notifications/Handlers/PageCreationNotificationHandler.php b/app/Activity/Notifications/Handlers/PageCreationNotificationHandler.php index a61df48ae..8f19b3558 100644 --- a/app/Activity/Notifications/Handlers/PageCreationNotificationHandler.php +++ b/app/Activity/Notifications/Handlers/PageCreationNotificationHandler.php @@ -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)); + } + } } } diff --git a/app/Activity/Tools/EntityWatchers.php b/app/Activity/Tools/EntityWatchers.php index 20375ef45..38ba8c591 100644 --- a/app/Activity/Tools/EntityWatchers.php +++ b/app/Activity/Tools/EntityWatchers.php @@ -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(); } } diff --git a/app/Entities/Controllers/ChapterController.php b/app/Entities/Controllers/ChapterController.php index 7dcb66903..c28db3800 100644 --- a/app/Entities/Controllers/ChapterController.php +++ b/app/Entities/Controllers/ChapterController.php @@ -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(), diff --git a/app/Entities/Controllers/PageController.php b/app/Entities/Controllers/PageController.php index e96d41bb1..dad05d034 100644 --- a/app/Entities/Controllers/PageController.php +++ b/app/Entities/Controllers/PageController.php @@ -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), diff --git a/resources/views/books/show.blade.php b/resources/views/books/show.blade.php index 5c8b0a772..b52a103a4 100644 --- a/resources/views/books/show.blade.php +++ b/resources/views/books/show.blade.php @@ -139,12 +139,12 @@