paint-brush
Construindo ChatPlus: o PWA de código aberto que parece um aplicativo móvelpor@aladinyo
1,148 leituras
1,148 leituras

Construindo ChatPlus: o PWA de código aberto que parece um aplicativo móvel

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

Muito longo; Para ler

ChatPlus é um dos melhores aplicativos que fiz, atrai a atenção com sua interface de usuário vibrante e múltiplas funcionalidades de mensagens e chamadas implementadas na web, oferece uma experiência multiplataforma, pois seu frontend foi projetado para ser um aplicativo PWA que tem a capacidade de ser instalado em qualquer lugar e funcionar como um aplicativo independente, com recursos como notificações push, ChatPlus é a definição de um aplicativo leve que pode fornecer todos os recursos de um aplicativo móvel com tecnologias web.
featured image - Construindo ChatPlus: o PWA de código aberto que parece um aplicativo móvel
Alaa Eddine Boubekeur HackerNoon profile picture
0-item
1-item

ChatPlus, um ótimo PWA para bate-papo 💬✨🤩

ChatPlus é um aplicativo web progressivo desenvolvido com React, NodeJS, Firebase e outros serviços.

Você pode conversar com todos os seus amigos em tempo real 🗣️✨🧑‍🤝‍🧑❤️

Você pode ligar para seus amigos e fazer chamadas de vídeo e áudio com eles 🎥🔉🤩

Envie imagens para seus amigos e também mensagens de áudio e você terá uma IA que converte sua fala em texto, quer você fale francês, inglês ou espanhol 🤖✨

O aplicativo web pode ser instalado em qualquer dispositivo e pode receber notificações ⬇️🔔🎉

Eu agradeceria muito o seu apoio, deixe-nos uma estrela no repositório do Github e compartilhe com seus amigos ⭐✨

Confira este repositório Github para documentação completa de instalação e implantação: https://github.com/aladinyo/ChatPlus

UI e UX simples e limpos

Visualização móvel do ChatPlus


Visualização da área de trabalho do ChatPlus


Então, o que é ChatPlus?

ChatPlus é um dos melhores aplicativos que fiz, atrai a atenção com sua interface de usuário vibrante e múltiplas funcionalidades de mensagens e chamadas implementadas na web, oferece uma experiência multiplataforma, pois seu frontend foi projetado para ser um aplicativo PWA que tem a capacidade de ser instalado em qualquer lugar e funcionar como um aplicativo independente, com recursos como notificações push, ChatPlus é a definição de um aplicativo leve que pode fornecer todos os recursos de um aplicativo móvel com tecnologias web.

Arquitetura de software

Nosso aplicativo segue a arquitetura de software MVC, MVC (Model-View-Controller) é um padrão em design de software comumente usado para implementar interfaces de usuário, dados e lógica de controle. Ele enfatiza a separação entre a lógica de negócios e a exibição do software. Esta “separação de preocupações” proporciona uma melhor divisão do trabalho e uma melhor manutenção.

Camada de interface do usuário (Visualização)

Consiste em vários componentes da interface React:

  • MaterialUI: Material UI é uma biblioteca de componentes React de código aberto que implementa o Material Design do Google. É abrangente e pode ser usado imediatamente na produção, vamos usá-lo para botões de ícones e alguns elementos em nossa interface, como um controle deslizante de áudio.

  • LoginView: Uma visualização de login simples que permite ao usuário colocar seu nome de usuário e senha ou fazer login no google.

  • SidebarViews : vários componentes de visualização como 'sidebar__header', 'sidebar__search', 'sidebar menu' e 'SidebarChats', consiste em um cabeçalho para informações do usuário, uma barra de pesquisa para pesquisar usuários, um menu da barra lateral para navegar entre seus chats, seus grupos e usuários, e um componente SidebarChats para exibir todos os bate-papos recentes que você enviou, mostra o nome de usuário e a foto e a última mensagem.

  • ChatViews: consiste em vários componentes como segue:

    1 .'chat__header' que contém as informações do usuário com quem você está conversando, seu status online, foto do perfil e exibe botões para chamada de áudio e videochamada, também mostra se o usuário está digitando.

    2 .'chat__body--container' contém as informações de nossas mensagens com os demais usuários possui componente de mensagens que exibe mensagens de texto, imagens com sua mensagem e também mensagens de áudio com suas informações como tempo de áudio e se o áudio foi reproduzido e no final deste componente temos o elemento 'visto' que mostra se as mensagens foram vistas.

    3.'AudioPlayer ': um componente React que pode nos exibir um áudio com um controle deslizante para navegar nele, exibe o tempo completo e o horário atual do áudio, este componente de visualização é carregado dentro de 'chat__body-- container'.

    4 .'ChatFooter': contém uma entrada para digitar uma mensagem, um botão para enviar uma mensagem ao digitar a entrada, caso contrário o botão permitirá gravar o áudio, um botão para importar imagens e arquivos.

    5.'MediaPreview ': um componente React que permite visualizar as imagens ou arquivos que selecionamos para enviar em nosso chat, eles são exibidos em um carrossel e o usuário pode deslizar as imagens ou arquivos e digitar uma mensagem específica para cada um

    6 .'ImagePreview': Quando temos imagens enviadas em nosso chat este componente irá exibir as imagens em tela cheia com uma animação suave, o componente é montado após clicar em uma imagem.

  • scalePage: uma função de visualização que aumenta o tamanho do nosso aplicativo da web quando exibido em telas grandes, como telas full HD e telas 4K.

  • CallViews: um monte de componentes de reação que contêm todos os elementos de visualização de chamadas, eles podem ser arrastados por toda a tela e consistem em:

    1 'Botões': um botão de chamada com uma versão vermelha e um botão verde de videochamada.

    2 'AudioCallView': um componente de visualização que permite atender chamadas de áudio recebidas e exibir a chamada com um temporizador e permite cancelar a chamada.

    3 'StartVideoCallView': um componente de visualização que exibe um vídeo nosso conectando-se ao MediaAPI local e espera que o outro usuário aceite a chamada ou exibe um botão para atendermos uma chamada de vídeo recebida.

    4 'VideoCallView': um componente de visualização que exibe um vídeo nosso e do outro usuário que permite trocar de câmera, desabilitar câmera e áudio, também pode ir para tela cheia.

  • RouteViews: Componentes React que contém todas as nossas visualizações para criar navegação de componentes locais, temos 'VideoCallRoute', 'SideBarMenuRoute' e 'ChatsRoute'

Modelos do lado do cliente (modelo)

Os modelos do lado do cliente são a lógica que permite que nosso frontend interaja com bancos de dados e múltiplas APIs locais e do servidor e consiste em:

  • Firebase SDK: é um SDK usado para construir o banco de dados de nosso aplicativo web.
  • AppModel: Um modelo que gera um usuário após a autenticação e também garante que tenhamos a versão mais recente de nossos ativos web.
  • ChatModels: consiste em lógicas de modelo de envio de mensagens para o banco de dados, estabelecendo ouvintes para ouvir novas mensagens, ouvindo se o outro usuário está online e se está digitando, também envia nossas mídias como imagens e áudios para o armazenamento do banco de dados. SidebarChatsModel: Lógica que escuta as últimas mensagens dos usuários e nos fornece um array de todas as novas mensagens dos usuários, também fornece o número de mensagens não lidas e o status online dos usuários, também organiza os usuários com base na hora da última mensagem.
  • UsersSearchModel: Lógica que busca usuários em nosso banco de dados, utiliza algolia search que possui uma lista de nossos usuários vinculando-a ao nosso banco de dados no servidor
  • CallModel: Lógica que utiliza o Daily SDK para criar uma chamada em nosso web app e também enviar os dados para nosso servidor e interagir com DailyAPI.

Controladores do lado do cliente (controlador)

Consiste em componentes React que vinculam nossas visualizações aos seus modelos específicos:

  • App Controller: vincula o usuário autenticado a todos os componentes e executa a função scalePage para ajustar o tamanho do nosso aplicativo, também carrega o firebase e anexa todos os componentes, podemos considerá-lo um wrapper para nossos componentes.
  • SideBarController: Vincula os dados do usuário e a lista de seus últimos chats, também vincula nossos menus com sua lógica de modelo, também vincula a barra de pesquisa com a API de pesquisa algolia.
  • ChatController: este é um controlador muito grande que conecta a maioria dos recursos de mensagens e bate-papo.
  • CallController: vincula o modelo de chamada às suas visualizações.

Modelo do lado do servidor

Nem todos os recursos são feitos no frontend, pois os SDKs que usamos exigem algumas funcionalidades do lado do servidor e consistem em:

  • CallServerModel: Lógica que nos permite criar salas para chamadas interagindo com a API Daily e atualizando nosso banco de dados firestore.
  • TranscriptModel: Lógica no servidor que recebe um arquivo de áudio e interage com a API de fala para texto do Google Cloud e fornece uma transcrição para mensagens de áudio.
  • Manipulador de status online: um ouvinte que escuta o status online dos usuários e atualiza o banco de dados de acordo. Modelo de notificação: um serviço que envia notificações para outros usuários.
  • AlgoliaSaver: Um ouvinte que escuta novos usuários em nosso banco de dados e atualiza o algolia de acordo para que possamos usá-lo para o recurso de pesquisa no frontend.
  • Controladores do lado do servidor: CallServer: um endpoint de API que contém callModel Worker: um serviço de trabalho que executa todos os nossos serviços de manipulação do Firebase.

Fluxograma de bate-papo

Fluxograma de bate-papo


Fluxograma da barra lateral

Fluxograma da barra lateral

Fluxograma de chamada de modelo:

Fluxograma de chamada de modelo

Ver fluxograma de chamadas

Ver fluxograma de chamadas

Fluxograma do trabalhador de back-end

Fluxograma do trabalhador de back-end

Projeto de banco de dados

Nosso aplicativo Web usa o Firestore para armazenar nosso banco de dados que é um banco de dados Firebase NoSql, armazenamos informações dos usuários, armazenamos uma lista de todas as mensagens, armazenamos lista de chats e também armazenamos chats em salas, esses são os dados armazenados em nosso base de dados:

  • Dados dos usuários após autenticação.
  • Salas que contêm todos os detalhes das mensagens.
  • Lista dos chats mais recentes para cada usuário.
  • Lista de notificações a serem enviadas.
  • Lista de áudio a ser transcrito.

Banco de dados UML

Explicações do Código Mágico 🔮

Nos próximos capítulos darei uma explicação rápida e tutoriais sobre certas funcionalidades do ChatPlus, mostrarei o código JS e explicarei o algoritmo por trás dele e também fornecerei a ferramenta de integração certa para vincular seu código com o banco de dados.

Lidando com status on-line

O status online dos usuários foi implementado usando o recurso de conectividade do banco de dados do Firebase, conectando-se ao “.info/connected” no frontend e atualizando o Firestore e o banco de dados de acordo:

 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); } };

E em nosso backend também configuramos um ouvinte que escuta as alterações em nosso banco de dados e atualiza o firestore de acordo. Esta função também pode nos fornecer o status online do usuário em tempo real para que possamos executar outras funções dentro dele também:

 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) } }

Notificações

As notificações são um ótimo recurso e são implementadas usando mensagens do Firebase. Em nosso frontend, se o navegador do usuário suportar notificações, nós o configuramos e recuperamos o token de mensagens do Firebase do usuário:

 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 }); }); }

sempre que um usuário envia uma mensagem, adicionamos uma notificação ao nosso banco de dados:

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


e em nosso backend ouvimos a coleção de notificações e usamos as mensagens do Firebase para enviá-las ao usuário

 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); }; }; }; }); }; });

Transcrição de áudio AI

Nosso aplicativo web permite que os usuários enviem mensagens de áudio entre si, e um de seus recursos é a capacidade de converter esse áudio em texto para áudio gravado em inglês, francês e espanhol e esse recurso foi implementado com o recurso Google Cloud Speech to Text, Nosso back-end escuta novas transcrições adicionadas ao Firestore e as transcreve e depois as grava no banco de dados:

 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, seus olhos estão olhando para a função textToAudio e você está se perguntando como eu fiz isso, não se preocupe, peguei você:

 // 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;

Recurso de videochamada

Nosso aplicativo da web usa API diária para implementar conexões Web RTC em tempo real, permitindo que os usuários façam videochamadas entre si. Primeiro, configuramos um servidor de chamada de back-end que possui muitos pontos de entrada de API para criar e excluir salas no 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();

e em nosso frontend temos múltiplas funções para chamar 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) }); };

Ótimo, agora é hora de criar salas de chamada e usar o JS SDK diário para conectar-se a essas salas e enviar e receber dados delas:

 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 e UserQuery são apenas caminhos de documentos do Firebase Firestore, agora o resto do aplicativo tem componentes de visualização que reagem às mudanças de estado que são acionadas por esta função acima e nossos elementos de UI de chamada aparecerão de acordo.

Mova-se pelo elemento de chamada:

A próxima função é o Magic que permite arrastar o elemento Call por toda a 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(); }; };

Arraste e solte imagens

Você pode arrastar e soltar imagens em seu chat e enviá-las para outro usuário, esta funcionalidade é possível executando isto:

 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); }, []);

O mediaIndexer é uma função simples que indexa o blob de imagens que fornecemos a ele:

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