diff --git a/src/MessageQueueContext.tsx b/src/MessageQueueContext.tsx index 499b10b..3727b03 100644 --- a/src/MessageQueueContext.tsx +++ b/src/MessageQueueContext.tsx @@ -53,6 +53,7 @@ export const MessageQueueProvider = ({ children }) => { // Function to process the message queue const processQueue = useCallback((newMessages = [], groupDirectId) => { + processingPromiseRef.current = processingPromiseRef.current .then(() => processQueueInternal(newMessages, groupDirectId)) .catch((err) => console.error('Error in processQueue:', err)); @@ -61,33 +62,7 @@ export const MessageQueueProvider = ({ children }) => { // Internal function to handle queue processing const processQueueInternal = async (newMessages, groupDirectId) => { // Remove any messages from the queue that match the specialId from newMessages - if (newMessages.length > 0) { - messageQueueRef.current = messageQueueRef.current.filter((msg) => { - return !newMessages.some(newMsg => newMsg?.specialId === msg?.specialId); - }); - - // Remove corresponding entries in queueChats for the provided groupDirectId - setQueueChats((prev) => { - const updatedChats = { ...prev }; - if (updatedChats[groupDirectId]) { - updatedChats[groupDirectId] = updatedChats[groupDirectId].filter((chat) => { - return !newMessages.some(newMsg => newMsg?.specialId === chat?.message?.specialId); - }); - - // Remove messages with status 'failed-permanent' - updatedChats[groupDirectId] = updatedChats[groupDirectId].filter((chat) => { - return chat?.status !== 'failed-permanent'; - }); - - // If no more chats for this group, delete the groupDirectId entry - if (updatedChats[groupDirectId].length === 0) { - delete updatedChats[groupDirectId]; - } - } - return updatedChats; - }); - } - + // If the queue is empty, no need to process if (messageQueueRef.current.length === 0) return; @@ -112,11 +87,11 @@ export const MessageQueueProvider = ({ children }) => { try { // Execute the function stored in the messageQueueRef + await currentMessage.func(); // Remove the message from the queue after successful sending messageQueueRef.current.shift(); - // Remove the message from queueChats setQueueChats((prev) => { const updatedChats = { ...prev }; @@ -167,7 +142,33 @@ export const MessageQueueProvider = ({ children }) => { // Method to process with new messages and groupDirectId const processWithNewMessages = (newMessages, groupDirectId) => { - processQueue(newMessages, groupDirectId); + if (newMessages.length > 0) { + messageQueueRef.current = messageQueueRef.current.filter((msg) => { + return !newMessages.some(newMsg => newMsg?.specialId === msg?.specialId); + }); + + // Remove corresponding entries in queueChats for the provided groupDirectId + setQueueChats((prev) => { + const updatedChats = { ...prev }; + if (updatedChats[groupDirectId]) { + updatedChats[groupDirectId] = updatedChats[groupDirectId].filter((chat) => { + return !newMessages.some(newMsg => newMsg?.specialId === chat?.message?.specialId); + }); + + // Remove messages with status 'failed-permanent' + updatedChats[groupDirectId] = updatedChats[groupDirectId].filter((chat) => { + return chat?.status !== 'failed-permanent'; + }); + + // If no more chats for this group, delete the groupDirectId entry + if (updatedChats[groupDirectId].length === 0) { + delete updatedChats[groupDirectId]; + } + } + return updatedChats; + }); + } + }; return ( diff --git a/src/common/ErrorBoundary.tsx b/src/common/ErrorBoundary.tsx new file mode 100644 index 0000000..58b3185 --- /dev/null +++ b/src/common/ErrorBoundary.tsx @@ -0,0 +1,36 @@ +import React, { ReactNode } from 'react' + +interface ErrorBoundaryProps { + children: ReactNode + fallback: ReactNode +} + +interface ErrorBoundaryState { + hasError: boolean +} + +class ErrorBoundary extends React.Component< + ErrorBoundaryProps, + ErrorBoundaryState +> { + state: ErrorBoundaryState = { + hasError: false + } + + static getDerivedStateFromError(_: Error): ErrorBoundaryState { + return { hasError: true } + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { + // You can log the error and errorInfo here, for example, to an error reporting service. + console.error('Error caught in ErrorBoundary:', error, errorInfo) + } + + render(): React.ReactNode { + if (this.state.hasError) return this.props.fallback + + return this.props.children + } +} + +export default ErrorBoundary diff --git a/src/components/Chat/ChatDirect.tsx b/src/components/Chat/ChatDirect.tsx index b2b6a90..5bf24ac 100644 --- a/src/components/Chat/ChatDirect.tsx +++ b/src/components/Chat/ChatDirect.tsx @@ -38,6 +38,9 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi const [infoSnack, setInfoSnack] = React.useState(null); const [publicKeyOfRecipient, setPublicKeyOfRecipient] = React.useState("") const hasInitializedWebsocket = useRef(false) + const [onEditMessage, setOnEditMessage] = useState(null) + const [chatReferences, setChatReferences] = useState({}) + const editorRef = useRef(null); const socketRef = useRef(null); const timeoutIdRef = useRef(null); @@ -67,7 +70,15 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi const tempMessages = useMemo(()=> { if(!selectedDirect?.address) return [] if(queueChats[selectedDirect?.address]){ - return queueChats[selectedDirect?.address] + return queueChats[selectedDirect?.address]?.filter((item)=> !item?.chatReference) + } + return [] + }, [selectedDirect?.address, queueChats]) + + const tempChatReferences = useMemo(()=> { + if(!selectedDirect?.address) return [] + if(queueChats[selectedDirect?.address]){ + return queueChats[selectedDirect?.address]?.filter((item)=> !!item?.chatReference) } return [] }, [selectedDirect?.address, queueChats]) @@ -99,50 +110,81 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi console.error(error); } } + const decryptMessages = (encryptedMessages: any[], isInitiated: boolean)=> { + try { + return new Promise((res, rej)=> { + window.sendMessage("decryptDirect", { + data: encryptedMessages, + involvingAddress: selectedDirect?.address, + }) + .then((response) => { + if (!response?.error) { + processWithNewMessages(response, selectedDirect?.address); + res(response); + + if (isInitiated) { + const formatted = response.filter((rawItem) => !rawItem?.chatReference).map((item) => ({ + ...item, + id: item.signature, + text: item.message, + unread: item?.sender === myAddress ? false : true, + })); + setMessages((prev) => [...prev, ...formatted]); + setChatReferences((prev) => { + const organizedChatReferences = { ...prev }; + + response.filter((rawItem) => !!rawItem?.chatReference && rawItem?.type === 'edit').forEach((item) => { + try { + organizedChatReferences[item.chatReference] = { + ...(organizedChatReferences[item.chatReference] || {}), + edit: item + }; + } catch(error){ - const decryptMessages = (encryptedMessages: any[], isInitiated: boolean)=> { - try { - return new Promise((res, rej)=> { - window.sendMessage("decryptDirect", { - data: encryptedMessages, - involvingAddress: selectedDirect?.address, - }) - .then((response) => { - if (!response?.error) { - processWithNewMessages(response, selectedDirect?.address); - res(response); - - if (isInitiated) { - const formatted = response.map((item) => ({ - ...item, - id: item.signature, - text: item.message, - unread: item?.sender === myAddress ? false : true, - })); - setMessages((prev) => [...prev, ...formatted]); - } else { - const formatted = response.map((item) => ({ - ...item, - id: item.signature, - text: item.message, - unread: false, - })); - setMessages(formatted); - hasInitialized.current = true; } - return; - } - rej(response.error); - }) - .catch((error) => { - rej(error.message || "An error occurred"); - }); - - }) - } catch (error) { - - } - } + }) + return organizedChatReferences + }) + } else { + hasInitialized.current = true; + const formatted = response.filter((rawItem) => !rawItem?.chatReference) + .map((item) => ({ + ...item, + id: item.signature, + text: item.message, + unread: false, + })); + setMessages(formatted); + + setChatReferences((prev) => { + const organizedChatReferences = { ...prev }; + + response.filter((rawItem) => !!rawItem?.chatReference && rawItem?.type === 'edit').forEach((item) => { + try { + organizedChatReferences[item.chatReference] = { + ...(organizedChatReferences[item.chatReference] || {}), + edit: item + }; + } catch(error){ + + } + }) + return organizedChatReferences + }) + } + return; + } + rej(response.error); + }) + .catch((error) => { + rej(error.message || "An error occurred"); + }); + + }) + } catch (error) { + + } +} const forceCloseWebSocket = () => { if (socketRef.current) { @@ -334,81 +376,108 @@ useEffect(() => { }, [editorRef?.current]); - const sendMessage = async ()=> { - try { +const sendMessage = async ()=> { + try { - - if(+balance < 4) throw new Error('You need at least 4 QORT to send a message') - if(isSending) return - if (editorRef.current) { - const htmlContent = editorRef.current.getHTML(); - - if(!htmlContent?.trim() || htmlContent?.trim() === '

') return - setIsSending(true) - pauseAllQueues() - const message = JSON.stringify(htmlContent) - - - if(isNewChat){ - await sendChatDirect({ messageText: htmlContent}, null, null, true) - return - } - let repliedTo = replyMessage?.signature - - if (replyMessage?.chatReference) { - repliedTo = replyMessage?.chatReference - } - const otherData = { - specialId: uid.rnd(), - repliedTo - } - const sendMessageFunc = async () => { - return await sendChatDirect({ chatReference: undefined, messageText: htmlContent, otherData}, selectedDirect?.address, publicKeyOfRecipient, false) - }; - - + + if(+balance < 4) throw new Error('You need at least 4 QORT to send a message') + if(isSending) return + if (editorRef.current) { + const htmlContent = editorRef.current.getHTML(); + + if(!htmlContent?.trim() || htmlContent?.trim() === '

') return + setIsSending(true) + pauseAllQueues() + const message = JSON.stringify(htmlContent) + - // Add the function to the queue - const messageObj = { - message: { - text: htmlContent, - timestamp: Date.now(), - senderName: myName, - sender: myAddress, - ...(otherData || {}) - }, - - } - addToQueue(sendMessageFunc, messageObj, 'chat-direct', - selectedDirect?.address ); - setTimeout(() => { - executeEvent("sent-new-message-group", {}) - }, 150); - clearEditorContent() - setReplyMessage(null) - } - // send chat message - } catch (error) { - const errorMsg = error?.message || error - setInfoSnack({ - type: "error", - message: errorMsg === 'invalid signature' ? 'You need at least 4 QORT to send a message' : errorMsg, - }); - setOpenSnack(true); - console.error(error) - } finally { - setIsSending(false) - resumeAllQueues() - } + if(isNewChat){ + await sendChatDirect({ messageText: htmlContent}, null, null, true) + return } + let repliedTo = replyMessage?.signature - const onReply = useCallback((message)=> { - setReplyMessage(message) - editorRef?.current?.chain().focus() + if (replyMessage?.chatReference) { + repliedTo = replyMessage?.chatReference + } + let chatReference = onEditMessage?.signature - }, []) + const otherData = { + ...(onEditMessage?.decryptedData || {}), + specialId: uid.rnd(), + repliedTo: onEditMessage ? onEditMessage?.repliedTo : repliedTo, + type: chatReference ? 'edit' : '' + } + const sendMessageFunc = async () => { + return await sendChatDirect({ chatReference, messageText: htmlContent, otherData}, selectedDirect?.address, publicKeyOfRecipient, false) + }; + + // Add the function to the queue + const messageObj = { + message: { + timestamp: Date.now(), + senderName: myName, + sender: myAddress, + ...(otherData || {}), + text: htmlContent, + }, + chatReference + } + addToQueue(sendMessageFunc, messageObj, 'chat-direct', + selectedDirect?.address ); + setTimeout(() => { + executeEvent("sent-new-message-group", {}) + }, 150); + clearEditorContent() + setReplyMessage(null) + setOnEditMessage(null) + + } + // send chat message + } catch (error) { + const errorMsg = error?.message || error + setInfoSnack({ + type: "error", + message: errorMsg === 'invalid signature' ? 'You need at least 4 QORT to send a message' : errorMsg, + }); + setOpenSnack(true); + console.error(error) + } finally { + setIsSending(false) + resumeAllQueues() + } +} + + + const onReply = useCallback((message)=> { + if(onEditMessage){ + editorRef.current.chain().focus().clearContent().run() + } + setReplyMessage(message) + setOnEditMessage(null) + setIsFocusedParent(true); + + setTimeout(() => { + editorRef?.current?.chain().focus() + + }, 250); + }, [onEditMessage]) + + + const onEdit = useCallback((message)=> { + setOnEditMessage(message) + setReplyMessage(null) + setIsFocusedParent(true); + setTimeout(() => { + editorRef.current.chain().focus().setContent(message?.text).run(); + + }, 250); + + + }, []) + return (
{ )} - +
{ { setReplyMessage(null) + setOnEditMessage(null) + + }} + > + + + + )} + {onEditMessage && ( + + + + { + setReplyMessage(null) + setOnEditMessage(null) + + editorRef.current.chain().focus().clearContent().run() + }} > )} -
{ onClick={()=> { if(isSending) return setIsFocusedParent(false) + setReplyMessage(null) + setOnEditMessage(null) clearEditorContent() // Unfocus the editor }} diff --git a/src/components/Chat/ChatGroup.tsx b/src/components/Chat/ChatGroup.tsx index 8746257..90cb20b 100644 --- a/src/components/Chat/ChatGroup.tsx +++ b/src/components/Chat/ChatGroup.tsx @@ -32,6 +32,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, const [isSending, setIsSending] = useState(false) const [isLoading, setIsLoading] = useState(false) const [messageSize, setMessageSize] = useState(0) + const [onEditMessage, setOnEditMessage] = useState(null) const [isMoved, setIsMoved] = useState(false); const [openSnack, setOpenSnack] = React.useState(false); @@ -179,186 +180,204 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, } - const decryptMessages = (encryptedMessages: any[], isInitiated: boolean )=> { - try { - if(!secretKeyRef.current){ - checkForFirstSecretKeyNotification(encryptedMessages) - return - } - return new Promise((res, rej)=> { - window.sendMessage("decryptSingle", { - data: encryptedMessages, - secretKeyObject: secretKey, - }) - .then((response) => { - if (!response?.error) { - const filterUIMessages = encryptedMessages.filter((item) => !isExtMsg(item.data)); - const decodedUIMessages = decodeBase64ForUIChatMessages(filterUIMessages); - - const combineUIAndExtensionMsgs = [...decodedUIMessages, ...response]; - processWithNewMessages( - combineUIAndExtensionMsgs.map((item) => ({ - ...item, - ...(item?.decryptedData || {}), - })), - selectedGroup - ); - res(combineUIAndExtensionMsgs); - - if (isInitiated) { - const formatted = combineUIAndExtensionMsgs - .filter((rawItem) => !rawItem?.chatReference) - .map((item) => ({ - ...item, - id: item.signature, - text: item?.decryptedData?.message || "", - repliedTo: item?.repliedTo || item?.decryptedData?.repliedTo, - unread: item?.sender === myAddress ? false : !!item?.chatReference ? false : true, - isNotEncrypted: !!item?.messageText, - })); - setMessages((prev) => [...prev, ...formatted]); - - setChatReferences((prev) => { - const organizedChatReferences = { ...prev }; - - combineUIAndExtensionMsgs - .filter((rawItem) => rawItem && rawItem.chatReference && rawItem.decryptedData?.type === "reaction") - .forEach((item) => { - try { - const content = item.decryptedData?.content; - const sender = item.sender; - const newTimestamp = item.timestamp; - const contentState = item.decryptedData?.contentState; - - if (!content || typeof content !== "string" || !sender || typeof sender !== "string" || !newTimestamp) { - console.warn("Invalid content, sender, or timestamp in reaction data", item); - return; - } - - organizedChatReferences[item.chatReference] = { - ...(organizedChatReferences[item.chatReference] || {}), - reactions: organizedChatReferences[item.chatReference]?.reactions || {}, - }; - - organizedChatReferences[item.chatReference].reactions[content] = - organizedChatReferences[item.chatReference].reactions[content] || []; - - let latestTimestampForSender = null; - - organizedChatReferences[item.chatReference].reactions[content] = - organizedChatReferences[item.chatReference].reactions[content].filter((reaction) => { - if (reaction.sender === sender) { - latestTimestampForSender = Math.max(latestTimestampForSender || 0, reaction.timestamp); - } - return reaction.sender !== sender; - }); - - if (latestTimestampForSender && newTimestamp < latestTimestampForSender) { - return; - } - - if (contentState !== false) { - organizedChatReferences[item.chatReference].reactions[content].push(item); - } - - if (organizedChatReferences[item.chatReference].reactions[content].length === 0) { - delete organizedChatReferences[item.chatReference].reactions[content]; - } - } catch (error) { - console.error("Error processing reaction item:", error, item); - } - }); - - return organizedChatReferences; - }); - } else { - let firstUnreadFound = false; - const formatted = combineUIAndExtensionMsgs - .filter((rawItem) => !rawItem?.chatReference) - .map((item) => { - const divide = lastReadTimestamp.current && !firstUnreadFound && item.timestamp > lastReadTimestamp.current && myAddress !== item?.sender; - - if(divide){ - firstUnreadFound = true - } - return { - ...item, - id: item.signature, - text: item?.decryptedData?.message || "", - repliedTo: item?.repliedTo || item?.decryptedData?.repliedTo, - isNotEncrypted: !!item?.messageText, - unread: false, - divide - } - }); - setMessages(formatted); - - setChatReferences((prev) => { - const organizedChatReferences = { ...prev }; - - combineUIAndExtensionMsgs - .filter((rawItem) => rawItem && rawItem.chatReference && rawItem.decryptedData?.type === "reaction") - .forEach((item) => { - try { - const content = item.decryptedData?.content; - const sender = item.sender; - const newTimestamp = item.timestamp; - const contentState = item.decryptedData?.contentState; - - if (!content || typeof content !== "string" || !sender || typeof sender !== "string" || !newTimestamp) { - console.warn("Invalid content, sender, or timestamp in reaction data", item); - return; - } - - organizedChatReferences[item.chatReference] = { - ...(organizedChatReferences[item.chatReference] || {}), - reactions: organizedChatReferences[item.chatReference]?.reactions || {}, - }; - - organizedChatReferences[item.chatReference].reactions[content] = - organizedChatReferences[item.chatReference].reactions[content] || []; - - let latestTimestampForSender = null; - - organizedChatReferences[item.chatReference].reactions[content] = - organizedChatReferences[item.chatReference].reactions[content].filter((reaction) => { - if (reaction.sender === sender) { - latestTimestampForSender = Math.max(latestTimestampForSender || 0, reaction.timestamp); - } - return reaction.sender !== sender; - }); - - if (latestTimestampForSender && newTimestamp < latestTimestampForSender) { - return; - } - - if (contentState !== false) { - organizedChatReferences[item.chatReference].reactions[content].push(item); - } - - if (organizedChatReferences[item.chatReference].reactions[content].length === 0) { - delete organizedChatReferences[item.chatReference].reactions[content]; - } - } catch (error) { - console.error("Error processing reaction item:", error, item); - } - }); - - return organizedChatReferences; - }); - } - } - rej(response.error); - }) - .catch((error) => { - rej(error.message || "An error occurred"); - }); - - }) - } catch (error) { - - } + const decryptMessages = (encryptedMessages: any[], isInitiated: boolean )=> { + try { + if(!secretKeyRef.current){ + checkForFirstSecretKeyNotification(encryptedMessages) + return } + return new Promise((res, rej)=> { + window.sendMessage("decryptSingle", { + data: encryptedMessages, + secretKeyObject: secretKey, + }) + .then((response) => { + if (!response?.error) { + const filterUIMessages = encryptedMessages.filter((item) => !isExtMsg(item.data)); + const decodedUIMessages = decodeBase64ForUIChatMessages(filterUIMessages); + + const combineUIAndExtensionMsgs = [...decodedUIMessages, ...response]; + processWithNewMessages( + combineUIAndExtensionMsgs.map((item) => ({ + ...item, + ...(item?.decryptedData || {}), + })), + selectedGroup + ); + res(combineUIAndExtensionMsgs); + + if (isInitiated) { + + const formatted = combineUIAndExtensionMsgs + .filter((rawItem) => !rawItem?.chatReference) + .map((item) => { + + return { + ...item, + id: item.signature, + text: item?.decryptedData?.message || "", + repliedTo: item?.repliedTo || item?.decryptedData?.repliedTo, + unread: item?.sender === myAddress ? false : !!item?.chatReference ? false : true, + isNotEncrypted: !!item?.messageText, + } + }); + setMessages((prev) => [...prev, ...formatted]); + + setChatReferences((prev) => { + const organizedChatReferences = { ...prev }; + combineUIAndExtensionMsgs + .filter((rawItem) => rawItem && rawItem.chatReference && (rawItem.decryptedData?.type === "reaction" || rawItem.decryptedData?.type === "edit")) + .forEach((item) => { + try { + if(item.decryptedData?.type === "edit"){ + organizedChatReferences[item.chatReference] = { + ...(organizedChatReferences[item.chatReference] || {}), + edit: item.decryptedData, + }; + } else { + const content = item.decryptedData?.content; + const sender = item.sender; + const newTimestamp = item.timestamp; + const contentState = item.decryptedData?.contentState; + + if (!content || typeof content !== "string" || !sender || typeof sender !== "string" || !newTimestamp) { + console.warn("Invalid content, sender, or timestamp in reaction data", item); + return; + } + + organizedChatReferences[item.chatReference] = { + ...(organizedChatReferences[item.chatReference] || {}), + reactions: organizedChatReferences[item.chatReference]?.reactions || {}, + }; + + organizedChatReferences[item.chatReference].reactions[content] = + organizedChatReferences[item.chatReference].reactions[content] || []; + + let latestTimestampForSender = null; + + organizedChatReferences[item.chatReference].reactions[content] = + organizedChatReferences[item.chatReference].reactions[content].filter((reaction) => { + if (reaction.sender === sender) { + latestTimestampForSender = Math.max(latestTimestampForSender || 0, reaction.timestamp); + } + return reaction.sender !== sender; + }); + + if (latestTimestampForSender && newTimestamp < latestTimestampForSender) { + return; + } + + if (contentState !== false) { + organizedChatReferences[item.chatReference].reactions[content].push(item); + } + + if (organizedChatReferences[item.chatReference].reactions[content].length === 0) { + delete organizedChatReferences[item.chatReference].reactions[content]; + } + } + + } catch (error) { + console.error("Error processing reaction/edit item:", error, item); + } + }); + + return organizedChatReferences; + }); + } else { + let firstUnreadFound = false; + const formatted = combineUIAndExtensionMsgs + .filter((rawItem) => !rawItem?.chatReference) + .map((item) => { + const divide = lastReadTimestamp.current && !firstUnreadFound && item.timestamp > lastReadTimestamp.current && myAddress !== item?.sender; + + if(divide){ + firstUnreadFound = true + } + return { + ...item, + id: item.signature, + text: item?.decryptedData?.message || "", + repliedTo: item?.repliedTo || item?.decryptedData?.repliedTo, + isNotEncrypted: !!item?.messageText, + unread: false, + divide + } + }); + setMessages(formatted); + + setChatReferences((prev) => { + const organizedChatReferences = { ...prev }; + + combineUIAndExtensionMsgs + .filter((rawItem) => rawItem && rawItem.chatReference && (rawItem.decryptedData?.type === "reaction" || rawItem.decryptedData?.type === "edit")) + .forEach((item) => { + try { + if(item.decryptedData?.type === "edit"){ + organizedChatReferences[item.chatReference] = { + ...(organizedChatReferences[item.chatReference] || {}), + edit: item.decryptedData, + }; + } else { + const content = item.decryptedData?.content; + const sender = item.sender; + const newTimestamp = item.timestamp; + const contentState = item.decryptedData?.contentState; + + if (!content || typeof content !== "string" || !sender || typeof sender !== "string" || !newTimestamp) { + console.warn("Invalid content, sender, or timestamp in reaction data", item); + return; + } + + organizedChatReferences[item.chatReference] = { + ...(organizedChatReferences[item.chatReference] || {}), + reactions: organizedChatReferences[item.chatReference]?.reactions || {}, + }; + + organizedChatReferences[item.chatReference].reactions[content] = + organizedChatReferences[item.chatReference].reactions[content] || []; + + let latestTimestampForSender = null; + + organizedChatReferences[item.chatReference].reactions[content] = + organizedChatReferences[item.chatReference].reactions[content].filter((reaction) => { + if (reaction.sender === sender) { + latestTimestampForSender = Math.max(latestTimestampForSender || 0, reaction.timestamp); + } + return reaction.sender !== sender; + }); + + if (latestTimestampForSender && newTimestamp < latestTimestampForSender) { + return; + } + + if (contentState !== false) { + organizedChatReferences[item.chatReference].reactions[content].push(item); + } + + if (organizedChatReferences[item.chatReference].reactions[content].length === 0) { + delete organizedChatReferences[item.chatReference].reactions[content]; + } + } + } catch (error) { + console.error("Error processing reaction item:", error, item); + } + }); + + return organizedChatReferences; + }); + } + } + rej(response.error); + }) + .catch((error) => { + rej(error.message || "An error occurred"); + }); + + }) + } catch (error) { + + } +} @@ -551,74 +570,79 @@ const clearEditorContent = () => { }; - const sendMessage = async ()=> { - try { - if(isSending) return - if(+balance < 4) throw new Error('You need at least 4 QORT to send a message') - pauseAllQueues() - if (editorRef.current) { - const htmlContent = editorRef.current.getHTML(); - - if(!htmlContent?.trim() || htmlContent?.trim() === '

') return - setIsSending(true) - const message = htmlContent - const secretKeyObject = await getSecretKey(false, true) +const sendMessage = async ()=> { + try { + if(isSending) return + if(+balance < 4) throw new Error('You need at least 4 QORT to send a message') + pauseAllQueues() + if (editorRef.current) { + const htmlContent = editorRef.current.getHTML(); + + if(!htmlContent?.trim() || htmlContent?.trim() === '

') return + setIsSending(true) + const message = htmlContent + const secretKeyObject = await getSecretKey(false, true) - let repliedTo = replyMessage?.signature + let repliedTo = replyMessage?.signature - if (replyMessage?.chatReference) { - repliedTo = replyMessage?.chatReference - } - const otherData = { - specialId: uid.rnd(), - repliedTo - } - const objectMessage = { - message, - ...(otherData || {}) - } - const message64: any = await objectToBase64(objectMessage) - - const encryptSingle = await encryptChatMessage(message64, secretKeyObject) - // const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle}) - - const sendMessageFunc = async () => { - return await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle}) - }; - - // Add the function to the queue - const messageObj = { - message: { - text: message, - timestamp: Date.now(), - senderName: myName, - sender: myAddress, - ...(otherData || {}) - }, - - } - addToQueue(sendMessageFunc, messageObj, 'chat', - selectedGroup ); - setTimeout(() => { - executeEvent("sent-new-message-group", {}) - }, 150); - clearEditorContent() - setReplyMessage(null) - } - // send chat message - } catch (error) { - const errorMsg = error?.message || error - setInfoSnack({ - type: "error", - message: errorMsg, - }); - setOpenSnack(true); - console.error(error) - } finally { - setIsSending(false) - resumeAllQueues() - } + if (replyMessage?.chatReference) { + repliedTo = replyMessage?.chatReference } + let chatReference = onEditMessage?.signature + + const otherData = { + repliedTo, + ...(onEditMessage?.decryptedData || {}), + type: chatReference ? 'edit' : '', + specialId: uid.rnd(), + } + const objectMessage = { + ...(otherData || {}), + message + } + const message64: any = await objectToBase64(objectMessage) + + const encryptSingle = await encryptChatMessage(message64, secretKeyObject) + // const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle}) + + const sendMessageFunc = async () => { + return await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle, chatReference}) + }; + + // Add the function to the queue + const messageObj = { + message: { + text: message, + timestamp: Date.now(), + senderName: myName, + sender: myAddress, + ...(otherData || {}) + }, + chatReference + } + addToQueue(sendMessageFunc, messageObj, 'chat', + selectedGroup ); + setTimeout(() => { + executeEvent("sent-new-message-group", {}) + }, 150); + clearEditorContent() + setReplyMessage(null) + setOnEditMessage(null) + } + // send chat message + } catch (error) { + const errorMsg = error?.message || error + setInfoSnack({ + type: "error", + message: errorMsg, + }); + setOpenSnack(true); + console.error(error) + } finally { + setIsSending(false) + resumeAllQueues() + } +} useEffect(() => { if (hide) { @@ -629,8 +653,29 @@ const clearEditorContent = () => { }, [hide]); const onReply = useCallback((message)=> { + if(onEditMessage){ + editorRef.current.chain().focus().clearContent().run() + } setReplyMessage(message) - editorRef?.current?.chain().focus() + setOnEditMessage(null) + setIsFocusedParent(true); + + setTimeout(() => { + editorRef?.current?.chain().focus() + + }, 250); + }, [onEditMessage]) + + + const onEdit = useCallback((message)=> { + setOnEditMessage(message) + setReplyMessage(null) + setIsFocusedParent(true); + setTimeout(() => { + editorRef.current.chain().focus().setContent(message?.text).run(); + + }, 250); + }, []) const handleReaction = useCallback(async (reaction, chatMessage, reactionState = true)=> { @@ -710,7 +755,7 @@ const clearEditorContent = () => { left: hide && '-100000px', }}> - +
{ { setReplyMessage(null) + setOnEditMessage(null) + + }} + > + + + + )} + +{onEditMessage && ( + + + + { + setReplyMessage(null) + setOnEditMessage(null) + + editorRef.current.chain().focus().clearContent().run() + }} > @@ -790,6 +860,8 @@ const clearEditorContent = () => { onClick={()=> { if(isSending) return setIsFocusedParent(false) + setReplyMessage(null) + setOnEditMessage(null) clearEditorContent() // Unfocus the editor diff --git a/src/components/Chat/ChatList.tsx b/src/components/Chat/ChatList.tsx index 31f6f23..76f173f 100644 --- a/src/components/Chat/ChatList.tsx +++ b/src/components/Chat/ChatList.tsx @@ -3,8 +3,11 @@ import { useVirtualizer } from '@tanstack/react-virtual'; import { MessageItem } from './MessageItem'; import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events'; import { useInView } from 'react-intersection-observer' +import { Typography } from '@mui/material'; +import ErrorBoundary from '../../common/ErrorBoundary'; -export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onReply, handleReaction, chatReferences, tempChatReferences }) => { +export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onReply, handleReaction, chatReferences, tempChatReferences, onEdit +}) => { const parentRef = useRef(); const [messages, setMessages] = useState(initialMessages); const [showScrollButton, setShowScrollButton] = useState(false); @@ -210,38 +213,94 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR }} > {rowVirtualizer.getVirtualItems().map((virtualRow) => { - const index = virtualRow.index; - let message = messages[index]; - let replyIndex = messages.findIndex((msg) => msg?.signature === message?.repliedTo); - let reply; - let reactions = null; + const index = virtualRow.index; + let message = messages[index] || null; // Safeguard against undefined + let replyIndex = -1; + let reply = null; + let reactions = null; + let isUpdating = false; + + try { + // Safeguard for message existence + if (message) { + // Check for repliedTo logic + replyIndex = messages.findIndex( + (msg) => msg?.signature === message?.repliedTo + ); + + if (message?.repliedTo && replyIndex !== -1) { + reply = { ...(messages[replyIndex] || {}) }; + if (chatReferences?.[reply?.signature]?.edit) { + reply.decryptedData = chatReferences[reply?.signature]?.edit; + reply.text = chatReferences[reply?.signature]?.edit?.message; + } + } + + // GroupDirectId logic + if (message?.message && message?.groupDirectId) { + replyIndex = messages.findIndex( + (msg) => msg?.signature === message?.message?.repliedTo + ); + if (message?.message?.repliedTo && replyIndex !== -1) { + reply = messages[replyIndex] || null; + } + message = { + ...(message?.message || {}), + isTemp: true, + unread: false, + status: message?.status, + }; + } + + // Check for reactions and edits + if (chatReferences?.[message.signature]) { + reactions = chatReferences[message.signature]?.reactions || null; + + if (chatReferences[message.signature]?.edit?.message && message?.text) { + message.text = chatReferences[message.signature]?.edit?.message; + message.isEdit = true + } - if (message?.repliedTo && replyIndex !== -1) { - reply = messages[replyIndex]; - } + + } + + // Check if message is updating + if ( + tempChatReferences?.some( + (item) => item?.chatReference === message?.signature + ) + ) { + isUpdating = true; + } + } + } catch (error) { + console.error("Error processing message:", error, { index, message }); + // Gracefully handle the error by providing fallback data + message = null; + reply = null; + reactions = null; + } - if (message?.message && message?.groupDirectId) { - replyIndex = messages.findIndex((msg) => msg?.signature === message?.message?.repliedTo); - if (message?.message?.repliedTo && replyIndex !== -1) { - reply = messages[replyIndex]; - } - message = { - ...(message?.message || {}), - isTemp: true, - unread: false, - status: message?.status - }; - } - - if (chatReferences && chatReferences[message?.signature]) { - if (chatReferences[message.signature]?.reactions) { - reactions = chatReferences[message.signature]?.reactions; - } - } - - let isUpdating = false; - if (tempChatReferences && tempChatReferences?.find((item) => item?.chatReference === message?.signature)) { - isUpdating = true; + if (!message) { + return ( +
+ Error loading message. +
+ ); } return ( @@ -263,6 +322,13 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR gap: '5px' }} > + + Error loading content: Invalid Data + + } + > rowVirtualizer.scrollToIndex(idx)} @@ -278,6 +345,7 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR reactions={reactions} isUpdating={isUpdating} /> +
); })} diff --git a/src/components/Chat/MessageItem.tsx b/src/components/Chat/MessageItem.tsx index 519c83c..d669c7f 100644 --- a/src/components/Chat/MessageItem.tsx +++ b/src/components/Chat/MessageItem.tsx @@ -16,6 +16,8 @@ import ReplyIcon from "@mui/icons-material/Reply"; import { Spacer } from "../../common/Spacer"; import { ReactionPicker } from "../ReactionPicker"; import KeyOffIcon from '@mui/icons-material/KeyOff'; +import EditIcon from '@mui/icons-material/Edit'; + export const MessageItem = ({ message, onSeen, @@ -30,7 +32,8 @@ export const MessageItem = ({ handleReaction, reactions, isUpdating, - lastSignature + lastSignature, + onEdit }) => { const { ref, inView } = useInView({ threshold: 0.7, // Fully visible @@ -128,6 +131,15 @@ export const MessageItem = ({ gap: '10px', alignItems: 'center' }}> + {message?.sender === myAddress && !message?.isNotEncrypted && ( + { + onEdit(message); + }} + > + + + )} {!isShowingAsReply && ( { @@ -285,6 +297,19 @@ export const MessageItem = ({ {message?.status === 'failed-permanent' ? 'Failed to send' : 'Sending...'} ) : ( + <> + {message?.isEdit && ( + + Edited + + )} {formatTimestamp(message.timestamp)} + )}
@@ -316,7 +342,7 @@ export const MessageItem = ({ }; -export const ReplyPreview = ({message})=> { +export const ReplyPreview = ({message, isEdit})=> { return ( { - Replied to {message?.senderName || message?.senderAddress} + {isEdit ? ( + Editing Message + ) : ( + Replied to {message?.senderName || message?.senderAddress} + )} + {message?.messageText && (