Gestión de un Chat

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 '&nbsp;&nbsp;&nbsp;<b>Message Pending Reading:</b>&nbsp;<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.

Adjuntos

Archivo Tamaño de archivo Descargas
zip Proyecto PHPRunner 10.5 y backup de base de datos 2 MB 713

Blog personal para facilitar soporte gratuito a usuarios de React y PHPRunner