paint-brush
Создание ChatPlus: PWA с открытым исходным кодом, похожее на мобильное приложениек@aladinyo
1,148 чтения
1,148 чтения

Создание ChatPlus: PWA с открытым исходным кодом, похожее на мобильное приложение

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

Слишком долго; Читать

ChatPlus — одно из лучших приложений, которые я создал. Оно привлекает внимание своим ярким пользовательским интерфейсом и многочисленными функциями обмена сообщениями и вызовами, которые реализованы в Интернете. Оно обеспечивает кроссплатформенность, поскольку его интерфейс разработан как приложение PWA, которое ChatPlus может быть установлен где угодно и действовать как отдельное приложение, благодаря такой функции, как push-уведомления. Это определение легкого приложения, которое может предоставить все функции мобильного приложения с веб-технологиями.
featured image - Создание ChatPlus: PWA с открытым исходным кодом, похожее на мобильное приложение
Alaa Eddine Boubekeur HackerNoon profile picture
0-item
1-item

ChatPlus — отличное PWA для общения 💬✨🤩

ChatPlus — это прогрессивное веб-приложение, разработанное с использованием React, NodeJS, Firebase и других сервисов.

Вы можете общаться со всеми своими друзьями в режиме реального времени 🗣️✨🧑‍🤝‍🧑❤️

Вы можете звонить своим друзьям и совершать с ними видео- и аудиозвонки 🎥🔉🤩

Отправляйте изображения своим друзьям, а также аудиосообщения, и у вас будет искусственный интеллект, который преобразует вашу речь в текст, независимо от того, говорите ли вы по-французски, по-английски или по-испански 🤖✨

Веб-приложение можно установить на любые устройства и получать уведомления ⬇️🔔🎉

Я буду очень признателен за вашу поддержку, оставьте нам звездочку в репозитории Github и поделитесь с друзьями ⭐✨

Полную документацию по установке и развертыванию можно найти в репозитории Github: https://github.com/aladinyo/ChatPlus.

Чистый простой пользовательский интерфейс и UX

ChatPlus для мобильных устройств


Просмотр ChatPlus на рабочем столе


Так что же такое ChatPlus?

ChatPlus — одно из лучших приложений, которые я создал. Оно привлекает внимание своим ярким пользовательским интерфейсом и многочисленными функциями обмена сообщениями и вызовами, которые реализованы в Интернете. Оно обеспечивает кроссплатформенность, поскольку его интерфейс разработан как приложение PWA, которое ChatPlus может быть установлен где угодно и действовать как отдельное приложение, благодаря такой функции, как push-уведомления. Это определение легкого приложения, которое может предоставить все функции мобильного приложения с веб-технологиями.

Архитектура программного обеспечения

Наше приложение соответствует архитектуре программного обеспечения MVC. MVC (Модель-Представление-Контроллер) — это шаблон проектирования программного обеспечения, обычно используемый для реализации пользовательских интерфейсов, данных и управляющей логики. Он подчеркивает разделение между бизнес-логикой программного обеспечения и отображением. Такое «разделение ответственности» обеспечивает лучшее разделение труда и улучшение обслуживания.

Уровень пользовательского интерфейса (View)

Он состоит из нескольких компонентов интерфейса React:

  • MaterialUI: Material UI — это библиотека компонентов React с открытым исходным кодом, реализующая Material Design от Google. Он всеобъемлющий и может быть использован в производстве прямо из коробки. Мы собираемся использовать его для значков кнопок и некоторых элементов внешнего интерфейса, таких как аудио-слайдер.

  • LoginView: Простое представление входа в систему, которое позволяет пользователю ввести свое имя пользователя и пароль или войти в систему с помощью Google.

  • SidebarViews : несколько компонентов представления, таких как «sidebar__header», «sidebar__search», «меню боковой панели» и «SidebarChats», он состоит из заголовка для информации о пользователе, панели поиска для поиска пользователей, меню боковой панели для навигации между вашими чатами и группами. и пользователей, а также компонент SidebarChats для отображения всех ваших последних чатов, в которых вы отправляли сообщения, он показывает имя пользователя, фотографию и последнее сообщение.

  • ChatViews: состоит из множества следующих компонентов:

    1 .'chat__header', который содержит информацию о пользователе, с которым вы разговариваете, его онлайн-статус, изображение профиля, а также отображает кнопки для аудиовызова и видеовызова, а также показывает, печатает ли пользователь.

    2 .'chat__body--container' содержит информацию о наших сообщениях с другими пользователями. У него есть компонент сообщений, который отображает текстовые сообщения, изображения с их сообщениями, а также аудиосообщения с их информацией, такой как время звука и то, воспроизводился ли звук и в конце этого компонента у нас есть элемент «seen», который показывает, были ли просмотрены сообщения.

    3. «AudioPlayer »: компонент React, который может отображать нам аудио с помощью ползунка для навигации по нему, отображает полное и текущее время звука. Этот компонент представления загружается внутри контейнера «chat__body--».

    4. «ChatFooter»: содержит поле для ввода сообщения, кнопку для отправки сообщения при вводе, в противном случае кнопка позволит вам записать звук, кнопку для импорта изображений и файлов.

    5. «MediaPreview»: компоненты React, которые позволяют просматривать изображения или файлы, которые мы выбрали для отправки в наш чат. Они отображаются в карусели, и пользователь может перемещать изображения или файлы и вводить конкретное сообщение для каждого из них.

    6. «ImagePreview»: когда в наш чат отправляются изображения, этот компонент будет отображать изображения в полноэкранном режиме с плавной анимацией, компонент монтируется после щелчка по изображению.

  • ScalePage: функция просмотра, которая увеличивает размер нашего веб-приложения при его отображении на больших экранах, таких как экраны с разрешением Full HD и 4K.

  • CallViews: набор реагирующих компонентов, которые содержат элементы просмотра всех вызовов. Их можно перетаскивать по всему экрану, и они состоят из:

    1 «Кнопки»: кнопка вызова красного цвета и зеленая кнопка видеовызова.

    2 «AudioCallView»: компонент просмотра, который позволяет отвечать на входящий аудиовызов и отображать вызов с таймером, а также позволяет отменить вызов.

    3 «StartVideoCallView»: компонент просмотра, который отображает наше видео при подключении к локальному MediaAPI и ожидает, пока другой пользователь примет вызов, или отображает кнопку, позволяющую нам ответить на входящий видеовызов.

    4 «VideoCallView»: компонент просмотра, который отображает видео с нами и другим пользователем. Он позволяет переключать камеры, отключать камеру и звук, а также может работать в полноэкранном режиме.

  • RouteViews: компоненты React, содержащие все наши представления, для создания навигации по локальным компонентам. Мы получили VideoCallRoute, SideBarMenuRoute и ChatsRoute.

Клиентские модели (модель)

Модели на стороне клиента — это логика, которая позволяет нашему интерфейсу взаимодействовать с базами данных и множеством локальных и серверных API, и состоит из:

  • Firebase SDK: это SDK, который используется для создания базы данных нашего веб-приложения.
  • AppModel: модель, которая генерирует пользователя после аутентификации, а также обеспечивает наличие последней версии наших веб-ресурсов.
  • ChatModels: он состоит из модельной логики отправки сообщений в базу данных, создания прослушивателей для прослушивания новых сообщений, прослушивания того, находится ли другой пользователь в сети и печатает ли он, а также отправляет наши медиафайлы, такие как изображения и аудио, в хранилище базы данных. SidebarChatsModel: логика, которая прослушивает последние сообщения пользователей и предоставляет нам массив всех ваших новых сообщений от пользователей, а также показывает количество непрочитанных сообщений и онлайн-статус пользователей, а также организует пользователей на основе времени последнего сообщения.
  • UsersSearchModel: логика, которая ищет пользователей в нашей базе данных. Она использует поиск Algolia, в котором есть список наших пользователей, связывая его с нашей базой данных на сервере.
  • CallModel: логика, которая использует Daily SDK для создания вызова в нашем веб-приложении, а также отправки данных на наш сервер и взаимодействия с DailyAPI.

Клиентские контроллеры (контроллер)

Он состоит из компонентов React, которые связывают наши представления с их конкретными моделями:

  • Контроллер приложений: связывает аутентифицированного пользователя со всеми компонентами и запускает функцию ScalePage для настройки размера нашего приложения. Он также загружает Firebase и присоединяет все компоненты. Мы можем считать его оберткой для наших компонентов.
  • SideBarController: связывает данные пользователя и список его последних чатов, он также связывает наши меню с логикой их модели, а также связывает панель поиска с API поиска Algolia.
  • ChatController: это очень большой контроллер, который связывает большинство функций обмена сообщениями и чата.
  • CallController: связывает модель вызова с ее представлениями.

Серверная модель

Не все функции выполняются во внешнем интерфейсе, поскольку используемые нами SDK требуют некоторых функций на стороне сервера, и они включают в себя:

  • CallServerModel: логика, которая позволяет нам создавать комнаты для звонков, взаимодействуя с Daily API и обновляя нашу базу данных Firestore.
  • TranscriptModel: логика на сервере, которая получает аудиофайл и взаимодействует с API преобразования речи в текст Google Cloud и предоставляет расшифровку аудиосообщений.
  • Обработчик онлайн-статуса: прослушиватель, который прослушивает онлайн-статус пользователей и соответствующим образом обновляет базу данных. Модель уведомления: служба, отправляющая уведомления другим пользователям.
  • AlgoliaSaver: прослушиватель, который прослушивает новых пользователей в нашей базе данных и соответствующим образом обновляет algolia, чтобы мы могли использовать его для функции поиска во внешнем интерфейсе.
  • Контроллеры на стороне сервера: CallServer: конечная точка API, содержащая callModel Worker: рабочая служба, которая запускает все наши службы обработки Firebase.

Блок-схема чата

Блок-схема чата


Блок-схема боковой панели

Блок-схема боковой панели

Блок-схема вызова модели:

Блок-схема вызова модели

Посмотреть блок-схему звонков

Посмотреть блок-схему звонков

Блок-схема бэкэнд-работника

Блок-схема бэкэнд-работника

Проектирование базы данных

Наше веб-приложение использует Firestore для хранения нашей базы данных, которая представляет собой базу данных Firebase NoSql, мы храним информацию о пользователях, мы храним список всех сообщений, мы храним список чатов, а также мы храним чаты в комнатах, это данные, хранящиеся на нашем база данных:

  • Данные пользователей после аутентификации.
  • Комнаты, содержащие всю информацию о сообщениях.
  • Список последних чатов для каждого пользователя.
  • Список уведомлений для отправки.
  • Список аудио для расшифровки.

UML базы данных

Пояснения к магическому коду 🔮

В следующих главах я собираюсь дать краткое объяснение и учебные пособия по определенным функциям ChatPlus, я собираюсь показать вам код JS и объяснить лежащий в его основе алгоритм, а также предоставить правильный инструмент интеграции для связывания вашего кода с база данных.

Обработка онлайн-статуса

Онлайн-статус пользователей был реализован с помощью функции подключения к базе данных Firebase путем подключения к «.info/connected» на внешнем интерфейсе и соответствующего обновления Firestore и базы данных:

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

А на нашем бэкэнде мы также настраиваем прослушиватель, который прослушивает изменения в нашей базе данных и соответствующим образом обновляет Firestore. Эта функция также может предоставлять нам онлайн-статус пользователя в режиме реального времени, чтобы мы могли запускать внутри него и другие функции:

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

Уведомления

Уведомления — отличная функция, и они реализованы с использованием обмена сообщениями Firebase. На нашем интерфейсе, если браузер пользователя поддерживает уведомления, мы настраиваем его и получаем токен обмена сообщениями Firebase пользователя:

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

всякий раз, когда пользователь отправляет сообщение, мы добавляем уведомление в нашу базу данных:

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


и на нашем бэкэнде мы слушаем коллекцию уведомлений и используем обмен сообщениями Firebase, чтобы отправить их пользователю.

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

Аудио транскрипция AI

Наше веб-приложение позволяет пользователям отправлять друг другу аудиосообщения, и одной из его функций является возможность конвертировать этот звук в текст для аудио, записанного на английском, французском и испанском языках, и эта функция была реализована с помощью функции Google Cloud Speech to Text. Наш бэкэнд прослушивает новые расшифровки, добавленные в Firestore, и расшифровывает их, а затем записывает в базу данных:

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

Очевидно, ваши глаза смотрят на эту функцию textToAudio, и вы задаетесь вопросом, как я это сделал, не волнуйтесь, я вас понял:

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

Функция видеозвонка

Наше веб-приложение использует Daily API для реализации веб-соединений RTC в реальном времени. Оно позволяет пользователям совершать видеозвонки друг другу, поэтому сначала мы настраиваем внутренний сервер вызовов, который имеет множество точек входа API для создания и удаления комнат в Daily:

 const app = express(); app.use(cors()); app.use(express.json()); app.delete("/delete-call", async (req, res) => { console.log("delete call data: ", req.body); deleteCallFromUser(req.body.id1); deleteCallFromUser(req.body.id2); try { fetch("https://api.daily.co/v1/rooms/" + req.body.roomName, { headers: { Authorization: `Bearer ${dailyApiKey}`, "Content-Type": "application/json" }, method: "DELETE" }); } catch(e) { console.log("error deleting room for call delete!!"); console.log(e); } res.status(200).send("delete-call success !!"); }); app.post("/create-room/:roomName", async (req, res) => { var room = await fetch("https://api.daily.co/v1/rooms/", { headers: { Authorization: `Bearer ${dailyApiKey}`, "Content-Type": "application/json" }, method: "POST", body: JSON.stringify({ name: req.params.roomName }) }); room = await room.json(); console.log(room); res.json(room); }); app.delete("/delete-room/:roomName", async (req, res) => { var deleteResponse = await fetch("https://api.daily.co/v1/rooms/" + req.params.roomName, { headers: { Authorization: `Bearer ${dailyApiKey}`, "Content-Type": "application/json" }, method: "DELETE" }); deleteResponse = await deleteResponse.json(); console.log(deleteResponse); res.json(deleteResponse); }) app.listen(process.env.PORT || 7000, () => { console.log("call server is running"); }); const deleteCallFromUser = userID => db.collection("users").doc(userID).collection("call").doc("call").delete();

и в нашем интерфейсе у нас есть несколько функций для вызова этого 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) }); };

Отлично, теперь пришло время создавать комнаты звонков и использовать ежедневный JS SDK для подключения к этим комнатам и отправки и получения данных из них:

 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 и UserQuery — это просто пути к документам Firebase Firestore, теперь остальная часть приложения имеет компоненты представления, которые реагируют на изменения состояния, которые инициируются этой функцией выше, и наши элементы пользовательского интерфейса вызова будут отображаться соответствующим образом.

Перемещение по элементу вызова:

Следующая функция — это Magic, которая позволяет перетаскивать элемент «Вызов» по всей странице:

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

Перетаскивание изображений

Вы можете перетаскивать изображения в свой чат и отправлять их другому пользователю. Эта функция стала возможной благодаря следующему запуску:

 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 — это простая функция, индексирующая набор изображений, которые мы ей предоставляем:

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