paint-brush
Xây dựng ChatPlus: PWA mã nguồn mở giống như một ứng dụng di độngtừ tác giả@aladinyo
896 lượt đọc
896 lượt đọc

Xây dựng ChatPlus: PWA mã nguồn mở giống như một ứng dụng di động

từ tác giả Alaa Eddine Boubekeur29m2024/07/10
Read on Terminal Reader

dài quá đọc không nổi

ChatPlus là một trong những ứng dụng tuyệt vời nhất mà tôi đã tạo ra, nó thu hút sự chú ý với giao diện người dùng sống động cùng nhiều chức năng nhắn tin và gọi điện đều được triển khai trên web, nó mang lại trải nghiệm đa nền tảng vì giao diện người dùng của nó được thiết kế để trở thành một ứng dụng PWA. có khả năng được cài đặt ở mọi nơi và hoạt động như một ứng dụng độc lập, với tính năng như thông báo đẩy, ChatPlus là định nghĩa về một ứng dụng nhẹ có thể cung cấp đầy đủ các tính năng của một ứng dụng di động với công nghệ web.
featured image - Xây dựng ChatPlus: PWA mã nguồn mở giống như một ứng dụng di động
Alaa Eddine Boubekeur HackerNoon profile picture
0-item
1-item

ChatPlus một PWA tuyệt vời để trò chuyện 💬✨🤩

ChatPlus là một ứng dụng web tiến bộ được phát triển với React, NodeJS, Firebase và các dịch vụ khác.

Bạn có thể Trò chuyện với tất cả bạn bè của mình trong thời gian thực 🗣️✨🧑‍🤝‍🧑❤️

Bạn có thể gọi cho bạn bè của mình và thực hiện cuộc gọi video và âm thanh với họ 🎥🔉🤩

Gửi hình ảnh cho bạn bè cũng như tin nhắn âm thanh và bạn có AI chuyển đổi lời nói của bạn thành văn bản cho dù bạn nói tiếng Pháp, tiếng Anh hay tiếng Tây Ban Nha 🤖✨

Ứng dụng web có thể được cài đặt trên mọi thiết bị và có thể nhận thông báo ⬇️🔔🎉

Tôi đánh giá cao sự hỗ trợ của bạn rất nhiều, hãy để lại cho chúng tôi một ngôi sao trên kho Github và chia sẻ với bạn bè của bạn ⭐✨

Kiểm tra kho lưu trữ Github này để biết tài liệu cài đặt và triển khai đầy đủ: https://github.com/aladinyo/ChatPlus

Giao diện người dùng & UX đơn giản sạch sẽ

Chế độ xem ChatPlus trên thiết bị di động


Chế độ xem máy tính để bàn ChatPlus


Vậy ChatPlus là gì?

ChatPlus là một trong những ứng dụng tuyệt vời nhất mà tôi đã tạo ra, nó thu hút sự chú ý với giao diện người dùng sống động cùng nhiều chức năng nhắn tin và gọi điện đều được triển khai trên web, nó mang lại trải nghiệm đa nền tảng vì giao diện người dùng của nó được thiết kế để trở thành một ứng dụng PWA. có khả năng được cài đặt ở mọi nơi và hoạt động như một ứng dụng độc lập, với tính năng như thông báo đẩy, ChatPlus là định nghĩa về một ứng dụng nhẹ có thể cung cấp đầy đủ các tính năng của một ứng dụng di động với công nghệ web.

Kiến trúc phần mềm

Ứng dụng của chúng tôi tuân theo kiến trúc phần mềm MVC, MVC (Model-View-Controller) là một mẫu trong thiết kế phần mềm thường được sử dụng để triển khai giao diện người dùng, dữ liệu và logic điều khiển. Nó nhấn mạnh sự tách biệt giữa logic nghiệp vụ và hiển thị của phần mềm. Sự "tách biệt các mối quan tâm" này mang lại sự phân công lao động tốt hơn và cải thiện việc bảo trì.

Lớp giao diện người dùng (Xem)

Nó bao gồm nhiều thành phần giao diện React:

  • MaterialUI: Material UI là thư viện thành phần React mã nguồn mở triển khai Material Design của Google. Nó toàn diện và có thể được sử dụng ngay trong sản xuất, chúng tôi sẽ sử dụng nó cho các nút biểu tượng và một số thành phần trong giao diện bên ngoài như thanh trượt âm thanh.

  • Chế độ xem đăng nhập: Chế độ xem đăng nhập đơn giản cho phép người dùng đặt tên người dùng và mật khẩu hoặc đăng nhập bằng google.

  • SidebarViews : nhiều thành phần xem như 'sidebar__header', 'sidebar__search', 'menu thanh bên' và 'SidebarChats', nó bao gồm tiêu đề cho thông tin người dùng, thanh tìm kiếm để tìm kiếm người dùng, menu thanh bên để điều hướng giữa các cuộc trò chuyện, nhóm của bạn và người dùng, cũng như thành phần SidebarChats để hiển thị tất cả các cuộc trò chuyện gần đây mà bạn đã nhắn tin, nó hiển thị tên người dùng và ảnh cũng như tin nhắn cuối cùng.

  • ChatViews: nó bao gồm nhiều thành phần như sau:

    1 .'chat__header' chứa thông tin của người dùng mà bạn đang nói chuyện, trạng thái trực tuyến, ảnh hồ sơ của họ và nó hiển thị các nút cho cuộc gọi âm thanh và cuộc gọi video, nó cũng hiển thị xem người dùng có đang gõ hay không.

    2 .'chat__body--container' nó chứa thông tin tin nhắn của chúng tôi với những người dùng khác. Nó có thành phần tin nhắn hiển thị tin nhắn văn bản, hình ảnh với tin nhắn của họ và cả tin nhắn âm thanh với thông tin của họ như thời gian âm thanh và liệu âm thanh có được phát hay không và ở cuối thành phần này, chúng ta có phần tử 'seen' hiển thị xem tin nhắn có được nhìn thấy hay không.

    3 .'AudioPlayer': một thành phần React có thể hiển thị cho chúng ta một âm thanh có thanh trượt để điều hướng nó, hiển thị toàn bộ thời gian và thời gian hiện tại của âm thanh, thành phần view này được tải bên trong 'chat__body-- container'.

    4 .'ChatFooter': nó chứa đầu vào để nhập tin nhắn, nút gửi tin nhắn khi nhập vào đầu vào, nếu không nút này sẽ cho phép bạn ghi âm, nút để nhập hình ảnh và tệp.

    5 .'MediaPreview': một thành phần React cho phép xem trước hình ảnh hoặc tệp mà chúng tôi đã chọn để gửi trong cuộc trò chuyện của mình, chúng được hiển thị trên băng chuyền và người dùng có thể trượt hình ảnh hoặc tệp và nhập một tin nhắn cụ thể cho từng tệp

    6 .'ImagePreview': Khi chúng tôi gửi hình ảnh trong cuộc trò chuyện của mình, thành phần này sẽ hiển thị hình ảnh trên toàn màn hình với hình ảnh động mượt mà, thành phần sẽ gắn kết sau khi nhấp vào hình ảnh.

  • ScalPage: chức năng xem giúp tăng kích thước ứng dụng web của chúng tôi khi hiển thị trên màn hình lớn như màn hình full HD và màn hình 4K.

  • CallViews: một loạt các thành phần phản ứng chứa tất cả phần tử xem cuộc gọi, chúng có khả năng được kéo khắp màn hình của chúng tôi và nó bao gồm:

    1 'Nút': nút gọi có phiên bản màu đỏ và nút gọi video màu xanh lục.

    2 'AudioCallView': thành phần xem cho phép trả lời cuộc gọi âm thanh đến và hiển thị cuộc gọi bằng đồng hồ hẹn giờ và cho phép hủy cuộc gọi.

    3 'StartVideoCallView': một thành phần xem hiển thị video của chính chúng tôi bằng cách kết nối với MediaAPI cục bộ và nó chờ người dùng khác chấp nhận cuộc gọi hoặc nó hiển thị một nút để chúng tôi trả lời cuộc gọi điện video đến.

    4 'VideoCallView': một thành phần xem hiển thị video của chúng tôi và người dùng khác, nó cho phép chuyển đổi camera, tắt camera và âm thanh, nó cũng có thể chuyển sang chế độ toàn màn hình.

  • RouteViews: Các thành phần phản ứng chứa tất cả các chế độ xem của chúng tôi để tạo điều hướng các thành phần cục bộ, chúng tôi có 'VideoCallRoute', 'SideBarMenuRoute' và 'ChatRoute'

Mô hình phía khách hàng (Model)

Các mô hình phía máy khách là logic cho phép giao diện người dùng của chúng tôi tương tác với cơ sở dữ liệu cũng như nhiều API cục bộ và phía máy chủ và nó bao gồm:

  • SDK Firebase: Đó là SDK được sử dụng để xây dựng cơ sở dữ liệu cho ứng dụng web của chúng tôi.
  • AppModel: Một mô hình tạo ra người dùng sau khi xác thực và nó cũng đảm bảo rằng chúng tôi có phiên bản mới nhất của nội dung web của mình.
  • ChatModels: nó bao gồm các logic mô hình gửi tin nhắn đến cơ sở dữ liệu, thiết lập trình nghe để nghe tin nhắn mới, lắng nghe xem người dùng khác có trực tuyến hay không và liệu anh ta có đang gõ hay không, nó cũng gửi phương tiện của chúng ta như hình ảnh và âm thanh đến bộ lưu trữ cơ sở dữ liệu. SidebarChatsModel: Logic lắng nghe tin nhắn mới nhất của người dùng và cung cấp cho chúng tôi một loạt tất cả tin nhắn mới của bạn từ người dùng, nó cũng cung cấp số lượng tin nhắn chưa đọc và trạng thái trực tuyến của người dùng, nó cũng sắp xếp người dùng dựa trên thời gian của tin nhắn cuối cùng.
  • UsersSearchModel: Logic tìm kiếm người dùng trên cơ sở dữ liệu của chúng tôi, nó sử dụng tìm kiếm algolia có danh sách người dùng của chúng tôi bằng cách liên kết nó với cơ sở dữ liệu của chúng tôi trên máy chủ
  • CallModel: Logic sử dụng SDK hàng ngày để tạo cuộc gọi trên ứng dụng web của chúng tôi, đồng thời gửi dữ liệu đến máy chủ của chúng tôi và tương tác với DailyAPI.

Bộ điều khiển phía máy khách (Bộ điều khiển)

Nó bao gồm các thành phần React liên kết các view của chúng ta với các model cụ thể của chúng:

  • Trình điều khiển ứng dụng: Liên kết người dùng được xác thực với tất cả các thành phần và chạy chức năng thang đo để điều chỉnh kích thước ứng dụng của chúng tôi, nó cũng tải firebase và gắn tất cả các thành phần, chúng tôi có thể coi đó là trình bao bọc cho các thành phần của mình.
  • SideBarController: Liên kết dữ liệu người dùng và danh sách các cuộc trò chuyện mới nhất của anh ấy, nó cũng liên kết các menu của chúng tôi với logic mô hình của họ, nó cũng liên kết thanh tìm kiếm với API tìm kiếm algolia.
  • ChatController: đây là bộ điều khiển rất lớn liên kết hầu hết các tính năng nhắn tin và trò chuyện.
  • CallController: Liên kết mô hình cuộc gọi với các khung nhìn của nó.

Mô hình phía máy chủ

Không phải tất cả các tính năng đều được thực hiện trên giao diện người dùng vì SDK mà chúng tôi sử dụng yêu cầu một số chức năng phía máy chủ và chúng bao gồm:

  • CallServerModel: Logic cho phép chúng tôi tạo phòng cho cuộc gọi bằng cách tương tác với API hàng ngày và cập nhật cơ sở dữ liệu firestore của chúng tôi.
  • TranscriptModel: Logic trên máy chủ nhận tệp âm thanh và tương tác với API lời nói thành văn bản của Google Cloud và cung cấp bản ghi cho tin nhắn âm thanh.
  • Trình xử lý trạng thái trực tuyến: Trình nghe lắng nghe trạng thái trực tuyến của người dùng và cập nhật cơ sở dữ liệu phù hợp Mô hình thông báo: Dịch vụ gửi thông báo cho người dùng khác.
  • AlgoliaSaver: Trình nghe lắng nghe người dùng mới trên cơ sở dữ liệu của chúng tôi và cập nhật thuật toán tương ứng để chúng tôi có thể sử dụng nó cho tính năng tìm kiếm trên giao diện người dùng.
  • Bộ điều khiển phía máy chủ: CallServer: điểm cuối API chứa callModel Worker: một dịch vụ công nhân chạy tất cả các dịch vụ xử lý căn cứ hỏa lực của chúng tôi.

Biểu đồ luồng trò chuyện

Biểu đồ luồng trò chuyện


Biểu đồ luồng thanh bên

Biểu đồ luồng thanh bên

Biểu đồ luồng cuộc gọi mẫu:

Biểu đồ luồng cuộc gọi mẫu

Xem biểu đồ luồng cuộc gọi

Xem biểu đồ luồng cuộc gọi

Biểu đồ luồng công nhân phụ trợ

Biểu đồ luồng công nhân phụ trợ

Thiết kế cơ sở dữ liệu

Ứng dụng web của chúng tôi sử dụng Firestore để lưu trữ cơ sở dữ liệu của chúng tôi là cơ sở dữ liệu Firebase NoSql, chúng tôi lưu trữ thông tin người dùng, chúng tôi lưu trữ danh sách tất cả tin nhắn, chúng tôi lưu trữ danh sách các cuộc trò chuyện và chúng tôi cũng lưu trữ các cuộc trò chuyện trên các phòng, đây là những dữ liệu được lưu trữ trên cơ sở dữ liệu:

  • Dữ liệu người dùng sau khi xác thực.
  • Phòng chứa tất cả các chi tiết của tin nhắn.
  • Danh sách các cuộc trò chuyện mới nhất cho mỗi người dùng.
  • Danh sách các thông báo sẽ được gửi đi.
  • Danh sách âm thanh được ghi âm.

UML cơ sở dữ liệu

Giải thích về Mã ma thuật 🔮

Trong các chương tiếp theo, tôi sẽ giải thích nhanh và hướng dẫn về một số chức năng nhất định trong ChatPlus, tôi sẽ cho bạn xem mã JS và giải thích thuật toán đằng sau nó, đồng thời cung cấp công cụ tích hợp phù hợp để liên kết mã của bạn với kho dữ liệu.

Xử lý trạng thái trực tuyến

Trạng thái trực tuyến của người dùng được triển khai bằng cách sử dụng tính năng kết nối cơ sở dữ liệu firebase bằng cách kết nối với “.info/connected“ trên giao diện người dùng và cập nhật cả firestore và cơ sở dữ liệu tương ứng:

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

Và trên phần phụ trợ của chúng tôi, chúng tôi cũng thiết lập một trình nghe để lắng nghe các thay đổi trên cơ sở dữ liệu của chúng tôi và cập nhật firestore tương ứng, chức năng này cũng có thể cung cấp cho chúng tôi trạng thái trực tuyến của người dùng theo thời gian thực để chúng tôi cũng có thể chạy các chức năng khác bên trong nó:

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

Thông báo

Thông báo là một tính năng tuyệt vời và chúng được triển khai bằng cách sử dụng tính năng nhắn tin firebase, trên giao diện người dùng của chúng tôi nếu trình duyệt của người dùng hỗ trợ thông báo thì chúng tôi sẽ định cấu hình nó và truy xuất mã thông báo nhắn tin firebase của người dùng:

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

bất cứ khi nào người dùng gửi tin nhắn, chúng tôi sẽ thêm thông báo vào cơ sở dữ liệu của mình:

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


và trong phần phụ trợ của chúng tôi, chúng tôi lắng nghe bộ sưu tập thông báo và chúng tôi sử dụng tin nhắn firebase để gửi nó cho người dùng

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

Phiên âm âm thanh AI

Ứng dụng web của chúng tôi cho phép người dùng gửi tin nhắn âm thanh cho nhau và một trong những tính năng của nó là khả năng chuyển đổi âm thanh này thành văn bản để ghi âm bằng tiếng Anh, tiếng Pháp và tiếng Tây Ban Nha và tính năng này đã được triển khai với tính năng Google Cloud Speech to Text, Phần phụ trợ của chúng tôi lắng nghe các bản ghi mới được thêm vào Firestore và ghi lại chúng sau đó ghi chúng vào cơ sở dữ liệu:

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

Rõ ràng, mắt bạn đang nhìn vào hàm textToAudio đó và bạn đang tự hỏi tôi đã tạo ra nó như thế nào, đừng lo, tôi hiểu rồi:

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

Tính năng gọi video

Ứng dụng web của chúng tôi sử dụng API hàng ngày để triển khai các kết nối Web RTC theo thời gian thực, nó cho phép người dùng gọi điện video với nhau, vì vậy trước tiên chúng tôi thiết lập một máy chủ cuộc gọi phụ trợ có nhiều điểm truy cập api để tạo và xóa phòng trong Hàng ngày:

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

và trên giao diện người dùng của chúng tôi, chúng tôi có nhiều chức năng để gọi API này:

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

Tuyệt vời, giờ chỉ là lúc tạo phòng gọi và sử dụng JS SDK hàng ngày để kết nối với các phòng này cũng như gửi và nhận dữ liệu từ chúng:

 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 và UserQuery chỉ là các đường dẫn tài liệu firebase firestore, giờ đây, phần còn lại của ứng dụng có các thành phần xem phản ứng với những thay đổi trạng thái được kích hoạt bởi chức năng này ở trên và các thành phần UI cuộc gọi của chúng tôi sẽ xuất hiện tương ứng.

Di chuyển xung quanh phần tử cuộc gọi:

Chức năng tiếp theo này là Magic cho phép bạn kéo phần tử Call khắp trang:

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

Kéo và thả hình ảnh

Bạn có thể kéo và thả hình ảnh trong cuộc trò chuyện của mình và gửi chúng cho người dùng khác, chức năng này có thể thực hiện được bằng cách chạy lệnh này:

 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 là một hàm đơn giản giúp lập chỉ mục các đốm hình ảnh mà chúng tôi cung cấp cho nó:

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