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
@@ -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) {
|
||||
</div>
|
||||
@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
|
||||
<div class="ch-settings-card" id="notifPrefsCard">
|
||||
<div class="ch-settings-card-title"><i class="bi bi-bell-fill"></i> Notification Preferences</div>
|
||||
|
||||
{{-- Bell (In-App) --}}
|
||||
<div class="notif-section-label"><i class="bi bi-bell"></i> In-App Notifications</div>
|
||||
|
||||
@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])
|
||||
<div class="notif-pref-row">
|
||||
<span class="notif-pref-label">{{ $label }}</span>
|
||||
<label class="notif-toggle-wrap">
|
||||
<input type="checkbox" class="notif-toggle-input"
|
||||
data-key="{{ $key }}"
|
||||
{{ notifVal($notifPrefs, $notifDefs, $key) ? 'checked' : '' }}
|
||||
onchange="saveNotifPref('{{ $key }}', this.checked)">
|
||||
<span class="notif-toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
{{-- Email --}}
|
||||
<div class="notif-section-label" style="margin-top:20px;"><i class="bi bi-envelope"></i> Email Notifications</div>
|
||||
|
||||
@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])
|
||||
<div class="notif-pref-row">
|
||||
<span class="notif-pref-label">{{ $label }}</span>
|
||||
<label class="notif-toggle-wrap">
|
||||
<input type="checkbox" class="notif-toggle-input"
|
||||
data-key="{{ $key }}"
|
||||
{{ notifVal($notifPrefs, $notifDefs, $key) ? 'checked' : '' }}
|
||||
onchange="saveNotifPref('{{ $key }}', this.checked)">
|
||||
<span class="notif-toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
<p class="notif-pref-note">Security emails (verification, password reset) are always sent and cannot be disabled.</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="ch-settings-card">
|
||||
<div class="ch-settings-card-title"><i class="bi bi-info-circle-fill"></i> Account</div>
|
||||
<div class="ch-settings-field">
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@if($isOwner && !$preview)
|
||||
|
||||
Reference in new issue
Block a user