Les Notifications

11

Traduction complétée à

Au programme de ce chapitre :

  • Ajouter une collection notifications pour notifier les utilisateurs des actions des autres utilisateurs.
  • Apprendre comment partager uniquement les notifications pertinentes pour un utilisateur donné.
  • En apprendre plus sur les publications et les souscriptions.
  • Maintenant que les utilisateurs peuvent commenter les articles de chacun, il serait bien de les avertir qu'une conversation a commencé.

    Pour ce faire, nous notifierons à l'auteur de l'article qu'il y a eu un commentaire, et nous lui fournirons un lien pour voir le commentaire.

    C'est sur ce type de fonctionnalité que Meteor resplendit : parce que Meteor est en temps réel par défaut, nous afficherons ces notifications instantanément. Nous n'avons pas besoin d'attendre que l'utilisateur rafraîchisse la page ou vérifie par un quelconque moyen, nous pouvons simplement faire apparaître de nouvelles notifications sans même écrire de code spécial.

    Créer des Notifications

    Nous allons créer une notification quand quelqu'un commente sur vos articles. Dans le future, les notifications pourront être étendues pour couvrir beaucoup d'autres scénarios, mais pour l'instant ce sera suffisant pour garder les utilisateurs informés de ce qu'il se passe.

    Créons notre collection Notifications, ainsi qu'une fonction createCommentNotification qui insérera une notification correspondante pour chaque nouveau commentaire sur un de vos articles.

    Puisque nous allons mettre à jour les notifications depuis le client, nous devons nous assurer que notre appel allow est blindé. Nous allons donc vérifier que :

    • l'utilisateur qui lance l'appel update possède la notification qui est en train d'être modifiée.
    • l'utilisateur essaie seulement de mettre à jour un seul champ.
    • ce seul champ est la propriété read de nos notifications.
    Notifications = new Mongo.Collection('notifications');
    
    Notifications.allow({
      update: function(userId, doc, fieldNames) {
        return ownsDocument(userId, doc) &&
          fieldNames.length === 1 && fieldNames[0] === 'read';
      }
    });
    
    createCommentNotification = function(comment) {
      var post = Posts.findOne(comment.postId);
      if (comment.userId !== post.userId) {
        Notifications.insert({
          userId: post.userId,
          postId: post._id,
          commentId: comment._id,
          commenterName: comment.author,
          read: false
        });
      }
    };
    
    lib/collections/notifications.js

    Comme les articles ou commentaires, cette collection Notifications sera partagée côté client et serveur. Comme nous avons besoin de mettre à jour les notifications une fois que l'utilisateur les a vues, nous autorisons également les mises à jours, en s'assurant comme d'habitude de bien limiter les permissions aux propres données de l'utilisateur.

    Nous avons également créé une simple fonction qui surveille l'article que l'utilisateur commente, détermine qui devrait être notifié à ce moment, et insère une nouvelle notification.

    Nous créons déjà des commentaires dans une méthode côté serveur, donc nous pouvons juste compléter cette méthode pour appeler notre fonction. Nous remplacerons return Comments.insert(comment); par comment._id = Comments.insert(comment) afin de sauvegarder l’_id du commentaire nouvellement créé dans une variable, puis appellerons notre fonction createCommentNotification :

    Comments = new Mongo.Collection('comments');
    
    Meteor.methods({
      commentInsert: function(commentAttributes) {
    
        //...
    
        comment = _.extend(commentAttributes, {
          userId: user._id,
          author: user.username,
          submitted: new Date()
        });
    
        // met à jour le post avec le nombre de commentaires
        Posts.update(comment.postId, {$inc: {commentsCount: 1}});
    
        // crée le commentaire et enregistre l'id
        comment._id = Comments.insert(comment);
    
        // crée maintenant une notification, informant l'utilisateur qu'il y a eu un commentaire
        createCommentNotification(comment);
    
        return comment._id;
      }
    });
    
    lib/collections/comments.js

    Publions également les notifications :

    Meteor.publish('posts', function() {
      return Posts.find();
    });
    
    Meteor.publish('comments', function(postId) {
      check(postId, String);
      return Comments.find({postId: postId});
    });
    
    Meteor.publish('notifications', function() {
      return Notifications.find();
    });
    
    server/publications.js

    Et souscrivons sur le client :

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      notFoundTemplate: 'notFound',
      waitOn: function() {
        return [Meteor.subscribe('posts'), Meteor.subscribe('notifications')]
      }
    });
    
    lib/router.js

    Commit 11-1

    Ajout d'une collection basique de notifications.

    Affichage des notifications

    Maintenant nous pouvons continuer et ajouter une liste de notifications à l'en-tête.

    <template name="header">
      <nav class="navbar navbar-default" role="navigation">
        <div class="navbar-header">
          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navigation">
            <span class="sr-only">Toggle navigation</span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </button>
          <a class="navbar-brand" href="{{pathFor 'postsList'}}">Microscope</a>
        </div>
        <div class="collapse navbar-collapse" id="navigation">
          <ul class="nav navbar-nav">
            {{#if currentUser}}
              <li>
                <a href="{{pathFor 'postSubmit'}}">Submit Post</a>
              </li>
              <li class="dropdown">
                {{> notifications}}
              </li>
            {{/if}}
          </ul>
          <ul class="nav navbar-nav navbar-right">
            {{> loginButtons}}
          </ul>
        </div>
      </nav>
    </template>
    
    client/templates/includes/header.html

    Et créer les modèles de notifications et notificationItem (ils partageront un même fichier notifications.html) :

    <template name="notifications">
      <a href="#" class="dropdown-toggle" data-toggle="dropdown">
        Notifications
        {{#if notificationCount}}
          <span class="badge badge-inverse">{{notificationCount}}</span>
        {{/if}}
        <b class="caret"></b>
      </a>
      <ul class="notification dropdown-menu">
        {{#if notificationCount}}
          {{#each notifications}}
            {{> notificationItem}}
          {{/each}}
        {{else}}
          <li><span>No Notifications</span></li>
        {{/if}}
      </ul>
    </template>
    
    <template name="notificationItem">
      <li>
        <a href="{{notificationPostPath}}">
          <strong>{{commenterName}}</strong> a commenté votre article
        </a>
      </li>
    </template>
    
    client/templates/notifications/notifications.html

    Nous pouvons voir que l'idée est de fournir dans chaque notification un lien vers l'article qui a été commenté, et le nom de l'utilisateur qui l'a commenté.

    Ensuite, nous avons besoin de nous assurer que nous sélectionnons la bonne liste de notifications dans notre helper, et mettons à jour les notifications comme “lues” quand l'utilisateur clique sur le lien vers lequel elles pointent.

    Template.notifications.helpers({
      notifications: function() {
        return Notifications.find({userId: Meteor.userId(), read: false});
      },
      notificationCount: function(){
        return Notifications.find({userId: Meteor.userId(), read: false}).count();
      }
    });
    
    Template.notificationItem.helpers({
      notificationPostPath: function() {
        return Router.routes.postPage.path({_id: this.postId});
      }
    });
    
    Template.notificationItem.events({
      'click a': function() {
        Notifications.update(this._id, {$set: {read: true}});
      }
    });
    
    client/templates/notifications/notifications.js

    Commit 11-2

    Afficher des notifications dans l'en-tête.

    Vous pouvez penser que les notifications ne sont pas si différentes des erreurs, et c'est vrai que leur structure est très similaire. Il y a néanmoins une différence clé : nous avons créé une collection client / serveur synchronisée. Cela signifie que nos notifications sont persistantes et, tant que nous gardons le même compte utilisateur, survivront au rafraîchissement des navigateurs et des différents appareils.

    Essayez-le : ouvrez un deuxième navigateur (disons Firefox), créez un nouveau compte utilisateur, et commentez sur un article que vous avez créé avec votre compte utilisateur principal (que vous avez laissé ouvert dans Chrome). Vous devriez voir apparaître quelque chose comme ça :

    Afficher des notifications.
    Afficher des notifications.

    Contrôler l'accès aux notifications

    Les notifications fonctionnent bien. Cependant, il y a un petit problème : nos notifications sont publiques.

    Si vous avez encore votre deuxième navigateur ouvert, essayez d’exécuter le code suivant dans une console navigateur :

     Notifications.find().count();
    1
    
    Console du navigateur

    Ce nouvel utilisateur (celui qui a commenté) ne devrait pas avoir de notifications. La notification qu'il voit dans la collection Notifications appartient en fait à notre utilisateur original.

    En marge de ces potentiels problèmes de confidentialité, nous ne pouvons pas nous permettre d'avoir toutes les notifications des utilisateurs chargées dans tous les navigateurs des autres utilisateurs. Sur un assez gros site, cela pourrait surcharger la mémoire disponible du navigateur et faire apparaître des sérieux problèmes de performance.

    Nous corrigeons ce problème avec les publications. Nous pouvons utiliser nos publications pour spécifier précisément quelle partie de notre collection nous voulons partager dans chaque navigateur.

    Pour accomplir cela, nous avons besoin de retourner un curseur différent dans notre publication de Notifications.find(). Plus exactement, nous voulons retourner un curseur qui correspond aux notifications de l'utilisateur courant.

    Faire cela est assez direct, puisqu’ une fonction publish a l’id de l'utilisateur courant disponible via this.userdId :

    Meteor.publish('notifications', function() {
      return Notifications.find({userId: this.userId, read: false});
    });
    
    server/publications.js

    Commit 11-3

    Synchroniser uniquement les notifications pertinentes pou…

    Maintenant, si nous vérifions dans nos deux fenêtres de navigateur, nous devrions voir deux collections de notifications différentes :

     Notifications.find().count();
    1
    
    Console du navigateur (utilisateur 1)
     Notifications.find().count();
    0
    
    Console du navigateur (utilisateur 2)

    En fait, la liste de Notifications devrait même changer selon que vous vous authentifiez et vous déconnectiez de votre application. Tout cela parce que les publications sont republiées automatiquement quand le compte utilisateur change.

    Notre application devient de plus en plus fonctionnelle, et à mesure que des utilisateurs s'inscrivent et ajoutent des liens nous courons le risque de finir avec une page d'accueil sans fin. Nous allons nous occuper de ça dans le prochain chapitre en implémentant une pagination.