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
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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';
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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