From 3e9e16bbb9df7afc363ee88dd9bdd2642f625a6c Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Thu, 20 Aug 2026 15:27:38 +0100 Subject: [PATCH 1/2] Stop emailing everyone on every plugin approval Approving a plugin no longer dispatches SendNewPluginNotifications, so users are no longer emailed about every single new plugin. The author still receives their PluginApproved email, and the job plus the plugins:resend-new-plugin-notifications command remain for sending a deliberate batch. Adds a sortable approved_at column to the admin Plugins table so recently approved plugins are easy to find when composing that batch. Co-Authored-By: Claude Opus 5 (1M context) --- app/Filament/Resources/PluginResource.php | 6 ++++++ app/Models/Plugin.php | 6 ------ tests/Feature/Filament/PluginResourceTest.php | 16 ++++++++++++++++ .../Notifications/NewPluginAvailableTest.php | 6 ++---- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/app/Filament/Resources/PluginResource.php b/app/Filament/Resources/PluginResource.php index 7a9875e1..8e6abc0d 100644 --- a/app/Filament/Resources/PluginResource.php +++ b/app/Filament/Resources/PluginResource.php @@ -339,6 +339,12 @@ public static function table(Table $table): Table ->label('Submitted') ->dateTime() ->sortable(), + + Tables\Columns\TextColumn::make('approved_at') + ->label('Approved') + ->dateTime() + ->placeholder('-') + ->sortable(), ]) ->filters([ Tables\Filters\SelectFilter::make('status') diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index f0026d32..4e3f9c9d 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -7,7 +7,6 @@ use App\Enums\PluginTier; use App\Enums\PluginType; use App\Enums\PriceTier; -use App\Jobs\SendNewPluginNotifications; use App\Notifications\PluginApproved; use App\Notifications\PluginRejected; use App\Services\OgImageService; @@ -555,7 +554,6 @@ public function getRepositoryOwnerAndName(): ?array public function approve(int $approvedById): void { $previousStatus = $this->status; - $isFirstApproval = $this->approved_at === null; $this->update([ 'status' => PluginStatus::Approved, @@ -574,10 +572,6 @@ public function approve(int $approvedById): void $this->user->notify(new PluginApproved($this)); - if ($isFirstApproval) { - SendNewPluginNotifications::dispatch($this); - } - resolve(PluginSyncService::class)->sync($this); } diff --git a/tests/Feature/Filament/PluginResourceTest.php b/tests/Feature/Filament/PluginResourceTest.php index 739c80de..514efbbe 100644 --- a/tests/Feature/Filament/PluginResourceTest.php +++ b/tests/Feature/Filament/PluginResourceTest.php @@ -4,6 +4,7 @@ use App\Filament\Resources\PluginResource; use App\Filament\Resources\PluginResource\Pages\EditPlugin; +use App\Filament\Resources\PluginResource\Pages\ListPlugins; use App\Filament\Resources\UserResource; use App\Models\Plugin; use App\Models\User; @@ -129,6 +130,21 @@ public function test_free_plugin_license_links_to_github(): void $response->assertSee('https://github.com/acme/free-license-222/blob/main/LICENSE'); } + public function test_plugins_table_can_be_sorted_by_approved_at(): void + { + $oldest = Plugin::factory()->approved()->create(['approved_at' => now()->subDays(10)]); + $newest = Plugin::factory()->approved()->create(['approved_at' => now()->subDay()]); + $middle = Plugin::factory()->approved()->create(['approved_at' => now()->subDays(5)]); + + Livewire::actingAs($this->admin) + ->test(ListPlugins::class) + ->assertCanRenderTableColumn('approved_at') + ->sortTable('approved_at', 'desc') + ->assertCanSeeTableRecords([$newest, $middle, $oldest], inOrder: true) + ->sortTable('approved_at', 'asc') + ->assertCanSeeTableRecords([$oldest, $middle, $newest], inOrder: true); + } + public function test_submission_info_shows_go_to_user_action(): void { $user = User::factory()->create(); diff --git a/tests/Feature/Notifications/NewPluginAvailableTest.php b/tests/Feature/Notifications/NewPluginAvailableTest.php index 07c14157..b1c78a11 100644 --- a/tests/Feature/Notifications/NewPluginAvailableTest.php +++ b/tests/Feature/Notifications/NewPluginAvailableTest.php @@ -24,7 +24,7 @@ protected function setUp(): void }); } - public function test_notification_job_is_dispatched_on_first_approval(): void + public function test_notification_job_is_not_dispatched_on_first_approval(): void { Bus::fake(SendNewPluginNotifications::class); @@ -34,9 +34,7 @@ public function test_notification_job_is_dispatched_on_first_approval(): void $plugin->approve($admin->id); - Bus::assertDispatched(SendNewPluginNotifications::class, function ($job) use ($plugin) { - return $job->plugin->id === $plugin->id; - }); + Bus::assertNotDispatched(SendNewPluginNotifications::class); } public function test_notification_job_is_not_dispatched_on_re_approval(): void From e9192126aa67622994c1e054571ef4a5784cdecc Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Thu, 20 Aug 2026 15:34:48 +0100 Subject: [PATCH 2/2] Announce new plugins in-app only, not by email Switches the approach: instead of dropping the notification entirely on approval, NewPluginAvailable now goes out on the database channel only. Opted-in users still get the in-app notification for every new plugin; nobody gets an email. The opt-out preference still silences it. Restores the SendNewPluginNotifications dispatch in Plugin::approve(), returning that method to its original behaviour. Adds a regression test asserting the mail channel is never returned. Co-Authored-By: Claude Opus 5 (1M context) --- app/Models/Plugin.php | 6 +++++ app/Notifications/NewPluginAvailable.php | 5 ++++- .../Notifications/NewPluginAvailableTest.php | 22 +++++++++++++++---- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index 4e3f9c9d..f0026d32 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -7,6 +7,7 @@ use App\Enums\PluginTier; use App\Enums\PluginType; use App\Enums\PriceTier; +use App\Jobs\SendNewPluginNotifications; use App\Notifications\PluginApproved; use App\Notifications\PluginRejected; use App\Services\OgImageService; @@ -554,6 +555,7 @@ public function getRepositoryOwnerAndName(): ?array public function approve(int $approvedById): void { $previousStatus = $this->status; + $isFirstApproval = $this->approved_at === null; $this->update([ 'status' => PluginStatus::Approved, @@ -572,6 +574,10 @@ public function approve(int $approvedById): void $this->user->notify(new PluginApproved($this)); + if ($isFirstApproval) { + SendNewPluginNotifications::dispatch($this); + } + resolve(PluginSyncService::class)->sync($this); } diff --git a/app/Notifications/NewPluginAvailable.php b/app/Notifications/NewPluginAvailable.php index c031a396..4701b5b0 100644 --- a/app/Notifications/NewPluginAvailable.php +++ b/app/Notifications/NewPluginAvailable.php @@ -19,6 +19,9 @@ public function __construct( ) {} /** + * New plugins are announced in-app only. Emailing every opted-in user on + * every approval was far too much mail for something that isn't urgent. + * * @return array */ public function via(object $notifiable): array @@ -27,7 +30,7 @@ public function via(object $notifiable): array return []; } - return ['mail', 'database']; + return ['database']; } public function toMail(object $notifiable): MailMessage diff --git a/tests/Feature/Notifications/NewPluginAvailableTest.php b/tests/Feature/Notifications/NewPluginAvailableTest.php index b1c78a11..04005bd0 100644 --- a/tests/Feature/Notifications/NewPluginAvailableTest.php +++ b/tests/Feature/Notifications/NewPluginAvailableTest.php @@ -24,7 +24,7 @@ protected function setUp(): void }); } - public function test_notification_job_is_not_dispatched_on_first_approval(): void + public function test_notification_job_is_dispatched_on_first_approval(): void { Bus::fake(SendNewPluginNotifications::class); @@ -34,7 +34,9 @@ public function test_notification_job_is_not_dispatched_on_first_approval(): voi $plugin->approve($admin->id); - Bus::assertNotDispatched(SendNewPluginNotifications::class); + Bus::assertDispatched(SendNewPluginNotifications::class, function ($job) use ($plugin) { + return $job->plugin->id === $plugin->id; + }); } public function test_notification_job_is_not_dispatched_on_re_approval(): void @@ -62,14 +64,26 @@ public function test_via_returns_empty_array_when_user_opted_out(): void $this->assertEmpty($notification->via($user)); } - public function test_via_returns_mail_and_database_when_user_opted_in(): void + public function test_via_returns_database_only_when_user_opted_in(): void { $user = User::factory()->create(['receives_new_plugin_notifications' => true]); $plugin = Plugin::factory()->for($user)->create(); $notification = new NewPluginAvailable($plugin); - $this->assertEquals(['mail', 'database'], $notification->via($user)); + $this->assertEquals(['database'], $notification->via($user)); + } + + public function test_via_never_includes_the_mail_channel(): void + { + $optedIn = User::factory()->create(['receives_new_plugin_notifications' => true]); + $optedOut = User::factory()->create(['receives_new_plugin_notifications' => false]); + $plugin = Plugin::factory()->create(); + + $notification = new NewPluginAvailable($plugin); + + $this->assertNotContains('mail', $notification->via($optedIn)); + $this->assertNotContains('mail', $notification->via($optedOut)); } public function test_mail_contains_plugin_name(): void