added edit messages

This commit is contained in:
PhilReact 2024-11-25 02:50:51 +02:00
parent 0b05b59b90
commit d9d1aab54d
6 changed files with 730 additions and 425 deletions

View File

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

View File

@ -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

View File

@ -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() === '<p></p>') 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() === '<p></p>') 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 (
<div style={{
height: isMobile ? '100%' : '100vh',
@ -514,7 +583,7 @@ useEffect(() => {
</>
)}
<ChatList onReply={onReply} chatId={selectedDirect?.address} initialMessages={messages} myAddress={myAddress} tempMessages={tempMessages}/>
<ChatList chatReferences={chatReferences} onEdit={onEdit} onReply={onReply} chatId={selectedDirect?.address} initialMessages={messages} myAddress={myAddress} tempMessages={tempMessages} tempChatReferences={tempChatReferences}/>
<div style={{
@ -554,13 +623,36 @@ useEffect(() => {
<ButtonBase
onClick={() => {
setReplyMessage(null)
setOnEditMessage(null)
}}
>
<ExitIcon />
</ButtonBase>
</Box>
)}
{onEditMessage && (
<Box sx={{
display: 'flex',
gap: '5px',
alignItems: 'flex-start',
width: '100%'
}}>
<ReplyPreview isEdit message={onEditMessage} />
<ButtonBase
onClick={() => {
setReplyMessage(null)
setOnEditMessage(null)
editorRef.current.chain().focus().clearContent().run()
}}
>
<ExitIcon />
</ButtonBase>
</Box>
)}
<Tiptap isFocusedParent={isFocusedParent} setEditorRef={setEditorRef} onEnter={sendMessage} isChat disableEnter={isMobile ? true : false} setIsFocusedParent={setIsFocusedParent}/>
</div>
<Box sx={{
@ -576,6 +668,8 @@ useEffect(() => {
onClick={()=> {
if(isSending) return
setIsFocusedParent(false)
setReplyMessage(null)
setOnEditMessage(null)
clearEditorContent()
// Unfocus the editor
}}

View File

@ -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() === '<p></p>') 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() === '<p></p>') 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',
}}>
<ChatList enableMentions onReply={onReply} chatId={selectedGroup} initialMessages={messages} myAddress={myAddress} tempMessages={tempMessages} handleReaction={handleReaction} chatReferences={chatReferences} tempChatReferences={tempChatReferences} members={members} myName={myName} selectedGroup={selectedGroup}/>
<ChatList enableMentions onReply={onReply} onEdit={onEdit} chatId={selectedGroup} initialMessages={messages} myAddress={myAddress} tempMessages={tempMessages} handleReaction={handleReaction} chatReferences={chatReferences} tempChatReferences={tempChatReferences} members={members} myName={myName} selectedGroup={selectedGroup}/>
<div style={{
@ -750,6 +795,31 @@ const clearEditorContent = () => {
<ButtonBase
onClick={() => {
setReplyMessage(null)
setOnEditMessage(null)
}}
>
<ExitIcon />
</ButtonBase>
</Box>
)}
{onEditMessage && (
<Box sx={{
display: 'flex',
gap: '5px',
alignItems: 'flex-start',
width: '100%'
}}>
<ReplyPreview isEdit message={onEditMessage} />
<ButtonBase
onClick={() => {
setReplyMessage(null)
setOnEditMessage(null)
editorRef.current.chain().focus().clearContent().run()
}}
>
<ExitIcon />
@ -790,6 +860,8 @@ const clearEditorContent = () => {
onClick={()=> {
if(isSending) return
setIsFocusedParent(false)
setReplyMessage(null)
setOnEditMessage(null)
clearEditorContent()
// Unfocus the editor

View File

@ -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 (
<div
key={virtualRow.index}
style={{
position: "absolute",
top: 0,
left: "50%",
transform: `translateY(${virtualRow.start}px) translateX(-50%)`,
width: "100%",
padding: "10px 0",
display: "flex",
alignItems: "center",
flexDirection: "column",
gap: "5px",
}}
>
<Typography>Error loading message.</Typography>
</div>
);
}
return (
@ -263,6 +322,13 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
gap: '5px'
}}
>
<ErrorBoundary
fallback={
<Typography>
Error loading content: Invalid Data
</Typography>
}
>
<MessageItem
isLast={index === messages.length - 1}
lastSignature={lastSignature}
@ -271,6 +337,7 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
isTemp={!!message?.isTemp}
myAddress={myAddress}
onReply={onReply}
onEdit={onEdit}
reply={reply}
replyIndex={replyIndex}
scrollToItem={(idx) => rowVirtualizer.scrollToIndex(idx)}
@ -278,6 +345,7 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
reactions={reactions}
isUpdating={isUpdating}
/>
</ErrorBoundary>
</div>
);
})}

View File

@ -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 && (
<ButtonBase
onClick={() => {
onEdit(message);
}}
>
<EditIcon />
</ButtonBase>
)}
{!isShowingAsReply && (
<ButtonBase
onClick={() => {
@ -285,6 +297,19 @@ export const MessageItem = ({
{message?.status === 'failed-permanent' ? 'Failed to send' : 'Sending...'}
</Typography>
) : (
<>
{message?.isEdit && (
<Typography
sx={{
fontSize: "14px",
color: "gray",
fontFamily: "Inter",
fontStyle: 'italic'
}}
>
Edited
</Typography>
)}
<Typography
sx={{
fontSize: "14px",
@ -294,6 +319,7 @@ export const MessageItem = ({
>
{formatTimestamp(message.timestamp)}
</Typography>
</>
)}
</Box>
</Box>
@ -316,7 +342,7 @@ export const MessageItem = ({
};
export const ReplyPreview = ({message})=> {
export const ReplyPreview = ({message, isEdit})=> {
return (
<Box
@ -340,10 +366,18 @@ export const ReplyPreview = ({message})=> {
<Box sx={{
padding: '5px'
}}>
<Typography sx={{
fontSize: '12px',
fontWeight: 600
}}>Replied to {message?.senderName || message?.senderAddress}</Typography>
{isEdit ? (
<Typography sx={{
fontSize: '12px',
fontWeight: 600
}}>Editing Message</Typography>
) : (
<Typography sx={{
fontSize: '12px',
fontWeight: 600
}}>Replied to {message?.senderName || message?.senderAddress}</Typography>
)}
{message?.messageText && (
<MessageDisplay
htmlContent={generateHTML(message?.messageText, [