From 5f1dc0c44105bf2e6fdb93a76da4b3c36199484f Mon Sep 17 00:00:00 2001 From: dhairyashil Date: Mon, 23 Dec 2024 19:02:19 +0530 Subject: [PATCH 1/3] added functionality to send message automatically once internet restored and some validations --- .../react/src/lib/createPendingMessage.js | 191 +++++++++++++++++- .../AttachmentPreview/AttachmentPreview.js | 49 ++++- .../views/ChatInput/AudioMessageRecorder.js | 78 ++++++- .../react/src/views/ChatInput/ChatInput.js | 69 ++++++- .../views/ChatInput/VideoMessageRecoder.js | 70 ++++++- 5 files changed, 445 insertions(+), 12 deletions(-) diff --git a/packages/react/src/lib/createPendingMessage.js b/packages/react/src/lib/createPendingMessage.js index da8bdc3044..2f6772585c 100644 --- a/packages/react/src/lib/createPendingMessage.js +++ b/packages/react/src/lib/createPendingMessage.js @@ -53,4 +53,193 @@ const createPendingMessage = ( }; }; -export default createPendingMessage; +const createPendingAudioMessage = ( + file, + user = { + _id: undefined, + username: undefined, + name: undefined, + } +) => { + const now = new Date(); + return { + isPending: true, + _id: `ec_${createRandomId()}`, + rid: 'GENERAL', + msg: '', + ts: now.toISOString(), + u: { + _id: user._id, + username: user.username, + name: user.name, + }, + _updatedAt: now.toISOString(), + urls: [], + mentions: [], + channels: [], + file: { + _id: file._id, + name: file.name, + type: file.type, + size: file.size, + format: file.format || '', + }, + files: [ + { + _id: file._id, + name: file.name, + type: file.type, + size: file.size, + format: file.format || '', + }, + ], + attachments: [ + { + ts: '1970-01-01T00:00:00.000Z', + title: file.name, + title_link: `/file-upload/${file._id}/${encodeURIComponent(file.name)}`, + title_link_download: true, + audio_url: `/file-upload/${file._id}/${encodeURIComponent(file.name)}`, + audio_type: file.type, + audio_size: file.size, + type: 'file', + description: '', + }, + ], + md: [], + }; +}; + +const createPendingVideoMessage = ( + file, + user = { + _id: undefined, + username: undefined, + name: undefined, + } +) => { + const now = new Date(); + return { + isPending: true, + _id: `ec_${createRandomId()}`, + rid: 'GENERAL', + msg: '', + ts: now.toISOString(), + u: { + _id: user._id, + username: user.username, + name: user.name, + }, + _updatedAt: now.toISOString(), + urls: [], + mentions: [], + channels: [], + file: { + _id: file._id, + name: file.name, + type: file.type, + size: file.size, + format: file.format || '', + }, + files: [ + { + _id: file._id, + name: file.name, + type: file.type, + size: file.size, + format: file.format || '', + }, + ], + attachments: [ + { + ts: '1970-01-01T00:00:00.000Z', + title: file.name, + title_link: `/file-upload/${file._id}/${encodeURIComponent(file.name)}`, + title_link_download: true, + video_url: `/file-upload/${file._id}/${encodeURIComponent(file.name)}`, + video_type: file.type, + video_size: file.size, + type: 'file', + description: '', + }, + ], + md: [], + }; +}; + +const createPendingFileMessage = ( + file, + user = { + _id: undefined, + username: undefined, + name: undefined, + }, + description = 'Hahahaha' +) => { + const now = new Date(); + return { + isPending: true, + _id: `ec_${createRandomId()}`, + rid: 'GENERAL', + msg: '', + ts: now.toISOString(), + u: { + _id: user._id, + username: user.username, + name: user.name, + }, + _updatedAt: now.toISOString(), + urls: [], + mentions: [], + channels: [], + file: { + _id: '', + name: file.name, + type: file.type, + size: file.size, + format: file.format || '', + }, + files: [ + { + _id: '', + name: file.name, + type: file.type, + size: file.size, + format: file.format || '', + }, + ], + attachments: [ + { + ts: '1970-01-01T00:00:00.000Z', + title: file.name, + title_link: `/file-upload/${file.name}/${encodeURIComponent( + file.name + )}`, + title_link_download: true, + type: 'file', + description, + format: file.format || '', + size: file.size, + descriptionMd: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: description, + }, + ], + }, + ], + }, + ], + md: [], + }; +}; + +export { + createPendingMessage, + createPendingAudioMessage, + createPendingVideoMessage, + createPendingFileMessage, +}; diff --git a/packages/react/src/views/AttachmentPreview/AttachmentPreview.js b/packages/react/src/views/AttachmentPreview/AttachmentPreview.js index 209c9e33e8..729a40f36d 100644 --- a/packages/react/src/views/AttachmentPreview/AttachmentPreview.js +++ b/packages/react/src/views/AttachmentPreview/AttachmentPreview.js @@ -1,12 +1,20 @@ import React, { useContext, useState } from 'react'; import { css } from '@emotion/react'; -import { Box, Icon, Button, Input, Modal } from '@embeddedchat/ui-elements'; +import { + Box, + Icon, + Button, + Input, + Modal, + useToastBarDispatch, +} from '@embeddedchat/ui-elements'; import useAttachmentWindowStore from '../../store/attachmentwindow'; import CheckPreviewType from './CheckPreviewType'; import RCContext from '../../context/RCInstance'; -import { useMessageStore } from '../../store'; +import { useUserStore, useMessageStore } from '../../store'; import getAttachmentPreviewStyles from './AttachmentPreview.styles'; import { parseEmoji } from '../../lib/emoji'; +import { createPendingFileMessage } from '../../lib/createPendingMessage'; const AttachmentPreview = () => { const { RCInstance, ECOptions } = useContext(RCContext); @@ -29,15 +37,48 @@ const AttachmentPreview = () => { setFileDescription(parseEmoji(e.target.value)); }; + const upsertMessage = useMessageStore((state) => state.upsertMessage); + const removeMessage = useMessageStore((state) => state.removeMessage); + const dispatchToastMessage = useToastBarDispatch(); + + const { username, userId, name } = useUserStore((state) => ({ + username: state.username, + userId: state.userId, + name: state.name, + })); + const userInfo = { _id: userId, username, name }; + const submit = async () => { setIsPending(true); - await RCInstance.sendAttachment( + + let pendingFileMessage = createPendingFileMessage( + data, + userInfo, + fileDescription + ); + upsertMessage(pendingFileMessage, ECOptions.enableThreads); + toggle(); + + if (!navigator.onLine) { + dispatchToastMessage({ + type: 'error', + message: 'Please try again after connecting to internet!', + }); + removeMessage(pendingFileMessage._id); + return; + } + + const res = await RCInstance.sendAttachment( data, fileName, fileDescription, ECOptions?.enableThreads ? threadId : undefined ); - toggle(); + + if (res.success) { + removeMessage(pendingFileMessage._id); + } + setData(null); setIsPending(false); }; diff --git a/packages/react/src/views/ChatInput/AudioMessageRecorder.js b/packages/react/src/views/ChatInput/AudioMessageRecorder.js index caef86e700..4583c06ef5 100644 --- a/packages/react/src/views/ChatInput/AudioMessageRecorder.js +++ b/packages/react/src/views/ChatInput/AudioMessageRecorder.js @@ -5,11 +5,18 @@ import React, { useContext, useRef, } from 'react'; -import { Box, Icon, ActionButton, useTheme } from '@embeddedchat/ui-elements'; +import { + Box, + Icon, + ActionButton, + useTheme, + useToastBarDispatch, +} from '@embeddedchat/ui-elements'; import { useMediaRecorder } from '../../hooks/useMediaRecorder'; import RCContext from '../../context/RCInstance'; -import useMessageStore from '../../store/messageStore'; +import { useUserStore, useMessageStore } from '../../store'; import { getCommonRecorderStyles } from './ChatInput.styles'; +import { createPendingAudioMessage } from '../../lib/createPendingMessage'; const AudioMessageRecorder = () => { const videoRef = useRef(null); @@ -26,6 +33,22 @@ const AudioMessageRecorder = () => { const [file, setFile] = useState(null); const [isRecorded, setIsRecorded] = useState(false); const threadId = useMessageStore((_state) => _state.threadMainMessage?._id); + const upsertMessage = useMessageStore((state) => state.upsertMessage); + const removeMessage = useMessageStore((state) => state.removeMessage); + const dispatchToastMessage = useToastBarDispatch(); + + const { username, userId, name } = useUserStore((state) => ({ + username: state.username, + userId: state.userId, + name: state.name, + })); + const userInfo = { _id: userId, username, name }; + + const [messageQueue, setMessageQueue] = useState([]); + const addMessageInMessageQueue = (key, value) => { + setMessageQueue((prevState) => [...prevState, { key, value }]); + }; + const onStop = (audioChunks) => { const audioBlob = new Blob(audioChunks, { type: 'audio/mpeg' }); const fileName = 'Audio record.mp3'; @@ -119,14 +142,63 @@ const AudioMessageRecorder = () => { handleMount(); }, [handleMount]); + useEffect(() => { + const handleOnline = async () => { + if (navigator.onLine && messageQueue.length > 0) { + for (let i = 0; i < messageQueue.length; i++) { + const { key, value } = messageQueue[i]; + const pendingAudioMessage = JSON.parse(value); + + const res = await RCInstance.sendAttachment( + key, + undefined, + undefined, + ECOptions.enableThreads ? threadId : undefined + ); + + if (res.success) { + removeMessage(pendingAudioMessage._id); + } + } + setMessageQueue([]); + } + }; + + window.addEventListener('online', handleOnline); + return () => { + window.removeEventListener('online', handleOnline); + }; + }, [messageQueue]); + + const handleOffline = (file, pendingAudioMessage) => { + addMessageInMessageQueue(file, JSON.stringify(pendingAudioMessage)); + + dispatchToastMessage({ + type: 'info', + message: 'Audio will be sent automatically once you are back online!', + }); + }; + useEffect(() => { const sendRecording = async () => { - await RCInstance.sendAttachment( + let pendingAudioMessage = createPendingAudioMessage(file, userInfo); + upsertMessage(pendingAudioMessage, ECOptions.enableThreads); + + if (!navigator.onLine) { + handleOffline(file, pendingAudioMessage); + return; + } + + const res = await RCInstance.sendAttachment( file, undefined, undefined, ECOptions.enableThreads ? threadId : undefined ); + + if (res.success) { + removeMessage(pendingAudioMessage._id); + } }; if (isRecorded && file) { sendRecording(); diff --git a/packages/react/src/views/ChatInput/ChatInput.js b/packages/react/src/views/ChatInput/ChatInput.js index 86b4b5082a..b0f699fe28 100644 --- a/packages/react/src/views/ChatInput/ChatInput.js +++ b/packages/react/src/views/ChatInput/ChatInput.js @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState, useRef, useEffect, useContext } from 'react'; import { css } from '@emotion/react'; import { Box, @@ -24,7 +24,7 @@ import ChatInputFormattingToolbar from './ChatInputFormattingToolbar'; import useAttachmentWindowStore from '../../store/attachmentwindow'; import MembersList from '../Mentions/MembersList'; import { TypingUsers } from '../TypingUsers'; -import createPendingMessage from '../../lib/createPendingMessage'; +import { createPendingMessage } from '../../lib/createPendingMessage'; import { CommandsList } from '../CommandList'; import useSettingsStore from '../../store/settingsStore'; import ChannelState from '../ChannelState/ChannelState'; @@ -58,6 +58,7 @@ const ChatInput = ({ scrollToBottom }) => { const [showCommandList, setShowCommandList] = useState(false); const [filteredCommands, setFilteredCommands] = useState([]); const [isMsgLong, setIsMsgLong] = useState(false); + const [messageQueue, setMessageQueue] = useState([]); const { isUserAuthenticated, @@ -162,6 +163,42 @@ const ChatInput = ({ scrollToBottom }) => { } }, [editMessage]); + useEffect(() => { + const handleOnline = async () => { + if (navigator.onLine && messageQueue.length > 0) { + for (let i = 0; i < messageQueue.length; i++) { + const pendingMessage = JSON.parse(messageQueue[i]); + const res = await RCInstance.sendMessage( + { + msg: pendingMessage.msg, + _id: pendingMessage._id, + }, + ECOptions.enableThreads ? threadId : undefined + ); + if (res.success) { + clearQuoteMessages(); + replaceMessage(pendingMessage, res.message); + } + } + setMessageQueue([]); + localStorage.removeItem('pendingMessage'); + } + }; + + window.addEventListener('online', handleOnline); + return () => { + window.removeEventListener('online', handleOnline); + }; + }, [messageQueue]); + + useEffect(() => { + const pendingMessage = localStorage.getItem('pendingMessage'); + if (pendingMessage) { + messageRef.current.value = JSON.parse(pendingMessage).msg || ''; + localStorage.removeItem('pendingMessage'); + } + }, []); + const getMessageLink = async (id) => { const host = RCInstance.getHost(); const res = await RCInstance.channelInfo(); @@ -252,6 +289,21 @@ const ChatInput = ({ scrollToBottom }) => { } }; + const handleOffline = (pendingMessage) => { + localStorage.removeItem('pendingMessage'); + localStorage.setItem('pendingMessage', JSON.stringify(pendingMessage)); + + setMessageQueue((prevQueue) => [ + ...prevQueue, + JSON.stringify(pendingMessage), + ]); + + dispatchToastMessage({ + type: 'info', + message: 'Message will be sent automatically once you are back online!', + }); + }; + const handleSendNewMessage = async (message) => { messageRef.current.value = ''; setDisableButton(true); @@ -293,6 +345,11 @@ const ChatInput = ({ scrollToBottom }) => { upsertMessage(pendingMessage, ECOptions.enableThreads); + if (!navigator.onLine) { + handleOffline(pendingMessage); + return; + } + const res = await RCInstance.sendMessage( { msg: pendingMessage.msg, @@ -308,6 +365,14 @@ const ChatInput = ({ scrollToBottom }) => { }; const handleEditMessage = async (message) => { + if (!navigator.onLine) { + dispatchToastMessage({ + type: 'error', + message: 'Please try again after connecting to internet!', + }); + return; + } + messageRef.current.value = ''; setDisableButton(true); const editMessageId = editMessage._id; diff --git a/packages/react/src/views/ChatInput/VideoMessageRecoder.js b/packages/react/src/views/ChatInput/VideoMessageRecoder.js index 4d5b00810d..4c5c0b1a4b 100644 --- a/packages/react/src/views/ChatInput/VideoMessageRecoder.js +++ b/packages/react/src/views/ChatInput/VideoMessageRecoder.js @@ -12,11 +12,13 @@ import { ActionButton, Modal, useTheme, + useToastBarDispatch, } from '@embeddedchat/ui-elements'; import { useMediaRecorder } from '../../hooks/useMediaRecorder'; import RCContext from '../../context/RCInstance'; -import useMessageStore from '../../store/messageStore'; +import { useUserStore, useMessageStore } from '../../store'; import { getCommonRecorderStyles } from './ChatInput.styles'; +import { createPendingVideoMessage } from '../../lib/createPendingMessage'; const VideoMessageRecorder = () => { const videoRef = useRef(null); @@ -34,6 +36,21 @@ const VideoMessageRecorder = () => { const [file, setFile] = useState(null); const [isRecorded, setIsRecorded] = useState(false); const threadId = useMessageStore((_state) => _state.threadMainMessage?._id); + const upsertMessage = useMessageStore((state) => state.upsertMessage); + const removeMessage = useMessageStore((state) => state.removeMessage); + const dispatchToastMessage = useToastBarDispatch(); + + const { username, userId, name } = useUserStore((state) => ({ + username: state.username, + userId: state.userId, + name: state.name, + })); + const userInfo = { _id: userId, username, name }; + + const [messageQueue, setMessageQueue] = useState([]); + const addMessageInMessageQueue = (key, value) => { + setMessageQueue((prevState) => [...prevState, { key, value }]); + }; const onStop = (videoChunks) => { const videoBlob = new Blob(videoChunks, { type: 'video/mp4' }); @@ -134,14 +151,63 @@ const VideoMessageRecorder = () => { handleMount(); }, [handleMount]); + useEffect(() => { + const handleOnline = async () => { + if (navigator.onLine && messageQueue.length > 0) { + for (let i = 0; i < messageQueue.length; i++) { + const { key, value } = messageQueue[i]; + const pendingVideoMessage = JSON.parse(value); + + const res = await RCInstance.sendAttachment( + key, + undefined, + undefined, + ECOptions.enableThreads ? threadId : undefined + ); + + if (res.success) { + removeMessage(pendingVideoMessage._id); + } + } + setMessageQueue([]); + } + }; + + window.addEventListener('online', handleOnline); + return () => { + window.removeEventListener('online', handleOnline); + }; + }, [messageQueue]); + + const handleOffline = (file, pendingVideoMessage) => { + addMessageInMessageQueue(file, JSON.stringify(pendingVideoMessage)); + + dispatchToastMessage({ + type: 'info', + message: 'Video will be sent automatically once you are back online!', + }); + }; + useEffect(() => { const sendRecording = async () => { - await RCInstance.sendAttachment( + let pendingVideoMessage = createPendingVideoMessage(file, userInfo); + upsertMessage(pendingVideoMessage, ECOptions.enableThreads); + + if (!navigator.onLine) { + handleOffline(file, pendingVideoMessage); + return; + } + + const res = await RCInstance.sendAttachment( file, undefined, undefined, ECOptions.enableThreads ? threadId : undefined ); + + if (res.success) { + removeMessage(pendingVideoMessage._id); + } }; if (isRecorded && file) { sendRecording(); From 0c6d0d51360c055459d961628330976bc041205b Mon Sep 17 00:00:00 2001 From: dhairyashil Date: Sun, 12 Jan 2025 17:04:22 +0530 Subject: [PATCH 2/3] Revert the changes in videomessagerecorder --- .../react/src/views/ChatInput/VideoMessageRecoder.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/react/src/views/ChatInput/VideoMessageRecoder.js b/packages/react/src/views/ChatInput/VideoMessageRecoder.js index 91783d676a..911df74c69 100644 --- a/packages/react/src/views/ChatInput/VideoMessageRecoder.js +++ b/packages/react/src/views/ChatInput/VideoMessageRecoder.js @@ -164,7 +164,10 @@ const VideoMessageRecorder = ({ disabled }) => {