En la actualidad, con el teletrabajo, se hace muy necesario tener comunicación directa e inmediata con el resto del equipo de proyecto.
Hay muchos productos comerciales con gran variedad de posibilidades, pero, para mí, era un reto hacer una aplicación que pudiera intercambiar mensajes con ficheros de forma inmediata, simulando en lo posible, al famoso WhatsApp.
En realidad la aplicación es una excusa, porque lo que quería hacer es una aplicación PHPRunner que tuviera algunas características de actualización de contenidos (con notificaciones incluidas) sin que tuviéramos que estar refrescando continuamente la página y para ello, he elegido hacer un ejemplo de aplicación Chat.
Objetivos funcionales
Los objetivos que me marqué son:
- A ser posible, que funcionara en PC y en móvil, con buenas prestaciones e interfaz en ambos.
- Que tuviera notificaciones de nuevos mensajes, sin que tuviéramos que refrescar la página.
- Que tuviera, además de la notificación, actualización de la información de la página, sin tener que hacer acción por el usuario.
- Que tuviera comunicación de persona a persona, pero que también dispusiera de grupos de distribución de los mensajes.
- Que dispusiera de EMOJI. Porque no es posible utilizar mensajes cortos sin EMOJI.
- Que además se pudiera enviar archivos (imágenes, PDF, etc.)
- Que el interface de la aplicación sea muy, muy simple y que dispusiera de pantallas muy dinámicas (diferentes presentaciones dependiendo de situación y contenido).
DEMO: https://fhumanes.com/chat
Usuarios de ejemplo (el login es igual que la password): admin, fhumanes, friend1, friend2.
Interfaz de la aplicación
Se ha incluido características de fondos de pantallas e icono de la aplicación, para poder instalarla tanto en Windows como en los móviles.
Se ha definido un interfaz muy diferente al habitual de PHPRunner para mostrar todos los contactos del usuario logado. Se ha utilizado la imagen del contacto de grupo, cuadrada y borde rojo y del contacto individual, redondo con borde azul. El orden de aparición es por fecha del último mensaje recibido y aparecerá una bola verde, con los mensajes que no han sido leídos de ese contacto.
En la cabecera aparece el total de mensajes sin leer. Este número se actualizará cuando lleguen nuevos mensajes, sin que el usuario tenga que hacer nada.
Seleccionado el contacto se muestra los mensajes intercambiado con el mismo. El último aparece el primero. Si no ha sido mostrado nunca, aparecerá una bola verde a la izquierda del mensaje. Junto con al autor, aparece la fecha y la hora del mensaje. Si el autor y la fecha, es el mismo que el anterior, no se muestra para facilitar la lectura.
A la derecha está el mensaje en HTML, y debajo del menaje estarán los ficheros adjuntos que tenga dicho mensaje.
Ahora mismo, tiene un funcionamiento básico, pero podrá ampliarse según las necesidades de vuestro proyecto.
Tanto en esta pantalla como en la anterior, se refresca el número de mensajes pendientes de leer (bola verde) y se muestran las notificaciones de nuevos mensajes en el borde inferior derecho de la pantalla. Si se hace clic en la notificación, se refresca la página en la que está el usuario y se muestran los nuevos contenidos.
Solución técnica
El modelo de datos que he utilizado es este:
Con este modelo se facilita que un mismo mensaje se muestre (con estados diferentes) para cada uno de los destinatarios. Es la forma de no duplicar los contenidos, siendo esta la más amplia información de todo el sistema.
En la tabla «contact» se mantiene la información de la relación de usuario a usuario y la información de un grupo. Podremos apreciar en el código, que la información se muestra dependiendo del tipo de contacto. En todos los casos de tipo de contacto siempre se utiliza la tabla «member» para contener todos los usuarios de ese contacto.
En la tabla «message» se disponen los datos generales de los mensajes ,pero nos ayudamos de varias tablas.
- «recipients» para gestionar la información de todos los destinatarios y el estado de ese mensaje para cada destinatario.
- «contents» para almacenar y gestionar el mensaje (que puede ser de gran tamaño) y los ficheros adjuntos. La desnormalización de esta tabla se hace para mejorar rendimientos del gestor de base de datos.
En el desarrollo se han utilizado los siguientes Plugins (que podéis descargar desde este portal) :
- Summernote.- Para introducir el texto de los mensajes.
- Switch.- Como interfaz, para la decisión en los contenidos.
También, se ha utilizado la librería de JavaScript notify.js. https://github.com/jpillora/notifyjs como gestor de las notificaciones. (en dispositivos móviles, estas notificaciones no se producen pero no genera error.
Para los que estén aprendiendo PHPRunner les recomiendo la revisión del código de este ejemplo. Es poco código, pero está lleno de detalles, de funcionalidades o usos que no están en el manual de PHPRunner y facilita un montón de ideas para utilizar en otros proyectos.
Facilito algunos ejemplos de la programación:
chat_ajax.php Programa que resuelve las peticiones automáticas que hace el navegador sin interacción del usuario.
<?php @ini_set("display_errors","1"); @ini_set("display_startup_errors","1"); require_once("include/dbcommon.php"); header("Expires: Thu, 01 Jan 1970 00:00:01 GMT"); $id_user = $_SESSION['id_user']; $rs = DB::Query("SELECT count(*) count FROM recipients WHERE isRead = 0 AND user_id = $id_user"); $data = $rs->fetchAssoc(); $count = $data['count']; $message = '*'; $rs2 = DB::Query("SELECT count(*) count FROM recipients WHERE isNotified = 0 AND user_id = $id_user order by dateAdd desc"); $data2 = $rs2->fetchAssoc(); $count2 = $data2['count']; $rs2 = DB::Query("SELECT * FROM recipients WHERE isNotified = 0 AND user_id = $id_user order by dateAdd desc"); if ( $count2 <> 0 ){ // There are messages to notify $data2 = $rs2->fetchAssoc(); $id_recipients = $data2['id_recipients']; $data = array(); $keyvalues = array(); $data["isNotified"] = "1"; $keyvalues["id_recipients"] = $id_recipients; DB::Update("recipients", $data, $keyvalues ); $id_contact = $data2['contact_id']; $sql = <<<EOT SELECT contact.id_contact, contact.administrator_id, contact.isGroup, if(contact.isGroup = 1, contact.nameGroup, user.name) AS name FROM contact AS contact JOIN member AS member ON ( member.contact_id = contact.id_contact ) JOIN user user ON ( member.user_id = user.id_user ) WHERE (contact.isDelete = 0 and user.id_user <> $id_user and contact.id_contact = $id_contact) EOT; $rs = DB::Query($sql); $data = $rs->fetchAssoc(); $name = $data['name']; $message = "You have received a new message from: '$name'"; } echo "$count;$message;"; ?>
PHPRunner crea el fichero mfhandler.php para gestionar la subida y bajada de ficheros. Para eliminar las restricciones de mostrar las fotos de los usuarios he creado el fichero custom_mfhandler.php.
Para mostrar en la cabecera el total de mensajes que están sin leer y programar el refresco de los datos y las notificaciones he programado notify_snnipet:
$id_user = $_SESSION['id_user']; $rs = DB::Query("SELECT count(*) count FROM recipients WHERE isRead = 0 AND user_id = $id_user"); $data = $rs->fetchAssoc(); echo ' <b>Message Pending Reading:</b> <span id="pending_reading" class="badge badge-info">'.$data['count'].'</span>'; $js = <<<EOT <script src="notify/notify.js"></script> <script> // ----------------------------------------------------------------------------------------------------- var site ='chat_ajax.php'; // To recover accountant new messages var HttpClient = function() { this.get = function(aUrl, aCallback) { var anHttpRequest = new XMLHttpRequest(); anHttpRequest.onreadystatechange = function() { if (anHttpRequest.readyState == 4 && anHttpRequest.status == 200) aCallback(anHttpRequest.responseText); } anHttpRequest.open( "GET", aUrl, true ); anHttpRequest.send( null ); } } // ----------------------------------------------------------------------------------------------------- function onShowNotification () { console.log('notification is shown!'); } function onCloseNotification () { console.log('notification is closed!'); } function onClickNotification () { window.location.reload(false); console.log('notification was clicked!'); } function onErrorNotification () { console.error('Error showing notification. You may need to request permission.'); } function onPermissionGranted () { console.log('Permission has been granted by the user'); doNotification(); } function onPermissionDenied () { console.warn('Permission has been denied by the user'); } function doNotification (response='') { if (!Notify.needsPermission) { var myNotification = new Notify('PHPRuner CHAT', { body: 'Notice: '+response, // tag: task, notifyShow: onShowNotification, notifyClose: onCloseNotification, notifyClick: onClickNotification, notifyError: onErrorNotification, timeout: 10 }); myNotification.show(); } else if (Notify.isSupported()) { Notify.requestPermission(onPermissionGranted, onPermissionDenied); } } function loadlink(){ var client = new HttpClient(); client.get(site, function(response) { console.log('Response: '+response); resp = response.split(';'); if ( resp[1] != '*' ){ doNotification(resp[1]); } $('#pending_reading').text(resp[0]); // Update count total within reading }) } loadlink(); // This will run on page load setInterval(function(){ loadlink(); // this will run after every 15 seconds }, 15000); </script> EOT; echo $js;
Como podéis ver, se controla con total facilidad los eventos sobre las notificaciones y se puede adaptar los tiempos de control de cambios de información.
No voy a poner mucho más código porque todo él está lleno de detalles, pero para muestra de lo «potente» que puede ser PHPRunner, os dejo los query’s de las tablas «contact» y «message»:
SELECT id_contact, contact.administrator_id, contact.isGroup, contact.nameGroup, contact.user_id, contact.photo, contact.isDelete, user1.name AS name1, user2.name AS name2, member.user_id AS member_user_id, statistics.lastMessage, (statistics.countMessage - statistics.sumRead) pendingMessage FROM contact LEFT OUTER JOIN `user` AS user1 ON contact.administrator_id = user1.id_user LEFT OUTER JOIN `user` AS user2 ON contact.user_id = user2.id_user INNER JOIN member ON contact.id_contact = member.contact_id LEFT JOIN ( SELECT user_id, contact_id, max(dateAdd) lastMessage, count(isRead) countMessage, sum(isRead) sumRead FROM recipients group by user_id, contact_id) statistics on (statistics.user_id = member.user_id and statistics.contact_id = contact.id_contact)
SELECT message.id_message, message.author_id, message.contact_id, message.isDelete, message.dateAdd, SUBSTRING(message.dateAdd, 1, 10) AS dateCreation, concat(SUBSTRING(message.dateAdd,1,16), ':00') AS timeCreation, message.dateDelete, contents.id_contents, contents.message_id, contents.text, 0 AS isAttached, contents.attachedfiles, recipients.id_recipients, recipients.user_id, recipients.isRead, recipients.isNotified FROM message LEFT OUTER JOIN contents ON message.id_message = contents.message_id LEFT OUTER JOIN recipients ON message.id_message = recipients.message_id
Espero que os guste el ejemplo, sobre todo, que os sea de utilidad y para cualquier duda o lo que necesitéis, podéis contactar conmigo a través del email [email protected].
Como siempre, os dejo todo el código para que lo podáis descargar e instalar en vuestros Windows y podáis hacer todos los cambios que requiera vuestro sistema.