diff --git a/app/Console/Commands/SendWeeklyDigest.php b/app/Console/Commands/SendWeeklyDigest.php new file mode 100644 index 0000000..a3fa399 --- /dev/null +++ b/app/Console/Commands/SendWeeklyDigest.php @@ -0,0 +1,33 @@ +get(); + + $sent = 0; + foreach ($users as $user) { + if (! $user->notificationPref('email_weekly_digest')) { + continue; + } + try { + $user->notify(new WeeklyDigestNotification($user)); + $sent++; + } catch (\Throwable $e) { + \Log::error('Weekly digest failed for user ' . $user->id . ': ' . $e->getMessage()); + } + } + + $this->info("Sent weekly digest to {$sent} users."); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index e73a682..547a418 100755 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -25,6 +25,12 @@ class Kernel extends ConsoleKernel $nas->clearNasCache(24); } })->daily()->name('nas-cache-evict')->withoutOverlapping(); + + // Weekly activity digest — every Monday at 9:00 AM (Bahrain time) + $schedule->command('digest:weekly') + ->weeklyOn(1, '09:00') + ->withoutOverlapping() + ->runInBackground(); } /** diff --git a/app/Http/Controllers/PostController.php b/app/Http/Controllers/PostController.php index 6b0db23..93a14eb 100644 --- a/app/Http/Controllers/PostController.php +++ b/app/Http/Controllers/PostController.php @@ -6,6 +6,7 @@ use App\Models\Post; use App\Models\PostImage; use App\Models\PostVideo; use App\Models\User; +use App\Notifications\NewPostNotification; use App\Services\NasSyncService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -115,6 +116,12 @@ class PostController extends Controller } } + // Notify subscribers of new post + $author = $user->fresh(); + $author->subscribers()->each(function (User $subscriber) use ($post, $author) { + try { $subscriber->notify(new NewPostNotification($post, $author)); } catch (\Throwable) {} + }); + return back()->with('toast_success', 'Post shared!'); } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index d9a254b..6390f69 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -7,6 +7,8 @@ use App\Models\AuditLog; use App\Models\Post; use App\Models\User; use App\Models\Video; +use App\Notifications\NewSubscriberNotification; +use App\Notifications\VideoLikedNotification; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; @@ -151,6 +153,22 @@ class UserController extends Controller return redirect()->route('channel')->with('toast_success', 'Password updated!')->with('_open_tab', 'settings'); } + // Save a single notification preference toggle (AJAX) + public function updateNotificationPreferences(Request $request) + { + $request->validate([ + 'key' => ['required', 'string', 'in:' . implode(',', array_keys(User::notifDefaults()))], + 'value' => ['required', 'boolean'], + ]); + + $user = Auth::user(); + $prefs = $user->notification_preferences ?? []; + $prefs[$request->key] = (bool) $request->value; + $user->update(['notification_preferences' => $prefs]); + + return response()->json(['ok' => true]); + } + // Logout all other devices public function logoutAllDevices(Request $request) { @@ -348,6 +366,9 @@ class UserController extends Controller } else { $video->likes()->attach($user->id); $liked = true; + if ($video->user_id && $video->user_id !== $user->id) { + try { $video->user->notify(new VideoLikedNotification($video, $user)); } catch (\Throwable) {} + } } return response()->json([ @@ -370,6 +391,7 @@ class UserController extends Controller } else { $me->subscriptions()->attach($user->id); $subscribed = true; + try { $user->notify(new NewSubscriberNotification($me)); } catch (\Throwable) {} } return response()->json([ diff --git a/app/Models/User.php b/app/Models/User.php index 6170e2e..fa44e31 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -43,6 +43,7 @@ class User extends Authenticatable implements MustVerifyEmail 'two_factor_secret', 'two_factor_enabled', 'banner', + 'notification_preferences', ]; protected $hidden = [ @@ -51,11 +52,46 @@ class User extends Authenticatable implements MustVerifyEmail ]; protected $casts = [ - 'email_verified_at' => 'datetime', - 'password' => 'hashed', - 'two_factor_enabled' => 'boolean', + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + 'two_factor_enabled' => 'boolean', + 'notification_preferences' => 'array', ]; + // Defaults for each preference key (true = on, false = off) + private const NOTIF_DEFAULTS = [ + 'notif_new_comment' => true, + 'notif_new_reply' => true, + 'notif_comment_like' => true, + 'notif_video_like' => true, + 'notif_new_subscriber' => true, + 'notif_new_video_from_sub' => true, + 'notif_new_post_from_sub' => true, + 'notif_new_user_reg' => true, + 'email_new_comment' => true, + 'email_new_reply' => true, + 'email_comment_like' => false, + 'email_video_like' => false, + 'email_new_subscriber' => true, + 'email_new_video_from_sub' => true, + 'email_new_post_from_sub' => false, + 'email_video_processed' => true, + 'email_new_user_reg' => true, + 'email_weekly_digest' => true, + ]; + + public function notificationPref(string $key): bool + { + $prefs = $this->notification_preferences ?? []; + $default = self::NOTIF_DEFAULTS[$key] ?? true; + return isset($prefs[$key]) ? (bool) $prefs[$key] : $default; + } + + public static function notifDefaults(): array + { + return self::NOTIF_DEFAULTS; + } + protected $appends = ['avatar_url', 'banner_url']; // Auto-generate a unique slug-based username when creating a user without one diff --git a/app/Notifications/NewCommentLikeNotification.php b/app/Notifications/NewCommentLikeNotification.php index 5b086e8..6526457 100644 --- a/app/Notifications/NewCommentLikeNotification.php +++ b/app/Notifications/NewCommentLikeNotification.php @@ -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, + ]); + } } diff --git a/app/Notifications/NewCommentNotification.php b/app/Notifications/NewCommentNotification.php index ad53b20..8eb87e6 100644 --- a/app/Notifications/NewCommentNotification.php +++ b/app/Notifications/NewCommentNotification.php @@ -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, + ]); + } } diff --git a/app/Notifications/NewPostNotification.php b/app/Notifications/NewPostNotification.php new file mode 100644 index 0000000..6c0a5da --- /dev/null +++ b/app/Notifications/NewPostNotification.php @@ -0,0 +1,46 @@ +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, + ]); + } +} diff --git a/app/Notifications/NewReplyNotification.php b/app/Notifications/NewReplyNotification.php index 887b0f0..3620f4a 100644 --- a/app/Notifications/NewReplyNotification.php +++ b/app/Notifications/NewReplyNotification.php @@ -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, + ]); + } } diff --git a/app/Notifications/NewSubscriberNotification.php b/app/Notifications/NewSubscriberNotification.php new file mode 100644 index 0000000..fa3b786 --- /dev/null +++ b/app/Notifications/NewSubscriberNotification.php @@ -0,0 +1,41 @@ +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, + ]); + } +} diff --git a/app/Notifications/NewUserRegistered.php b/app/Notifications/NewUserRegistered.php index d60ecc9..9da29a9 100644 --- a/app/Notifications/NewUserRegistered.php +++ b/app/Notifications/NewUserRegistered.php @@ -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 diff --git a/app/Notifications/NewVideoUploaded.php b/app/Notifications/NewVideoUploaded.php index 2b52d89..84d411f 100644 --- a/app/Notifications/NewVideoUploaded.php +++ b/app/Notifications/NewVideoUploaded.php @@ -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, + ]); + } } diff --git a/app/Notifications/VideoLikedNotification.php b/app/Notifications/VideoLikedNotification.php new file mode 100644 index 0000000..6f93282 --- /dev/null +++ b/app/Notifications/VideoLikedNotification.php @@ -0,0 +1,46 @@ +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, + ]); + } +} diff --git a/app/Notifications/WeeklyDigestNotification.php b/app/Notifications/WeeklyDigestNotification.php new file mode 100644 index 0000000..d53d525 --- /dev/null +++ b/app/Notifications/WeeklyDigestNotification.php @@ -0,0 +1,60 @@ +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, + ]); + } +} diff --git a/database/migrations/2026_05_16_200000_add_notification_preferences_to_users_table.php b/database/migrations/2026_05_16_200000_add_notification_preferences_to_users_table.php new file mode 100644 index 0000000..68d8d3c --- /dev/null +++ b/database/migrations/2026_05_16_200000_add_notification_preferences_to_users_table.php @@ -0,0 +1,22 @@ +json('notification_preferences')->nullable()->after('banner'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('notification_preferences'); + }); + } +}; diff --git a/resources/views/emails/comment-liked.blade.php b/resources/views/emails/comment-liked.blade.php new file mode 100644 index 0000000..faa8962 --- /dev/null +++ b/resources/views/emails/comment-liked.blade.php @@ -0,0 +1,27 @@ + + +
👍
+ +

Your comment got a like!

+

On the video {{ $video->title }}.

+ +

Hi {{ $recipient->name }},

+

+ {{ $liker->name }} liked your comment: +

+ +
+ +
+ "{{ \Illuminate\Support\Str::limit(strip_tags($comment->body), 200) }}" +
+
+ +
+ +
+ +
+

You can manage your notification preferences in your account settings.

+ +
diff --git a/resources/views/emails/new-comment.blade.php b/resources/views/emails/new-comment.blade.php new file mode 100644 index 0000000..d1c630d --- /dev/null +++ b/resources/views/emails/new-comment.blade.php @@ -0,0 +1,27 @@ + + +
💬
+ +

New comment on your video

+

Someone left a comment on {{ $video->title }}.

+ +

Hi {{ $recipient->name }},

+

+ {{ $commenter->name }} commented on your video: +

+ +
+ +
+ "{{ \Illuminate\Support\Str::limit(strip_tags($comment->body), 200) }}" +
+
+ +
+ +
+ +
+

You can manage your notification preferences in your account settings.

+ +
diff --git a/resources/views/emails/new-post-from-sub.blade.php b/resources/views/emails/new-post-from-sub.blade.php new file mode 100644 index 0000000..b0712cd --- /dev/null +++ b/resources/views/emails/new-post-from-sub.blade.php @@ -0,0 +1,26 @@ + + +
📝
+ +

New post from {{ $author->name }}

+

A channel you follow shared something.

+ +

Hi {{ $recipient->name }},

+ + @if($post->body) +
+ +
+ "{{ \Illuminate\Support\Str::limit(strip_tags($post->body), 300) }}" +
+
+ @endif + +
+ +
+ +
+

You can manage your notification preferences in your account settings.

+ +
diff --git a/resources/views/emails/new-reply.blade.php b/resources/views/emails/new-reply.blade.php new file mode 100644 index 0000000..81c77db --- /dev/null +++ b/resources/views/emails/new-reply.blade.php @@ -0,0 +1,27 @@ + + +
↩️
+ +

Someone replied to your comment

+

On the video {{ $video->title }}.

+ +

Hi {{ $recipient->name }},

+

+ {{ $replier->name }} replied to your comment: +

+ +
+ +
+ "{{ \Illuminate\Support\Str::limit(strip_tags($reply->body), 200) }}" +
+
+ +
+ +
+ +
+

You can manage your notification preferences in your account settings.

+ +
diff --git a/resources/views/emails/new-subscriber.blade.php b/resources/views/emails/new-subscriber.blade.php new file mode 100644 index 0000000..2a90a2b --- /dev/null +++ b/resources/views/emails/new-subscriber.blade.php @@ -0,0 +1,21 @@ + + +
🔔
+ +

You have a new subscriber!

+

Your channel is growing.

+ +

Hi {{ $recipient->name }},

+

+ {{ $subscriber->name }} just subscribed to your channel. + Keep creating great content! +

+ +
+ +
+ +
+

You can manage your notification preferences in your account settings.

+ +
diff --git a/resources/views/emails/new-video-from-sub.blade.php b/resources/views/emails/new-video-from-sub.blade.php new file mode 100644 index 0000000..a351b93 --- /dev/null +++ b/resources/views/emails/new-video-from-sub.blade.php @@ -0,0 +1,44 @@ + + +
🎬
+ +

New video from {{ $uploader->name }}

+

A channel you follow just posted.

+ +

Hi {{ $recipient->name }},

+

+ {{ $uploader->name }} uploaded a new video: + "{{ $video->title }}" +

+ + @if($video->thumbnail) +
+ + {{ $video->title }} + +
+ @endif + +
+ @if($video->description) + + @endif + @if($video->formatted_duration) + + @endif +
+ +
+ +
+ +
+

You can manage your notification preferences in your account settings.

+ +
diff --git a/resources/views/emails/video-liked.blade.php b/resources/views/emails/video-liked.blade.php new file mode 100644 index 0000000..1f6b656 --- /dev/null +++ b/resources/views/emails/video-liked.blade.php @@ -0,0 +1,28 @@ + + +
❤️
+ +

Someone liked your video!

+ +

Hi {{ $recipient->name }},

+

+ {{ $liker->name }} liked your video + "{{ $video->title }}". +

+ + @if($video->thumbnail) +
+ + {{ $video->title }} + +
+ @endif + +
+ +
+ +
+

You can manage your notification preferences in your account settings.

+ +
diff --git a/resources/views/emails/weekly-digest.blade.php b/resources/views/emails/weekly-digest.blade.php new file mode 100644 index 0000000..eb652ab --- /dev/null +++ b/resources/views/emails/weekly-digest.blade.php @@ -0,0 +1,63 @@ + + +
📊
+ +

Your week in numbers

+

Here's how your channel did in the last 7 days.

+ +

Hi {{ $creator->name }},

+ +
+ + + + +
+ + @if($stats['top_video']) +

Your top video this week:

+ + @if($stats['top_video']->thumbnail) +
+ + {{ $stats['top_video']->title }} + +
+ @endif + +
+ + +
+ +
+ +
+ @else +

+ Upload your first video to start tracking performance! +

+
+ +
+ @endif + +
+

You can manage your notification preferences in your account settings.

+ +
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 2e4d282..0fcdd3b 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -1226,20 +1226,27 @@ case 'new_comment': return actor + ' commented on your video ' + title + ':' + preview; case 'new_reply': return actor + ' replied to your comment:' + preview; case 'comment_like':return actor + ' liked your comment on ' + title; - case 'new_user': return '🎉 ' + actor + ' just joined TAKEONE!'; - default: return actor + ' uploaded a new video: ' + title; + case 'new_user': return '🎉 ' + actor + ' just joined TAKEONE!'; + case 'new_subscriber': return '🔔 ' + actor + ' subscribed to your channel'; + case 'video_like': return '❤️ ' + actor + ' liked your video ' + title; + case 'new_post': return '📝 ' + actor + ' posted something new'; + default: return actor + ' uploaded a new video: ' + title; } } function notifHref(d) { - if (d.type === 'new_user') return '/channel/' + encodeURIComponent(d.user_channel || ''); + if (d.type === 'new_user') return '/channel/' + encodeURIComponent(d.user_channel || ''); + if (d.type === 'new_subscriber') return '/channel/' + encodeURIComponent(d.actor_channel || ''); + if (d.type === 'new_post') return '/channel/' + encodeURIComponent(d.author_channel || ''); return '/videos/' + escHtml(d.video_route_key || ''); } function notifThumb(d) { - if (d.type === 'new_user') { - return d.user_avatar - ? '' + var avatarTypes = ['new_user', 'new_subscriber', 'new_post']; + if (avatarTypes.indexOf(d.type) !== -1) { + var av = d.user_avatar || d.actor_avatar || d.author_avatar || ''; + return av + ? '' : '
'; } return d.video_thumbnail diff --git a/resources/views/user/channel.blade.php b/resources/views/user/channel.blade.php index 3ffd767..5e665db 100644 --- a/resources/views/user/channel.blade.php +++ b/resources/views/user/channel.blade.php @@ -1076,6 +1076,42 @@ margin-bottom: 20px; } .ch-settings-card:last-child { margin-bottom: 0; } + +/* ── Notification preference rows ── */ +.notif-section-label { + font-size: 11px; font-weight: 700; text-transform: uppercase; + letter-spacing: .07em; color: var(--text-secondary); + margin: 0 0 12px; display: flex; align-items: center; gap: 6px; +} +.notif-pref-row { + display: flex; align-items: center; justify-content: space-between; + padding: 10px 0; border-bottom: 1px solid rgba(255,255,255,.04); +} +.notif-pref-row:last-of-type { border-bottom: none; } +.notif-pref-label { font-size: 14px; color: var(--text-primary); } +.notif-pref-note { + font-size: 12px; color: var(--text-secondary); + margin-top: 16px; line-height: 1.5; +} +.notif-toggle-wrap { + position: relative; width: 42px; height: 24px; + flex-shrink: 0; cursor: pointer; +} +.notif-toggle-input { opacity: 0; width: 0; height: 0; position: absolute; } +.notif-toggle-slider { + position: absolute; inset: 0; + background: rgba(255,255,255,.15); border-radius: 24px; + transition: background .2s; +} +.notif-toggle-slider::before { + content: ''; position: absolute; + width: 18px; height: 18px; border-radius: 50%; + background: #fff; top: 3px; left: 3px; + transition: transform .2s; + box-shadow: 0 1px 3px rgba(0,0,0,.4); +} +.notif-toggle-input:checked + .notif-toggle-slider { background: var(--brand-red); } +.notif-toggle-input:checked + .notif-toggle-slider::before { transform: translateX(18px); } .ch-settings-card-title { font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; @@ -2396,6 +2432,85 @@ if ($oldLinks) { @endif + @if($isOwner) + @php + $notifPrefs = $user->notification_preferences ?? []; + $notifDefs = \App\Models\User::notifDefaults(); + function notifVal(array $prefs, array $defs, string $key): bool { + return isset($prefs[$key]) ? (bool)$prefs[$key] : ($defs[$key] ?? true); + } + @endphp +
+
Notification Preferences
+ + {{-- Bell (In-App) --}} +
In-App Notifications
+ + @php + $bellPrefs = [ + ['notif_new_comment', 'New comment on my video'], + ['notif_new_reply', 'Reply to my comment'], + ['notif_comment_like', 'Someone liked my comment'], + ['notif_video_like', 'Someone liked my video'], + ['notif_new_subscriber', 'New subscriber'], + ['notif_new_video_from_sub', 'New video from channels I follow'], + ['notif_new_post_from_sub', 'New post from channels I follow'], + ]; + if ($user->isSuperAdmin()) { + $bellPrefs[] = ['notif_new_user_reg', 'New user registration']; + } + @endphp + + @foreach($bellPrefs as [$key, $label]) +
+ {{ $label }} + +
+ @endforeach + + {{-- Email --}} +
Email Notifications
+ + @php + $emailPrefs = [ + ['email_new_comment', 'New comment on my video'], + ['email_new_reply', 'Reply to my comment'], + ['email_comment_like', 'Someone liked my comment'], + ['email_video_like', 'Someone liked my video'], + ['email_new_subscriber', 'New subscriber'], + ['email_new_video_from_sub', 'New video from channels I follow'], + ['email_new_post_from_sub', 'New post from channels I follow'], + ['email_video_processed', 'My video finished processing'], + ['email_weekly_digest', 'Weekly activity digest (every Monday)'], + ]; + if ($user->isSuperAdmin()) { + $emailPrefs[] = ['email_new_user_reg', 'New user registration']; + } + @endphp + + @foreach($emailPrefs as [$key, $label]) +
+ {{ $label }} + +
+ @endforeach + +

Security emails (verification, password reset) are always sent and cannot be disabled.

+
+ @endif +
Account
@@ -2957,6 +3072,27 @@ function onBannerSaved(url) { if (banner) banner.insertBefore(newImg, banner.firstChild); } } + +// ── Notification preference toggle ──────────────────────────────────────── +function saveNotifPref(key, value) { + var csrf = document.querySelector('meta[name="csrf-token"]'); + fetch('{{ route("settings.notifications") }}', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrf ? csrf.getAttribute('content') : '', + 'Accept': 'application/json', + }, + body: JSON.stringify({ key: key, value: value }), + }).then(function(r) { + if (!r.ok) throw new Error('save failed'); + }).catch(function() { + // Revert toggle on failure + var el = document.querySelector('[data-key="' + key + '"]'); + if (el) el.checked = !value; + if (window.showToast) showToast('Could not save preference.', 'error'); + }); +} @if($isOwner && !$preview) diff --git a/routes/web.php b/routes/web.php index 7027646..e5c2187 100644 --- a/routes/web.php +++ b/routes/web.php @@ -95,6 +95,7 @@ Route::middleware(['auth', 'verified'])->group(function () { // Settings Route::get('/settings', [UserController::class, 'settings'])->name('settings'); Route::put('/settings', [UserController::class, 'updateSettings'])->name('settings.update'); + Route::post('/settings/notifications', [UserController::class, 'updateNotificationPreferences'])->name('settings.notifications'); // History & Liked Route::get('/history', [UserController::class, 'history'])->name('history');