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;
/**
/**
* Get the parent comment this is in reply to (if existing).
*/
- public function parent()
+ public function parent(): BelongsTo
{
return $this->belongsTo(Comment::class);
}
/**
* 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();
}
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;
* @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();
}
// Send the notification
- $user->notify(new $notification($relatedModel, $initiator));
+ $user->notify(new $notification($detail, $initiator));
}
}
}
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;
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");
$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;
}
}
}
}
- $this->sendNotificationToUserIds(CommentCreationNotification::class, $watcherIds, $user, $page);
+ $this->sendNotificationToUserIds(CommentCreationNotification::class, $watcherIds, $user, $detail, $page);
}
}
namespace BookStack\Activity\Notifications\Handlers;
+use BookStack\Activity\Models\Activity;
use BookStack\Activity\Models\Loggable;
use BookStack\Users\Models\User;
{
/**
* 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;
}
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;
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);
}
}
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;
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()) {
}
}
- $this->sendNotificationToUserIds(PageUpdateNotification::class, $watcherIds, $user, $detail);
+ $this->sendNotificationToUserIds(PageUpdateNotification::class, $watcherIds, $user, $detail, $detail);
}
}
<?php
-namespace BookStack\Activity\Notifications;
+namespace BookStack\Activity\Notifications\MessageParts;
use Illuminate\Contracts\Support\Htmlable;
--- /dev/null
+<?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);
+ }
+}
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;
'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'),
+ );
+ }
}
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;
$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());
}
}
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;
$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());
}
}
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;
$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());
}
}
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;
*/
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);
}
}
$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);
}
$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')
--- /dev/null
+<?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',
+];
$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 ----------------------- */
'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 ------------------------------ */
<!-- Outro -->
@foreach ($outroLines as $line)
- <p style="{{ $style['paragraph'] }}">
+ <p style="{{ $style['paragraph-sub'] }}">
{{ $line }}
</p>
@endforeach