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 @@
+ On the video {{ $video->title }}. Hi {{ $recipient->name }},
+ {{ $liker->name }} liked your comment:
+ You can manage your notification preferences in your account settings. Someone left a comment on {{ $video->title }}. Hi {{ $recipient->name }},
+ {{ $commenter->name }} commented on your video:
+ You can manage your notification preferences in your account settings. A channel you follow shared something. Hi {{ $recipient->name }}, You can manage your notification preferences in your account settings. On the video {{ $video->title }}. Hi {{ $recipient->name }},
+ {{ $replier->name }} replied to your comment:
+ You can manage your notification preferences in your account settings. 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. A channel you follow just posted. Hi {{ $recipient->name }},
+ {{ $uploader->name }} uploaded a new video:
+ "{{ $video->title }}"
+ You can manage your notification preferences in your account settings. Hi {{ $recipient->name }},
+ {{ $liker->name }} liked your video
+ "{{ $video->title }}".
+ You can manage your notification preferences in your account settings. Here's how your channel did in the last 7 days. Hi {{ $creator->name }}, Your top video this week:
+ Upload your first video to start tracking performance!
+ You can manage your notification preferences in your account settings.Your comment got a like!
+
+ New comment on your video
+
+ New post from {{ $author->name }}
+
+ Someone replied to your comment
+
+ You have a new subscriber!
+
+ New video from {{ $uploader->name }}
+
+
+
+ Someone liked your video!
+
+
+
+
+ Your week in numbers
+
+
+
+ '
+ 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
+ ? '
'
: '
Security emails (verification, password reset) are always sent and cannot be disabled.
+