paint-brush
ChatPlus erstellen: Die Open-Source-PWA, die sich wie eine mobile App anfühltvon@aladinyo
1,148 Lesungen
1,148 Lesungen

ChatPlus erstellen: Die Open-Source-PWA, die sich wie eine mobile App anfühlt

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

Zu lang; Lesen

ChatPlus ist eine der besten Anwendungen, die ich erstellt habe. Es fällt mit seiner lebendigen Benutzeroberfläche und den zahlreichen Messaging- und Anruffunktionen auf, die alle im Web implementiert sind. Es bietet ein plattformübergreifendes Erlebnis, da sein Frontend als PWA-Anwendung konzipiert ist, die überall installiert werden kann und sich wie eine eigenständige App verhält. Mit Funktionen wie Push-Benachrichtigungen ist ChatPlus die Definition einer leichtgewichtigen App, die mit Webtechnologien alle Funktionen einer mobilen App bieten kann.
featured image - ChatPlus erstellen: Die Open-Source-PWA, die sich wie eine mobile App anfühlt
Alaa Eddine Boubekeur HackerNoon profile picture
0-item
1-item

ChatPlus ist eine großartige PWA zum Chatten 💬✨🤩

ChatPlus ist eine progressive Web-App, die mit React, NodeJS, Firebase und anderen Diensten entwickelt wurde.

Sie können in Echtzeit mit all Ihren Freunden sprechen 🗣️✨🧑‍🤝‍🧑❤️

Sie können Ihre Freunde anrufen und Video- und Audioanrufe mit ihnen führen 🎥🔉🤩

Senden Sie Ihren Freunden Bilder und auch Audionachrichten und Sie verfügen über eine KI, die Ihre Sprache in Text umwandelt, unabhängig davon, ob Sie Französisch, Englisch oder Spanisch sprechen 🤖✨

Die Web-App kann auf allen Geräten installiert werden und Benachrichtigungen empfangen ⬇️🔔🎉

Ich würde mich sehr über Ihre Unterstützung freuen. Hinterlassen Sie uns einen Stern im Github-Repository und teilen Sie es mit Ihren Freunden ⭐✨

Die vollständige Installations- und Bereitstellungsdokumentation finden Sie in diesem Github-Repository: https://github.com/aladinyo/ChatPlus

Saubere, einfache UI und UX

ChatPlus Mobile Ansicht


ChatPlus Desktop-Ansicht


Also, was ist ChatPlus?

ChatPlus ist eine der besten Anwendungen, die ich erstellt habe. Es fällt mit seiner lebendigen Benutzeroberfläche und den zahlreichen Messaging- und Anruffunktionen auf, die alle im Web implementiert sind. Es bietet ein plattformübergreifendes Erlebnis, da sein Frontend als PWA-Anwendung konzipiert ist, die überall installiert werden kann und sich wie eine eigenständige App verhält. Mit Funktionen wie Push-Benachrichtigungen ist ChatPlus die Definition einer leichtgewichtigen App, die mit Webtechnologien alle Funktionen einer mobilen App bieten kann.

Softwarearchitektur

Unsere App folgt der MVC-Softwarearchitektur. MVC (Model-View-Controller) ist ein Muster im Softwaredesign, das häufig zur Implementierung von Benutzeroberflächen, Daten und Steuerlogik verwendet wird. Es betont eine Trennung zwischen der Geschäftslogik der Software und der Anzeige. Diese „Trennung der Belange“ sorgt für eine bessere Arbeitsteilung und verbesserte Wartung.

Benutzeroberflächenebene (Ansicht)

Es besteht aus mehreren React-Schnittstellenkomponenten:

  • MaterialUI: Material UI ist eine Open-Source-React-Komponentenbibliothek, die Googles Material Design implementiert. Sie ist umfassend und kann sofort in der Produktion verwendet werden. Wir werden sie für Symbolschaltflächen und einige Elemente in unserer Benutzeroberfläche wie einen Audio-Schieberegler verwenden.

  • LoginView: Eine einfache Anmeldeansicht, die es dem Benutzer ermöglicht, seinen Benutzernamen und sein Passwort einzugeben oder sich mit Google anzumelden.

  • SidebarViews : mehrere Ansichtskomponenten wie „sidebar__header“, „sidebar__search“, „Sidebar-Menü“ und „SidebarChats“, bestehend aus einer Kopfzeile für Benutzerinformationen, einer Suchleiste zum Suchen von Benutzern, einem Sidebar-Menü zum Navigieren zwischen Ihren Chats, Ihren Gruppen und Benutzern und einer SidebarChats-Komponente zum Anzeigen aller Ihrer letzten Chats, denen Sie Nachrichten gesendet haben. Sie zeigt den Benutzernamen und das Foto sowie die letzte Nachricht.

  • ChatViews: Es besteht aus den folgenden vielen Komponenten:

    1 .'chat__header' enthält die Informationen des Benutzers, mit dem Sie sprechen, seinen Online-Status, sein Profilbild und zeigt Schaltflächen für Audio- und Videoanrufe an. Außerdem wird angezeigt, ob der Benutzer gerade tippt.

    2 .'chat__body--container' enthält die Informationen zu unseren Nachrichten an die anderen Benutzer. Es verfügt über eine Nachrichtenkomponente, die Textnachrichten, Bilder mit ihren Nachrichten sowie Audionachrichten mit ihren Informationen wie Audiodauer und Angabe, ob das Audio abgespielt wurde, anzeigt. Am Ende dieser Komponente haben wir das Element „gesehen“, das anzeigt, ob Nachrichten gesehen wurden.

    3. „ AudioPlayer“: eine React-Komponente, die uns Audio mit einem Schieberegler zur Navigation anzeigen kann und die vollständige und aktuelle Zeit des Audios anzeigt. Diese Ansichtskomponente wird im „chat__body-- container“ geladen.

    4. „ChatFooter“: enthält ein Eingabefeld zum Eingeben einer Nachricht, eine Schaltfläche zum Senden einer Nachricht beim Tippen in das Eingabefeld, andernfalls können Sie mit der Schaltfläche Audio aufnehmen und eine Schaltfläche zum Importieren von Bildern und Dateien.

    5. „MediaPreview“: eine React-Komponente, die eine Vorschau der Bilder oder Dateien ermöglicht, die wir zum Senden in unserem Chat ausgewählt haben. Sie werden in einem Karussell angezeigt und der Benutzer kann die Bilder oder Dateien verschieben und für jedes eine bestimmte Nachricht eingeben.

    6. „Bildvorschau“: Wenn wir Bilder in unserem Chat senden, zeigt diese Komponente die Bilder im Vollbildmodus mit einer flüssigen Animation an. Die Komponente wird nach dem Klicken auf ein Bild bereitgestellt.

  • scalePage: eine Ansichtsfunktion, die die Größe unserer Web-App bei der Anzeige auf großen Bildschirmen wie Full-HD-Bildschirmen und 4K-Bildschirmen vergrößert.

  • CallViews: eine Reihe von Reaktionskomponenten, die alle Call-View-Elemente enthalten. Sie können über den gesamten Bildschirm gezogen werden und bestehen aus:

    1 „Tasten“: eine Anruftaste in roter Ausführung und eine grüne Videoanruftaste.

    2 „AudioCallView“: eine Ansichtskomponente, die es ermöglicht, eingehende Audioanrufe anzunehmen, den Anruf mit einem Timer anzuzeigen und den Anruf abzubrechen.

    3 „StartVideoCallView“: eine Ansichtskomponente, die durch Verbindung mit der lokalen MediaAPI ein Video von uns anzeigt und darauf wartet, dass der andere Benutzer den Anruf annimmt, oder eine Schaltfläche anzeigt, mit der wir einen eingehenden Videoanruf annehmen können.

    4 „VideoCallView“: eine Ansichtskomponente, die ein Video von uns und dem anderen Benutzer anzeigt. Sie ermöglicht das Wechseln der Kamera, das Deaktivieren von Kamera und Audio und kann auch in den Vollbildmodus wechseln.

  • RouteViews: React-Komponenten, die alle unsere Ansichten enthalten, um eine lokale Komponentennavigation zu erstellen. Wir haben „VideoCallRoute“, „SideBarMenuRoute“ und „ChatsRoute“.

Clientseitige Modelle (Modell)

Clientseitige Modelle stellen die Logik dar, die unserem Frontend die Interaktion mit Datenbanken und mehreren lokalen und serverseitigen APIs ermöglicht. Sie bestehen aus:

  • Firebase SDK: Dies ist ein SDK, das zum Erstellen der Datenbank unserer Web-App verwendet wird.
  • AppModel: Ein Modell, das nach der Authentifizierung einen Benutzer generiert und außerdem sicherstellt, dass wir über die neueste Version unserer Web-Assets verfügen.
  • ChatModels: Es besteht aus Modelllogiken zum Senden von Nachrichten an die Datenbank, zum Einrichten von Listenern zum Abhören neuer Nachrichten, zum Abhören, ob der andere Benutzer online ist und ob er gerade tippt. Außerdem sendet es unsere Medien wie Bilder und Audios an den Datenbankspeicher. SidebarChatsModel: Logik, die die neuesten Nachrichten von Benutzern abhört und uns eine Liste aller Ihrer neuen Nachrichten von Benutzern liefert. Außerdem gibt sie die Anzahl ungelesener Nachrichten und den Onlinestatus von Benutzern an und organisiert die Benutzer nach der Zeit der letzten Nachricht.
  • UsersSearchModel: Logik, die in unserer Datenbank nach Benutzern sucht. Sie verwendet die Algolia-Suche, die eine Liste unserer Benutzer erstellt, indem sie diese mit unserer Datenbank auf dem Server verknüpft.
  • CallModel: Logik, die das Daily SDK verwendet, um einen Anruf in unserer Web-App zu erstellen, die Daten auch an unseren Server zu senden und mit DailyAPI zu interagieren.

Clientseitige Controller (Controller)

Es besteht aus React-Komponenten, die unsere Ansichten mit ihren spezifischen Modellen verknüpfen:

  • App-Controller: Verbindet den authentifizierten Benutzer mit allen Komponenten und führt die ScalePage-Funktion aus, um die Größe unserer App anzupassen. Außerdem lädt er Firebase und hängt alle Komponenten an. Wir können ihn als Wrapper für unsere Komponenten betrachten.
  • SideBarController: Verknüpft Benutzerdaten und die Liste seiner neuesten Chats. Es verknüpft außerdem unsere Menüs mit ihrer Modelllogik und verknüpft die Suchleiste mit der Algolia-Such-API.
  • ChatController: Dies ist ein sehr großer Controller, der die meisten Messaging- und Chat-Funktionen verknüpft.
  • CallController: Verknüpft das Anrufmodell mit seinen Ansichten.

Serverseitiges Modell

Nicht alle Funktionen werden auf dem Frontend ausgeführt, da die von uns verwendeten SDKs einige serverseitige Funktionen erfordern. Dazu gehören:

  • CallServerModel: Logik, die es uns ermöglicht, Räume für Anrufe zu erstellen, indem wir mit der Daily API interagieren und unsere Firestore-Datenbank aktualisieren.
  • TranscriptModel: Logik auf dem Server, die eine Audiodatei empfängt, mit der Google Cloud Speech-to-Text-API interagiert und ein Transkript für Audionachrichten erstellt.
  • Online-Status-Handler: Ein Listener, der den Online-Status von Benutzern abhört und die Datenbank entsprechend aktualisiert. Benachrichtigungsmodell: Ein Dienst, der Benachrichtigungen an andere Benutzer sendet.
  • AlgoliaSaver: Ein Listener, der auf neue Benutzer in unserer Datenbank hört und Algolia entsprechend aktualisiert, sodass wir es für die Suchfunktion im Frontend verwenden können.
  • Serverseitige Controller: CallServer: ein API-Endpunkt, der CallModel Worker enthält: ein Worker-Dienst, der alle unsere Firebase-Handling-Dienste ausführt.

Chat-Flussdiagramm

Chat-Flussdiagramm


Flussdiagramm in der Seitenleiste

Flussdiagramm in der Seitenleiste

Muster-Anrufflussdiagramm:

Muster-Anrufflussdiagramm

Anrufflussdiagramm anzeigen

Anrufflussdiagramm anzeigen

Flussdiagramm für Backend-Worker

Flussdiagramm für Backend-Worker

Datenbank Design

Unsere Webanwendung verwendet Firestore zum Speichern unserer Datenbank, bei der es sich um eine Firebase NoSql-Datenbank handelt. Wir speichern Benutzerinformationen, eine Liste aller Nachrichten, eine Liste der Chats und auch Chats in Räumen. Dies sind die in unserer Datenbank gespeicherten Daten:

  • Benutzerdaten nach der Authentifizierung.
  • Räume, die alle Details der Nachrichten enthalten.
  • Liste der neuesten Chats für jeden Benutzer.
  • Liste der zu sendenden Benachrichtigungen.
  • Liste der zu transkribierenden Audiodateien.

Datenbank-UML

Erklärungen des magischen Codes 🔮

In den nächsten Kapiteln werde ich eine kurze Erklärung und Tutorials zu bestimmten Funktionen in ChatPlus geben, ich werde Ihnen den JS-Code zeigen und den dahinter stehenden Algorithmus erklären und außerdem das richtige Integrationstool bereitstellen, um Ihren Code mit der Datenbank zu verknüpfen.

Umgang mit dem Onlinestatus

Der Onlinestatus der Benutzer wurde mithilfe der Firebase-Datenbankkonnektivitätsfunktion implementiert, indem eine Verbindung zu „.info/connected“ im Frontend hergestellt und sowohl Firestore als auch Datenbank entsprechend aktualisiert wurden:

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

Und auf unserem Backend haben wir auch einen Listener eingerichtet, der auf Änderungen in unserer Datenbank wartet und den Firestore entsprechend aktualisiert. Diese Funktion kann uns auch den Onlinestatus des Benutzers in Echtzeit mitteilen, sodass wir darin auch andere Funktionen ausführen können:

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

Benachrichtigungen

Benachrichtigungen sind eine tolle Funktion und werden mithilfe von Firebase Messaging implementiert. Wenn der Browser des Benutzers auf unserem Frontend Benachrichtigungen unterstützt, konfigurieren wir es und rufen das Firebase-Messaging-Token des Benutzers ab:

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

Immer wenn ein Benutzer eine Nachricht sendet, fügen wir unserer Datenbank eine Benachrichtigung hinzu:

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


und auf unserem Backend hören wir uns die Benachrichtigungssammlung an und verwenden die Firebase-Nachrichtenübermittlung, um sie an den Benutzer zu senden

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

KI-Audiotranskription

Unsere Webanwendung ermöglicht es Benutzern, einander Audionachrichten zu senden. Eine ihrer Funktionen besteht darin, diese Audionachrichten in Text umzuwandeln, z. B. in Englisch, Französisch und Spanisch. Diese Funktion wurde mit der Funktion „Sprache in Text“ von Google Cloud implementiert. Unser Backend hört sich neue Transkripte an, die zu Firestore hinzugefügt werden, transkribiert sie und schreibt sie dann in die Datenbank:

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

Offensichtlich haben Sie die Text-zu-Audio-Funktion im Blick und fragen sich, wie ich sie gemacht habe. Keine Sorge, ich habe Sie verstanden:

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

Videoanruffunktion

Unsere Webanwendung verwendet die Daily API, um Web-RTC-Verbindungen in Echtzeit zu implementieren. Sie ermöglicht Benutzern, Videoanrufe miteinander zu tätigen. Daher richten wir zunächst einen Backend-Anrufserver ein, der über viele API-Einstiegspunkte zum Erstellen und Löschen von Räumen in Daily verfügt:

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

und auf unserem Frontend haben wir mehrere Funktionen zum Aufrufen dieser 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) }); };

Großartig, jetzt ist es nur noch an der Zeit, Anrufräume zu erstellen und das tägliche JS SDK zu verwenden, um eine Verbindung zu diesen Räumen herzustellen und Daten von ihnen zu senden und zu empfangen:

 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 und UserQuery sind lediglich Firebase-Firestore-Dokumentpfade. Der Rest der App verfügt nun über Ansichtskomponenten, die auf die Statusänderungen reagieren, die durch die obige Funktion ausgelöst werden, und unsere Call-UI-Elemente werden entsprechend angezeigt.

Bewegen Sie sich im Anrufelement:

Mit der nächsten Funktion können Sie das Anrufelement über die gesamte Seite ziehen:

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

Ziehen und Ablegen von Bildern

Sie können Bilder per Drag & Drop in Ihren Chat ziehen und sie an andere Benutzer senden. Diese Funktion wird durch Ausführen des Folgenden ermöglicht:

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

Der mediaIndexer ist eine einfache Funktion, die den Bild-Blob indiziert, den wir ihr bereitstellen:

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