Skip to content

Commit a98ca42

Browse files
authored
Merge pull request #45 from kirschbaum-development/issue-10-subscribed-users
ISSUE-10: Implement comment subscription feature with event dispatching
2 parents 731fe44 + d44e7ec commit a98ca42

25 files changed

+1017
-26
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ node_modules
44
.pint.cache
55
.repomix/output
66
.cursor/rules/local.mdc
7-
7+
.idea
88
**/.claude/settings.local.json

README.md

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ use Kirschbaum\Commentions\Filament\Actions\CommentsAction;
103103
CommentsAction::make()
104104
->mentionables(User::all())
105105
])
106-
````
106+
```
107107

108108
3. Or as a header action:
109109

@@ -120,6 +120,100 @@ protected function getHeaderActions(): array
120120
}
121121
```
122122

123+
### Subscription Management
124+
125+
Commentions includes a subscription system that allows users to subscribe to receive notifications when new comments are added to a commentable resource.
126+
127+
#### Subscription Actions
128+
129+
You can add subscription actions to your Filament resources:
130+
131+
```php
132+
use Kirschbaum\Commentions\Filament\Actions\SubscriptionAction;
133+
134+
// In header actions
135+
protected function getHeaderActions(): array
136+
{
137+
return [
138+
SubscriptionAction::make(),
139+
];
140+
}
141+
142+
// In table actions (Filament 3)
143+
->actions([
144+
SubscriptionTableAction::make(),
145+
])
146+
147+
// In record actions (Filament 4)
148+
->recordActions([
149+
SubscriptionAction::make(),
150+
])
151+
```
152+
153+
#### Subscription Sidebar
154+
155+
When using comments in modals, a subscription sidebar is automatically displayed showing:
156+
- Subscribe/unsubscribe button for the current user
157+
- List of users currently subscribed to the commentable
158+
- Real-time updates when subscription status changes
159+
160+
##### Livewire options
161+
162+
When using the `commentions::comments` Livewire component directly, you can control the sidebar and its contents via component properties:
163+
164+
- `sidebarEnabled` (bool, default: true): toggles the entire subscription sidebar
165+
- `showSubscribers` (bool, default: `config('commentions.subscriptions.show_subscribers', true)`): toggles the subscribers list within the sidebar
166+
167+
Examples:
168+
169+
```php
170+
// Hide the sidebar entirely
171+
<livewire:commentions::comments :record="$record" :sidebar-enabled="false" />
172+
173+
// Keep the sidebar, but hide the subscribers list (uses config default if omitted)
174+
<livewire:commentions::comments :record="$record" :show-subscribers="false" />
175+
```
176+
177+
Inside the component/template you can also rely on these computed properties:
178+
179+
- `canSubscribe`: whether the current user can subscribe
180+
- `isSubscribed`: whether the current user is subscribed to the current record
181+
- `subscribers`: a collection of current subscribers
182+
183+
The component exposes a `toggleSubscription()` action which subscribes/unsubscribes the current user.
184+
185+
#### Disabling the Subscription Sidebar
186+
187+
You can disable the subscription sidebar if you don't want subscription functionality:
188+
189+
```php
190+
use Kirschbaum\Commentions\Filament\Actions\CommentsAction;
191+
192+
->recordActions([
193+
CommentsAction::make()
194+
->mentionables(User::all())
195+
->disableSidebar()
196+
])
197+
```
198+
199+
#### Subscription Methods
200+
201+
The `HasComments` trait provides methods for managing subscriptions programmatically:
202+
203+
```php
204+
// Subscribe a user
205+
$commentable->subscribe($user);
206+
207+
// Unsubscribe a user
208+
$commentable->unsubscribe($user);
209+
210+
// Check if a user is subscribed
211+
$isSubscribed = $commentable->isSubscribed($user);
212+
213+
// Get all subscribers
214+
$subscribers = $commentable->getSubscribers();
215+
```
216+
123217
***
124218

125219
### Configuration
@@ -322,12 +416,38 @@ return [
322416

323417
### Events
324418

325-
Two events are dispatched when a comment is created or reacted to:
419+
Events are dispatched when a comment is created, reacted to, or when users are mentioned or subscribed:
326420

327421
- `Kirschbaum\Commentions\Events\UserWasMentionedEvent`
422+
- `Kirschbaum\Commentions\Events\UserIsSubscribedToCommentableEvent`
328423
- `Kirschbaum\Commentions\Events\CommentWasCreatedEvent`
329424
- `Kirschbaum\Commentions\Events\CommentWasReactedEvent`
330425

426+
#### Subscription Events
427+
428+
When a new comment is created, all subscribed users receive notifications through the `UserIsSubscribedToCommentableEvent`. You can listen to this event to send custom notifications:
429+
430+
```php
431+
namespace App\Listeners;
432+
433+
use Illuminate\Queue\InteractsWithQueue;
434+
use Illuminate\Contracts\Queue\ShouldQueue;
435+
use App\Notifications\NewCommentNotification;
436+
use Kirschbaum\Commentions\Events\UserIsSubscribedToCommentableEvent;
437+
438+
class SendSubscribedUserNotification implements ShouldQueue
439+
{
440+
use InteractsWithQueue;
441+
442+
public function handle(UserIsSubscribedToCommentableEvent $event): void
443+
{
444+
$event->user->notify(
445+
new NewCommentNotification($event->comment)
446+
);
447+
}
448+
}
449+
```
450+
331451
### Sending notifications when a user is mentioned
332452

333453
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.

config/commentions.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
'tables' => [
1010
'comments' => 'comments',
1111
'comment_reactions' => 'comment_reactions',
12+
'comment_subscriptions' => 'comment_subscriptions',
1213
],
1314

1415
/*
@@ -38,4 +39,22 @@
3839
'reactions' => [
3940
'allowed' => ['👍', '❤️', '😂', '😮', '😢', '🤔'],
4041
],
42+
43+
/*
44+
|--------------------------------------------------------------------------
45+
| Subscriptions
46+
|--------------------------------------------------------------------------
47+
*/
48+
'subscriptions' => [
49+
// When true, subscribed users will also receive the same event as mentions
50+
// (UserWasMentionedEvent). When false, a distinct
51+
// UserIsSubscribedToCommentableEvent will be dispatched instead.
52+
'dispatch_as_mention' => false,
53+
// Controls whether the subscribers list is shown in the sidebar UI
54+
'show_subscribers' => true,
55+
// Automatically subscribe the author when they add a comment
56+
'auto_subscribe_on_comment' => true,
57+
// Automatically subscribe a user when they are mentioned in a comment
58+
'auto_subscribe_on_mention' => true,
59+
],
4160
];
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
public function up()
10+
{
11+
Schema::create(config('commentions.tables.comment_subscriptions', 'comment_subscriptions'), function (Blueprint $table) {
12+
$table->id();
13+
$table->morphs('subscribable');
14+
$table->morphs('subscriber');
15+
$table->timestamps();
16+
17+
$table->unique([
18+
'subscribable_type', 'subscribable_id', 'subscriber_type', 'subscriber_id'
19+
], 'commentions_subscriptions_unique');
20+
});
21+
}
22+
23+
public function down(): void
24+
{
25+
Schema::dropIfExists(config('commentions.tables.comment_subscriptions', 'comment_subscriptions'));
26+
}
27+
};
28+
29+

phpstan.neon

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ parameters:
77
- database/
88
- src/
99
- tests/
10-
10+
1111
excludePaths:
1212
- src/Filament/Actions/CommentsTableAction.php
13+
- src/Filament/Actions/SubscriptionTableAction.php
1314

1415
# Level 10 is the highest level
1516
level: 1

resources/dist/commentions.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

resources/views/comments-modal.blade.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@
88
:per-page="$perPage ?? 5"
99
:load-more-label="$loadMoreLabel ?? 'Show more'"
1010
:per-page-increment="$perPageIncrement ?? null"
11+
:sidebar-enabled="$sidebarEnabled ?? true"
12+
:show-subscribers="$showSubscribers ?? true"
1113
/>
1214
</div>

resources/views/comments.blade.php

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
@use('\Kirschbaum\Commentions\Config')
22

3-
<div class="comm:space-y-2" x-data="{ wasFocused: false }">
4-
@if (Config::resolveAuthenticatedUser()?->can('create', Config::getCommentModel()))
5-
<form wire:submit.prevent="save" x-cloak>
6-
{{-- tiptap editor --}}
7-
<div class="comm:relative tip-tap-container comm:mb-2" x-on:click="wasFocused = true" wire:ignore>
8-
<div
9-
x-data="editor(@js($commentBody), @js($this->mentions), 'comments')"
10-
>
11-
<div x-ref="element"></div>
3+
<div class="comm:flex comm:gap-4 comm:h-full" x-data="{ wasFocused: false }">
4+
{{-- Main Comments Area --}}
5+
<div class="comm:flex-1 comm:space-y-2">
6+
@if (Config::resolveAuthenticatedUser()?->can('create', Config::getCommentModel()))
7+
<form wire:submit.prevent="save" x-cloak>
8+
{{-- tiptap editor --}}
9+
<div class="comm:relative tip-tap-container comm:mb-2" x-on:click="wasFocused = true" wire:ignore>
10+
<div
11+
x-data="editor(@js($commentBody), @js($this->mentions), 'comments')"
12+
>
13+
<div x-ref="element"></div>
14+
</div>
1215
</div>
13-
</div>
1416

1517
<template x-if="wasFocused">
1618
<div>
@@ -30,13 +32,22 @@
3032
</form>
3133
@endif
3234

33-
<livewire:commentions::comment-list
34-
:record="$record"
35-
:mentionables="$this->mentions"
36-
:polling-interval="$pollingInterval"
37-
:paginate="$paginate ?? true"
38-
:per-page="$perPage ?? 5"
39-
:load-more-label="$loadMoreLabel ?? 'Show more'"
40-
:per-page-increment="$perPageIncrement ?? null"
41-
/>
35+
<livewire:commentions::comment-list
36+
:record="$record"
37+
:mentionables="$this->mentions"
38+
:polling-interval="$pollingInterval"
39+
:paginate="$paginate ?? true"
40+
:per-page="$perPage ?? 5"
41+
:load-more-label="$loadMoreLabel ?? 'Show more'"
42+
:per-page-increment="$perPageIncrement ?? null"
43+
/>
44+
</div>
45+
46+
{{-- Subscription Sidebar --}}
47+
@if ($this->canSubscribe && $this->resolvedSidebarEnabled)
48+
<livewire:commentions::subscription-sidebar
49+
:record="$record"
50+
:show-subscribers="$this->resolvedShowSubscribers"
51+
/>
52+
@endif
4253
</div>
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<div class="comm:w-48 comm:flex-shrink-0 comm:pl-2 comm:ml-2">
2+
<div class="comm:sticky comm:top-4">
3+
<div class="comm:bg-gray-50 comm:dark:bg-gray-800 comm:rounded-lg comm:p-4 comm:border comm:border-gray-200 comm:dark:border-gray-700">
4+
<div class="comm:flex comm:items-center comm:gap-2 comm:mb-3">
5+
<x-filament::icon
6+
icon="heroicon-o-bell"
7+
class="comm:w-4 comm:h-4 comm:text-gray-700 comm:dark:text-gray-300"
8+
/>
9+
<h3 class="comm:text-sm comm:font-bold comm:text-gray-900 comm:dark:text-gray-100">
10+
Notifications
11+
</h3>
12+
</div>
13+
14+
@if ($this->isSubscribed)
15+
<x-filament::button
16+
wire:click="toggleSubscription"
17+
wire:target="toggleSubscription"
18+
wire:loading.attr="disabled"
19+
color="gray"
20+
size="xs"
21+
class="comm:w-full comm:mb-2"
22+
>
23+
<x-filament::icon
24+
icon="heroicon-s-bell-slash"
25+
class="comm:w-3 comm:h-3 comm:mr-1"
26+
/>
27+
Unsubscribe
28+
</x-filament::button>
29+
@else
30+
<x-filament::button
31+
wire:click="toggleSubscription"
32+
wire:target="toggleSubscription"
33+
wire:loading.attr="disabled"
34+
color="gray"
35+
size="xs"
36+
class="comm:w-full comm:mb-2"
37+
>
38+
<x-filament::icon
39+
icon="heroicon-o-bell"
40+
class="comm:w-3 comm:h-3 comm:mr-1"
41+
/>
42+
Subscribe
43+
</x-filament::button>
44+
@endif
45+
46+
{{-- Subscribers List --}}
47+
@if ($showSubscribers && $this->subscribers->isNotEmpty())
48+
<div class="comm:border-t comm:border-gray-200 comm:dark:border-gray-600 comm:pt-3">
49+
<div class="comm:flex comm:items-center comm:gap-2 comm:mb-3">
50+
<x-filament::icon
51+
icon="heroicon-o-users"
52+
class="comm:w-4 comm:h-4 comm:text-gray-700 comm:dark:text-gray-300"
53+
/>
54+
<span class="comm:text-sm comm:font-bold comm:text-gray-900 comm:dark:text-gray-100">
55+
Subscribers ({{ $this->subscribers->count() }})
56+
</span>
57+
</div>
58+
<div class="comm:space-y-1">
59+
@foreach ($this->subscribers->take(5) as $subscriber)
60+
<div class="comm:flex comm:items-center comm:gap-2">
61+
@if ($subscriber instanceof \Filament\Models\Contracts\HasAvatar && $subscriber->getFilamentAvatarUrl())
62+
<img
63+
src="{{ $subscriber->getFilamentAvatarUrl() }}"
64+
alt="{{ $subscriber->name }}"
65+
class="comm:w-4 comm:h-4 comm:rounded-full comm:object-cover comm:flex-shrink-0"
66+
/>
67+
@else
68+
<div class="comm:w-4 comm:h-4 comm:rounded-full comm:bg-gray-300 comm:dark:bg-gray-600 comm:flex-shrink-0 comm:flex comm:items-center comm:justify-center">
69+
<span class="comm:text-xs comm:font-medium comm:text-gray-600 comm:dark:text-gray-300">
70+
{{ substr($subscriber->name, 0, 1) }}
71+
</span>
72+
</div>
73+
@endif
74+
<span class="comm:text-xs comm:text-gray-600 comm:dark:text-gray-400 comm:truncate">
75+
{{ $subscriber->name }}
76+
</span>
77+
</div>
78+
@endforeach
79+
@if ($this->subscribers->count() > 5)
80+
<div class="comm:text-xs comm:text-gray-500 comm:dark:text-gray-400 comm:pl-6">
81+
+{{ $this->subscribers->count() - 5 }} more
82+
</div>
83+
@endif
84+
</div>
85+
</div>
86+
@endif
87+
</div>
88+
</div>
89+
</div>
90+

0 commit comments

Comments
 (0)