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
// 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));
+ }
+ }
}
}
class EntityWatchers
{
+ /**
+ * @var int[]
+ */
protected array $watchers = [];
+
+ /**
+ * @var int[]
+ */
protected array $ignorers = [];
public function __construct(
$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 */
});
return $query->get([
- 'level', 'watchable_id', 'watchable_type', 'user_id'
+ 'level', 'watchable_id', 'watchable_type', 'user_id'
])->all();
}
}
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;
'chapter' => $chapter,
'current' => $chapter,
'sidebarTree' => $sidebarTree,
+ 'watchOptions' => new UserWatchOptions(user()),
'pages' => $pages,
'next' => $nextPreviousLocator->getNext(),
'previous' => $nextPreviousLocator->getPrevious(),
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;
'sidebarTree' => $sidebarTree,
'commentTree' => $commentTree,
'pageNav' => $pageNav,
+ 'watchOptions' => new UserWatchOptions(user()),
'next' => $nextPreviousLocator->getNext(),
'previous' => $nextPreviousLocator->getPrevious(),
'referenceCount' => $this->referenceFetcher->getPageReferenceCountToEntity($page),
<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
<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
<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)
<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