Notificaciones

11

Porcentaje completado

En este capítulo:

  • Añadiremos una colección para notificar acciones de otros usuarios.
  • Aprenderemos cómo compartir sólo notificaciones relevantes con un usuario.
  • Aprenderemos cómo cargar solo los comentarios del post actual.
  • Ahora que los usuarios pueden comentar los posts de otros usuarios, sería bueno hacerles saber que alguien ha comenzado una conversación.

    Para ello, notificaremos al autor, de que ha habido un comentario en su post, y le proporcionaremos un enlace para poder comentar.

    En este tipo de funcionalidad es en la que Meteor brilla. Como por defecto, Meteor trabaja en tiempo real, vamos a poder mostrar notificaciones instantáneamente. No necesitamos esperar a que el usuario actualice la página, podemos mostrar nuevas notificaciones sin tener que escribir ningún código especial.

    Creando notificaciones

    Crearemos una notificación cuando alguien comente uno de nuestros posts. En el futuro, las notificaciones podrían extenderse para cubrir muchos otros escenarios, pero, por ahora será suficiente con esto para mantener a los usuarios informados sobre lo que está pasando.

    Vamos a crear la colección Notifications y la función createCommentNotification que insertará una notificación para cada comentario que se haga en uno de nuestros posts.

    Puesto que estamos actualizando las notificaciones desde el lado del cliente, necesitamos asegurarnos que nuestra llamada allow es a prueba de balas. Por lo que deberemos comprobar que:

    • El usuario que hace la llamada update es el dueño de la notificación modificada.
    • El usuario solo está intentando modificar un solo campo.
    • El campo a modificar es la propiedad read de nuestra notificación.
    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

    Al igual que con los posts o los comentarios, esta colección estará compartida por clientes y servidor. Como tendremos que actualizar las notificaciones cuando un usuario las haya visto, permitimos hacer update siempre que se trate de los datos del propio usuario.

    También creamos una función que mira qué post está comentando el usuario, averigua qué usuario debe ser notificado e inserta una nueva notificación.

    Ya tenemos un método en el servidor para crear comentarios, por lo que podemos ampliarlo para que llame a nuestra nueva función. Para guardar el _id del nuevo comentario en una variable, cambiamos return Comments.insert(comment);, por comment._id = Comments.insert(comment) y llamamos a la función createCommentNotification:

    Comments = new Mongo.Collection('comments');
    
    Meteor.methods({
      commentInsert: function(commentAttributes) {
    
        //...
    
        comment = _.extend(commentAttributes, {
          userId: user._id,
          author: user.username,
          submitted: new Date()
        });
    
        // update the post with the number of comments
        Posts.update(comment.postId, {$inc: {commentsCount: 1}});
    
        // create the comment, save the id
        comment._id = Comments.insert(comment);
    
        // now create a notification, informing the user that there's been a comment
        createCommentNotification(comment);
    
        return comment._id;
      }
    });
    
    lib/collections/comments.js

    Tenemos que publicar las notificaciones:

    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

    Y suscribirnos en el cliente:

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

    Commit 11-1

    Añadida la colección de comentarios.

    Mostrando las notificaciones

    Ahora podemos seguir y añadir una lista de notificaciones a nuestra cabecera:

    <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

    Y crear las plantillas notifications y notificationItem (que pondremos en el archivo 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> commented on your post
        </a>
      </li>
    </template>
    
    client/templates/notifications/notifications.html

    Podemos ver que para cada notificación, tendremos un enlace al post que ha sido comentado junto con el usuario que lo ha hecho.

    A continuación, hay que asegurarse de que se selecciona la lista de notificaciones correcta desde nuestro ayudante, y actualizar las notificaciones como “leídas” cuando el usuario hace clic en el enlace al que apuntan.

    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

    Mostrar las notificaciones en la cabecera.

    Como podemos ver, las notificaciones no son muy diferentes de los errores, y su estructura es muy similar. Solo hay una diferencia clave: hemos creado una colección sincronizada cliente-servidor. Esto significa que nuestras notificaciones son persistentes y, siempre y cuando se utilice la misma cuenta de usuario, persistirá en distintos navegadores y dispositivos.

    Abre un segundo navegador, crea una nueva cuenta de usuario, y añade un comentario en un post del usuario anterior. Deberías ver algo así:

    Mostrando las notificaciones.
    Mostrando las notificaciones.

    Controlando el acceso a las notificaciones

    Las notificaciones van bien. Sin embargo, hay un pequeño problema: nuestras notificaciones son públicas.

    Si ejecutamos el siguiente comando en la consola del segundo navegador:

     Notifications.find().count();
    1
    
    Consola del navegador

    El nuevo usuario (el que ha comentado) no debería tener notificaciones. Las que vemos son las de los demás usuarios.

    Aparte de los posibles problemas de privacidad, simplemente no podemos permitirnos el lujo de cargar las notificaciones de todos los usuarios. Con un sitio lo suficientemente grande, esto podría sobrecargar la memoria disponible en el navegador y empezar a causar graves problemas de rendimiento.

    Resolveremos este problema mediante las publicaciones. Podemos usar las publicaciones para especificar qué parte de nuestra colección queremos compartir con el navegador.

    Para lograrlo, tenemos que cambiar Notifications.find(). Es decir, tenemos que devolver el cursor que correspondiente a las notificaciones del usuario actual.

    Hacer esto es bastante sencillo puesto que la función publish tiene el _id del usuario actual disponible en this.userId:

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

    Commit 11-3

    Sincronizar solo las notificaciones relevantes al usuario.

    Si ahora se busca en las consolas de los dos navegadores, deberíamos ver dos colecciones distintas de notificaciones:

     Notifications.find().count();
    1
    
    Consola del navegador (usuario 1)
     Notifications.find().count();
    0
    
    Consola del navegador (usuario 2)

    De hecho, la lista de notificaciones cambiará si accedes y sales de la aplicación. Esto se debe a que las publicaciones se republican automáticamente cada vez que cambia el estado del usuario.

    Nuestra aplicación es cada vez más funcional, y a medida que cada vez más usuarios entran y empiezan a publicar enlaces, corremos el riesgo de acabar con una página de inicio demasiado larga. Vamos a abordar este problema en el próximo capítulo: la paginación.