Cómo construir un chat sincronizado en tiempo real – Proyecto – Parte 3

Última actualización: 17 octubre, 2020

Tercera (¡y última!) parte de este proyecto basado en JavaScript, donde estamos construyendo un chat en tiempo real. Si acabas de aterrizar aquí, puedes ver las partes #1 y #2 antes. En ellas vimos cómo conectar un proyecto a Firebase, cómo crear y añadir JS classes a Firestore, cómo configurar un real time listener, cómo filtrar y ordenar datos y cómo mostrar el resultado de nuestro código en la UI, entre otras cosas.

Cómo mostrar nuevos mensajes en la UI

Para poder mostrar nuevos mensajes o chats en nuestra web, debemos configurar el comportamiento de uno de nuestros <form>s. Para eso, añadiremos un event listener al <form> para obtener el valor que el usuario escriba, y posteriormente actualizaremos la base de datos con el nuevo mensaje.

1. Obtenemos una referencia al <form> destinado a los mensajes. Este:

<!-- new chat form (message) -->
        <form class="new-chat my-3">

Apuntamos la referencia en el app.js.

const newChatForm = document.querySelector('.new-chat');

2. Creamos la funcionalidad para añadir mensajes a la UI. Aquí es donde agregamos un event listener al newChatFormvinculado a un submit event

A la callback function le pasamos el event object, porque necesitamos evitar que la página se recargue por defecto al hacer click en el botón send. 

Averiguamos el valor de lo que el usuario escriba y envíe. Para eso, creamos una variable llamada message y le damos el valor de lo que el usuario escriba, al cual tenemos acceso a través de id del <input> de dentro del <form>.

Usamos el método trim() para deshacernos de cualquier espacio en blanco anterior o posterior al input del usuario. 

// add a new chat
newChatForm.addEventListener('submit', e => {
        e.preventDefault();
        const message = newChatForm.message.value.trim();
});

3. Añadimos (finalmente) un mensaje a la UI. Para eso, usaremos el método addChat() del chat.js. Recordemos que ese método espera un argumento en forma de string (un mensaje, vaya). Así que llamamos al método, aplicándoselo a la instance de la clase Chatroom. 

newChatForm.addEventListener('submit', e => {
    e.preventDefault();
    const message = newChatForm.message.value.trim();
    chatroom.addChat(message)
});

🧐 Ten en cuenta que podemos acceder a la clase Chatroom y a sus métodos porque en el momento en el que el usuario envía un mensaje, el código referente a las clases de JS ya se ha ejecutado.

Recuerda que addChat() es un método asíncrono, así que devuelve una promesa. Por tanto debemos vincularle los métodos then() catch(). Cuando la promesa se cumpla, reseteamos el formulario usando el método reset(). Así el campo se quedará vacío cuando el usuario envíe su mensaje.

En método catch() simplemente se encarga de avisarnos del error que sucede en caso de que la promesa sea rechazada.

Como previamente hemos añadido un real time listenerahora cada vez que algún chat sea añadido a la database, ésta lo observará y nos mantendrá informados en caso de que se produzca algún cambio relacionado con ese chat. 🕵️‍♀️

Si ahora guardas y escribes algo en el campo del mensaje y lo envías, verás que ya aparece en tu web. ¡Estupendo! ✌

chatroom.addChat(message)
        .then(() => newChatForm.reset())
        .catch(err => console.log(err))
UI screenshot chat añadido

Cómo actualizar el username en la UI

 Hasta ahora estamos iniciando nuestra app con un usuario llamado Jim:

const chatroom = new Chatroom('general', 'Jim');

Pero lo suyo sería que el usuario pueda decidir qué nombre usar, así que vamos a darle esa posibilidad. Vamos a hacer que por defecto, un usuario tenga el nombre de "Anonymous" y que luego pueda cambiarlo por el que elija. Más adelante utilizaremos local storage para almacenar el username que el usuario haya elegido, para así poder mostrárselo cuando vuelva a la app.

1. En el app.js, en la sección DOM queries, obtenemos una referencia a este <form>:

 <form class="new-name my-3">

al que llamamos newUsernameForm.

const newUsernameForm = document.querySelector('.new-name');

2. Justo arriba de crear las instances de las clases, añadimos un event listener a dicho formulario. Nuevamente lo vinculamos a un submit event y le pasamos el event object como parámetro de la callback function. 

Evitamos que la web se recargue y obtenemos la información que el usuario envíe a través de este formulario, de manera muy parecida a como lo hemos hecho en la sección anterior.

// update username 
newUsernameForm.addEventListener('submit', e => {
    e.preventDefault();
    const newUsername = newUsernameForm.name.value.trim();
});

3. Dentro del event listener, llamamos al método updateUsername() y le pasamos la variable newUsername como argumento.

4. Limpiamos el formulario después de que el usuario lo envíe.

    // update username via chatroom instance
    const newUsername = newUsernameForm.name.value.trim();
    chatroom.updateUsername(newUsername);
    // reset the form
    newUsernameForm.reset();

5. Informamos al usuario de que su username se ha actualizado cuando así haya sido. Para eso vamos a echar mano de esta parte del index.html:

            <div class="update-message"></div>

Obtenemos una referencia a ese div en la sección DOM queries del app.js. 

const messageUpdate = document.querySelector('.update-message');

En el event listener, hacemos aparecer y desaparecer el mensaje usando la propiedad innerText y la función setTimeoutSi simplemente hacemos esto: 

    // show and hide the update message
    messageUpdate.innerText = `Username updated to ${newUsername}`;

Verás en tu UI que el mensaje aparece, pero se queda ahí para siempre. Para hacerlo desaparecer al cabo de unos segundos, usamos el setTimeout a continuación.

    setTimeout(() => messageUpdate.innerText = '', 3000);

¡Ya tenemos nuestra pequeña notificación configurada! Vamos a darle un retoque para mejorar su aspecto. Utilizamos para eso algunas clases de bootstrap dentro del index.html.

            <div class="update-message text-center my-3"></div>

Si ahora actualizamos el username mike, por ejemplo, verás que en tu consola, si accedes a la instance de Chatroom, el username se ha actualizado correctamente. 😃

username updated

Prueba a escribir y enviar un nuevo mensaje y verás que el nombre del usuario es mike. Puedes probar a actualizar el username varias veces para asegurarte de que funciona como esperamos. El problema aquí es que, si ahora recargas la página, los chats escritos hasta el momento se mantienen, pero el username se ha reseteado al username que le hemos dado al principio al crear esta instance: 

const chatroom = new Chatroom('general', 'Jim');

Vamos a cambiar eso en la siguiente sección. ¡Local storage al rescate! 🛫

Añadiendo local storage

Lo que vamos a hacer es guardar en local storage el username que el usuario elija usando el formulario. Comprobaremos si el usuario ha introducido un username o no.  Si no ha elegido ninguno, por defecto mostraremos Anonymous. Pero si ha elegido un username, éste ya estará registrado en local storage. De ser así, iniciaremos la instance de Chatroom con ese username. 

1. Vamos al chat.js, al método updateName(), y guardamos en local storage el nombre que el usuario elija como username. 

    updateUsername(username) {
        this.username = username;
        localStorage.setItem('username', username);
    }

Si ahora introduces cualquier nombre y vas a la pestaña Application de tu consola, verás que ese nombre está ahora guardado ahí, en local storage. ¡Genial! 👌

2. Comprobamos si un username ya existe en local storage. Esto lo hacemos cuando la web se inicia, en el app.js, antes de que creemos las instances de las clases. Guardamos el resultado en una variable llamada username y hacemos la comprobación utilizando un ternary operator. 

🧐 Fíjate que el último valor del ternary operator será el valor por defecto en caso de que el usuario no haya establecido ningún username. 

Sustituimos el segundo argumento de la instance "chatroom", que hasta ahora estaba hardcoded, por la variable username recién creada.

// check if username already exists in local storage
const username = localStorage.username ? localStorage.username : 'Anonymous';

// class instances
const chatUI = new ChatUI(chatList);
const chatroom = new Chatroom('general', username);

En mi caso, si ahora guardo y recargo la página, el nombre de mike debería estar guardado como el actual username. Si ahora escribo un mensaje y lo envío, me saldrá que "mike" ha sido quien ha enviado el mensaje. ¡Perfecto! 👍

Puedes probar a borrar cualquier dato del local storage (desde Application) y recargar la páginaSi ahora escribes algo, verás que, como no hay nada registrado en local storage, tu nombre de usuario es Anonymous. 😎

👉 Si en algún momento ves que tu página se llena de chats debido a todas tus pruebas, recuerda que puedes ir a Firebase y borrar los chats que no necesites. 

Cómo actualizar el canal en la UI

Uno de los retoques finales es apañar los botones de los canales para que cada vez que hagamos click en alguno, cambiemos de canal y sólo podamos ver los mensajes referentes a ese canal. Para eso, vamos a adjuntar event listeners al parent element de esos botones, para luego usar event delegationcomprobando en qué botón el usuario ha hecho click

El parent element de los botones es este div:

        <div class="chat-rooms mb-3 text-center">

Así que obtenemos una referencia ese div en el app.js, en la sección DOM queries. Llamamos a la referencia channels. 

const channels = document.querySelector('.chat-rooms');

Justo arriba de la configuración del local storageincluiremos el código para cambiar de canal. 

1. Añadimos un event listener channels vinculado a un click event y le pasamos el event object como parámetro de la callback function. Necesitamos ese event object para comprobar dónde ha hecho click el usuario (cuál ha sido su target).

// update channel 
channels.addEventListener('click', e => {
    if(e.target.tagName === 'BUTTON') {
        
    }
});

Si efectivamente el usuario ha hecho click en uno de los botones de los canales, borraremos todos los chats que se estén mostrando actualmente para dar paso a los chats vinculados al nuevo canal. Para eso debemos definir un método en la clase ChatUI. Lo llamaremos clear(). Lo único que hará será coger la lista (this.list) y borrar su contenido. Podemos hacer eso dándole el valor de un string vacío y usando la propiedad innerHTML.

class ChatUI {
    constructor(list) {
        this.list = list;
    }

    clear() {
        this.list.innerHTML = '';
    }

2. Llamamos al método clear() en app.js, a través de la instance de ChatUI. 

channels.addEventListener('click', e => {
    if (e.target.tagName === 'BUTTON') {
        chatUI.clear();
    }
});

Si ahora probamos a hacer click en cualquier botón para cambiar de canal, verás que la ventana de los chats se borra. Perfecto, ahora sólo nos falta actualizar el canal sobre que el que hagamos click para que muestre las conversaciones pertenecientes a ese canal. 

Para eso tenemos un método en la clase Chatroom, el updateChannel(). Así que lo invocamos dentro del bloque donde actualizamos nuestro canal. 

channels.addEventListener('click', e => {
    if (e.target.tagName === 'BUTTON') {
        chatUI.clear();
        chatroom.updateChannel();
    }
});

Recordemos que el método updateChannel() espera el nuevo canal como argumento. Así que accedemos a los canales a través de su id.

        chatroom.updateChannel(e.target.getAttribute('id'));

Al utilizar el método updateChannel(), también estamos dejando de escuchar los cambios que sucedan en el canal que acabamos de abandonar. Eso es justo lo que queremos. Pero aún tenemos que configurar un real time listener para el canal al que acabamos de cambiar. 

Para hacer eso simplemente tenemos que llamar al método getChats(), porque ahí es donde tenemos configurado el real time listener. Recuerda que este método espera una callback function como argumento. A su vez, la callback function espera un argumento, que, si repasas el método getChats(), verás que es:

callbackFunc(change.doc.data());

Es decir, la información de un document (un chat). Por eso establecemos un chat como parámetro de la callback function. El objetivo de esta función será mostrar ese chat en el UI. Para conseguir eso, usamos la instance del ChatUI y le aplicamos el método render(), al que le pasamos el argumento chat. 

    if (e.target.tagName === 'BUTTON') {
        chatUI.clear();
        chatroom.updateChannel(e.target.getAttribute('id'));
        chatroom.getChats(chat => chatUI.render(chat));
    }

Esta última adición sigue el mismo patrón que en este bloque, recordemos:

// get chats and render to the DOM
chatroom.getChats(data => chatUI.render(data));

¡Y ya está! Ahora puedes cambiar los canales y ver qué se cuece en cada canal. 😉

Nuestra app está lista, puedes probar a abrir varias ventanas en incógnito y simular que eres varias personas, hablar contigo mismo y ver que todo funciona. 😄

THE END!

¡Y con esto terminamos nuestro proyecto de chat en tiempo real! Espero que hayas aprendido algo nuevo 😊.  Si te queda alguna duda, ¡nos vemos en los comentarios!

Y si crees que este post puede serle útil a alguien, ¡compártelo!

Otros artículos que pueden interesarte

Días del 353 al 386
Objetivos versus realidad Y nuevamente, llegó otro día clave. Llegó…y pasó. El pasado 4 de marzo este Reto Computer Geek[...]
Construye Minioland: Tu primera aplicación con Angular | Parte #1
¿Qué vamos a construir?Minioland, o así he decidido llamar a esta sencilla app, totalmente responsive y de tipo Single Page[...]
Angular: Entendiendo la Directiva ngModel
Angular es un framework que nos permite, entre otras cosas, añadir contenido dinámico a nuestros archivos HTML. Una de las formas[...]
Si crees que este post puede serle útil a alguien, por favor, ¡compártelo!:

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Como toda web legal que se precie, utilizamos cookies para asegurar que damos la mejor experiencia al usuario en nuestro sitio web. Si continúas utilizando este sitio asumiremos que estás de acuerdo. más información

Los ajustes de cookies de esta web están configurados para "permitir cookies" y así ofrecerte la mejor experiencia de navegación posible. Si sigues utilizando esta web sin cambiar tus ajustes de cookies o haces clic en "Aceptar" estarás dando tu consentimiento a esto.

Cerrar