import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react' import { objectToBase64 } from '../../qdn/encryption/group-encryption' import { ChatList } from './ChatList' import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css"; import Tiptap from './TipTap' import { CustomButton } from '../../App-styles' import CircularProgress from '@mui/material/CircularProgress'; import { Box, ButtonBase, Input, Typography } from '@mui/material'; import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar'; import { getNameInfo } from '../Group/Group'; import { Spacer } from '../../common/Spacer'; import { CustomizedSnackbars } from '../Snackbar/Snackbar'; import { getBaseApiReact, getBaseApiReactSocket, isMobile, pauseAllQueues, resumeAllQueues } from '../../App'; import { getPublicKey } from '../../background'; import { useMessageQueue } from '../../MessageQueueContext'; import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from '../../utils/events'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ShortUniqueId from "short-unique-id"; import { ReturnIcon } from '../../assets/Icons/ReturnIcon'; import { ExitIcon } from '../../assets/Icons/ExitIcon'; import { MessageItem, ReplyPreview } from './MessageItem'; const uid = new ShortUniqueId({ length: 5 }); export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDirect, setNewChat, getTimestampEnterChat, myName, balance, close, setMobileViewModeKeepOpen}) => { const { queueChats, addToQueue, processWithNewMessages} = useMessageQueue(); const [isFocusedParent, setIsFocusedParent] = useState(false); const [messages, setMessages] = useState([]) const [isSending, setIsSending] = useState(false) const [directToValue, setDirectToValue] = useState('') const hasInitialized = useRef(false) const [isLoading, setIsLoading] = useState(false) const [openSnack, setOpenSnack] = React.useState(false); 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); const groupSocketTimeoutRef = useRef(null); const [replyMessage, setReplyMessage] = useState(null) const setEditorRef = (editorInstance) => { editorRef.current = editorInstance; }; const [messageSize, setMessageSize] = useState(0) const [, forceUpdate] = useReducer((x) => x + 1, 0); const triggerRerender = () => { forceUpdate(); // Trigger re-render by updating the state }; const publicKeyOfRecipientRef = useRef(null) const getPublicKeyFunc = async (address)=> { try { const publicKey = await getPublicKey(address) if(publicKeyOfRecipientRef.current !== selectedDirect?.address) return setPublicKeyOfRecipient(publicKey) } catch (error) { } } const tempMessages = useMemo(()=> { if(!selectedDirect?.address) return [] if(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]) useEffect(()=> { if(selectedDirect?.address){ publicKeyOfRecipientRef.current = selectedDirect?.address getPublicKeyFunc(publicKeyOfRecipientRef.current) } }, [selectedDirect?.address]) const middletierFunc = async (data: any, selectedDirectAddress: string, myAddress: string) => { try { if (hasInitialized.current) { decryptMessages(data, true); return; } hasInitialized.current = true; const url = `${getBaseApiReact()}/chat/messages?involving=${selectedDirectAddress}&involving=${myAddress}&encoding=BASE64&limit=0&reverse=false`; const response = await fetch(url, { method: "GET", headers: { "Content-Type": "application/json", }, }); const responseData = await response.json(); decryptMessages(responseData, false); } catch (error) { 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){ } }) 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) { clearTimeout(timeoutIdRef.current); clearTimeout(groupSocketTimeoutRef.current); socketRef.current.close(1000, 'forced'); socketRef.current = null; } }; const pingWebSocket = () => { try { if (socketRef.current?.readyState === WebSocket.OPEN) { socketRef.current.send('ping'); timeoutIdRef.current = setTimeout(() => { if (socketRef.current) { socketRef.current.close(); clearTimeout(groupSocketTimeoutRef.current); } }, 5000); // Close if no pong in 5 seconds } } catch (error) { console.error('Error during ping:', error); } }; const initWebsocketMessageGroup = () => { forceCloseWebSocket(); // Close any existing connection if (!selectedDirect?.address || !myAddress) return; const socketLink = `${getBaseApiReactSocket()}/websockets/chat/messages?involving=${selectedDirect?.address}&involving=${myAddress}&encoding=BASE64&limit=100`; socketRef.current = new WebSocket(socketLink); socketRef.current.onopen = () => { setTimeout(pingWebSocket, 50); // Initial ping }; socketRef.current.onmessage = (e) => { try { if (e.data === 'pong') { clearTimeout(timeoutIdRef.current); groupSocketTimeoutRef.current = setTimeout(pingWebSocket, 45000); // Ping every 45 seconds } else { middletierFunc(JSON.parse(e.data), selectedDirect?.address, myAddress) setIsLoading(false); } } catch (error) { console.error('Error handling WebSocket message:', error); } }; socketRef.current.onclose = (event) => { clearTimeout(groupSocketTimeoutRef.current); clearTimeout(timeoutIdRef.current); console.warn(`WebSocket closed: ${event.reason || 'unknown reason'}`); if (event.reason !== 'forced' && event.code !== 1000) { setTimeout(() => initWebsocketMessageGroup(), 10000); // Retry after 10 seconds } }; socketRef.current.onerror = (error) => { console.error('WebSocket error:', error); clearTimeout(groupSocketTimeoutRef.current); clearTimeout(timeoutIdRef.current); if (socketRef.current) { socketRef.current.close(); } }; }; const setDirectChatValueFunc = async (e)=> { setDirectToValue(e.detail.directToValue) } useEffect(() => { subscribeToEvent("setDirectToValueNewChat", setDirectChatValueFunc); return () => { unsubscribeFromEvent("setDirectToValueNewChat", setDirectChatValueFunc); }; }, []); useEffect(() => { if (hasInitializedWebsocket.current || isNewChat) return; setIsLoading(true); initWebsocketMessageGroup(); hasInitializedWebsocket.current = true; return () => { forceCloseWebSocket(); // Clean up WebSocket on component unmount }; }, [selectedDirect?.address, myAddress, isNewChat]); const sendChatDirect = async ({ chatReference = undefined, messageText, otherData}: any, address, publicKeyOfRecipient, isNewChatVar)=> { try { const directTo = isNewChatVar ? directToValue : address if(!directTo) return return new Promise((res, rej)=> { window.sendMessage("sendChatDirect", { directTo, chatReference, messageText, otherData, publicKeyOfRecipient, address: directTo, }) .then(async (response) => { if (!response?.error) { if (isNewChatVar) { let getRecipientName = null; try { getRecipientName = await getNameInfo(response.recipient); } catch (error) { console.error("Error fetching recipient name:", error); } setSelectedDirect({ address: response.recipient, name: getRecipientName, timestamp: Date.now(), sender: myAddress, senderName: myName, }); setNewChat(null); window.sendMessage("addTimestampEnterChat", { timestamp: Date.now(), groupId: response.recipient, }).catch((error) => { console.error("Failed to add timestamp:", error.message || "An error occurred"); }); setTimeout(() => { getTimestampEnterChat(); }, 400); } res(response); return; } rej(response.error); }) .catch((error) => { rej(error.message || "An error occurred"); }); }) } catch (error) { throw new Error(error) } finally { } } const clearEditorContent = () => { if (editorRef.current) { editorRef.current.chain().focus().clearContent().run(); setMessageSize(0) if(isMobile){ setTimeout(() => { editorRef.current?.chain().blur().run(); setIsFocusedParent(false) executeEvent("sent-new-message-group", {}) setTimeout(() => { triggerRerender(); }, 300); }, 200); } } }; useEffect(() => { if (!editorRef?.current) return; const handleUpdate = () => { const htmlContent = editorRef?.current.getHTML(); const stringified = JSON.stringify(htmlContent); const size = new Blob([stringified]).size; setMessageSize(size + 100); }; // Add a listener for the editorRef?.current's content updates editorRef?.current.on('update', handleUpdate); // Cleanup the listener on unmount return () => { editorRef?.current.off('update', handleUpdate); }; }, [editorRef?.current]); const sendMessage = async ()=> { try { if(+balance < 4) throw new Error('You need at least 4 QORT to send a message') if(isSending) return if (editorRef.current) { const htmlContent = editorRef.current.getHTML(); if(!htmlContent?.trim() || htmlContent?.trim() === '

') return setIsSending(true) pauseAllQueues() const message = JSON.stringify(htmlContent) if(isNewChat){ await sendChatDirect({ messageText: htmlContent}, null, null, true) return } let repliedTo = replyMessage?.signature if (replyMessage?.chatReference) { repliedTo = replyMessage?.chatReference } 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 (
{!isMobile && ( Close Direct Chat )} {isMobile && ( { close() }} > {isNewChat ? '' : selectedDirect?.name || (selectedDirect?.address?.slice(0,10) + '...')} { setSelectedDirect(null) setMobileViewModeKeepOpen('') setNewChat(false) }} > )} {isNewChat && ( <> setDirectToValue(e.target.value)} /> )}
{replyMessage && ( )} {onEditMessage && ( )}
{isFocusedParent && ( { if(isSending) return setIsFocusedParent(false) setReplyMessage(null) setOnEditMessage(null) clearEditorContent() // Unfocus the editor }} style={{ marginTop: 'auto', alignSelf: 'center', cursor: isSending ? 'default' : 'pointer', background: 'red', flexShrink: 0, padding: isMobile && '5px' }} > {` Close`} )} { if(messageSize > 4000) return if(isSending) return sendMessage() }} style={{ marginTop: 'auto', alignSelf: 'center', cursor: isSending ? 'default' : 'pointer', background: isSending ? 'rgba(0, 0, 0, 0.8)' : 'var(--green)', flexShrink: 0, padding: isMobile && '5px' }} > {isSending && ( )} {` Send`} {isFocusedParent && messageSize > 750 && ( 4000 ? 'var(--unread)' : 'unset' }}>{`size ${messageSize} of 4000`} )}
) }