mirror of
https://github.com/Qortal/qortal-mobile.git
synced 2025-03-15 04:12:32 +00:00
added edit messages
This commit is contained in:
parent
0b05b59b90
commit
d9d1aab54d
@ -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 (
|
||||
|
36
src/common/ErrorBoundary.tsx
Normal file
36
src/common/ErrorBoundary.tsx
Normal 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
|
@ -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
|
||||
}}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
@ -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, [
|
||||
|
Loading…
x
Reference in New Issue
Block a user