Système de notifications in-app (cloche). Schéma de la table notifications : voir entities/communication.md.
Ce qui déclenche réellement une notification
Type (NotificationType) |
Déclenché par |
|---|---|
new_comment |
comments/services/comment.service.ts (createCommentForMedia) |
new_chat_message |
chats/services/chat-message.service.ts (saveMessage) |
media_added, piste_update, new_comment_reply, new_chat_message_reply |
jamais émis — définis dans l'enum/l'entité mais aucun code ne les déclenche |
Les réponses (à un commentaire ou un message) réutilisent le type parent (new_comment/new_chat_message) : le front distingue une réponse via comment.originalCommentId, pas via notification.type.
Groupage (groupCount)
- Clé de regroupement :
chatChannelId>mediaId>pisteId(le premier non-null l'emporte). - À la création, si une notification non lue existe déjà pour le même type + user + clé, on incrémente son
groupCountet on rafraîchit soncreatedAt(elle remonte en haut de liste) au lieu de créer une nouvelle ligne. - Le groupage ne vaut que tant que la notification reste non lue — une fois lue, l'événement suivant repart sur une nouvelle ligne à
groupCount = 1. - Pas de transaction ni de verrou : c'est un read-modify-write simple, appelé en fire-and-forget par les deux déclencheurs ci-dessus (non attendu par la requête HTTP). Deux événements quasi simultanés pour la même cible peuvent donc créer deux lignes au lieu d'incrémenter une seule.
Temps réel (SSE)
GET /notifications/stream(Server-Sent Events). Le token JWT est passé en query param (?token=) carEventSourcene permet pas d'envoyer des headers custom.- Le bus est un
SubjectRxJS en mémoire, local à l'instance de l'API — si l'API tourne sur plusieurs instances/pods, un client connecté sur l'instance A ne reçoit rien des événements traités par l'instance B. - Reconnexion front : backoff exponentiel, abandon silencieux après 5 tentatives (
console.errorseulement, rien remonté à l'UI).
Lu / non lu
Endpoints (voir bruno/Notifications/) : GET /notifications, GET /notifications/unread, GET /notifications/unread/count, POST /notifications/:id/read, POST /notifications/read-all, POST /notifications/chat-channel/:channelId/read.
POST /notifications/:id/readne vérifie pas que la notification appartient à l'utilisateur courant — seulJwtAuthGuardprotège la route, sans filtreWHERE userId = .... Un utilisateur authentifié qui devine un id peut donc marquer comme lue la notification d'un autre.
Piège : deux compteurs "non lu" indépendants pour le chat
Le badge de discussion (pastille sur l’onglet Discussion) doit utiliser unreadChatMessages (calculé via chat_channels_users.lastMessageReadIndex vs chat_channels.lastMessageIndex, exposé par GET /user-notifications). Le badge de la cloche utilise unreadMessages (notifs commentaires non lues, hors types chat).
Ne pas brancher la pastille Discussion sur unreadMessages : sinon elle s’allume dès qu’il y a des réponses à des commentaires, même sans aucune conversation (bug corrigé INSP-480).
Marquer une notification cloche comme lue ne touche pas le compteur chat, et inversement. Les notifications de type chat sont filtrées de la liste affichée dans la cloche — elles n’existent en pratique que pour nourrir le badge Discussion.
Ce n'est pas de l'email ni du push
Purement in-app — aucun email ou push n'est envoyé quand une ligne notifications est créée.
Un mécanisme totalement séparé et indépendant, chats/services/chat-notification.service.ts (crons quotidiens 19h/20h, voir cron-tasks.md), envoie des emails de relance ("vous avez un message non lu depuis N jours") — il ne lit ni n'écrit la table notifications, ne pas confondre les deux systèmes.
INSP-480 — Réponses aux commentaires (comportement actuel)
Quand quelqu'un répond à un commentaire vidéo (originalCommentId renseigné), CommentService.createCommentForMedia crée immédiatement des notifications new_comment (pas new_comment_reply) pour :
- le créateur du média ;
- les EE dont la piste est liée au média (
parcours-eclaireurs→pistes→related-medias) ; - l'auteur du commentaire original (réponse) ;
puis exclut l'auteur de la nouvelle réponse. Livraison : SSE à la création + backfill syncCommentNotifications à la connexion SSE. Pas de cron horaire.
UI cloche (NotificationItem.vue) : avatar, prénom gras + « a répondu à ton commentaire » si comment.originalCommentId, durée relative (formatDateDifference), extrait du texte, clic → /inspiration?mediaId=&openComments=true&commentId=.
Écarts vs critères d'acceptation INSP-480
| Attendu CA | État actuel |
|---|---|
| 1 notification par réponse | Groupage : 1 ligne / mediaId non lu (groupCount) |
| Type dédié / wording via type | Toujours new_comment ; wording déduit de originalCommentId |
| Réponse complète | Texte tronqué à 100 caractères dans la cloche |
| Tag piste / article | Chip si piste?.name ou media?.piste?.name ; à la création pas de pisteId ; l'API joint media.relatedMedias.piste (pas media.piste) — chip souvent absent ; pas de fallback article |
| Fil de discussion en haut de la liste | Scroll + highlight 2s seulement ; ordre chronologique inchangé |
| Batch toutes les heures (option CA) | Non implémenté (immédiat uniquement) |
Jeu de données local (demo)
cd api && yarn console seed-comment-notifications-demo
# ou ciblé :
yarn console seed-comment-notifications-demo vlecsei@gmail.com
# Discussions (chat) — pastille Discussion indépendante de la cloche :
yarn console seed-chat-discussions-demo vlecsei@gmail.com
Crée des comptes demo-notif-*@inspire.local (mdp DemoNotif480!), un média lié à une piste, commentaires / réponses et notifs via CommentService. Affiche les URLs deep link en fin de run. Voir le script api/src/core/scripts/seed-comment-notifications-demo.ts.
Checklist manuelle : login LY → cloche (wording réponse, avatar, durée) → clic deep link → modale + highlight ; login EE → parcours inverse ; 2e réponse non lue → groupCount ; après lecture + nouvelle réponse → nouvelle ligne.
Historique
La migration fix-notifications-media-fk a corrigé une FK qui pointait par erreur vers related-medias au lieu de medias, ce qui empêchait la création d'une notification de commentaire dès que le média commenté n'était pas coïncidentellement aussi un id valide dans related-medias.
core/users/services/notification.service.ts existe encore dans le code mais n'est appelé nulle part (module mort, superseded par core/notifications) — ne pas s'y fier si on cherche où les notifications sont réellement créées.