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
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.
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.
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, ö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:
Görüşlerimizi belirli modellere bağlayan React bileşenlerinden oluşur:
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:
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ı:
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ı.
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 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); }; }; }; }); }; });
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;
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.
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ö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 }; }