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 && (