import React, { useCallback, useEffect, useMemo, 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, 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 { 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"; const uid = new ShortUniqueId({ length: 5 }); export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDirect, setNewChat, getTimestampEnterChat, myName, balance, close}) => { 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 editorRef = useRef(null); const socketRef = useRef(null); const timeoutIdRef = useRef(null); const groupSocketTimeoutRef = useRef(null); const setEditorRef = (editorInstance) => { editorRef.current = editorInstance; }; 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] } return [] }, [selectedDirect?.address, queueChats]) useEffect(()=> { if(selectedDirect?.address){ publicKeyOfRecipientRef.current = selectedDirect?.address getPublicKeyFunc(publicKeyOfRecipientRef.current) } }, [selectedDirect?.address]) const decryptMessages = (encryptedMessages: any[])=> { try { return new Promise((res, rej)=> { chrome?.runtime?.sendMessage({ action: "decryptDirect", payload: { data: encryptedMessages, involvingAddress: selectedDirect?.address }}, (response) => { if (!response?.error) { processWithNewMessages(response, selectedDirect?.address) res(response) if(hasInitialized.current){ const formatted = response.map((item: any)=> { return { ...item, id: item.signature, text: item.message, unread: item?.sender === myAddress ? false : true } } ) setMessages((prev)=> [...prev, ...formatted]) } else { const formatted = response.map((item: any)=> { return { ...item, id: item.signature, text: item.message, unread: false } } ) setMessages(formatted) hasInitialized.current = true } } rej(response.error) }); }) } catch (error) { } } const forceCloseWebSocket = () => { if (socketRef.current) { console.log('Force closing the WebSocket'); 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 = () => { console.log('WebSocket connection opened'); 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 { decryptMessages(JSON.parse(e.data)); 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)=> { chrome?.runtime?.sendMessage({ action: "sendChatDirect", payload: { directTo, chatReference, messageText, otherData, publicKeyOfRecipient, address: directTo }}, async (response) => { if (!response?.error) { if(isNewChatVar){ let getRecipientName = null try { getRecipientName = await getNameInfo(response.recipient) } catch (error) { } setSelectedDirect({ "address": response.recipient, "name": getRecipientName, "timestamp": Date.now(), "sender": myAddress, "senderName": myName }) setNewChat(null) chrome?.runtime?.sendMessage({ action: "addTimestampEnterChat", payload: { timestamp: Date.now(), groupId: response.recipient, }, }); setTimeout(() => { getTimestampEnterChat() }, 400); } res(response) return } rej(response.error) }); }) } catch (error) { throw new Error(error) } finally { } } const clearEditorContent = () => { if (editorRef.current) { editorRef.current.chain().focus().clearContent().run(); if(isMobile){ setTimeout(() => { editorRef.current?.chain().blur().run(); setIsFocusedParent(false) executeEvent("sent-new-message-group", {}) }, 200); } } }; 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 } const otherData = { specialId: uid.rnd() } const sendMessageFunc = async () => { await sendChatDirect({ messageText: htmlContent, otherData}, selectedDirect?.address, publicKeyOfRecipient, false) }; // 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() } // 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() } } return (
Close Direct Chat {isNewChat && ( <> setDirectToValue(e.target.value)} /> )}
{isFocusedParent && ( { if(isSending) return setIsFocusedParent(false) clearEditorContent() // Unfocus the editor }} style={{ marginTop: 'auto', alignSelf: 'center', cursor: isSending ? 'default' : 'pointer', background: 'red', flexShrink: 0, padding: isMobile && '5px' }} > {` Close`} )} { if(isSending) return sendMessage() }} style={{ marginTop: 'auto', alignSelf: 'center', cursor: isSending ? 'default' : 'pointer', background: isSending && 'rgba(0, 0, 0, 0.8)', flexShrink: 0, padding: isMobile && '5px' }} > {isSending && ( )} {` Send`}
) }