paint-brush
Creación de ChatPlus: la PWA de código abierto que parece una aplicación móvilpor@aladinyo
1,148 lecturas
1,148 lecturas

Creación de ChatPlus: la PWA de código abierto que parece una aplicación móvil

por Alaa Eddine Boubekeur29m2024/07/10
Read on Terminal Reader

Demasiado Largo; Para Leer

ChatPlus es una de las mejores aplicaciones que creé, atrae la atención con su vibrante interfaz de usuario y múltiples funcionalidades de mensajería y llamadas, todas implementadas en la web, brinda una experiencia multiplataforma ya que su interfaz está diseñada para ser una aplicación PWA que tiene la capacidad de instalarse en todas partes y actuar como una aplicación independiente, con funciones como notificaciones automáticas, ChatPlus es la definición de una aplicación liviana que puede brindar todas las funciones de una aplicación móvil con tecnologías web.
featured image - Creación de ChatPlus: la PWA de código abierto que parece una aplicación móvil
Alaa Eddine Boubekeur HackerNoon profile picture
0-item
1-item

ChatPlus una gran PWA para chatear 💬✨🤩

ChatPlus es una aplicación web progresiva desarrollada con React, NodeJS, Firebase y otros servicios.

Puedes hablar con todos tus amigos en tiempo real 🗣️✨🧑‍🤝‍🧑❤️

Puedes llamar a tus amigos y tener llamadas de video y audio con ellos 🎥🔉🤩

Envía imágenes a tus amigos y también mensajes de audio y tendrás una IA que convierte tu voz en texto, ya sea que hables francés, inglés o español 🤖✨

La aplicación web se puede instalar en cualquier dispositivo y puede recibir notificaciones ⬇️🔔🎉

Agradecería mucho tu apoyo, déjanos una estrella en el repositorio de Github y compártela con tus amigos ⭐✨

Consulte este repositorio de Github para obtener documentación completa de instalación e implementación: https://github.com/aladinyo/ChatPlus

UI y UX simples y limpias

Vista móvil de ChatPlus


Vista de escritorio de ChatPlus


Entonces, ¿qué es ChatPlus?

ChatPlus es una de las mejores aplicaciones que creé, atrae la atención con su vibrante interfaz de usuario y múltiples funcionalidades de mensajería y llamadas, todas implementadas en la web, brinda una experiencia multiplataforma ya que su interfaz está diseñada para ser una aplicación PWA que tiene la capacidad de instalarse en todas partes y actuar como una aplicación independiente, con funciones como notificaciones automáticas, ChatPlus es la definición de una aplicación liviana que puede brindar todas las funciones de una aplicación móvil con tecnologías web.

Arquitectura de software

Nuestra aplicación sigue la arquitectura de software MVC, MVC (Modelo-Vista-Controlador) es un patrón en el diseño de software comúnmente utilizado para implementar interfaces de usuario, datos y lógica de control. Enfatiza una separación entre la lógica empresarial y la visualización del software. Esta "separación de preocupaciones" permite una mejor división del trabajo y un mejor mantenimiento.

Capa de interfaz de usuario (Ver)

Consta de múltiples componentes de interfaz React:

  • MaterialUI: Material UI es una biblioteca de componentes React de código abierto que implementa Material Design de Google. Es completo y se puede usar en producción desde el primer momento, lo usaremos para botones de íconos y algunos elementos en nuestra interfaz, como un control deslizante de audio.

  • LoginView: una vista de inicio de sesión simple que permite al usuario ingresar su nombre de usuario y contraseña o iniciar sesión con Google.

  • SidebarViews : múltiples componentes de vista como 'sidebar__header', 'sidebar__search', 'sidebar menu' y 'SidebarChats', consta de un encabezado para información del usuario, una barra de búsqueda para buscar usuarios, un menú de barra lateral para navegar entre sus chats, sus grupos. y usuarios, y un componente SidebarChats para mostrar todos los chats recientes en los que envió mensajes, muestra el nombre de usuario, la foto y el último mensaje.

  • ChatViews: consta de muchos componentes de la siguiente manera:

    1 .'chat__header' que contiene la información del usuario con el que estás hablando, su estado en línea, foto de perfil y muestra botones para llamada de audio y videollamada, también muestra si el usuario está escribiendo.

    2 .'chat__body--container' contiene la información de nuestros mensajes con los otros usuarios, tiene un componente de mensajes que muestra mensajes de texto, imágenes con su mensaje y también mensajes de audio con su información, como la hora del audio y si el audio se reprodujo y Al final de este componente tenemos el elemento "visto" que muestra si se vieron los mensajes.

    3 .'AudioPlayer': un componente de React que puede mostrarnos un audio con un control deslizante para navegar por él, muestra el tiempo completo y el tiempo actual del audio, este componente de vista se carga dentro de 'chat__body-- contenedor'.

    4. 'ChatFooter': contiene una entrada para escribir un mensaje, un botón para enviar un mensaje cuando se escribe la entrada, de lo contrario, el botón le permitirá grabar el audio, un botón para importar imágenes y archivos.

    5 .'MediaPreview': un componente de React que permite previsualizar las imágenes o archivos que hemos seleccionado para enviar en nuestro chat, se muestran en un carrusel y el usuario puede deslizar las imágenes o archivos y escribir un mensaje específico para cada uno.

    6 .'ImagePreview': Cuando tenemos imágenes enviadas en nuestro chat, este componente mostrará las imágenes en pantalla completa con una animación suave, el componente se monta después de hacer clic en una imagen.

  • scalePage: una función de visualización que aumenta el tamaño de nuestra aplicación web cuando se muestra en pantallas grandes como pantallas Full HD y pantallas 4K.

  • CallViews: un conjunto de componentes de reacción que contienen todos los elementos de vista de llamadas, tienen la capacidad de arrastrarse por toda nuestra pantalla y consta de:

    1 'Botones': un botón de llamada con una versión roja y un botón verde de videollamada.

    2 'AudioCallView': un componente de visualización que permite responder llamadas de audio entrantes y mostrar la llamada con un temporizador y permite cancelar la llamada.

    3 'StartVideoCallView': un componente de visualización que muestra un vídeo de nosotros mismos conectándonos a la MediaAPI local y espera a que el otro usuario acepte la llamada o muestra un botón para que respondamos una videollamada entrante.

    4 'VideoCallView': un componente de visualización que muestra un video de nosotros y del otro usuario. Permite cambiar de cámara, desactivar la cámara y el audio, también puede ir a pantalla completa.

  • RouteViews: componentes de React que contienen todas nuestras vistas para crear componentes de navegación locales, tenemos 'VideoCallRoute', 'SideBarMenuRoute' y 'ChatsRoute'

Modelos del lado del cliente (modelo)

Los modelos del lado del cliente son la lógica que permite que nuestro frontend interactúe con bases de datos y múltiples API locales y del lado del servidor y consta de:

  • Firebase SDK: Es un SDK que se utiliza para construir la base de datos de nuestra aplicación web.
  • AppModel: Un modelo que genera un usuario después de autenticarse y también asegura que tengamos la última versión de nuestros activos web.
  • ChatModels: consta de lógicas modelo para enviar mensajes a la base de datos, establecer oyentes para escuchar nuevos mensajes, escuchar si el otro usuario está en línea y si está escribiendo, también envía nuestros medios como imágenes y audios al almacenamiento de la base de datos. SidebarChatsModel: Lógica que escucha los últimos mensajes de los usuarios y nos brinda una matriz de todos los mensajes nuevos de los usuarios, también brinda la cantidad de mensajes no leídos y el estado en línea de los usuarios, también organiza a los usuarios según la hora del último mensaje.
  • UsersSearchModel: Lógica que busca usuarios en nuestra base de datos, utiliza algolia de búsqueda que tiene una lista de nuestros usuarios vinculándola a nuestra base de datos en el servidor.
  • CallModel: Lógica que utiliza el SDK de Daily para crear una llamada en nuestra aplicación web y también enviar los datos a nuestro servidor e interactúa con DailyAPI.

Controladores del lado del cliente (controlador)

Consta de componentes de React que vinculan nuestras vistas con sus modelos específicos:

  • Controlador de aplicación: vincula el usuario autenticado a todos los componentes y ejecuta la función scalePage para ajustar el tamaño de nuestra aplicación, también carga firebase y adjunta todos los componentes, podemos considerarlo un contenedor para nuestros componentes.
  • SideBarController: Vincula los datos de los usuarios y la lista de sus últimos chats, también vincula nuestros menús con su lógica de modelo, también vincula la barra de búsqueda con la API de búsqueda de algolia.
  • ChatController: este es un controlador muy grande que vincula la mayoría de las funciones de mensajería y chat.
  • CallController: Vincula el modelo de llamada con sus vistas.

Modelo del lado del servidor

No todas las funciones se realizan en la interfaz, ya que los SDK que utilizamos requieren algunas funcionalidades del lado del servidor y consisten en:

  • CallServerModel: Lógica que nos permite crear salas para llamadas interactuando con Daily API y actualizando nuestra base de datos de firestore.
  • TranscriptModel: lógica en el servidor que recibe un archivo de audio e interactúa con la API de voz a texto de Google Cloud y proporciona una transcripción de los mensajes de audio.
  • Controlador de estado en línea: un oyente que escucha el estado en línea de los usuarios y actualiza la base de datos en consecuencia. Modelo de notificación: un servicio que envía notificaciones a otros usuarios.
  • AlgoliaSaver: un oyente que escucha a nuevos usuarios en nuestra base de datos y actualiza algolia en consecuencia para que podamos usarlo para la función de búsqueda en la interfaz.
  • Controladores del lado del servidor: CallServer: un punto final API que contiene callModel Worker: un servicio de trabajo que ejecuta todos nuestros servicios de manejo de Firebase.

Diagrama de flujo de chat

Diagrama de flujo de chat


Diagrama de flujo de la barra lateral

Diagrama de flujo de la barra lateral

Diagrama de flujo de llamadas modelo:

Diagrama de flujo de llamadas modelo

Ver diagrama de flujo de llamadas

Ver diagrama de flujo de llamadas

Diagrama de flujo del trabajador backend

Diagrama de flujo del trabajador backend

Diseño de base de datos

Nuestra aplicación web utiliza Firestore para almacenar nuestra base de datos, que es una base de datos Firebase NoSql, almacenamos información de los usuarios, almacenamos una lista de todos los mensajes, almacenamos una lista de chats y también almacenamos chats en salas, estos son los datos almacenados en nuestra base de datos:

  • Datos de los usuarios después de la autenticación.
  • Salas que contienen todos los detalles de los mensajes.
  • Lista de chats más recientes para cada usuario.
  • Listado de notificaciones a enviar.
  • Lista de audio a transcribir.

UML de base de datos

Explicaciones del Código Mágico 🔮

En los próximos capítulos, daré una explicación rápida y tutoriales sobre ciertas funcionalidades en ChatPlus, le mostraré el código JS y le explicaré el algoritmo detrás de él y también le proporcionaré la herramienta de integración adecuada para vincular su código. la base de datos.

Manejo del estado en línea

El estado en línea de los usuarios se implementó mediante la función de conectividad de la base de datos de Firebase conectándose a ".info/connected" en la interfaz y actualizando tanto Firestore como la base de datos en consecuencia:

 var disconnectRef; function setOnlineStatus(uid) { try { console.log("setting up online status"); const isOfflineForDatabase = { state: 'offline', last_changed: createTimestamp2, id: uid, }; const isOnlineForDatabase = { state: 'online', last_changed: createTimestamp2, id: uid }; const userStatusFirestoreRef = db.collection("users").doc(uid); const userStatusDatabaseRef = db2.ref('/status/' + uid); // Firestore uses a different server timestamp value, so we'll // create two more constants for Firestore state. const isOfflineForFirestore = { state: 'offline', last_changed: createTimestamp(), }; const isOnlineForFirestore = { state: 'online', last_changed: createTimestamp(), }; disconnectRef = db2.ref('.info/connected').on('value', function (snapshot) { console.log("listening to database connected info") if (snapshot.val() === false) { // Instead of simply returning, we'll also set Firestore's state // to 'offline'. This ensures that our Firestore cache is aware // of the switch to 'offline.' userStatusFirestoreRef.set(isOfflineForFirestore, { merge: true }); return; }; userStatusDatabaseRef.onDisconnect().set(isOfflineForDatabase).then(function () { userStatusDatabaseRef.set(isOnlineForDatabase); // We'll also add Firestore set here for when we come online. userStatusFirestoreRef.set(isOnlineForFirestore, { merge: true }); }); }); } catch (error) { console.log("error setting onlins status: ", error); } };

Y en nuestro backend también configuramos un detector que escucha los cambios en nuestra base de datos y actualiza Firestore en consecuencia. Esta función también puede brindarnos el estado en línea del usuario en tiempo real para que podamos ejecutar otras funciones dentro de ella también:

 async function handleOnlineStatus(data, event) { try { console.log("setting online status with event: ", event); // Get the data written to Realtime Database const eventStatus = data.val(); // Then use other event data to create a reference to the // corresponding Firestore document. const userStatusFirestoreRef = db.doc(`users/${eventStatus.id}`); // It is likely that the Realtime Database change that triggered // this event has already been overwritten by a fast change in // online / offline status, so we'll re-read the current data // and compare the timestamps. const statusSnapshot = await data.ref.once('value'); const status = statusSnapshot.val(); // If the current timestamp for this data is newer than // the data that triggered this event, we exit this function. if (eventStatus.state === "online") { console.log("event status: ", eventStatus) console.log("status: ", status) } if (status.last_changed <= eventStatus.last_changed) { // Otherwise, we convert the last_changed field to a Date eventStatus.last_changed = new Date(eventStatus.last_changed); //handle the call delete handleCallDelete(eventStatus); // ... and write it to Firestore. await userStatusFirestoreRef.set(eventStatus, { merge: true }); console.log("user: " + eventStatus.id + " online status was succesfully updated with data: " + eventStatus.state); } else { console.log("next status timestamp is newer for user: ", eventStatus.id); } } catch (error) { console.log("handle online status crashed with error :", error) } }

Notificaciones

Las notificaciones son una característica excelente y se implementan mediante mensajería de Firebase. En nuestra interfaz, si el navegador del usuario admite notificaciones, lo configuramos y recuperamos el token de mensajería de Firebase del usuario:

 const configureNotif = (docID) => { messaging.getToken().then((token) => { console.log(token); db.collection("users").doc(docID).set({ token: token }, { merge: true }) }).catch(e => { console.log(e.message); db.collection("users").doc(docID).set({ token: "" }, { merge: true }); }); }

Cada vez que un usuario envía un mensaje, agregamos una notificación a nuestra base de datos:

 db.collection("notifications").add({ userID: user.uid, title: user.displayName, body: inputText, photoURL: user.photoURL, token: token, });


y en nuestro backend escuchamos la colección de notificaciones y usamos la mensajería de Firebase para enviársela al usuario.

 let listening = false; db.collection("notifications").onSnapshot(snap => { if (!listening) { console.log("listening for notifications..."); listening = true; } const docs = snap.docChanges(); if (docs.length > 0) { docs.forEach(async change => { if (change.type === "added") { const data = change.doc.data(); if (data) { const message = { data: data, token: data.token }; await db.collection("notifications").doc(change.doc.id).delete(); try { const response = await messaging.send(message); console.log("notification successfully sent :", data); } catch (error) { console.log("error sending notification ", error); }; }; }; }); }; });

Transcripción de audio con IA

Nuestra aplicación web permite a los usuarios enviarse mensajes de audio entre sí, y una de sus características es la capacidad de convertir este audio en texto para audio grabado en inglés, francés y español y esta característica se implementó con la función Google Cloud Speech to Text. Nuestro backend escucha las nuevas transcripciones agregadas a Firestore, las transcribe y luego las escribe en la base de datos:

 db.collection("transcripts").onSnapshot(snap => { const docs = snap.docChanges(); if (docs.length > 0) { docs.forEach(async change => { if (change.type === "added") { const data = change.doc.data(); if (data) { db.collection("transcripts").doc(change.doc.id).delete(); try { const text = await textToAudio(data.audioName, data.short, data.supportWebM); const roomRef = db.collection("rooms").doc(data.roomID).collection("messages").doc(data.messageID); db.runTransaction(async transaction => { const roomDoc = await transaction.get(roomRef); if (roomDoc.exists && !roomDoc.data()?.delete) { transaction.update(roomRef, { transcript: text }); console.log("transcript added with text: ", text); return; } else { console.log("room is deleted"); return; } }) db.collection("rooms").doc(data.roomID).collection("messages").doc(data.messageID).update({ transcript: text }); } catch (error) { console.log("error transcripting audio: ", error); }; }; }; }); }; });

Obviamente, tus ojos están mirando esa función textToAudio y te preguntas cómo la hice, no te preocupes, te entiendo:

 // Imports the Google Cloud client library const speech = require('@google-cloud/speech').v1p1beta1; const { gcsUriLink } = require("./configKeys") // Creates a client const client = new speech.SpeechClient({ keyFilename: "./audio_transcript.json" }); async function textToAudio(audioName, isShort) { // The path to the remote LINEAR16 file const gcsUri = gcsUriLink + "/audios/" + audioName; // The audio file's encoding, sample rate in hertz, and BCP-47 language code const audio = { uri: gcsUri, }; const config = { encoding: "MP3", sampleRateHertz: 48000, languageCode: 'en-US', alternativeLanguageCodes: ['es-ES', 'fr-FR'] }; console.log("audio config: ", config); const request = { audio: audio, config: config, }; // Detects speech in the audio file if (isShort) { const [response] = await client.recognize(request); return response.results.map(result => result.alternatives[0].transcript).join('\n'); } const [operation] = await client.longRunningRecognize(request); const [response] = await operation.promise().catch(e => console.log("response promise error: ", e)); return response.results.map(result => result.alternatives[0].transcript).join('\n'); }; module.exports = textToAudio;

Función de videollamada

Nuestra aplicación web utiliza Daily API para implementar conexiones Web RTC en tiempo real, permite a los usuarios realizar videollamadas entre sí, por lo que primero configuramos un servidor de llamadas backend que tiene muchos puntos de entrada de API para crear y eliminar salas en Daily:

 const app = express(); app.use(cors()); app.use(express.json()); app.delete("/delete-call", async (req, res) => { console.log("delete call data: ", req.body); deleteCallFromUser(req.body.id1); deleteCallFromUser(req.body.id2); try { fetch("https://api.daily.co/v1/rooms/" + req.body.roomName, { headers: { Authorization: `Bearer ${dailyApiKey}`, "Content-Type": "application/json" }, method: "DELETE" }); } catch(e) { console.log("error deleting room for call delete!!"); console.log(e); } res.status(200).send("delete-call success !!"); }); app.post("/create-room/:roomName", async (req, res) => { var room = await fetch("https://api.daily.co/v1/rooms/", { headers: { Authorization: `Bearer ${dailyApiKey}`, "Content-Type": "application/json" }, method: "POST", body: JSON.stringify({ name: req.params.roomName }) }); room = await room.json(); console.log(room); res.json(room); }); app.delete("/delete-room/:roomName", async (req, res) => { var deleteResponse = await fetch("https://api.daily.co/v1/rooms/" + req.params.roomName, { headers: { Authorization: `Bearer ${dailyApiKey}`, "Content-Type": "application/json" }, method: "DELETE" }); deleteResponse = await deleteResponse.json(); console.log(deleteResponse); res.json(deleteResponse); }) app.listen(process.env.PORT || 7000, () => { console.log("call server is running"); }); const deleteCallFromUser = userID => db.collection("users").doc(userID).collection("call").doc("call").delete();

y en nuestra interfaz tenemos múltiples funciones para llamar a esta API:

 import { callAPI as api } from "../../configKeys"; //const api = "http://localhost:7000" export async function createRoom(roomName) { var room = await fetch(`${api}/create-room/${roomName}`, { method: "POST", }); room = await room.json(); console.log(room); return room; } export async function deleteRoom(roomName) { var deletedRoom = await fetch(`${api}/delete-room/${roomName}`, { method: "DELETE", }); deletedRoom = await deletedRoom.json(); console.log(deletedRoom); console.log("deleted"); }; export function deleteCall() { window.callDelete && fetch(`${api}/delete-call`, { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify(window.callDelete) }); };

Genial, ahora es el momento de crear salas de llamadas y usar el SDK JS diario para conectarse a estas salas y enviar y recibir datos de ellas:

 export default async function startVideoCall(dispatch, receiverQuery, userQuery, id, otherID, userName, otherUserName, sendNotif, userPhoto, otherPhoto, audio) { var room = null; const call = new DailyIframe.createCallObject(); const roomName = nanoid(); window.callDelete = { id1: id, id2: otherID, roomName } dispatch({ type: "set_other_user_name", otherUserName }); console.log("audio: ", audio); if (audio) { dispatch({ type: "set_other_user_photo", photo: otherPhoto }); dispatch({ type: "set_call_type", callType: "audio" }); } else { dispatch({ type: "set_other_user_photo", photo: null }); dispatch({ type: "set_call_type", callType: "video" }); } dispatch({ type: "set_caller", caller: true }); dispatch({ type: "set_call", call }); dispatch({ type: "set_call_state", callState: "state_creating" }); try { room = await createRoom(roomName); console.log("created room: ", room); dispatch({ type: "set_call_room", callRoom: room }); } catch (error) { room = null; console.log('Error creating room', error); await call.destroy(); dispatch({ type: "set_call_room", callRoom: null }); dispatch({ type: "set_call", call: null }); dispatch({ type: "set_call_state", callState: "state_idle" }); window.callDelete = null; //destroy the call object; }; if (room) { dispatch({ type: "set_call_state", callState: "state_joining" }); dispatch({ type: "set_call_queries", callQueries: { userQuery, receiverQuery } }); try { await db.runTransaction(async transaction => { console.log("runing transaction"); var userData = (await transaction.get(receiverQuery)).data(); //console.log("user data: ", userData); if (!userData || !userData?.callerID || userData?.otherUserLeft) { console.log("runing set"); transaction.set(receiverQuery, { room, callType: audio ? "audio" : "video", isCaller: false, otherUserLeft: false, callerID: id, otherID, otherUserName: userName, otherUserRatio: window.screen.width / window.screen.height, photo: audio ? userPhoto : "" }); transaction.set(userQuery, { room, callType: audio ? "audio" : "video", isCaller: true, otherUserLeft: false, otherUserJoined: false, callerID: id, otherID }); } else { console.log('transaction failed'); throw userData; } }); if (sendNotif) { sendNotif(); const notifTimeout = setInterval(() => { sendNotif(); }, 1500); dispatch({ type: "set_notif_tiemout", notifTimeout }); } call.join({ url: room.url, videoSource: !audio }); } catch (userData) { //delete the room we made deleteRoom(roomName); await call.destroy(); if (userData.otherID === id) { console.log("you and the other user are calling each other at the same time"); joinCall(dispatch, receiverQuery, userQuery, userData.room, userName, audio ? userPhoto : "", userData.callType); } else { console.log("other user already in a call"); dispatch({ type: "set_call_room", callRoom: null }); dispatch({ type: "set_call", call: null }); dispatch({ type: "set_call_state", callState: "state_otherUser_calling" }); } }; }; };

OtherUserQuery y UserQuery son solo rutas de documentos de Firebase Firestore, ahora el resto de la aplicación tiene componentes de vista que reaccionan a los cambios de estado que activa esta función anterior y nuestros elementos de la interfaz de usuario de llamada aparecerán en consecuencia.

Moverse por el elemento de llamada:

La siguiente función es la Magia que te permite arrastrar el elemento Llamada por toda la página:

 export function dragElement(elmnt, page) { var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0, top, left, prevTop = 0, prevLeft = 0, x, y, maxTop, maxLeft; const widthRatio = page.width / window.innerWidth; const heightRatio = page.height / window.innerHeight; //clear element's mouse listeners closeDragElement(); // setthe listener elmnt.addEventListener("mousedown", dragMouseDown); elmnt.addEventListener("touchstart", dragMouseDown, { passive: false }); function dragMouseDown(e) { e = e || window.event; // get the mouse cursor position at startup: if (e.type === "touchstart") { if (typeof(e.target.className) === "string") { if (!e.target.className.includes("btn")) { e.preventDefault(); } } else if (!typeof(e.target.className) === "function") { e.stopPropagation(); } pos3 = e.touches[0].clientX * widthRatio; pos4 = e.touches[0].clientY * heightRatio; } else { e.preventDefault(); pos3 = e.clientX * widthRatio; pos4 = e.clientY * heightRatio; }; maxTop = elmnt.offsetParent.offsetHeight - elmnt.offsetHeight; maxLeft = elmnt.offsetParent.offsetWidth - elmnt.offsetWidth; document.addEventListener("mouseup", closeDragElement); document.addEventListener("touchend", closeDragElement, { passive: false }); // call a function whenever the cursor moves: document.addEventListener("mousemove", elementDrag); document.addEventListener("touchmove", elementDrag, { passive: false }); } function elementDrag(e) { e = e || window.event; e.preventDefault(); // calculate the new cursor position: if (e.type === "touchmove") { x = e.touches[0].clientX * widthRatio; y = e.touches[0].clientY * heightRatio; } else { e.preventDefault(); x = e.clientX * widthRatio; y = e.clientY * heightRatio; }; pos1 = pos3 - x; pos2 = pos4 - y; pos3 = x pos4 = y; // set the element's new position: top = elmnt.offsetTop - pos2; left = elmnt.offsetLeft - pos1; //prevent the element from overflowing the viewport if (top >= 0 && top <= maxTop) { elmnt.style.top = top + "px"; } else if ((top > maxTop && pos4 < prevTop) || (top < 0 && pos4 > prevTop)) { elmnt.style.top = top + "px"; }; if (left >= 0 && left <= maxLeft) { elmnt.style.left = left + "px"; } else if ((left > maxLeft && pos3 < prevLeft) || (left < 0 && pos3 > prevLeft)) { elmnt.style.left = left + "px"; }; prevTop = y; prevLeft = x; } function closeDragElement() { // stop moving when mouse button is released: document.removeEventListener("mouseup", closeDragElement); document.removeEventListener("touchend", closeDragElement); document.removeEventListener("mousemove", elementDrag); document.removeEventListener("touchmove", elementDrag); }; return function() { elmnt.removeEventListener("mousedown", dragMouseDown); elmnt.removeEventListener("touchstart", dragMouseDown); closeDragElement(); }; };

Arrastrar y soltar imágenes

Puedes arrastrar y soltar imágenes en tu chat y enviarlas al otro usuario. Esta funcionalidad es posible ejecutando esto:

 useEffect(() => { const dropArea = document.querySelector(".chat"); ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { dropArea.addEventListener(eventName, e => { e.preventDefault(); e.stopPropagation(); }, false); }); ['dragenter', 'dragover'].forEach(eventName => { dropArea.addEventListener(eventName, () => setShowDrag(true), false) }); ['dragleave', 'drop'].forEach(eventName => { dropArea.addEventListener(eventName, () => setShowDrag(false), false) }); dropArea.addEventListener('drop', e => { if (window.navigator.onLine) { if (e.dataTransfer?.files) { const dropedFile = e.dataTransfer.files; console.log("dropped file: ", dropedFile); const { imageFiles, imagesSrc } = mediaIndexer(dropedFile); setSRC(prevImages => [...prevImages, ...imagesSrc]); setImage(prevFiles => [...prevFiles, ...imageFiles]); setIsMedia("images_dropped"); }; }; }, false); }, []);

mediaIndexer es una función simple que indexa el blob de imágenes que le proporcionamos:

 function mediaIndexer(files) { const imagesSrc = []; const filesArray = Array.from(files); filesArray.forEach((file, index) => { imagesSrc[index] = URL.createObjectURL(file); }); return { imagesSrc, imageFiles: filesArray }; }