Skip to content

Commit 84d6e1d

Browse files
committed
Implement opt-in notifications for user mentions in comments
1 parent 79fae49 commit 84d6e1d

File tree

7 files changed

+338
-1
lines changed

7 files changed

+338
-1
lines changed

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ class SendSubscribedUserNotification implements ShouldQueue
450450

451451
### Sending notifications when a user is mentioned
452452

453-
Every time a user is mentioned, the `Kirschbaum\Commentions\Events\UserWasMentionedEvent` is dispatched. You can listen to this event and send notifications to the mentioned user.
453+
Every time a user is mentioned, the `Kirschbaum\Commentions\Events\UserWasMentionedEvent` is dispatched. Commentions ships an optional, opt-in notification you can enable via configuration, or you can listen to the event and handle it yourself.
454454

455455
Example usage:
456456

@@ -477,6 +477,30 @@ class SendUserMentionedNotification implements ShouldQueue
477477

478478
If you have [event auto-discovery](https://laravel.com/docs/11.x/events#registering-events-and-listeners), this should be enough. Otherwise, make sure to register your listener on the `EventServiceProvider`.
479479

480+
#### Built-in opt-in notifications
481+
482+
Enable notifications for mentions in your `config/commentions.php`:
483+
484+
```php
485+
'notifications' => [
486+
'mentions' => [
487+
'enabled' => true,
488+
'channels' => ['mail', 'database'],
489+
],
490+
],
491+
```
492+
493+
Optionally, provide a URL resolver so emails/links point users to the right place:
494+
495+
```php
496+
use Kirschbaum\Commentions\Config;
497+
498+
Config::resolveCommentUrlUsing(function (\Kirschbaum\Commentions\Comment $comment) {
499+
// Return a URL to view the record and scroll to the comment
500+
return route('projects.show', $comment->commentable) . '#comment-' . $comment->getId();
501+
});
502+
```
503+
480504
### Resolving the authenticated user
481505

482506
By default, when a new comment is made, the `Commenter` is automatically set to the current user logged in user (`auth()->user()`). If you want to change this behavior, you can implement your own resolver:

config/commentions.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,28 @@
5757
// Automatically subscribe a user when they are mentioned in a comment
5858
'auto_subscribe_on_mention' => true,
5959
],
60+
61+
/*
62+
|--------------------------------------------------------------------------
63+
| Notifications (opt-in)
64+
|--------------------------------------------------------------------------
65+
|
66+
| Configure notification delivery when a user is mentioned in a comment.
67+
| Disabled by default; enable and choose the channels you want to use.
68+
|
69+
*/
70+
'notifications' => [
71+
'mentions' => [
72+
'enabled' => false,
73+
74+
'channels' => ['mail'],
75+
76+
'listener' => \Kirschbaum\Commentions\Listeners\SendUserMentionedNotification::class,
77+
'notification' => \Kirschbaum\Commentions\Notifications\UserMentionedInComment::class,
78+
79+
'mail' => [
80+
'subject' => 'You were mentioned in a comment',
81+
],
82+
],
83+
],
6084
];

src/CommentionsServiceProvider.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
use Filament\Support\Assets\Css;
66
use Filament\Support\Assets\Js;
77
use Filament\Support\Facades\FilamentAsset;
8+
use Illuminate\Support\Facades\Event;
89
use Illuminate\Support\Facades\Gate;
910
use Kirschbaum\Commentions\Comment as CommentModel;
11+
use Kirschbaum\Commentions\Events\UserWasMentionedEvent;
12+
use Kirschbaum\Commentions\Listeners\SendUserMentionedNotification;
1013
use Kirschbaum\Commentions\Livewire\Comment;
1114
use Kirschbaum\Commentions\Livewire\CommentList;
1215
use Kirschbaum\Commentions\Livewire\Comments;
@@ -67,5 +70,10 @@ public function packageBooted(): void
6770
$this->publishes([
6871
__DIR__ . '/../resources/lang' => resource_path('lang/vendor/commentions'),
6972
], 'commentions-lang');
73+
74+
if (config('commentions.notifications.mentions.enabled', false)) {
75+
$listenerClass = (string) config('commentions.notifications.mentions.listener', SendUserMentionedNotification::class);
76+
Event::listen(UserWasMentionedEvent::class, $listenerClass);
77+
}
7078
}
7179
}

src/Config.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ class Config
1212

1313
protected static ?Closure $resolveAuthenticatedUser = null;
1414

15+
protected static ?Closure $resolveCommentUrl = null;
16+
1517
public static function resolveAuthenticatedUserUsing(Closure $callback): void
1618
{
1719
static::$resolveAuthenticatedUser = $callback;
@@ -39,6 +41,24 @@ public static function getCommentReactionTable(): string
3941
return config('commentions.tables.comment_reactions', 'comment_reactions');
4042
}
4143

44+
public static function resolveCommentUrlUsing(Closure $callback): void
45+
{
46+
static::$resolveCommentUrl = $callback;
47+
}
48+
49+
public static function resolveCommentUrl(?Comment $comment): ?string
50+
{
51+
if ($comment === null) {
52+
return null;
53+
}
54+
55+
if (static::$resolveCommentUrl instanceof Closure) {
56+
return call_user_func(static::$resolveCommentUrl, $comment);
57+
}
58+
59+
return null;
60+
}
61+
4262
public static function getCommentModel(): string
4363
{
4464
return config('commentions.comment.model', Comment::class);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace Kirschbaum\Commentions\Listeners;
4+
5+
use Illuminate\Contracts\Queue\ShouldQueue;
6+
use Illuminate\Queue\InteractsWithQueue;
7+
use Illuminate\Support\Facades\Notification;
8+
use Kirschbaum\Commentions\Events\UserWasMentionedEvent;
9+
use Kirschbaum\Commentions\Notifications\UserMentionedInComment;
10+
11+
class SendUserMentionedNotification implements ShouldQueue
12+
{
13+
use InteractsWithQueue;
14+
15+
public function handle(UserWasMentionedEvent $event): void
16+
{
17+
$user = $event->user;
18+
19+
if (! config('commentions.notifications.mentions.enabled', false)) {
20+
return;
21+
}
22+
23+
$channels = (array) config('commentions.notifications.mentions.channels', []);
24+
if (empty($channels)) {
25+
return;
26+
}
27+
28+
$notificationClass = (string) config('commentions.notifications.mentions.notification', UserMentionedInComment::class);
29+
$notification = app($notificationClass, ['comment' => $event->comment, 'channels' => $channels]);
30+
31+
Notification::send($user, $notification);
32+
}
33+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace Kirschbaum\Commentions\Notifications;
4+
5+
use Illuminate\Bus\Queueable;
6+
use Illuminate\Contracts\Queue\ShouldQueue;
7+
use Illuminate\Notifications\Messages\BroadcastMessage;
8+
use Illuminate\Notifications\Messages\MailMessage;
9+
use Illuminate\Notifications\Notification;
10+
use Kirschbaum\Commentions\Comment;
11+
use Kirschbaum\Commentions\Config;
12+
use Kirschbaum\Commentions\Manager;
13+
14+
class UserMentionedInComment extends Notification implements ShouldQueue
15+
{
16+
use Queueable;
17+
18+
public function __construct(
19+
protected Comment $comment,
20+
protected array $channels
21+
) {}
22+
23+
public function via(object $notifiable): array
24+
{
25+
return $this->channels;
26+
}
27+
28+
public function toMail(object $notifiable): MailMessage
29+
{
30+
$url = Config::resolveCommentUrl($this->comment) ?? url('/');
31+
32+
return (new MailMessage())
33+
->subject((string) config('commentions.notifications.mentions.mail.subject', 'You were mentioned in a comment'))
34+
->greeting('Hi ' . Manager::getName($notifiable))
35+
->line('You were mentioned in a comment by ' . $this->comment->getAuthorName() . '.')
36+
->line(strip_tags($this->comment->getBodyMarkdown()))
37+
->action('View comment', $url);
38+
}
39+
40+
public function toArray(object $notifiable): array
41+
{
42+
return [
43+
'comment_id' => $this->comment->getId(),
44+
'comment_body' => $this->comment->getBody(),
45+
'author_name' => $this->comment->getAuthorName(),
46+
'url' => Config::resolveCommentUrl($this->comment),
47+
];
48+
}
49+
50+
public function toBroadcast(object $notifiable): BroadcastMessage
51+
{
52+
return new BroadcastMessage($this->toArray($notifiable));
53+
}
54+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<?php
2+
3+
use Illuminate\Notifications\Notification as BaseNotification;
4+
use Illuminate\Support\Facades\Notification as NotificationFacade;
5+
use Kirschbaum\Commentions\Comment;
6+
use Kirschbaum\Commentions\Config;
7+
use Kirschbaum\Commentions\Events\UserWasMentionedEvent;
8+
use Kirschbaum\Commentions\Listeners\SendUserMentionedNotification;
9+
use Kirschbaum\Commentions\Notifications\UserMentionedInComment;
10+
use Tests\Models\Post;
11+
use Tests\Models\User;
12+
13+
test('it sends a mention notification when enabled with configured channels', function () {
14+
config()->set('commentions.notifications.mentions.enabled', true);
15+
config()->set('commentions.notifications.mentions.channels', ['mail', 'database']);
16+
17+
/** @var User $author */
18+
$author = User::factory()->create();
19+
/** @var User $mentioned */
20+
$mentioned = User::factory()->create();
21+
/** @var Post $post */
22+
$post = Post::factory()->create();
23+
24+
$comment = $post->comment(
25+
sprintf('Hey <span data-type="mention" data-id="%s">@%s</span>', $mentioned->id, $mentioned->name),
26+
$author
27+
);
28+
29+
NotificationFacade::fake();
30+
31+
$listener = app(SendUserMentionedNotification::class);
32+
$listener->handle(new UserWasMentionedEvent($comment, $mentioned));
33+
34+
NotificationFacade::assertSentTo(
35+
$mentioned,
36+
UserMentionedInComment::class,
37+
function (UserMentionedInComment $notification, array $channels) use ($comment) {
38+
expect($channels)->toEqualCanonicalizing(['mail', 'database']);
39+
40+
$payload = $notification->toArray($comment->author);
41+
expect($payload['comment_id'])->toBe($comment->getId());
42+
expect($payload['author_name'])->toBe($comment->getAuthorName());
43+
44+
return true;
45+
}
46+
);
47+
});
48+
49+
class TestCustomMentionNotification extends BaseNotification
50+
{
51+
public function __construct(public Comment $comment, public array $channels) {}
52+
53+
public function via(object $notifiable): array
54+
{
55+
return ['database'];
56+
}
57+
58+
public function toArray(object $notifiable): array
59+
{
60+
return [
61+
'custom' => true,
62+
'comment_id' => $this->comment->getId(),
63+
];
64+
}
65+
}
66+
67+
test('it allows overriding the notification class via config', function () {
68+
config()->set('commentions.notifications.mentions.enabled', true);
69+
config()->set('commentions.notifications.mentions.channels', ['mail']);
70+
config()->set('commentions.notifications.mentions.notification', TestCustomMentionNotification::class);
71+
72+
/** @var User $author */
73+
$author = User::factory()->create();
74+
/** @var User $mentioned */
75+
$mentioned = User::factory()->create();
76+
/** @var Post $post */
77+
$post = Post::factory()->create();
78+
79+
$comment = $post->comment('Hello world', $author);
80+
81+
NotificationFacade::fake();
82+
83+
$listener = app(SendUserMentionedNotification::class);
84+
$listener->handle(new UserWasMentionedEvent($comment, $mentioned));
85+
86+
NotificationFacade::assertSentTo(
87+
$mentioned,
88+
TestCustomMentionNotification::class,
89+
function (TestCustomMentionNotification $notification) use ($comment) {
90+
$payload = $notification->toArray($comment->author);
91+
expect($payload['custom'])->toBeTrue();
92+
expect($payload['comment_id'])->toBe($comment->getId());
93+
94+
return true;
95+
}
96+
);
97+
});
98+
99+
test('it does not send a mention notification when disabled', function () {
100+
config()->set('commentions.notifications.mentions.enabled', false);
101+
config()->set('commentions.notifications.mentions.channels', ['mail']);
102+
103+
/** @var User $author */
104+
$author = User::factory()->create();
105+
/** @var User $mentioned */
106+
$mentioned = User::factory()->create();
107+
/** @var Post $post */
108+
$post = Post::factory()->create();
109+
110+
$comment = $post->comment('Hello world', $author);
111+
112+
NotificationFacade::fake();
113+
114+
$listener = app(SendUserMentionedNotification::class);
115+
$listener->handle(new UserWasMentionedEvent($comment, $mentioned));
116+
117+
NotificationFacade::assertNothingSent();
118+
});
119+
120+
test('it does not send a mention notification when channels are empty', function () {
121+
config()->set('commentions.notifications.mentions.enabled', true);
122+
config()->set('commentions.notifications.mentions.channels', []);
123+
124+
/** @var User $author */
125+
$author = User::factory()->create();
126+
/** @var User $mentioned */
127+
$mentioned = User::factory()->create();
128+
/** @var Post $post */
129+
$post = Post::factory()->create();
130+
131+
$comment = $post->comment('Hello world', $author);
132+
133+
NotificationFacade::fake();
134+
135+
$listener = app(SendUserMentionedNotification::class);
136+
$listener->handle(new UserWasMentionedEvent($comment, $mentioned));
137+
138+
NotificationFacade::assertNothingSent();
139+
});
140+
141+
test('it uses the configured comment URL resolver in notification payload', function () {
142+
config()->set('commentions.notifications.mentions.enabled', true);
143+
config()->set('commentions.notifications.mentions.channels', ['database']);
144+
145+
$resolvedUrl = 'https://example.test/some-resource#comment-123';
146+
Config::resolveCommentUrlUsing(function (Comment $comment) use ($resolvedUrl) {
147+
return $resolvedUrl;
148+
});
149+
150+
/** @var User $author */
151+
$author = User::factory()->create();
152+
/** @var User $mentioned */
153+
$mentioned = User::factory()->create();
154+
/** @var Post $post */
155+
$post = Post::factory()->create();
156+
157+
$comment = $post->comment('Hello world', $author);
158+
159+
NotificationFacade::fake();
160+
161+
$listener = app(SendUserMentionedNotification::class);
162+
$listener->handle(new UserWasMentionedEvent($comment, $mentioned));
163+
164+
NotificationFacade::assertSentTo(
165+
$mentioned,
166+
UserMentionedInComment::class,
167+
function (UserMentionedInComment $notification) use ($resolvedUrl, $mentioned) {
168+
$payload = $notification->toArray($mentioned);
169+
expect($payload['url'])->toBe($resolvedUrl);
170+
171+
return true;
172+
}
173+
);
174+
});

0 commit comments

Comments
 (0)