Notifications: Cleaned up mails, added debounce for updates
- Updated mail notification design to be a bit prettier, and extracted text to new lang file for translation. - Added debounce logic for page update notifications. - Fixed watch options not being filtered to current user.
This commit is contained in:
parent
371779205a
commit
615741af9d
17 changed files with 152 additions and 70 deletions
|
@ -5,6 +5,7 @@ namespace BookStack\Activity\Models;
|
|||
use BookStack\App\Model;
|
||||
use BookStack\Users\Models\HasCreatorAndUpdater;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
/**
|
||||
|
@ -35,7 +36,7 @@ class Comment extends Model implements Loggable
|
|||
/**
|
||||
* Get the parent comment this is in reply to (if existing).
|
||||
*/
|
||||
public function parent()
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Comment::class);
|
||||
}
|
||||
|
@ -50,20 +51,16 @@ class Comment extends Model implements Loggable
|
|||
|
||||
/**
|
||||
* Get created date as a relative diff.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCreatedAttribute()
|
||||
public function getCreatedAttribute(): string
|
||||
{
|
||||
return $this->created_at->diffForHumans();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get updated date as a relative diff.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getUpdatedAttribute()
|
||||
public function getUpdatedAttribute(): string
|
||||
{
|
||||
return $this->updated_at->diffForHumans();
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace BookStack\Activity\Notifications\Handlers;
|
||||
|
||||
use BookStack\Activity\Models\Loggable;
|
||||
use BookStack\Activity\Notifications\Messages\BaseActivityNotification;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Permissions\PermissionApplicator;
|
||||
|
@ -18,7 +19,7 @@ abstract class BaseNotificationHandler implements NotificationHandler
|
|||
* @param class-string<BaseActivityNotification> $notification
|
||||
* @param int[] $userIds
|
||||
*/
|
||||
protected function sendNotificationToUserIds(string $notification, array $userIds, User $initiator, Entity $relatedModel): void
|
||||
protected function sendNotificationToUserIds(string $notification, array $userIds, User $initiator, string|Loggable $detail, Entity $relatedModel): void
|
||||
{
|
||||
$users = User::query()->whereIn('id', array_unique($userIds))->get();
|
||||
|
||||
|
@ -39,7 +40,7 @@ abstract class BaseNotificationHandler implements NotificationHandler
|
|||
}
|
||||
|
||||
// Send the notification
|
||||
$user->notify(new $notification($relatedModel, $initiator));
|
||||
$user->notify(new $notification($detail, $initiator));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace BookStack\Activity\Notifications\Handlers;
|
||||
|
||||
use BookStack\Activity\Models\Activity;
|
||||
use BookStack\Activity\Models\Comment;
|
||||
use BookStack\Activity\Models\Loggable;
|
||||
use BookStack\Activity\Notifications\Messages\CommentCreationNotification;
|
||||
|
@ -12,7 +13,7 @@ use BookStack\Users\Models\User;
|
|||
|
||||
class CommentCreationNotificationHandler extends BaseNotificationHandler
|
||||
{
|
||||
public function handle(string $activityType, Loggable|string $detail, User $user): void
|
||||
public function handle(Activity $activity, Loggable|string $detail, User $user): void
|
||||
{
|
||||
if (!($detail instanceof Comment)) {
|
||||
throw new \InvalidArgumentException("Detail for comment creation notifications must be a comment");
|
||||
|
@ -24,10 +25,10 @@ class CommentCreationNotificationHandler extends BaseNotificationHandler
|
|||
$watcherIds = $watchers->getWatcherUserIds();
|
||||
|
||||
// Page owner if user preferences allow
|
||||
if (!$watchers->isUserIgnoring($detail->owned_by) && $detail->ownedBy) {
|
||||
$userNotificationPrefs = new UserNotificationPreferences($detail->ownedBy);
|
||||
if (!$watchers->isUserIgnoring($detail->created_by) && $detail->createdBy) {
|
||||
$userNotificationPrefs = new UserNotificationPreferences($detail->createdBy);
|
||||
if ($userNotificationPrefs->notifyOnOwnPageComments()) {
|
||||
$watcherIds[] = $detail->owned_by;
|
||||
$watcherIds[] = $detail->created_by;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,6 +41,6 @@ class CommentCreationNotificationHandler extends BaseNotificationHandler
|
|||
}
|
||||
}
|
||||
|
||||
$this->sendNotificationToUserIds(CommentCreationNotification::class, $watcherIds, $user, $page);
|
||||
$this->sendNotificationToUserIds(CommentCreationNotification::class, $watcherIds, $user, $detail, $page);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace BookStack\Activity\Notifications\Handlers;
|
||||
|
||||
use BookStack\Activity\Models\Activity;
|
||||
use BookStack\Activity\Models\Loggable;
|
||||
use BookStack\Users\Models\User;
|
||||
|
||||
|
@ -9,8 +10,8 @@ interface NotificationHandler
|
|||
{
|
||||
/**
|
||||
* Run this handler.
|
||||
* Provides the activity type, related activity detail/model
|
||||
* Provides the activity, related activity detail/model
|
||||
* along with the user that triggered the activity.
|
||||
*/
|
||||
public function handle(string $activityType, string|Loggable $detail, User $user): void;
|
||||
public function handle(Activity $activity, string|Loggable $detail, User $user): void;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace BookStack\Activity\Notifications\Handlers;
|
||||
|
||||
use BookStack\Activity\Models\Activity;
|
||||
use BookStack\Activity\Models\Loggable;
|
||||
use BookStack\Activity\Notifications\Messages\PageCreationNotification;
|
||||
use BookStack\Activity\Tools\EntityWatchers;
|
||||
|
@ -11,13 +12,13 @@ use BookStack\Users\Models\User;
|
|||
|
||||
class PageCreationNotificationHandler extends BaseNotificationHandler
|
||||
{
|
||||
public function handle(string $activityType, Loggable|string $detail, User $user): void
|
||||
public function handle(Activity $activity, Loggable|string $detail, User $user): void
|
||||
{
|
||||
if (!($detail instanceof Page)) {
|
||||
throw new \InvalidArgumentException("Detail for page create notifications must be a page");
|
||||
}
|
||||
|
||||
$watchers = new EntityWatchers($detail, WatchLevels::NEW);
|
||||
$this->sendNotificationToUserIds(PageCreationNotification::class, $watchers->getWatcherUserIds(), $user, $detail);
|
||||
$this->sendNotificationToUserIds(PageCreationNotification::class, $watchers->getWatcherUserIds(), $user, $detail, $detail);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace BookStack\Activity\Notifications\Handlers;
|
||||
|
||||
use BookStack\Activity\ActivityType;
|
||||
use BookStack\Activity\Models\Activity;
|
||||
use BookStack\Activity\Models\Loggable;
|
||||
use BookStack\Activity\Notifications\Messages\PageUpdateNotification;
|
||||
use BookStack\Activity\Tools\EntityWatchers;
|
||||
|
@ -12,15 +14,31 @@ use BookStack\Users\Models\User;
|
|||
|
||||
class PageUpdateNotificationHandler extends BaseNotificationHandler
|
||||
{
|
||||
public function handle(string $activityType, Loggable|string $detail, User $user): void
|
||||
public function handle(Activity $activity, Loggable|string $detail, User $user): void
|
||||
{
|
||||
if (!($detail instanceof Page)) {
|
||||
throw new \InvalidArgumentException("Detail for page update notifications must be a page");
|
||||
}
|
||||
|
||||
// Get last update from activity
|
||||
$lastUpdate = $detail->activity()
|
||||
->where('type', '=', ActivityType::PAGE_UPDATE)
|
||||
->where('id', '!=', $activity->id)
|
||||
->latest('created_at')
|
||||
->first();
|
||||
|
||||
// Return if the same user has already updated the page in the last 15 mins
|
||||
if ($lastUpdate && $lastUpdate->user_id === $user->id) {
|
||||
if ($lastUpdate->created_at->gt(now()->subMinutes(15))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get active watchers
|
||||
$watchers = new EntityWatchers($detail, WatchLevels::UPDATES);
|
||||
$watcherIds = $watchers->getWatcherUserIds();
|
||||
|
||||
// Add page owner if preferences allow
|
||||
if (!$watchers->isUserIgnoring($detail->owned_by) && $detail->ownedBy) {
|
||||
$userNotificationPrefs = new UserNotificationPreferences($detail->ownedBy);
|
||||
if ($userNotificationPrefs->notifyOnOwnPageChanges()) {
|
||||
|
@ -28,6 +46,6 @@ class PageUpdateNotificationHandler extends BaseNotificationHandler
|
|||
}
|
||||
}
|
||||
|
||||
$this->sendNotificationToUserIds(PageUpdateNotification::class, $watcherIds, $user, $detail);
|
||||
$this->sendNotificationToUserIds(PageUpdateNotification::class, $watcherIds, $user, $detail, $detail);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace BookStack\Activity\Notifications;
|
||||
namespace BookStack\Activity\Notifications\MessageParts;
|
||||
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
|
26
app/Activity/Notifications/MessageParts/ListMessageLine.php
Normal file
26
app/Activity/Notifications/MessageParts/ListMessageLine.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace BookStack\Activity\Notifications\MessageParts;
|
||||
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
|
||||
/**
|
||||
* A bullet point list of content, where the keys of the given list array
|
||||
* are bolded header elements, and the values follow.
|
||||
*/
|
||||
class ListMessageLine implements Htmlable
|
||||
{
|
||||
public function __construct(
|
||||
protected array $list
|
||||
) {
|
||||
}
|
||||
|
||||
public function toHtml(): string
|
||||
{
|
||||
$list = [];
|
||||
foreach ($this->list as $header => $content) {
|
||||
$list[] = '<strong>' . e($header) . '</strong> ' . e($content);
|
||||
}
|
||||
return implode("<br>\n", $list);
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
namespace BookStack\Activity\Notifications\Messages;
|
||||
|
||||
use BookStack\Activity\Models\Loggable;
|
||||
use BookStack\Activity\Notifications\MessageParts\LinkedMailMessageLine;
|
||||
use BookStack\Users\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
@ -47,4 +48,16 @@ abstract class BaseActivityNotification extends Notification
|
|||
'activity_creator' => $this->user,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the common reason footer line used in mail messages.
|
||||
*/
|
||||
protected function buildReasonFooterLine(): LinkedMailMessageLine
|
||||
{
|
||||
return new LinkedMailMessageLine(
|
||||
url('/preferences/notifications'),
|
||||
trans('notifications.footer_reason'),
|
||||
trans('notifications.footer_reason_link'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
namespace BookStack\Activity\Notifications\Messages;
|
||||
|
||||
use BookStack\Activity\Models\Comment;
|
||||
use BookStack\Activity\Notifications\LinkedMailMessageLine;
|
||||
use BookStack\Activity\Notifications\MessageParts\ListMessageLine;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
|
@ -17,16 +17,14 @@ class CommentCreationNotification extends BaseActivityNotification
|
|||
$page = $comment->entity;
|
||||
|
||||
return (new MailMessage())
|
||||
->subject("New Comment on Page: " . $page->getShortName())
|
||||
->line("A user has commented on a page in " . setting('app-name') . ':')
|
||||
->line("Page Name: " . $page->name)
|
||||
->line("Commenter: " . $this->user->name)
|
||||
->line("Comment: " . strip_tags($comment->html))
|
||||
->action('View Comment', $page->getUrl('#comment' . $comment->local_id))
|
||||
->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',
|
||||
));
|
||||
->subject(trans('notifications.new_comment_subject', ['pageName' => $page->getShortName()]))
|
||||
->line(trans('notifications.new_comment_intro', ['appName' => setting('app-name')]))
|
||||
->line(new ListMessageLine([
|
||||
trans('notifications.detail_page_name') => $page->name,
|
||||
trans('notifications.detail_commenter') => $this->user->name,
|
||||
trans('notifications.detail_comment') => strip_tags($comment->html),
|
||||
]))
|
||||
->action(trans('notifications.action_view_comment'), $page->getUrl('#comment' . $comment->local_id))
|
||||
->line($this->buildReasonFooterLine());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace BookStack\Activity\Notifications\Messages;
|
||||
|
||||
use BookStack\Activity\Notifications\LinkedMailMessageLine;
|
||||
use BookStack\Activity\Notifications\MessageParts\ListMessageLine;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
|
@ -14,15 +14,13 @@ class PageCreationNotification extends BaseActivityNotification
|
|||
$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',
|
||||
));
|
||||
->subject(trans('notifications.new_page_subject', ['pageName' => $page->getShortName()]))
|
||||
->line(trans('notifications.new_page_intro', ['appName' => setting('app-name')]))
|
||||
->line(new ListMessageLine([
|
||||
trans('notifications.detail_page_name') => $page->name,
|
||||
trans('notifications.detail_created_by') => $this->user->name,
|
||||
]))
|
||||
->action(trans('notifications.action_view_page'), $page->getUrl())
|
||||
->line($this->buildReasonFooterLine());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace BookStack\Activity\Notifications\Messages;
|
||||
|
||||
use BookStack\Activity\Notifications\LinkedMailMessageLine;
|
||||
use BookStack\Activity\Notifications\MessageParts\ListMessageLine;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
|
@ -14,16 +14,14 @@ class PageUpdateNotification extends BaseActivityNotification
|
|||
$page = $this->detail;
|
||||
|
||||
return (new MailMessage())
|
||||
->subject("Updated Page: " . $page->getShortName())
|
||||
->line("A page has been updated in " . setting('app-name') . ':')
|
||||
->line("Page Name: " . $page->name)
|
||||
->line("Updated By: " . $this->user->name)
|
||||
->line("To prevent a mass of notifications, for a while you won't be sent notifications for further edits to this page by the same editor.")
|
||||
->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',
|
||||
));
|
||||
->subject(trans('notifications.updated_page_subject', ['pageName' => $page->getShortName()]))
|
||||
->line(trans('notifications.updated_page_intro', ['appName' => setting('app-name')]))
|
||||
->line(new ListMessageLine([
|
||||
trans('notifications.detail_page_name') => $page->name,
|
||||
trans('notifications.detail_updated_by') => $this->user->name,
|
||||
]))
|
||||
->line(trans('notifications.updated_page_debounce'))
|
||||
->action(trans('notifications.action_view_page'), $page->getUrl())
|
||||
->line($this->buildReasonFooterLine());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace BookStack\Activity\Notifications;
|
||||
|
||||
use BookStack\Activity\ActivityType;
|
||||
use BookStack\Activity\Models\Activity;
|
||||
use BookStack\Activity\Models\Loggable;
|
||||
use BookStack\Activity\Notifications\Handlers\CommentCreationNotificationHandler;
|
||||
use BookStack\Activity\Notifications\Handlers\NotificationHandler;
|
||||
|
@ -17,13 +18,14 @@ class NotificationManager
|
|||
*/
|
||||
protected array $handlers = [];
|
||||
|
||||
public function handle(string $activityType, string|Loggable $detail, User $user): void
|
||||
public function handle(Activity $activity, string|Loggable $detail, User $user): void
|
||||
{
|
||||
$activityType = $activity->type;
|
||||
$handlersToRun = $this->handlers[$activityType] ?? [];
|
||||
foreach ($handlersToRun as $handlerClass) {
|
||||
/** @var NotificationHandler $handler */
|
||||
$handler = app()->make($handlerClass);
|
||||
$handler->handle($activityType, $detail, $user);
|
||||
$handler->handle($activity, $detail, $user);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ class ActivityLogger
|
|||
|
||||
$this->setNotification($type);
|
||||
$this->dispatchWebhooks($type, $detail);
|
||||
$this->notifications->handle($type, $detail, user());
|
||||
$this->notifications->handle($activity, $detail, user());
|
||||
Theme::dispatch(ThemeEvents::ACTIVITY_LOGGED, $type, $detail);
|
||||
}
|
||||
|
||||
|
|
|
@ -76,14 +76,16 @@ class UserEntityWatchOptions
|
|||
$entities[] = $this->entity->chapter;
|
||||
}
|
||||
|
||||
$query = Watch::query()->where(function (Builder $subQuery) use ($entities) {
|
||||
foreach ($entities as $entity) {
|
||||
$subQuery->orWhere(function (Builder $whereQuery) use ($entity) {
|
||||
$whereQuery->where('watchable_type', '=', $entity->getMorphClass())
|
||||
$query = Watch::query()
|
||||
->where('user_id', '=', $this->user->id)
|
||||
->where(function (Builder $subQuery) use ($entities) {
|
||||
foreach ($entities as $entity) {
|
||||
$subQuery->orWhere(function (Builder $whereQuery) use ($entity) {
|
||||
$whereQuery->where('watchable_type', '=', $entity->getMorphClass())
|
||||
->where('watchable_id', '=', $entity->id);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$this->watchMap = $query->get(['watchable_type', 'level'])
|
||||
->pluck('level', 'watchable_type')
|
||||
|
|
26
lang/en/notifications.php
Normal file
26
lang/en/notifications.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
/**
|
||||
* Text used for activity-based notifications.
|
||||
*/
|
||||
return [
|
||||
|
||||
'new_comment_subject' => 'New comment on page: :pageName',
|
||||
'new_comment_intro' => 'A user has commented on a page in :appName:',
|
||||
'new_page_subject' => 'New page: :pageName',
|
||||
'new_page_intro' => 'A new page has been created in :appName:',
|
||||
'updated_page_subject' => 'Updated page: :pageName',
|
||||
'updated_page_intro' => 'A page has been updated in :appName:',
|
||||
'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\'t be sent notifications for further edits to this page by the same editor.',
|
||||
|
||||
'detail_page_name' => 'Page Name:',
|
||||
'detail_commenter' => 'Commenter:',
|
||||
'detail_comment' => 'Comment:',
|
||||
'detail_created_by' => 'Created By:',
|
||||
'detail_updated_by' => 'Updated By:',
|
||||
|
||||
'action_view_comment' => 'View Comment',
|
||||
'action_view_page' => 'View Page',
|
||||
|
||||
'footer_reason' => 'This notification was sent to you because :link cover this type of activity for this item.',
|
||||
'footer_reason_link' => 'your notification preferences',
|
||||
];
|
|
@ -30,7 +30,7 @@
|
|||
$style = [
|
||||
/* Layout ------------------------------ */
|
||||
|
||||
'body' => 'margin: 0; padding: 0; width: 100%; background-color: #F2F4F6;',
|
||||
'body' => 'margin: 0; padding: 0; width: 100%; background-color: #F2F4F6;color:#444444;',
|
||||
'email-wrapper' => 'width: 100%; margin: 0; padding: 0; background-color: #F2F4F6;',
|
||||
|
||||
/* Masthead ----------------------- */
|
||||
|
@ -54,8 +54,8 @@ $style = [
|
|||
|
||||
'anchor' => 'color: '.setting('app-color').';overflow-wrap: break-word;word-wrap: break-word;word-break: break-all;word-break:break-word;',
|
||||
'header-1' => 'margin-top: 0; color: #2F3133; font-size: 19px; font-weight: bold; text-align: left;',
|
||||
'paragraph' => 'margin-top: 0; color: #74787E; font-size: 16px; line-height: 1.5em;',
|
||||
'paragraph-sub' => 'margin-top: 0; color: #74787E; font-size: 12px; line-height: 1.5em;',
|
||||
'paragraph' => 'margin-top: 0; color: #444444; font-size: 16px; line-height: 1.5em;',
|
||||
'paragraph-sub' => 'margin-top: 0; color: #444444; font-size: 12px; line-height: 1.5em;',
|
||||
'paragraph-center' => 'text-align: center;',
|
||||
|
||||
/* Buttons ------------------------------ */
|
||||
|
@ -147,7 +147,7 @@ $style = [
|
|||
|
||||
<!-- Outro -->
|
||||
@foreach ($outroLines as $line)
|
||||
<p style="{{ $style['paragraph'] }}">
|
||||
<p style="{{ $style['paragraph-sub'] }}">
|
||||
{{ $line }}
|
||||
</p>
|
||||
@endforeach
|
||||
|
|
Loading…
Reference in a new issue