paint-brush
ChatPlus Oluşturma: Mobil Uygulama Gibi Hisseden Açık Kaynak PWAile@aladinyo
896 okumalar
896 okumalar

ChatPlus Oluşturma: Mobil Uygulama Gibi Hisseden Açık Kaynak PWA

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

Çok uzun; Okumak

ChatPlus yaptığım en harika uygulamalardan biri, tamamı web üzerinde uygulanan canlı kullanıcı arayüzü ve çoklu mesajlaşma ve çağrı işlevleriyle dikkat çekiyor, ön yüzü bir PWA uygulaması olacak şekilde tasarlandığı için çapraz platform deneyimi sunuyor. Her yere kurulabilen ve bağımsız bir uygulama gibi davranabilen, anlık bildirimler gibi özellikleriyle ChatPlus, web teknolojileriyle bir mobil uygulamanın tüm özelliklerini verebilen hafif bir uygulamanın tanımıdır.
featured image - ChatPlus Oluşturma: Mobil Uygulama Gibi Hisseden Açık Kaynak PWA
Alaa Eddine Boubekeur HackerNoon profile picture
0-item
1-item

ChatPlus Sohbet için Harika bir PWA 💬✨🤩

ChatPlus, React, NodeJS, Firebase ve diğer hizmetlerle geliştirilmiş ilerici bir web uygulamasıdır.

Tüm arkadaşlarınızla gerçek zamanlı olarak konuşabilirsiniz 🗣️✨🧑‍🤝‍🧑❤️

Arkadaşlarınızı arayabilir, onlarla görüntülü ve sesli görüşme yapabilirsiniz 🎥🔉🤩

Arkadaşlarınıza görseller ve sesli mesajlar gönderin; Fransızca, İngilizce veya İspanyolca konuşsanız da konuşmanızı metne dönüştüren bir yapay zekaya sahip olursunuz 🤖✨

Web uygulaması her cihaza kurulabilir ve bildirim alabilir ⬇️🔔🎉

Desteğinizi çok isterim, Github deposuna bize bir yıldız bırakın ve arkadaşlarınızla paylaşın ⭐✨

Tam kurulum ve dağıtım belgeleri için bu Github deposuna göz atın: https://github.com/aladinyo/ChatPlus

Temiz Basit Kullanıcı Arayüzü ve UX

ChatPlus Mobil Görünümü


ChatPlus Masaüstü Görünümü


Peki ChatPlus Nedir?

ChatPlus yaptığım en harika uygulamalardan biri, tamamı web üzerinde uygulanan canlı kullanıcı arayüzü ve çoklu mesajlaşma ve çağrı işlevleriyle dikkat çekiyor, ön yüzü bir PWA uygulaması olacak şekilde tasarlandığı için çapraz platform deneyimi sunuyor. Her yere kurulabilen ve bağımsız bir uygulama gibi davranabilen, anlık bildirimler gibi özellikleriyle ChatPlus, web teknolojileriyle bir mobil uygulamanın tüm özelliklerini verebilen hafif bir uygulamanın tanımıdır.

Yazılım mimarisi

Uygulamamız MVC yazılım mimarisini takip eder; MVC (Model-View-Controller), kullanıcı arayüzlerini, verileri ve kontrol mantığını uygulamak için yaygın olarak kullanılan yazılım tasarımında bir modeldir. Yazılımın iş mantığı ile ekranı arasındaki ayrımı vurgular. Bu "ilgilerin ayrılması" daha iyi bir işbölümü ve daha iyi bakım sağlar.

Kullanıcı arayüzü katmanı (Görünüm)

Birden fazla React arayüz bileşeninden oluşur:

  • MaterialUI: Material UI, Google'ın Materyal Tasarımını uygulayan açık kaynaklı bir React bileşen kitaplığıdır. Kapsamlıdır ve kutudan çıktığı haliyle üretimde kullanılabilir. Bunu simgeler düğmeleri ve ses kaydırıcısı gibi çıkış arayüzündeki bazı öğeler için kullanacağız.

  • LoginView: Kullanıcının kullanıcı adını ve şifresini girmesine veya google ile giriş yapmasına olanak tanıyan basit bir giriş görünümü.

  • SidebarViews : 'sidebar__header', 'sidebar__search', 'sidebar menu' ve 'SidebarChats' gibi çoklu görünüm bileşenleri, kullanıcı bilgileri için bir başlıktan, kullanıcıları aramak için bir arama çubuğundan, sohbetleriniz ve gruplarınız arasında gezinmek için bir kenar çubuğu menüsünden oluşur ve kullanıcılar ve mesajlaştığınız tüm son sohbetlerinizi görüntülemek için bir SidebarChats bileşeni, kullanıcı adını, fotoğrafı ve son mesajı gösterir.

  • ChatViews: aşağıdaki gibi birçok bileşenden oluşur:

    1. ' chat__header', konuştuğunuz kullanıcının bilgilerini, çevrimiçi durumunu, profil resmini içerir ve sesli arama ve görüntülü arama tuşlarını görüntüler, ayrıca kullanıcının yazıp yazmadığını da gösterir.

    2.'chat__body --container' diğer kullanıcılarla olan mesajlarımızın bilgilerini içerir, kısa mesajları, mesajlarıyla birlikte görselleri ve ayrıca sesli mesajları ses zamanı, sesin çalınıp çalınmadığı ve çalınıp çalınmadığı gibi bilgileri içeren mesaj bileşenine sahiptir. Bu bileşenin sonunda mesajların görülüp görülmediğini gösteren 'görüldü' öğesi var.

    3 .'AudioPlayer': Bize bir ses içinde gezinmek için bir kaydırıcıyla görüntüleyebilen, sesin tam zamanını ve geçerli zamanını görüntüleyen bir React bileşeni, bu görünüm bileşeni 'chat__body-- konteyner' içine yüklenir.

    4 .'ChatFooter': mesaj yazmak için bir giriş içerir, girişe yazdığınızda mesaj göndermek için bir düğme içerir, aksi takdirde bu düğme sesi kaydetmenize izin verir, görüntüleri ve dosyaları içe aktarmak için bir düğme.

    5 .'MediaPreview': Sohbetimizde göndermeyi seçtiğimiz görsellerin veya dosyaların ön izlemesini sağlayan bir React bileşeni, bunlar bir atlıkarıncada görüntülenir ve kullanıcı görselleri veya dosyaları kaydırıp her biri için özel bir mesaj yazabilir.

    6 .'ImagePreview': Sohbetimize görseller gönderildiğinde, bu bileşen görüntüleri düzgün bir animasyonla tam ekranda görüntüleyecektir, bileşen bir görsele tıklandıktan sonra monte edilir.

  • ScalePage: Full HD ekranlar ve 4K ekranlar gibi geniş ekranlarda görüntülendiğinde web uygulamamızın boyutunu artıran bir görüntüleme işlevi.

  • CallViews: tüm çağrı görüntüleme öğelerini içeren bir grup reaksiyon bileşeni, ekranımızın her yerine sürüklenebilme özelliğine sahiptirler ve aşağıdakilerden oluşur:

    1 'Düğmeler': kırmızı versiyonu ve yeşil görüntülü arama düğmesi olan bir çağrı düğmesi.

    2 'AudioCallView': gelen sesli aramayı yanıtlamaya, aramayı bir zamanlayıcıyla görüntülemeye ve aramayı iptal etmeye olanak tanıyan bir görünüm bileşeni.

    3 'StartVideoCallView': yerel MediaAPI'ye bağlanarak kendi videomuzu görüntüleyen ve diğer kullanıcının çağrıyı kabul etmesini bekleyen veya gelen bir video çağrısını yanıtlamamız için bize bir düğme görüntüleyen bir görüntüleme bileşeni.

    4 'VideoCallView': bizim ve diğer kullanıcının videosunu görüntüleyen bir görüntüleme bileşeni, kameralar arasında geçiş yapılmasına, kamera ve sesin devre dışı bırakılmasına olanak tanır, aynı zamanda tam ekrana da geçebilir.

  • RouteViews: Yerel bileşen navigasyonu oluşturmak için tüm görünümlerimizi içeren React bileşenlerini aldık, 'VideoCallRoute', 'SideBarMenuRoute' ve 'ChatsRoute' var

İstemci Tarafı Modelleri (Model)

İstemci tarafı modelleri, ön uçumuzun veritabanları ve birden fazla yerel ve sunucu tarafı API'si ile etkileşime girmesine olanak tanıyan mantıktır ve aşağıdakilerden oluşur:

  • Firebase SDK: Web uygulamamızın veritabanını oluşturmak için kullanılan bir SDK'dır.
  • AppModel: Kimlik doğrulamanın ardından bir kullanıcı oluşturan ve aynı zamanda web varlıklarımızın en son sürümüne sahip olmamızı sağlayan bir model.
  • ChatModels: veritabanına mesaj gönderme, yeni mesajları dinleyecek dinleyiciler oluşturma, diğer kullanıcının çevrimiçi olup olmadığını ve yazı yazıp yazmadığını dinleme gibi model mantıklarından oluşur, ayrıca resim ve ses gibi medyalarımızı da veritabanı deposuna gönderir. SidebarChatsModel: Kullanıcıların en son mesajlarını dinleyen ve kullanıcılardan gelen tüm yeni mesajlarınızın bir dizisini bize veren mantık, aynı zamanda okunmamış mesaj sayısını ve kullanıcıların çevrimiçi durumunu da verir, ayrıca kullanıcıları son mesajın zamanına göre düzenler.
  • UsersSearchModel: Veritabanımızdaki kullanıcıları arayan mantık, sunucudaki veritabanımıza bağlayarak kullanıcılarımızın listesini içeren algolia aramasını kullanır.
  • CallModel: Web uygulamamızda bir çağrı oluşturmak için Daily SDK'yı kullanan ve ayrıca verileri sunucumuza gönderen ve DailyAPI ile etkileşime giren mantık.

İstemci Tarafı Denetleyicileri (Denetleyici)

Görüşlerimizi belirli modellere bağlayan React bileşenlerinden oluşur:

  • Uygulama Denetleyicisi: Kimliği doğrulanmış kullanıcıyı tüm bileşenlere bağlar ve uygulamamızın boyutunu ayarlamak için ScalePage işlevini çalıştırır, aynı zamanda firebase'i yükler ve tüm bileşenleri ekler; bunu bileşenlerimiz için bir sarmalayıcı olarak düşünebiliriz.
  • SideBarController: Kullanıcı verilerini ve en son sohbetlerinin listesini bağlayın, aynı zamanda menülerimizi model mantığına bağlar, ayrıca arama çubuğunu algolia arama API'sine bağlar.
  • ChatController: Bu, mesajlaşma ve sohbet özelliklerinin çoğunu birbirine bağlayan çok büyük bir denetleyicidir.
  • CallController: Çağrı modelini görünümlerine bağlar.

Sunucu Tarafı Modeli

Kullandığımız SDK'lar bazı sunucu tarafı işlevleri gerektirdiğinden tüm özellikler ön uçta yapılmaz ve bunlar aşağıdakilerden oluşur:

  • CallServerModel: Daily API ile etkileşim kurarak ve firestore veritabanımızı güncelleyerek çağrılar için odalar oluşturmamıza olanak sağlayan mantık.
  • TranscriptModel: Bir ses dosyası alan ve Google Cloud konuşmadan metne API'si ile etkileşime giren ve sesli mesajlar için bir transkript veren sunucudaki mantık.
  • Çevrimiçi Durum İşleyicisi: Kullanıcıların çevrimiçi durumunu dinleyen ve veritabanını buna göre güncelleyen bir dinleyici Bildirim Modeli: Diğer kullanıcılara bildirim gönderen bir hizmet.
  • AlgoliaSaver: Veritabanımızdaki yeni kullanıcıları dinleyen ve algolia'yı ön uçtaki arama özelliği için kullanabilmemiz için buna göre güncelleyen bir dinleyici.
  • Sunucu Tarafı Denetleyicileri: CallServer: callModel Worker'ı içeren bir API uç noktası: tüm firebase işleme hizmetlerimizi çalıştıran bir çalışan hizmeti.

Sohbet Akış Şeması

Sohbet Akış Şeması


Kenar Çubuğu Akış Şeması

Kenar Çubuğu Akış Şeması

Model Çağrı Akış Şeması:

Model Çağrı Akış Şeması

Çağrı Akış Şemasını Görüntüle

Çağrı Akış Şemasını Görüntüle

Arka Uç Çalışan Akış Şeması

Arka Uç Çalışan Akış Şeması

Veri tabanı tasarımı

Web uygulamamız, Firebase NoSql veritabanı olan veritabanımızı depolamak için Firestore'u kullanıyor, kullanıcı bilgilerini saklıyoruz, tüm mesajların bir listesini saklıyoruz, sohbetlerin listesini saklıyoruz ve ayrıca sohbetleri odalarda saklıyoruz, bunlar bizim veri tabanı:

  • Kimlik Doğrulamadan Sonra Kullanıcı Verileri.
  • Mesajların tüm ayrıntılarını içeren odalar.
  • Her kullanıcı için en son sohbetlerin listesi.
  • Gönderilecek bildirimlerin listesi.
  • Metne dönüştürülecek ses listesi.

Veritabanı UML'si

Sihirli Kodun Açıklamaları 🔮

Sonraki bölümlerde ChatPlus'taki belirli işlevler hakkında hızlı bir açıklama ve eğitimler vereceğim, size JS kodunu göstereceğim, arkasındaki algoritmayı açıklayacağım ve ayrıca kodunuzu veritabanı.

Çevrimiçi Durum İşleme

Kullanıcıların çevrimiçi durumu, ön uçtaki ".info/connected" dosyasına bağlanılarak firebase veritabanı bağlantı özelliği kullanılarak ve hem firestore hem de veritabanı buna göre güncellenerek uygulandı:

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

Ve arka uçta ayrıca veritabanımızdaki değişiklikleri dinleyen ve firestore'u buna göre güncelleyen bir dinleyici kurduk, bu işlev aynı zamanda bize kullanıcının çevrimiçi durumunu gerçek zamanlı olarak verebilir, böylece onun içindeki diğer işlevleri de çalıştırabiliriz:

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

Bildirimler

Bildirimler harika bir özelliktir ve firebase mesajlaşma kullanılarak uygulanırlar; eğer kullanıcının tarayıcısı bildirimleri destekliyorsa ön uçta bunu yapılandırırız ve kullanıcının firebase mesajlaşma jetonunu alırız:

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

bir kullanıcı mesaj gönderdiğinde veritabanımıza bir bildirim ekliyoruz:

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


ve arka uçta bildirim koleksiyonunu dinliyoruz ve bunu kullanıcıya göndermek için firebase mesajlaşmasını kullanıyoruz

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

Yapay Zeka Ses Transkripsiyonu

Web uygulamamız kullanıcıların birbirlerine sesli mesaj göndermesine olanak sağlamaktadır ve özelliklerinden biri de İngilizce, Fransızca ve İspanyolca kaydedilen ses için bu sesi metne dönüştürebilme özelliğidir ve bu özellik Google Cloud Speech to Text özelliği ile hayata geçirilmiştir, Arka ucumuz, Firestore'a eklenen yeni transkriptleri dinler ve bunları transkript eder ve ardından bunları veritabanına yazar:

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

Açıkçası gözleriniz şu textToAudio işlevine bakıyor ve bunu nasıl yaptığımı merak ediyorsunuz, endişelenmeyin anladım:

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

Görüntülü Arama Özelliği

Web uygulamamız, gerçek zamanlı Web RTC bağlantılarını uygulamak için Daily API'yi kullanır; kullanıcıların birbirleriyle görüntülü arama yapmasına olanak tanır; bu nedenle, ilk olarak Daily'de odalar oluşturmak ve silmek için birçok API giriş noktasına sahip bir arka uç çağrı sunucusu kurarız:

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

ve ön uçta bu API'yi çağırmak için birden fazla işlevimiz var:

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

Harika, şimdi çağrı odaları oluşturmanın ve bu odalara bağlanmak ve onlardan veri gönderip almak için günlük JS SDK'yı kullanmanın tam zamanı:

 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 ve UserQuery yalnızca firebase firestore belge yollarıdır; artık uygulamanın geri kalanı yukarıdaki bu işlev tarafından tetiklenen durum değişikliklerine tepki veren görünüm bileşenlerine sahiptir ve çağrı kullanıcı arayüzü öğelerimiz buna göre görünecektir.

Çağrı Öğesinin Etrafında Hareket Edin:

Bu sonraki işlev, Çağrı öğesini sayfanın her yerine sürüklemenizi sağlayan Sihirdir:

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

Görselleri Sürükleyip Bırakın

Görüntüleri sohbetinize sürükleyip bırakabilir ve diğer kullanıcıya gönderebilirsiniz, bu işlevsellik şunu çalıştırarak mümkün olur:

 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, kendisine sağladığımız görüntü bloğunu indeksleyen basit bir işlevdir:

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