mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-04-23 19:37:52 +00:00
added edit messages
This commit is contained in:
parent
8cb88a6380
commit
25fe950438
@ -53,6 +53,7 @@ export const MessageQueueProvider = ({ children }) => {
|
|||||||
|
|
||||||
// Function to process the message queue
|
// Function to process the message queue
|
||||||
const processQueue = useCallback((newMessages = [], groupDirectId) => {
|
const processQueue = useCallback((newMessages = [], groupDirectId) => {
|
||||||
|
|
||||||
processingPromiseRef.current = processingPromiseRef.current
|
processingPromiseRef.current = processingPromiseRef.current
|
||||||
.then(() => processQueueInternal(newMessages, groupDirectId))
|
.then(() => processQueueInternal(newMessages, groupDirectId))
|
||||||
.catch((err) => console.error('Error in processQueue:', err));
|
.catch((err) => console.error('Error in processQueue:', err));
|
||||||
@ -61,33 +62,7 @@ export const MessageQueueProvider = ({ children }) => {
|
|||||||
// Internal function to handle queue processing
|
// Internal function to handle queue processing
|
||||||
const processQueueInternal = async (newMessages, groupDirectId) => {
|
const processQueueInternal = async (newMessages, groupDirectId) => {
|
||||||
// Remove any messages from the queue that match the specialId from newMessages
|
// 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 the queue is empty, no need to process
|
||||||
if (messageQueueRef.current.length === 0) return;
|
if (messageQueueRef.current.length === 0) return;
|
||||||
|
|
||||||
@ -112,11 +87,11 @@ export const MessageQueueProvider = ({ children }) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Execute the function stored in the messageQueueRef
|
// Execute the function stored in the messageQueueRef
|
||||||
|
|
||||||
await currentMessage.func();
|
await currentMessage.func();
|
||||||
|
|
||||||
// Remove the message from the queue after successful sending
|
// Remove the message from the queue after successful sending
|
||||||
messageQueueRef.current.shift();
|
messageQueueRef.current.shift();
|
||||||
|
|
||||||
// Remove the message from queueChats
|
// Remove the message from queueChats
|
||||||
setQueueChats((prev) => {
|
setQueueChats((prev) => {
|
||||||
const updatedChats = { ...prev };
|
const updatedChats = { ...prev };
|
||||||
@ -167,7 +142,33 @@ export const MessageQueueProvider = ({ children }) => {
|
|||||||
|
|
||||||
// Method to process with new messages and groupDirectId
|
// Method to process with new messages and groupDirectId
|
||||||
const processWithNewMessages = (newMessages, 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 (
|
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
|
@ -28,6 +28,7 @@ const uid = new ShortUniqueId({ length: 5 });
|
|||||||
export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDirect, setNewChat, getTimestampEnterChat, myName, balance, close, setMobileViewModeKeepOpen}) => {
|
export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDirect, setNewChat, getTimestampEnterChat, myName, balance, close, setMobileViewModeKeepOpen}) => {
|
||||||
const { queueChats, addToQueue, processWithNewMessages} = useMessageQueue();
|
const { queueChats, addToQueue, processWithNewMessages} = useMessageQueue();
|
||||||
const [isFocusedParent, setIsFocusedParent] = useState(false);
|
const [isFocusedParent, setIsFocusedParent] = useState(false);
|
||||||
|
const [onEditMessage, setOnEditMessage] = useState(null)
|
||||||
|
|
||||||
const [messages, setMessages] = useState([])
|
const [messages, setMessages] = useState([])
|
||||||
const [isSending, setIsSending] = useState(false)
|
const [isSending, setIsSending] = useState(false)
|
||||||
@ -38,6 +39,8 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
|||||||
const [infoSnack, setInfoSnack] = React.useState(null);
|
const [infoSnack, setInfoSnack] = React.useState(null);
|
||||||
const [publicKeyOfRecipient, setPublicKeyOfRecipient] = React.useState("")
|
const [publicKeyOfRecipient, setPublicKeyOfRecipient] = React.useState("")
|
||||||
const hasInitializedWebsocket = useRef(false)
|
const hasInitializedWebsocket = useRef(false)
|
||||||
|
const [chatReferences, setChatReferences] = useState({})
|
||||||
|
|
||||||
const editorRef = useRef(null);
|
const editorRef = useRef(null);
|
||||||
const socketRef = useRef(null);
|
const socketRef = useRef(null);
|
||||||
const timeoutIdRef = useRef(null);
|
const timeoutIdRef = useRef(null);
|
||||||
@ -66,10 +69,19 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
|||||||
const tempMessages = useMemo(()=> {
|
const tempMessages = useMemo(()=> {
|
||||||
if(!selectedDirect?.address) return []
|
if(!selectedDirect?.address) return []
|
||||||
if(queueChats[selectedDirect?.address]){
|
if(queueChats[selectedDirect?.address]){
|
||||||
return queueChats[selectedDirect?.address]
|
return queueChats[selectedDirect?.address]?.filter((item)=> !item?.chatReference)
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}, [selectedDirect?.address, queueChats])
|
}, [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])
|
||||||
|
|
||||||
useEffect(()=> {
|
useEffect(()=> {
|
||||||
if(selectedDirect?.address){
|
if(selectedDirect?.address){
|
||||||
publicKeyOfRecipientRef.current = selectedDirect?.address
|
publicKeyOfRecipientRef.current = selectedDirect?.address
|
||||||
@ -112,22 +124,54 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
|||||||
res(response);
|
res(response);
|
||||||
|
|
||||||
if (isInitiated) {
|
if (isInitiated) {
|
||||||
const formatted = response.map((item) => ({
|
const formatted = response.filter((rawItem) => !rawItem?.chatReference).map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
id: item.signature,
|
id: item.signature,
|
||||||
text: item.message,
|
text: item.message,
|
||||||
unread: item?.sender === myAddress ? false : true,
|
unread: item?.sender === myAddress ? false : true,
|
||||||
}));
|
}));
|
||||||
setMessages((prev) => [...prev, ...formatted]);
|
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){
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return organizedChatReferences
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
const formatted = response.map((item) => ({
|
hasInitialized.current = true;
|
||||||
|
const formatted = response.filter((rawItem) => !rawItem?.chatReference)
|
||||||
|
.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
id: item.signature,
|
id: item.signature,
|
||||||
text: item.message,
|
text: item.message,
|
||||||
unread: false,
|
unread: false,
|
||||||
}));
|
}));
|
||||||
setMessages(formatted);
|
setMessages(formatted);
|
||||||
hasInitialized.current = true;
|
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
@ -333,7 +377,7 @@ useEffect(() => {
|
|||||||
|
|
||||||
const sendMessage = async ()=> {
|
const sendMessage = async ()=> {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
|
||||||
if(+balance < 4) throw new Error('You need at least 4 QORT to send a message')
|
if(+balance < 4) throw new Error('You need at least 4 QORT to send a message')
|
||||||
if(isSending) return
|
if(isSending) return
|
||||||
@ -355,12 +399,16 @@ useEffect(() => {
|
|||||||
if (replyMessage?.chatReference) {
|
if (replyMessage?.chatReference) {
|
||||||
repliedTo = replyMessage?.chatReference
|
repliedTo = replyMessage?.chatReference
|
||||||
}
|
}
|
||||||
|
let chatReference = onEditMessage?.signature
|
||||||
|
|
||||||
const otherData = {
|
const otherData = {
|
||||||
|
...(onEditMessage?.decryptedData || {}),
|
||||||
specialId: uid.rnd(),
|
specialId: uid.rnd(),
|
||||||
repliedTo
|
repliedTo: onEditMessage ? onEditMessage?.repliedTo : repliedTo,
|
||||||
|
type: chatReference ? 'edit' : ''
|
||||||
}
|
}
|
||||||
const sendMessageFunc = async () => {
|
const sendMessageFunc = async () => {
|
||||||
return await sendChatDirect({ chatReference: undefined, messageText: htmlContent, otherData}, selectedDirect?.address, publicKeyOfRecipient, false)
|
return await sendChatDirect({ chatReference, messageText: htmlContent, otherData}, selectedDirect?.address, publicKeyOfRecipient, false)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -368,13 +416,13 @@ useEffect(() => {
|
|||||||
// Add the function to the queue
|
// Add the function to the queue
|
||||||
const messageObj = {
|
const messageObj = {
|
||||||
message: {
|
message: {
|
||||||
text: htmlContent,
|
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
senderName: myName,
|
senderName: myName,
|
||||||
sender: myAddress,
|
sender: myAddress,
|
||||||
...(otherData || {})
|
...(otherData || {}),
|
||||||
|
text: htmlContent,
|
||||||
},
|
},
|
||||||
|
chatReference
|
||||||
}
|
}
|
||||||
addToQueue(sendMessageFunc, messageObj, 'chat-direct',
|
addToQueue(sendMessageFunc, messageObj, 'chat-direct',
|
||||||
selectedDirect?.address );
|
selectedDirect?.address );
|
||||||
@ -383,6 +431,8 @@ useEffect(() => {
|
|||||||
}, 150);
|
}, 150);
|
||||||
clearEditorContent()
|
clearEditorContent()
|
||||||
setReplyMessage(null)
|
setReplyMessage(null)
|
||||||
|
setOnEditMessage(null)
|
||||||
|
|
||||||
}
|
}
|
||||||
// send chat message
|
// send chat message
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -399,12 +449,22 @@ useEffect(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onReply = useCallback((message)=> {
|
const onReply = useCallback((message)=> {
|
||||||
setReplyMessage(message)
|
if(onEditMessage){
|
||||||
editorRef?.current?.chain().focus()
|
editorRef.current.chain().focus().clearContent().run()
|
||||||
|
}
|
||||||
}, [])
|
setReplyMessage(message)
|
||||||
|
setOnEditMessage(null)
|
||||||
|
editorRef?.current?.chain().focus()
|
||||||
|
}, [onEditMessage])
|
||||||
|
|
||||||
|
|
||||||
|
const onEdit = useCallback((message)=> {
|
||||||
|
setOnEditMessage(message)
|
||||||
|
setReplyMessage(null)
|
||||||
|
editorRef.current.chain().focus().setContent(message?.text).run();
|
||||||
|
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
@ -511,7 +571,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={{
|
<div style={{
|
||||||
@ -551,6 +611,30 @@ useEffect(() => {
|
|||||||
<ButtonBase
|
<ButtonBase
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setReplyMessage(null)
|
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 />
|
<ExitIcon />
|
||||||
|
@ -35,6 +35,9 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
|||||||
const hasInitialized = useRef(false)
|
const hasInitialized = useRef(false)
|
||||||
const [isFocusedParent, setIsFocusedParent] = useState(false);
|
const [isFocusedParent, setIsFocusedParent] = useState(false);
|
||||||
const [replyMessage, setReplyMessage] = useState(null)
|
const [replyMessage, setReplyMessage] = useState(null)
|
||||||
|
const [onEditMessage, setOnEditMessage] = useState(null)
|
||||||
|
|
||||||
|
|
||||||
const [messageSize, setMessageSize] = useState(0)
|
const [messageSize, setMessageSize] = useState(0)
|
||||||
const hasInitializedWebsocket = useRef(false)
|
const hasInitializedWebsocket = useRef(false)
|
||||||
const socketRef = useRef(null); // WebSocket reference
|
const socketRef = useRef(null); // WebSocket reference
|
||||||
@ -218,52 +221,59 @@ const [messageSize, setMessageSize] = useState(0)
|
|||||||
|
|
||||||
setChatReferences((prev) => {
|
setChatReferences((prev) => {
|
||||||
const organizedChatReferences = { ...prev };
|
const organizedChatReferences = { ...prev };
|
||||||
|
|
||||||
combineUIAndExtensionMsgs
|
combineUIAndExtensionMsgs
|
||||||
.filter((rawItem) => rawItem && rawItem.chatReference && rawItem.decryptedData?.type === "reaction")
|
.filter((rawItem) => rawItem && rawItem.chatReference && (rawItem.decryptedData?.type === "reaction" || rawItem.decryptedData?.type === "edit"))
|
||||||
.forEach((item) => {
|
.forEach((item) => {
|
||||||
try {
|
try {
|
||||||
const content = item.decryptedData?.content;
|
if(item.decryptedData?.type === "edit"){
|
||||||
const sender = item.sender;
|
organizedChatReferences[item.chatReference] = {
|
||||||
const newTimestamp = item.timestamp;
|
...(organizedChatReferences[item.chatReference] || {}),
|
||||||
const contentState = item.decryptedData?.contentState;
|
edit: item.decryptedData,
|
||||||
|
};
|
||||||
if (!content || typeof content !== "string" || !sender || typeof sender !== "string" || !newTimestamp) {
|
} else {
|
||||||
console.warn("Invalid content, sender, or timestamp in reaction data", item);
|
const content = item.decryptedData?.content;
|
||||||
return;
|
const sender = item.sender;
|
||||||
}
|
const newTimestamp = item.timestamp;
|
||||||
|
const contentState = item.decryptedData?.contentState;
|
||||||
organizedChatReferences[item.chatReference] = {
|
|
||||||
...(organizedChatReferences[item.chatReference] || {}),
|
if (!content || typeof content !== "string" || !sender || typeof sender !== "string" || !newTimestamp) {
|
||||||
reactions: organizedChatReferences[item.chatReference]?.reactions || {},
|
console.warn("Invalid content, sender, or timestamp in reaction data", item);
|
||||||
};
|
return;
|
||||||
|
}
|
||||||
organizedChatReferences[item.chatReference].reactions[content] =
|
|
||||||
organizedChatReferences[item.chatReference].reactions[content] || [];
|
organizedChatReferences[item.chatReference] = {
|
||||||
|
...(organizedChatReferences[item.chatReference] || {}),
|
||||||
let latestTimestampForSender = null;
|
reactions: organizedChatReferences[item.chatReference]?.reactions || {},
|
||||||
|
};
|
||||||
organizedChatReferences[item.chatReference].reactions[content] =
|
|
||||||
organizedChatReferences[item.chatReference].reactions[content].filter((reaction) => {
|
organizedChatReferences[item.chatReference].reactions[content] =
|
||||||
if (reaction.sender === sender) {
|
organizedChatReferences[item.chatReference].reactions[content] || [];
|
||||||
latestTimestampForSender = Math.max(latestTimestampForSender || 0, reaction.timestamp);
|
|
||||||
}
|
let latestTimestampForSender = null;
|
||||||
return reaction.sender !== sender;
|
|
||||||
});
|
organizedChatReferences[item.chatReference].reactions[content] =
|
||||||
|
organizedChatReferences[item.chatReference].reactions[content].filter((reaction) => {
|
||||||
if (latestTimestampForSender && newTimestamp < latestTimestampForSender) {
|
if (reaction.sender === sender) {
|
||||||
return;
|
latestTimestampForSender = Math.max(latestTimestampForSender || 0, reaction.timestamp);
|
||||||
}
|
}
|
||||||
|
return reaction.sender !== sender;
|
||||||
if (contentState !== false) {
|
});
|
||||||
organizedChatReferences[item.chatReference].reactions[content].push(item);
|
|
||||||
}
|
if (latestTimestampForSender && newTimestamp < latestTimestampForSender) {
|
||||||
|
return;
|
||||||
if (organizedChatReferences[item.chatReference].reactions[content].length === 0) {
|
}
|
||||||
delete organizedChatReferences[item.chatReference].reactions[content];
|
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error("Error processing reaction item:", error, item);
|
console.error("Error processing reaction/edit item:", error, item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -295,9 +305,15 @@ const [messageSize, setMessageSize] = useState(0)
|
|||||||
const organizedChatReferences = { ...prev };
|
const organizedChatReferences = { ...prev };
|
||||||
|
|
||||||
combineUIAndExtensionMsgs
|
combineUIAndExtensionMsgs
|
||||||
.filter((rawItem) => rawItem && rawItem.chatReference && rawItem.decryptedData?.type === "reaction")
|
.filter((rawItem) => rawItem && rawItem.chatReference && (rawItem.decryptedData?.type === "reaction" || rawItem.decryptedData?.type === "edit"))
|
||||||
.forEach((item) => {
|
.forEach((item) => {
|
||||||
try {
|
try {
|
||||||
|
if(item.decryptedData?.type === "edit"){
|
||||||
|
organizedChatReferences[item.chatReference] = {
|
||||||
|
...(organizedChatReferences[item.chatReference] || {}),
|
||||||
|
edit: item.decryptedData,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
const content = item.decryptedData?.content;
|
const content = item.decryptedData?.content;
|
||||||
const sender = item.sender;
|
const sender = item.sender;
|
||||||
const newTimestamp = item.timestamp;
|
const newTimestamp = item.timestamp;
|
||||||
@ -337,6 +353,7 @@ const [messageSize, setMessageSize] = useState(0)
|
|||||||
if (organizedChatReferences[item.chatReference].reactions[content].length === 0) {
|
if (organizedChatReferences[item.chatReference].reactions[content].length === 0) {
|
||||||
delete organizedChatReferences[item.chatReference].reactions[content];
|
delete organizedChatReferences[item.chatReference].reactions[content];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error processing reaction item:", error, item);
|
console.error("Error processing reaction item:", error, item);
|
||||||
}
|
}
|
||||||
@ -549,13 +566,17 @@ const clearEditorContent = () => {
|
|||||||
if (replyMessage?.chatReference) {
|
if (replyMessage?.chatReference) {
|
||||||
repliedTo = replyMessage?.chatReference
|
repliedTo = replyMessage?.chatReference
|
||||||
}
|
}
|
||||||
|
let chatReference = onEditMessage?.signature
|
||||||
|
|
||||||
const otherData = {
|
const otherData = {
|
||||||
|
repliedTo,
|
||||||
|
...(onEditMessage?.decryptedData || {}),
|
||||||
|
type: chatReference ? 'edit' : '',
|
||||||
specialId: uid.rnd(),
|
specialId: uid.rnd(),
|
||||||
repliedTo
|
|
||||||
}
|
}
|
||||||
const objectMessage = {
|
const objectMessage = {
|
||||||
message,
|
...(otherData || {}),
|
||||||
...(otherData || {})
|
message
|
||||||
}
|
}
|
||||||
const message64: any = await objectToBase64(objectMessage)
|
const message64: any = await objectToBase64(objectMessage)
|
||||||
|
|
||||||
@ -563,7 +584,7 @@ const clearEditorContent = () => {
|
|||||||
// const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle})
|
// const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle})
|
||||||
|
|
||||||
const sendMessageFunc = async () => {
|
const sendMessageFunc = async () => {
|
||||||
return await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle})
|
return await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle, chatReference})
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add the function to the queue
|
// Add the function to the queue
|
||||||
@ -575,7 +596,7 @@ const clearEditorContent = () => {
|
|||||||
sender: myAddress,
|
sender: myAddress,
|
||||||
...(otherData || {})
|
...(otherData || {})
|
||||||
},
|
},
|
||||||
|
chatReference
|
||||||
}
|
}
|
||||||
addToQueue(sendMessageFunc, messageObj, 'chat',
|
addToQueue(sendMessageFunc, messageObj, 'chat',
|
||||||
selectedGroup );
|
selectedGroup );
|
||||||
@ -584,6 +605,7 @@ const clearEditorContent = () => {
|
|||||||
}, 150);
|
}, 150);
|
||||||
clearEditorContent()
|
clearEditorContent()
|
||||||
setReplyMessage(null)
|
setReplyMessage(null)
|
||||||
|
setOnEditMessage(null)
|
||||||
}
|
}
|
||||||
// send chat message
|
// send chat message
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -627,10 +649,21 @@ const clearEditorContent = () => {
|
|||||||
}, [hide]);
|
}, [hide]);
|
||||||
|
|
||||||
const onReply = useCallback((message)=> {
|
const onReply = useCallback((message)=> {
|
||||||
|
if(onEditMessage){
|
||||||
|
editorRef.current.chain().focus().clearContent().run()
|
||||||
|
}
|
||||||
setReplyMessage(message)
|
setReplyMessage(message)
|
||||||
|
setOnEditMessage(null)
|
||||||
editorRef?.current?.chain().focus()
|
editorRef?.current?.chain().focus()
|
||||||
}, [])
|
}, [onEditMessage])
|
||||||
|
|
||||||
|
|
||||||
|
const onEdit = useCallback((message)=> {
|
||||||
|
setOnEditMessage(message)
|
||||||
|
setReplyMessage(null)
|
||||||
|
editorRef.current.chain().focus().setContent(message?.text).run();
|
||||||
|
|
||||||
|
}, [])
|
||||||
const handleReaction = useCallback(async (reaction, chatMessage, reactionState = true)=> {
|
const handleReaction = useCallback(async (reaction, chatMessage, reactionState = true)=> {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
@ -708,7 +741,7 @@ const clearEditorContent = () => {
|
|||||||
left: hide && '-100000px',
|
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={{
|
<div style={{
|
||||||
@ -748,6 +781,31 @@ const clearEditorContent = () => {
|
|||||||
<ButtonBase
|
<ButtonBase
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setReplyMessage(null)
|
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 />
|
<ExitIcon />
|
||||||
|
@ -9,8 +9,9 @@ import { useVirtualizer } from "@tanstack/react-virtual";
|
|||||||
import { MessageItem } from "./MessageItem";
|
import { MessageItem } from "./MessageItem";
|
||||||
import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events";
|
import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events";
|
||||||
import { useInView } from "react-intersection-observer";
|
import { useInView } from "react-intersection-observer";
|
||||||
import { Box } from "@mui/material";
|
import { Box, Typography } from "@mui/material";
|
||||||
import { ChatOptions } from "./ChatOptions";
|
import { ChatOptions } from "./ChatOptions";
|
||||||
|
import ErrorBoundary from "../../common/ErrorBoundary";
|
||||||
|
|
||||||
export const ChatList = ({
|
export const ChatList = ({
|
||||||
initialMessages,
|
initialMessages,
|
||||||
@ -18,6 +19,7 @@ export const ChatList = ({
|
|||||||
tempMessages,
|
tempMessages,
|
||||||
chatId,
|
chatId,
|
||||||
onReply,
|
onReply,
|
||||||
|
onEdit,
|
||||||
handleReaction,
|
handleReaction,
|
||||||
chatReferences,
|
chatReferences,
|
||||||
tempChatReferences,
|
tempChatReferences,
|
||||||
@ -155,7 +157,6 @@ export const ChatList = ({
|
|||||||
|
|
||||||
// Check if the user is within 200px from the bottom
|
// Check if the user is within 200px from the bottom
|
||||||
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
||||||
console.log("distanceFromBottom", distanceFromBottom);
|
|
||||||
if (distanceFromBottom <= 700) {
|
if (distanceFromBottom <= 700) {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
@ -177,7 +178,6 @@ export const ChatList = ({
|
|||||||
const goToMessage = useCallback((idx) => {
|
const goToMessage = useCallback((idx) => {
|
||||||
rowVirtualizer.scrollToIndex(idx);
|
rowVirtualizer.scrollToIndex(idx);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@ -220,49 +220,98 @@ export const ChatList = ({
|
|||||||
width: "100%",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
||||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||||
const index = virtualRow.index;
|
const index = virtualRow.index;
|
||||||
let message = messages[index];
|
let message = messages[index] || null; // Safeguard against undefined
|
||||||
let replyIndex = messages.findIndex(
|
let replyIndex = -1;
|
||||||
(msg) => msg?.signature === message?.repliedTo
|
let reply = null;
|
||||||
);
|
|
||||||
let reply;
|
|
||||||
let reactions = null;
|
let reactions = null;
|
||||||
|
|
||||||
if (message?.repliedTo && replyIndex !== -1) {
|
|
||||||
reply = messages[replyIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
let isUpdating = false;
|
||||||
if (
|
|
||||||
tempChatReferences &&
|
try {
|
||||||
tempChatReferences?.find(
|
// Safeguard for message existence
|
||||||
(item) => item?.chatReference === message?.signature
|
if (message) {
|
||||||
)
|
// Check for repliedTo logic
|
||||||
) {
|
replyIndex = messages.findIndex(
|
||||||
isUpdating = true;
|
(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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
|
// Render fallback if message is null
|
||||||
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -283,6 +332,13 @@ export const ChatList = ({
|
|||||||
gap: "5px",
|
gap: "5px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<ErrorBoundary
|
||||||
|
fallback={
|
||||||
|
<Typography>
|
||||||
|
Error loading content: Invalid Data
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
>
|
||||||
<MessageItem
|
<MessageItem
|
||||||
isLast={index === messages.length - 1}
|
isLast={index === messages.length - 1}
|
||||||
lastSignature={lastSignature}
|
lastSignature={lastSignature}
|
||||||
@ -291,6 +347,7 @@ export const ChatList = ({
|
|||||||
isTemp={!!message?.isTemp}
|
isTemp={!!message?.isTemp}
|
||||||
myAddress={myAddress}
|
myAddress={myAddress}
|
||||||
onReply={onReply}
|
onReply={onReply}
|
||||||
|
onEdit={onEdit}
|
||||||
reply={reply}
|
reply={reply}
|
||||||
replyIndex={replyIndex}
|
replyIndex={replyIndex}
|
||||||
scrollToItem={goToMessage}
|
scrollToItem={goToMessage}
|
||||||
@ -298,9 +355,11 @@ export const ChatList = ({
|
|||||||
reactions={reactions}
|
reactions={reactions}
|
||||||
isUpdating={isUpdating}
|
isUpdating={isUpdating}
|
||||||
/>
|
/>
|
||||||
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,6 +16,8 @@ import ReplyIcon from "@mui/icons-material/Reply";
|
|||||||
import { Spacer } from "../../common/Spacer";
|
import { Spacer } from "../../common/Spacer";
|
||||||
import { ReactionPicker } from "../ReactionPicker";
|
import { ReactionPicker } from "../ReactionPicker";
|
||||||
import KeyOffIcon from '@mui/icons-material/KeyOff';
|
import KeyOffIcon from '@mui/icons-material/KeyOff';
|
||||||
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
|
|
||||||
export const MessageItem = ({
|
export const MessageItem = ({
|
||||||
message,
|
message,
|
||||||
onSeen,
|
onSeen,
|
||||||
@ -30,7 +32,8 @@ export const MessageItem = ({
|
|||||||
handleReaction,
|
handleReaction,
|
||||||
reactions,
|
reactions,
|
||||||
isUpdating,
|
isUpdating,
|
||||||
lastSignature
|
lastSignature,
|
||||||
|
onEdit
|
||||||
}) => {
|
}) => {
|
||||||
const { ref, inView } = useInView({
|
const { ref, inView } = useInView({
|
||||||
threshold: 0.7, // Fully visible
|
threshold: 0.7, // Fully visible
|
||||||
@ -46,6 +49,7 @@ export const MessageItem = ({
|
|||||||
}, [inView, message.id, isLast]);
|
}, [inView, message.id, isLast]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{message?.divide && (
|
{message?.divide && (
|
||||||
@ -130,6 +134,15 @@ export const MessageItem = ({
|
|||||||
gap: '10px',
|
gap: '10px',
|
||||||
alignItems: 'center'
|
alignItems: 'center'
|
||||||
}}>
|
}}>
|
||||||
|
{message?.sender === myAddress && !message?.isNotEncrypted && (
|
||||||
|
<ButtonBase
|
||||||
|
onClick={() => {
|
||||||
|
onEdit(message);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EditIcon />
|
||||||
|
</ButtonBase>
|
||||||
|
)}
|
||||||
{!isShowingAsReply && (
|
{!isShowingAsReply && (
|
||||||
<ButtonBase
|
<ButtonBase
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -289,6 +302,19 @@ export const MessageItem = ({
|
|||||||
{message?.status === 'failed-permanent' ? 'Failed to send' : 'Sending...'}
|
{message?.status === 'failed-permanent' ? 'Failed to send' : 'Sending...'}
|
||||||
</Typography>
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
|
{message?.isEdit && (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "gray",
|
||||||
|
fontFamily: "Inter",
|
||||||
|
fontStyle: 'italic'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edited
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
@ -298,6 +324,7 @@ export const MessageItem = ({
|
|||||||
>
|
>
|
||||||
{formatTimestamp(message.timestamp)}
|
{formatTimestamp(message.timestamp)}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@ -320,7 +347,7 @@ export const MessageItem = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const ReplyPreview = ({message})=> {
|
export const ReplyPreview = ({message, isEdit})=> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -344,10 +371,18 @@ export const ReplyPreview = ({message})=> {
|
|||||||
<Box sx={{
|
<Box sx={{
|
||||||
padding: '5px'
|
padding: '5px'
|
||||||
}}>
|
}}>
|
||||||
<Typography sx={{
|
{isEdit ? (
|
||||||
fontSize: '12px',
|
<Typography sx={{
|
||||||
fontWeight: 600
|
fontSize: '12px',
|
||||||
}}>Replied to {message?.senderName || message?.senderAddress}</Typography>
|
fontWeight: 600
|
||||||
|
}}>Editing Message</Typography>
|
||||||
|
) : (
|
||||||
|
<Typography sx={{
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: 600
|
||||||
|
}}>Replied to {message?.senderName || message?.senderAddress}</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
{message?.messageText && (
|
{message?.messageText && (
|
||||||
<MessageDisplay
|
<MessageDisplay
|
||||||
htmlContent={generateHTML(message?.messageText, [
|
htmlContent={generateHTML(message?.messageText, [
|
||||||
|
Loading…
x
Reference in New Issue
Block a user