diff --git a/VEZA_VERSIONS_ROADMAP.md b/VEZA_VERSIONS_ROADMAP.md index 262638c73..6073bd007 100644 --- a/VEZA_VERSIONS_ROADMAP.md +++ b/VEZA_VERSIONS_ROADMAP.md @@ -633,35 +633,37 @@ Améliorer le système de playlists avec les fonctionnalités collaboratives et ### v0.10.5 — Notifications Complètes (F551-F570) -**Statut** : ⏳ TODO +**Statut** : ✅ DONE **Priorité** : P2 **Durée estimée** : 2-3 jours **Prerequisite** : v0.10.3 complète +**Complété le** : 2026-03-10 **Objectif** Implémenter le système de notifications complet, respectueux du temps de l'utilisateur, sans FOMO. **Tâches** -- [ ] Notifications in-app en temps réel (WebSocket) (F551) -- [ ] Digest hebdomadaire email (opt-in) (F552) - - Résumé des nouvelles sorties dans les genres suivis +- [x] Notifications in-app en temps réel (WebSocket) (F551) +- [x] Digest hebdomadaire email (opt-in) (F552) + - Nouvelles sorties des artistes suivis (ORIGIN §8.1) - Pas de recommandations comportementales - Référence : ORIGIN_BUSINESS_LOGIC.md §8.1 -- [ ] Préférences de notifications granulaires (F553) +- [x] Préférences de notifications granulaires (F553) - Contrôle par type (follow, commentaire, message, etc.) - Quiet hours configurables + - Toggle « désactiver marketing » (likes + commentaires en un clic) -- [ ] Groupement de notifications (F554) +- [x] Groupement de notifications (F554) - "3 personnes ont commenté votre track" (pas 3 notifications séparées) -- [ ] Centre de notifications (page dédiée) (F555) +- [x] Centre de notifications (page dédiée) (F555) **Critères d'acceptation** -- [ ] Aucune notification "vous avez atteint X likes" (anti gamification) -- [ ] Par défaut : notifications push désactivées sauf messages directs et follows -- [ ] L'utilisateur peut désactiver toutes les notifications marketing en un clic +- [x] Aucune notification "vous avez atteint X likes" (anti gamification) +- [x] Par défaut : notifications push désactivées sauf messages directs et follows +- [x] L'utilisateur peut désactiver toutes les notifications marketing en un clic --- @@ -1208,7 +1210,7 @@ Toutes les conditions suivantes doivent être remplies avant de taguer v1.0.0 : | v0.10.2 | Recherche Elasticsearch | P4R | ✅ DONE | 4-5j | v0.10.1 | | v0.10.3 | Commentaires & Interactions | P4R | ✅ DONE | 3-4j | v0.10.0 | | v0.10.4 | Playlists Collaboratives | P4R | ✅ DONE | 3-4j | v0.10.0 | -| v0.10.5 | Notifications Complètes | P4R | ⏳ TODO | 2-3j | v0.10.3 | +| v0.10.5 | Notifications Complètes | P4R | ✅ DONE | 2-3j | v0.10.3 | | v0.10.6 | Livestreaming Basique | P4R | ⏳ TODO | 5-7j | v0.10.0 | | v0.10.7 | Collaboration Temps Réel | P4R | ⏳ TODO | 5-6j | v0.10.6 | | v0.10.8 | Portabilité Données RGPD | P4R | ⏳ TODO | 2-3j | v0.10.0 | diff --git a/apps/web/src/features/settings/components/NotificationSettings.tsx b/apps/web/src/features/settings/components/NotificationSettings.tsx index 252f357a7..b0f92fcf3 100644 --- a/apps/web/src/features/settings/components/NotificationSettings.tsx +++ b/apps/web/src/features/settings/components/NotificationSettings.tsx @@ -24,8 +24,11 @@ export function NotificationSettings({ return (
+ {/* v0.10.5: PushPreferencesSection first — source of truth (API-backed) */} + +
-

Notifications par email

+

Préférences emails (paramètres utilisateur)

@@ -181,9 +184,6 @@ export function NotificationSettings({
- - {/* N1.3: Push preferences from API */} -
); } diff --git a/apps/web/src/features/settings/components/PushPreferencesSection.tsx b/apps/web/src/features/settings/components/PushPreferencesSection.tsx index 4cec713e4..c008f4495 100644 --- a/apps/web/src/features/settings/components/PushPreferencesSection.tsx +++ b/apps/web/src/features/settings/components/PushPreferencesSection.tsx @@ -42,6 +42,25 @@ export function PushPreferencesSection() { updateMutation.mutate({ ...prefs, [field]: value }); }; + /** v0.10.5: One-click disable marketing (likes + comments) */ + const handleMarketingToggle = (enabled: boolean) => { + if (!prefs) return; + updateMutation.mutate({ ...prefs, push_like: enabled, push_comment: enabled }); + }; + + /** v0.10.5: One-click "disable all except messages and follows" */ + const handleDisableAllExceptDirect = () => { + if (!prefs) return; + updateMutation.mutate({ + ...prefs, + push_follow: true, + push_message: true, + push_like: false, + push_comment: false, + push_mention: false, + }); + }; + if (isLoading || !prefs) { return (
@@ -55,6 +74,8 @@ export function PushPreferencesSection() { if (ok) showSuccess('Notifications push activées'); }; + const marketingEnabled = prefs.push_like && prefs.push_comment; + return (
@@ -77,6 +98,31 @@ export function PushPreferencesSection() {

{subscribeError}

)}
+ {/* v0.10.5 F553: One-click disable all marketing notifications */} +
+
+
+ +

+ Recevoir des notifications likes et commentaires — désactiver en un clic +

+
+ + handleMarketingToggle(checked === true) + } + /> +
+ +
@@ -196,11 +242,11 @@ export function PushPreferencesSection() {
)}
- {/* v0.10.5 F552: Weekly digest opt-in */} + {/* v0.10.5 F552: Weekly digest — new tracks from followed artists (ORIGIN §8.1) */}

Digest hebdomadaire

- Recevoir un récapitulatif par email des notifications non lues chaque dimanche + Recevoir chaque dimanche un récapitulatif des nouvelles sorties des artistes que vous suivez

diff --git a/veza-backend-api/internal/services/notification_digest_worker.go b/veza-backend-api/internal/services/notification_digest_worker.go index b678e02fa..599a8081a 100644 --- a/veza-backend-api/internal/services/notification_digest_worker.go +++ b/veza-backend-api/internal/services/notification_digest_worker.go @@ -1,4 +1,4 @@ -// Package services - v0.10.5 F552: Weekly notification digest worker +// Package services - v0.10.5 F552: Weekly digest of new tracks from followed users (ORIGIN §8.1) package services import ( @@ -12,6 +12,14 @@ import ( "gorm.io/gorm" ) +// digestTrackItem represents a track for the weekly digest (new releases from followed artists) +type digestTrackItem struct { + TrackID string + Title string + Artist string + Link string +} + // NotificationDigestWorker sends weekly digest emails to users with weekly_digest_enabled type NotificationDigestWorker struct { db *gorm.DB @@ -81,55 +89,70 @@ func (w *NotificationDigestWorker) runDigest(ctx context.Context) error { if baseURL == "" { baseURL = "http://localhost:5173" } - notificationsURL := baseURL + "/notifications" for _, u := range users { - var notifs []struct { + // ORIGIN §8.1: Weekly digest = new tracks from followed users (not unread notifications) + var tracks []struct { + TrackID string Title string - Content string - Link string - Type string + Artist string } err := w.db.WithContext(ctx).Raw(` - SELECT title, content, COALESCE(link, '') as link, type - FROM notifications - WHERE user_id = ? AND read = FALSE AND created_at > NOW() - INTERVAL '7 days' - ORDER BY created_at DESC + SELECT t.id::text as track_id, t.title, + COALESCE(NULLIF(TRIM(t.artist), ''), up.display_name, u.username) as artist + FROM tracks t + INNER JOIN follows f ON f.followed_id = t.creator_id AND f.follower_id = ? + LEFT JOIN users u ON u.id = t.creator_id + LEFT JOIN user_profiles up ON up.user_id = t.creator_id + WHERE t.status = 'completed' AND t.is_public = true + AND t.created_at > NOW() - INTERVAL '7 days' + ORDER BY t.created_at DESC LIMIT 50 - `, u.UserID).Scan(¬ifs).Error + `, u.UserID).Scan(&tracks).Error if err != nil { - w.logger.Warn("Failed to get notifications for digest", zap.String("user_id", u.UserID.String()), zap.Error(err)) + w.logger.Warn("Failed to get feed tracks for digest", zap.String("user_id", u.UserID.String()), zap.Error(err)) continue } - if len(notifs) == 0 { + if len(tracks) == 0 { continue } - notifList := make([]map[string]string, len(notifs)) - for i, n := range notifs { - notifList[i] = map[string]string{ - "Title": n.Title, - "Content": n.Content, - "Link": n.Link, - "Type": n.Type, + trackList := make([]digestTrackItem, len(tracks)) + for i, t := range tracks { + trackList[i] = digestTrackItem{ + TrackID: t.TrackID, + Title: t.Title, + Artist: t.Artist, + Link: "/tracks/" + t.TrackID, + } + } + // Template expects []map[string]string for range + trackMaps := make([]map[string]string, len(trackList)) + for i, t := range trackList { + trackMaps[i] = map[string]string{ + "TrackID": t.TrackID, + "Title": t.Title, + "Artist": t.Artist, + "Link": t.Link, } } templateData := map[string]interface{}{ - "Username": u.Username, - "Notifications": notifList, - "BaseURL": baseURL, - "NotificationsURL": notificationsURL, + "Username": u.Username, + "Tracks": trackMaps, + "BaseURL": baseURL, + "FeedURL": baseURL + "/feed", + "TrackCount": len(tracks), } w.jobEnqueuer.EnqueueEmailJobWithTemplate( u.Email, - "Your Veza weekly digest", + "New releases from artists you follow — Veza", "notification_digest", templateData, ) - w.logger.Info("Digest queued", zap.String("user_id", u.UserID.String()), zap.Int("count", len(notifs))) + w.logger.Info("Digest queued", zap.String("user_id", u.UserID.String()), zap.Int("count", len(tracks))) } return nil diff --git a/veza-backend-api/internal/services/notification_service.go b/veza-backend-api/internal/services/notification_service.go index 5fe351047..9ec63d531 100644 --- a/veza-backend-api/internal/services/notification_service.go +++ b/veza-backend-api/internal/services/notification_service.go @@ -418,7 +418,8 @@ type NotificationPrefs struct { func (ns *NotificationService) GetPreferences(userID uuid.UUID) (*NotificationPrefs, error) { ctx := context.Background() - prefs := &NotificationPrefs{PushFollow: true, PushLike: true, PushComment: true, PushMessage: true, PushMention: true} + // v0.10.5: default push_message and push_follow only; others off (anti-FOMO) + prefs := &NotificationPrefs{PushFollow: true, PushMessage: true, PushLike: false, PushComment: false, PushMention: false} var startNullable, endNullable sql.NullString err := ns.db.QueryRowContext(ctx, ` @@ -456,10 +457,11 @@ func (ns *NotificationService) UpdatePreferences(userID uuid.UUID, pushFollow, p qhStart := nullIfEmpty(quietHoursStart) qhEnd := nullIfEmpty(quietHoursEnd) + // v0.10.5: defaults — push_follow, push_message true; push_like, push_comment, push_mention false _, err := ns.db.ExecContext(ctx, ` INSERT INTO notification_preferences (user_id, push_follow, push_like, push_comment, push_message, push_mention, quiet_hours_enabled, quiet_hours_start, quiet_hours_end, weekly_digest_enabled, updated_at) - VALUES ($1, COALESCE($2, true), COALESCE($3, true), COALESCE($4, true), COALESCE($5, true), COALESCE($6, true), + VALUES ($1, COALESCE($2, true), COALESCE($3, false), COALESCE($4, false), COALESCE($5, true), COALESCE($6, false), COALESCE($7, false), $8::time, $9::time, COALESCE($10, false), NOW()) ON CONFLICT (user_id) DO UPDATE SET push_follow = CASE WHEN $2 IS NOT NULL THEN $2 ELSE notification_preferences.push_follow END, diff --git a/veza-backend-api/migrations/941_notification_prefs_defaults_v0105.sql b/veza-backend-api/migrations/941_notification_prefs_defaults_v0105.sql new file mode 100644 index 000000000..57931e2fd --- /dev/null +++ b/veza-backend-api/migrations/941_notification_prefs_defaults_v0105.sql @@ -0,0 +1,15 @@ +-- Migration 941: v0.10.5 — Notification preferences defaults +-- Par défaut : push_message et push_follow activés ; likes/commentaires/mentions désactivés (anti-FOMO) + +ALTER TABLE notification_preferences + ALTER COLUMN push_follow SET DEFAULT true, + ALTER COLUMN push_message SET DEFAULT true, + ALTER COLUMN push_like SET DEFAULT false, + ALTER COLUMN push_comment SET DEFAULT false, + ALTER COLUMN push_mention SET DEFAULT false; + +COMMENT ON COLUMN notification_preferences.push_follow IS 'v0.10.5: default true — new followers'; +COMMENT ON COLUMN notification_preferences.push_message IS 'v0.10.5: default true — direct messages'; +COMMENT ON COLUMN notification_preferences.push_like IS 'v0.10.5: default false — engagement (opt-in)'; +COMMENT ON COLUMN notification_preferences.push_comment IS 'v0.10.5: default false — engagement (opt-in)'; +COMMENT ON COLUMN notification_preferences.push_mention IS 'v0.10.5: default false — mentions (opt-in)'; diff --git a/veza-backend-api/migrations/941_notification_prefs_defaults_v0105_down.sql b/veza-backend-api/migrations/941_notification_prefs_defaults_v0105_down.sql new file mode 100644 index 000000000..4eb5b2972 --- /dev/null +++ b/veza-backend-api/migrations/941_notification_prefs_defaults_v0105_down.sql @@ -0,0 +1,7 @@ +-- Rollback migration 941: restore original push defaults +ALTER TABLE notification_preferences + ALTER COLUMN push_follow SET DEFAULT true, + ALTER COLUMN push_message SET DEFAULT true, + ALTER COLUMN push_like SET DEFAULT true, + ALTER COLUMN push_comment SET DEFAULT true, + ALTER COLUMN push_mention SET DEFAULT true; diff --git a/veza-backend-api/templates/email/notification_digest.html b/veza-backend-api/templates/email/notification_digest.html index 755ac00ed..c06640431 100644 --- a/veza-backend-api/templates/email/notification_digest.html +++ b/veza-backend-api/templates/email/notification_digest.html @@ -3,27 +3,24 @@ - Your Veza weekly digest + New releases from artists you follow — Veza
-

Your weekly digest

+

New releases from artists you follow

Hello {{.Username}},

-

Here are your unread notifications from the past week:

+

Here are the new tracks from artists you follow this week:

    - {{range .Notifications}} + {{range .Tracks}}
  • - {{.Title}}
    - {{.Content}} - {{if .Link}} -
    View → - {{end}} + {{.Artist}} — {{.Title}} +
    Listen →
  • {{end}}