Add full notification preferences system
Users can now control which in-app and email notifications they receive from their Settings tab on their channel page. Bell (in-app) preferences: - New comment on my video (default: on) - Reply to my comment (default: on) - Comment liked (default: on) - Video liked (default: on) - New subscriber (default: on) - New video from channels I follow (default: on) - New post from channels I follow (default: on) - New user registration — super admins only (default: on) Email preferences (same set plus): - Comment liked (default: off — too noisy) - Video liked (default: off — too noisy) - New post from channels I follow (default: off) - My video finished processing (default: on) - Weekly activity digest every Monday (default: on) - New user registration — super admins only (default: on) Implementation: - Migration: notification_preferences JSON column on users table - User::notificationPref($key) helper with typed defaults - All existing notification classes updated to check prefs in via() - 4 new notification classes: NewSubscriberNotification, VideoLikedNotification, NewPostNotification, WeeklyDigestNotification - 8 new email views matching existing dark theme - SendWeeklyDigest artisan command, scheduled every Monday 09:00 - NewSubscriberNotification wired into UserController::toggleSubscribe - VideoLikedNotification wired into UserController::toggleLike - NewPostNotification wired into PostController::store (to all subscribers) - Bell renderer updated for new_subscriber, video_like, new_post types - Preferences saved via AJAX (POST /settings/notifications) — instant toggle with automatic revert on failure Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
1 parent
3fe167e33f
commit
f8d13457fa
26 files changed
+808
-18
No files matched your search
@@ -5,6 +5,7 @@ namespace App\Notifications;
|
||||
use App\Models\Comment;
|
||||
use App\Models\User;
|
||||
use App\Models\Video;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
@@ -18,7 +19,10 @@ class NewCommentLikeNotification extends Notification
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['database'];
|
||||
$ch = [];
|
||||
if ($notifiable->notificationPref('notif_comment_like')) $ch[] = 'database';
|
||||
if ($notifiable->notificationPref('email_comment_like')) $ch[] = 'mail';
|
||||
return $ch;
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
@@ -35,4 +39,16 @@ class NewCommentLikeNotification extends Notification
|
||||
'comment_preview' => Str::limit($this->comment->body, 80),
|
||||
];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject($this->liker->name . ' liked your comment')
|
||||
->view('emails.comment-liked', [
|
||||
'video' => $this->video,
|
||||
'comment' => $this->comment,
|
||||
'liker' => $this->liker,
|
||||
'recipient'=> $notifiable,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Notifications;
|
||||
use App\Models\Comment;
|
||||
use App\Models\User;
|
||||
use App\Models\Video;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
@@ -18,7 +19,10 @@ class NewCommentNotification extends Notification
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['database'];
|
||||
$ch = [];
|
||||
if ($notifiable->notificationPref('notif_new_comment')) $ch[] = 'database';
|
||||
if ($notifiable->notificationPref('email_new_comment')) $ch[] = 'mail';
|
||||
return $ch;
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
@@ -35,4 +39,16 @@ class NewCommentNotification extends Notification
|
||||
'comment_preview' => Str::limit($this->comment->body, 80),
|
||||
];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject($this->commenter->name . ' commented on your video')
|
||||
->view('emails.new-comment', [
|
||||
'video' => $this->video,
|
||||
'comment' => $this->comment,
|
||||
'commenter'=> $this->commenter,
|
||||
'recipient'=> $notifiable,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\Post;
|
||||
use App\Models\User;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class NewPostNotification extends Notification
|
||||
{
|
||||
public function __construct(public Post $post, public User $author) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$ch = [];
|
||||
if ($notifiable->notificationPref('notif_new_post_from_sub')) $ch[] = 'database';
|
||||
if ($notifiable->notificationPref('email_new_post_from_sub')) $ch[] = 'mail';
|
||||
return $ch;
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'type' => 'new_post',
|
||||
'post_id' => $this->post->id,
|
||||
'author_id' => $this->author->id,
|
||||
'author_name' => $this->author->name,
|
||||
'author_avatar' => $this->author->avatar_url,
|
||||
'author_channel' => $this->author->channel,
|
||||
'post_preview' => Str::limit(strip_tags($this->post->body ?? ''), 100),
|
||||
];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject($this->author->name . ' posted something new')
|
||||
->view('emails.new-post-from-sub', [
|
||||
'post' => $this->post,
|
||||
'author' => $this->author,
|
||||
'recipient' => $notifiable,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Notifications;
|
||||
use App\Models\Comment;
|
||||
use App\Models\User;
|
||||
use App\Models\Video;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
@@ -18,7 +19,10 @@ class NewReplyNotification extends Notification
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['database'];
|
||||
$ch = [];
|
||||
if ($notifiable->notificationPref('notif_new_reply')) $ch[] = 'database';
|
||||
if ($notifiable->notificationPref('email_new_reply')) $ch[] = 'mail';
|
||||
return $ch;
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
@@ -35,4 +39,16 @@ class NewReplyNotification extends Notification
|
||||
'comment_preview' => Str::limit($this->reply->body, 80),
|
||||
];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject($this->replier->name . ' replied to your comment')
|
||||
->view('emails.new-reply', [
|
||||
'video' => $this->video,
|
||||
'reply' => $this->reply,
|
||||
'replier' => $this->replier,
|
||||
'recipient'=> $notifiable,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class NewSubscriberNotification extends Notification
|
||||
{
|
||||
public function __construct(public User $subscriber) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$ch = [];
|
||||
if ($notifiable->notificationPref('notif_new_subscriber')) $ch[] = 'database';
|
||||
if ($notifiable->notificationPref('email_new_subscriber')) $ch[] = 'mail';
|
||||
return $ch;
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'type' => 'new_subscriber',
|
||||
'actor_id' => $this->subscriber->id,
|
||||
'actor_name' => $this->subscriber->name,
|
||||
'actor_avatar' => $this->subscriber->avatar_url,
|
||||
'actor_channel' => $this->subscriber->channel,
|
||||
];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject($this->subscriber->name . ' subscribed to your channel')
|
||||
->view('emails.new-subscriber', [
|
||||
'subscriber' => $this->subscriber,
|
||||
'recipient' => $notifiable,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class NewUserRegistered extends Notification
|
||||
{
|
||||
@@ -12,7 +12,10 @@ class NewUserRegistered extends Notification
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['database', 'mail'];
|
||||
$ch = [];
|
||||
if ($notifiable->notificationPref('notif_new_user_reg')) $ch[] = 'database';
|
||||
if ($notifiable->notificationPref('email_new_user_reg')) $ch[] = 'mail';
|
||||
return $ch;
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
|
||||
@@ -4,17 +4,19 @@ namespace App\Notifications;
|
||||
|
||||
use App\Models\Video;
|
||||
use App\Models\User;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class NewVideoUploaded extends Notification
|
||||
{
|
||||
public function __construct(public Video $video, public User $uploader)
|
||||
{
|
||||
}
|
||||
public function __construct(public Video $video, public User $uploader) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['database'];
|
||||
$ch = [];
|
||||
if ($notifiable->notificationPref('notif_new_video_from_sub')) $ch[] = 'database';
|
||||
if ($notifiable->notificationPref('email_new_video_from_sub')) $ch[] = 'mail';
|
||||
return $ch;
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
@@ -30,4 +32,15 @@ class NewVideoUploaded extends Notification
|
||||
'uploader_avatar' => $this->uploader->avatar_url,
|
||||
];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject($this->uploader->name . ' uploaded a new video')
|
||||
->view('emails.new-video-from-sub', [
|
||||
'video' => $this->video,
|
||||
'uploader' => $this->uploader,
|
||||
'recipient'=> $notifiable,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Video;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class VideoLikedNotification extends Notification
|
||||
{
|
||||
public function __construct(public Video $video, public User $liker) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$ch = [];
|
||||
if ($notifiable->notificationPref('notif_video_like')) $ch[] = 'database';
|
||||
if ($notifiable->notificationPref('email_video_like')) $ch[] = 'mail';
|
||||
return $ch;
|
||||
}
|
||||
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'type' => 'video_like',
|
||||
'video_id' => $this->video->id,
|
||||
'video_route_key' => $this->video->getRouteKey(),
|
||||
'video_title' => $this->video->title,
|
||||
'video_thumbnail' => $this->video->thumbnail,
|
||||
'actor_id' => $this->liker->id,
|
||||
'actor_name' => $this->liker->name,
|
||||
'actor_avatar' => $this->liker->avatar_url,
|
||||
];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject($this->liker->name . ' liked your video')
|
||||
->view('emails.video-liked', [
|
||||
'video' => $this->video,
|
||||
'liker' => $this->liker,
|
||||
'recipient'=> $notifiable,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class WeeklyDigestNotification extends Notification
|
||||
{
|
||||
public array $stats;
|
||||
|
||||
public function __construct(public User $creator)
|
||||
{
|
||||
$videoIds = $creator->videos()->pluck('id');
|
||||
|
||||
$views = DB::table('video_views')
|
||||
->whereIn('video_id', $videoIds)
|
||||
->where('created_at', '>=', now()->subWeek())
|
||||
->count();
|
||||
|
||||
$likes = DB::table('video_likes')
|
||||
->whereIn('video_id', $videoIds)
|
||||
->where('created_at', '>=', now()->subWeek())
|
||||
->count();
|
||||
|
||||
$newSubs = DB::table('user_subscriptions')
|
||||
->where('channel_id', $creator->id)
|
||||
->where('created_at', '>=', now()->subWeek())
|
||||
->count();
|
||||
|
||||
$topVideo = $creator->videos()
|
||||
->withCount(['viewers as week_views' => fn($q) => $q->where('video_views.created_at', '>=', now()->subWeek())])
|
||||
->orderByDesc('week_views')
|
||||
->first();
|
||||
|
||||
$this->stats = [
|
||||
'views' => $views,
|
||||
'likes' => $likes,
|
||||
'new_subs' => $newSubs,
|
||||
'top_video' => $topVideo,
|
||||
];
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return $notifiable->notificationPref('email_weekly_digest') ? ['mail'] : [];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject('Your weekly TAKEONE summary 📊')
|
||||
->view('emails.weekly-digest', [
|
||||
'creator' => $this->creator,
|
||||
'stats' => $this->stats,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user