mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-01-19 08:42:48 +08:00
Notifications: Started core user notification logic
Put together an initial notification. Started logic to query and identify watchers.
This commit is contained in:
parent
9d149e4d36
commit
9779c1a357
|
@ -3,10 +3,11 @@
|
|||
namespace BookStack\Activity\Notifications\Handlers;
|
||||
|
||||
use BookStack\Activity\Models\Loggable;
|
||||
use BookStack\Users\Models\User;
|
||||
|
||||
class CommentCreationNotificationHandler implements NotificationHandler
|
||||
{
|
||||
public function handle(string $activityType, Loggable|string $detail): void
|
||||
public function handle(string $activityType, Loggable|string $detail, User $user): void
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
|
|
@ -3,11 +3,14 @@
|
|||
namespace BookStack\Activity\Notifications\Handlers;
|
||||
|
||||
use BookStack\Activity\Models\Loggable;
|
||||
use BookStack\Users\Models\User;
|
||||
|
||||
interface NotificationHandler
|
||||
{
|
||||
/**
|
||||
* Run this handler.
|
||||
* Provides the activity type, related activity detail/model
|
||||
* along with the user that triggered the activity.
|
||||
*/
|
||||
public function handle(string $activityType, string|Loggable $detail): void;
|
||||
public function handle(string $activityType, string|Loggable $detail, User $user): void;
|
||||
}
|
||||
|
|
|
@ -3,11 +3,32 @@
|
|||
namespace BookStack\Activity\Notifications\Handlers;
|
||||
|
||||
use BookStack\Activity\Models\Loggable;
|
||||
use BookStack\Activity\Models\Watch;
|
||||
use BookStack\Activity\Tools\EntityWatchers;
|
||||
use BookStack\Activity\WatchLevels;
|
||||
use BookStack\Users\Models\User;
|
||||
|
||||
class PageCreationNotificationHandler implements NotificationHandler
|
||||
{
|
||||
public function handle(string $activityType, Loggable|string $detail): void
|
||||
public function handle(string $activityType, Loggable|string $detail, User $user): void
|
||||
{
|
||||
// TODO
|
||||
|
||||
// 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);
|
||||
|
||||
// TODO - need to check entity visibility and receive-notifications permissions.
|
||||
// Maybe abstract this to a generic late-stage filter?
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
namespace BookStack\Activity\Notifications\Handlers;
|
||||
|
||||
use BookStack\Activity\Models\Loggable;
|
||||
use BookStack\Users\Models\User;
|
||||
|
||||
class PageUpdateNotificationHandler implements NotificationHandler
|
||||
{
|
||||
public function handle(string $activityType, Loggable|string $detail): void
|
||||
public function handle(string $activityType, Loggable|string $detail, User $user): void
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
|
26
app/Activity/Notifications/LinkedMailMessageLine.php
Normal file
26
app/Activity/Notifications/LinkedMailMessageLine.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace BookStack\Activity\Notifications;
|
||||
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
|
||||
/**
|
||||
* A line of text with linked text included, intended for use
|
||||
* in MailMessages. The line should have a ':link' placeholder for
|
||||
* where the link should be inserted within the line.
|
||||
*/
|
||||
class LinkedMailMessageLine implements Htmlable
|
||||
{
|
||||
public function __construct(
|
||||
protected string $url,
|
||||
protected string $line,
|
||||
protected string $linkText,
|
||||
) {
|
||||
}
|
||||
|
||||
public function toHtml(): string
|
||||
{
|
||||
$link = '<a href="' . e($this->url) . '">' . e($this->linkText) . '</a>';
|
||||
return str_replace(':link', $link, e($this->line));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace BookStack\Activity\Notifications\Messages;
|
||||
|
||||
use BookStack\Activity\Models\Loggable;
|
||||
use BookStack\Users\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
abstract class BaseActivityNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
protected Loggable|string $detail,
|
||||
protected User $user,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*/
|
||||
abstract public function toMail(mixed $notifiable): MailMessage;
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($notifiable)
|
||||
{
|
||||
return [
|
||||
'activity_detail' => $this->detail,
|
||||
'activity_creator' => $this->user,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace BookStack\Activity\Notifications\Messages;
|
||||
|
||||
use BookStack\Activity\Notifications\LinkedMailMessageLine;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
class PageCreationNotification extends BaseActivityNotification
|
||||
{
|
||||
public function toMail(mixed $notifiable): MailMessage
|
||||
{
|
||||
/** @var Page $page */
|
||||
$page = $this->detail;
|
||||
|
||||
return (new MailMessage())
|
||||
->subject("New Page: " . $page->getShortName())
|
||||
->line("A new page has been created in " . setting('app-name') . ':')
|
||||
->line("Page Name: " . $page->name)
|
||||
->line("Created By: " . $this->user->name)
|
||||
->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',
|
||||
));
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ use BookStack\Activity\Notifications\Handlers\CommentCreationNotificationHandler
|
|||
use BookStack\Activity\Notifications\Handlers\NotificationHandler;
|
||||
use BookStack\Activity\Notifications\Handlers\PageCreationNotificationHandler;
|
||||
use BookStack\Activity\Notifications\Handlers\PageUpdateNotificationHandler;
|
||||
use BookStack\Users\Models\User;
|
||||
|
||||
class NotificationManager
|
||||
{
|
||||
|
@ -16,13 +17,13 @@ class NotificationManager
|
|||
*/
|
||||
protected array $handlers = [];
|
||||
|
||||
public function handle(string $activityType, string|Loggable $detail): void
|
||||
public function handle(string $activityType, string|Loggable $detail, User $user): void
|
||||
{
|
||||
$handlersToRun = $this->handlers[$activityType] ?? [];
|
||||
foreach ($handlersToRun as $handlerClass) {
|
||||
/** @var NotificationHandler $handler */
|
||||
$handler = app()->make($handlerClass);
|
||||
$handler->handle($activityType, $detail);
|
||||
$handler->handle($activityType, $detail, $user);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ class ActivityLogger
|
|||
|
||||
$this->setNotification($type);
|
||||
$this->dispatchWebhooks($type, $detail);
|
||||
$this->notifications->handle($type, $detail);
|
||||
$this->notifications->handle($type, $detail, user());
|
||||
Theme::dispatch(ThemeEvents::ACTIVITY_LOGGED, $type, $detail);
|
||||
}
|
||||
|
||||
|
|
55
app/Activity/Tools/EntityWatchers.php
Normal file
55
app/Activity/Tools/EntityWatchers.php
Normal file
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace BookStack\Activity\Tools;
|
||||
|
||||
use BookStack\Activity\Models\Watch;
|
||||
use BookStack\Entities\Models\BookChild;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class EntityWatchers
|
||||
{
|
||||
protected array $watchers = [];
|
||||
protected array $ignorers = [];
|
||||
|
||||
public function __construct(
|
||||
protected Entity $entity,
|
||||
protected int $watchLevel,
|
||||
) {
|
||||
$this->build();
|
||||
}
|
||||
|
||||
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.
|
||||
}
|
||||
|
||||
protected function getRelevantWatches(): array
|
||||
{
|
||||
/** @var Entity[] $entitiesInvolved */
|
||||
$entitiesInvolved = array_filter([
|
||||
$this->entity,
|
||||
$this->entity instanceof BookChild ? $this->entity->book : null,
|
||||
$this->entity instanceof Page ? $this->entity->chapter : null,
|
||||
]);
|
||||
|
||||
$query = Watch::query()->where(function (Builder $query) use ($entitiesInvolved) {
|
||||
foreach ($entitiesInvolved as $entity) {
|
||||
$query->orWhere(function (Builder $query) use ($entity) {
|
||||
$query->where('watchable_type', '=', $entity->getMorphClass())
|
||||
->where('watchable_id', '=', $entity->id);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return $query->get([
|
||||
'level', 'watchable_id', 'watchable_type', 'user_id'
|
||||
])->all();
|
||||
}
|
||||
}
|
|
@ -3,20 +3,13 @@
|
|||
namespace BookStack\Activity\Tools;
|
||||
|
||||
use BookStack\Activity\Models\Watch;
|
||||
use BookStack\Activity\WatchLevels;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Users\Models\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class UserWatchOptions
|
||||
{
|
||||
protected static array $levelByName = [
|
||||
'default' => -1,
|
||||
'ignore' => 0,
|
||||
'new' => 1,
|
||||
'updates' => 2,
|
||||
'comments' => 3,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected User $user,
|
||||
) {
|
||||
|
@ -30,7 +23,7 @@ class UserWatchOptions
|
|||
public function getEntityWatchLevel(Entity $entity): string
|
||||
{
|
||||
$levelValue = $this->entityQuery($entity)->first(['level'])->level ?? -1;
|
||||
return $this->levelValueToName($levelValue);
|
||||
return WatchLevels::levelValueToName($levelValue);
|
||||
}
|
||||
|
||||
public function isWatching(Entity $entity): bool
|
||||
|
@ -40,7 +33,7 @@ class UserWatchOptions
|
|||
|
||||
public function updateEntityWatchLevel(Entity $entity, string $level): void
|
||||
{
|
||||
$levelValue = $this->levelNameToValue($level);
|
||||
$levelValue = WatchLevels::levelNameToValue($level);
|
||||
if ($levelValue < 0) {
|
||||
$this->removeForEntity($entity);
|
||||
return;
|
||||
|
@ -71,28 +64,4 @@ class UserWatchOptions
|
|||
->where('watchable_type', '=', $entity->getMorphClass())
|
||||
->where('user_id', '=', $this->user->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function getAvailableLevelNames(): array
|
||||
{
|
||||
return array_keys(static::$levelByName);
|
||||
}
|
||||
|
||||
protected static function levelNameToValue(string $level): int
|
||||
{
|
||||
return static::$levelByName[$level] ?? -1;
|
||||
}
|
||||
|
||||
protected static function levelValueToName(int $level): string
|
||||
{
|
||||
foreach (static::$levelByName as $name => $value) {
|
||||
if ($level === $value) {
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
|
|
61
app/Activity/WatchLevels.php
Normal file
61
app/Activity/WatchLevels.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace BookStack\Activity;
|
||||
|
||||
class WatchLevels
|
||||
{
|
||||
/**
|
||||
* Default level, No specific option set
|
||||
* Typically not a stored status
|
||||
*/
|
||||
const DEFAULT = -1;
|
||||
|
||||
/**
|
||||
* Ignore all notifications.
|
||||
*/
|
||||
const IGNORE = 0;
|
||||
|
||||
/**
|
||||
* Watch for new content.
|
||||
*/
|
||||
const NEW = 1;
|
||||
|
||||
/**
|
||||
* Watch for updates and new content
|
||||
*/
|
||||
const UPDATES = 2;
|
||||
|
||||
/**
|
||||
* Watch for comments, updates and new content.
|
||||
*/
|
||||
const COMMENTS = 3;
|
||||
|
||||
/**
|
||||
* Get all the possible values as an option_name => value array.
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
$options = [];
|
||||
foreach ((new \ReflectionClass(static::class))->getConstants() as $name => $value) {
|
||||
$options[strtolower($name)] = $value;
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
public static function levelNameToValue(string $level): int
|
||||
{
|
||||
return static::all()[$level] ?? -1;
|
||||
}
|
||||
|
||||
public static function levelValueToName(int $level): string
|
||||
{
|
||||
foreach (static::all() as $name => $value) {
|
||||
if ($level === $value) {
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
|
||||
return 'default';
|
||||
}
|
||||
}
|
|
@ -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\Tools\UserWatchOptions::getAvailableLevelNames() as $option)
|
||||
@foreach(\BookStack\Activity\WatchLevels::all() as $option)
|
||||
<li>
|
||||
<button name="level" value="{{ $option }}" class="icon-item">
|
||||
@if($watchLevel === $option)
|
||||
|
|
Loading…
Reference in New Issue
Block a user