import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react' import { CreateCommonSecret } from './CreateCommonSecret' import { reusableGet } from '../../qdn/publish/pubish' import { uint8ArrayToObject } from '../../backgroundFunctions/encryption' import { base64ToUint8Array, decodeBase64ForUIChatMessages, objectToBase64 } from '../../qdn/encryption/group-encryption' import { ChatContainerComp } from './ChatContainer' 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 { LoadingSnackbar } from '../Snackbar/LoadingSnackbar' import { getBaseApiReact, getBaseApiReactSocket, isMobile, pauseAllQueues, resumeAllQueues } from '../../App' import { CustomizedSnackbars } from '../Snackbar/Snackbar' import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from '../../constants/codes' import { useMessageQueue } from '../../MessageQueueContext' import { executeEvent } from '../../utils/events' import { Box, ButtonBase, Divider, Typography } from '@mui/material' import ShortUniqueId from "short-unique-id"; import { ReplyPreview } from './MessageItem' import { ExitIcon } from '../../assets/Icons/ExitIcon' import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from '../../constants/resourceTypes' import { isExtMsg } from '../../background' import AppViewerContainer from '../Apps/AppViewerContainer' import CloseIcon from "@mui/icons-material/Close"; import { throttle } from 'lodash' const uid = new ShortUniqueId({ length: 5 }); export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, myAddress, handleNewEncryptionNotification, hide, handleSecretKeyCreationInProgress, triedToFetchSecretKey, myName, balance, getTimestampEnterChatParent, hideView}) => { const [messages, setMessages] = useState([]) const [chatReferences, setChatReferences] = useState({}) const [isSending, setIsSending] = useState(false) const [isLoading, setIsLoading] = useState(false) const [isMoved, setIsMoved] = useState(false); const [openSnack, setOpenSnack] = React.useState(false); const [infoSnack, setInfoSnack] = React.useState(null); const hasInitialized = useRef(false) const [isFocusedParent, setIsFocusedParent] = useState(false); const [replyMessage, setReplyMessage] = useState(null) const [onEditMessage, setOnEditMessage] = useState(null) const [isOpenQManager, setIsOpenQManager] = useState(null) const [messageSize, setMessageSize] = useState(0) const hasInitializedWebsocket = useRef(false) const socketRef = useRef(null); // WebSocket reference const timeoutIdRef = useRef(null); // Timeout ID reference const groupSocketTimeoutRef = useRef(null); // Group Socket Timeout reference const editorRef = useRef(null); const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue(); const [, forceUpdate] = useReducer((x) => x + 1, 0); const lastReadTimestamp = useRef(null) const handleUpdateRef = useRef(null); const getTimestampEnterChat = async () => { try { return new Promise((res, rej) => { window.sendMessage("getTimestampEnterChat") .then((response) => { if (!response?.error) { if(response && selectedGroup && response[selectedGroup]){ lastReadTimestamp.current = response[selectedGroup] window.sendMessage("addTimestampEnterChat", { timestamp: Date.now(), groupId: selectedGroup }).catch((error) => { console.error("Failed to add timestamp:", error.message || "An error occurred"); }); setTimeout(() => { getTimestampEnterChatParent(); }, 600); } res(response); return; } rej(response.error); }) .catch((error) => { rej(error.message || "An error occurred"); }); }); } catch (error) {} }; useEffect(()=> { getTimestampEnterChat() }, []) const members = useMemo(() => { const uniqueMembers = new Set(); messages.forEach((message) => { if (message?.senderName) { uniqueMembers.add(message?.senderName); } }); return Array.from(uniqueMembers); }, [messages]); const triggerRerender = () => { forceUpdate(); // Trigger re-render by updating the state }; const setEditorRef = (editorInstance) => { editorRef.current = editorInstance; }; const tempMessages = useMemo(()=> { if(!selectedGroup) return [] if(queueChats[selectedGroup]){ return queueChats[selectedGroup]?.filter((item)=> !item?.chatReference) } return [] }, [selectedGroup, queueChats]) const tempChatReferences = useMemo(()=> { if(!selectedGroup) return [] if(queueChats[selectedGroup]){ return queueChats[selectedGroup]?.filter((item)=> !!item?.chatReference) } return [] }, [selectedGroup, queueChats]) const secretKeyRef = useRef(null) useEffect(()=> { if(secretKey){ secretKeyRef.current = secretKey } }, [secretKey]) // const getEncryptedSecretKey = useCallback(()=> { // const response = getResource() // const decryptResponse = decryptResource() // return // }, []) const checkForFirstSecretKeyNotification = (messages)=> { messages?.forEach((message)=> { try { const decodeMsg = atob(message.data); if(decodeMsg === PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY){ handleSecretKeyCreationInProgress() return } } catch (error) { } }) } const middletierFunc = async (data: any, groupId: string) => { try { if (hasInitialized.current) { decryptMessages(data, true); return; } hasInitialized.current = true; const url = `${getBaseApiReact()}/chat/messages?txGroupId=${groupId}&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 { if(!secretKeyRef.current){ checkForFirstSecretKeyNotification(encryptedMessages) } 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) { } } const forceCloseWebSocket = () => { if (socketRef.current) { clearTimeout(timeoutIdRef.current); clearTimeout(groupSocketTimeoutRef.current); socketRef.current.close(1000, 'forced'); socketRef.current = null; } }; const pingGroupSocket = () => { 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 = () => { let socketLink = `${getBaseApiReactSocket()}/websockets/chat/messages?txGroupId=${selectedGroup}&encoding=BASE64&limit=100` socketRef.current = new WebSocket(socketLink) socketRef.current.onopen = () => { setTimeout(pingGroupSocket, 50) } socketRef.current.onmessage = (e) => { try { if (e.data === 'pong') { clearTimeout(timeoutIdRef.current); groupSocketTimeoutRef.current = setTimeout(pingGroupSocket, 45000); // Ping every 45 seconds } else { middletierFunc(JSON.parse(e.data), selectedGroup) setIsLoading(false) } } catch (error) { } } socketRef.current.onclose = () => { clearTimeout(groupSocketTimeoutRef.current); clearTimeout(timeoutIdRef.current); console.warn(`WebSocket closed: ${event.reason || 'unknown reason'}`); if (event.reason !== 'forced' && event.code !== 1000) { setTimeout(() => initWebsocketMessageGroup(), 1000); // Retry after 10 seconds } } socketRef.current.onerror = (e) => { clearTimeout(groupSocketTimeoutRef.current); clearTimeout(timeoutIdRef.current); if (socketRef.current) { socketRef.current.close(); } } } useEffect(()=> { if(hasInitializedWebsocket.current) return if(triedToFetchSecretKey && !secretKey){ forceCloseWebSocket() setMessages([]) setIsLoading(true) initWebsocketMessageGroup() } }, [triedToFetchSecretKey, secretKey]) useEffect(()=> { if(!secretKey || hasInitializedWebsocket.current) return forceCloseWebSocket() setMessages([]) setIsLoading(true) pauseAllQueues() setTimeout(() => { resumeAllQueues() }, 6000); initWebsocketMessageGroup() hasInitializedWebsocket.current = true }, [secretKey]) useEffect(()=> { const notifications = messages.filter((message)=> message?.decryptedData ?.type === 'notification') if(notifications.length === 0) return const latestNotification = notifications.reduce((latest, current) => { return current.timestamp > latest.timestamp ? current : latest; }, notifications[0]); handleNewEncryptionNotification(latestNotification) }, [messages]) const encryptChatMessage = async (data: string, secretKeyObject: any, reactiontypeNumber?: number)=> { try { return new Promise((res, rej)=> { window.sendMessage("encryptSingle", { data, secretKeyObject, typeNumber: reactiontypeNumber, }) .then((response) => { if (!response?.error) { res(response); return; } rej(response.error); }) .catch((error) => { rej(error.message || "An error occurred"); }); }) } catch (error) { } } const sendChatGroup = async ({groupId, typeMessage = undefined, chatReference = undefined, messageText}: any)=> { try { return new Promise((res, rej)=> { window.sendMessage("sendChatGroup", { groupId, typeMessage, chatReference, messageText, }, 120000) .then((response) => { if (!response?.error) { res(response); return; } rej(response.error); }) .catch((error) => { rej(error.message || "An error occurred"); }); }) } catch (error) { throw new Error(error) } } const clearEditorContent = () => { if (editorRef.current) { setMessageSize(0) editorRef.current.chain().focus().clearContent().run(); if(isMobile){ setTimeout(() => { editorRef.current?.chain().blur().run(); setIsFocusedParent(false) executeEvent("sent-new-message-group", {}) setTimeout(() => { triggerRerender(); }, 300); }, 200); } } }; 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() === '
') return setIsSending(true) const message = htmlContent const secretKeyObject = await getSecretKey(false, true) let repliedTo = replyMessage?.signature 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 (!editorRef?.current) return; handleUpdateRef.current = throttle(() => { const htmlContent = editorRef.current.getHTML(); const size = new TextEncoder().encode(htmlContent).length; setMessageSize(size + 100); }, 1200); const currentEditor = editorRef.current; currentEditor.on("update", handleUpdateRef.current); return () => { currentEditor.off("update", handleUpdateRef.current); }; }, [editorRef, setMessageSize]); useEffect(() => { if (hide) { setTimeout(() => setIsMoved(true), 500); // Wait for the fade-out to complete before moving } else { setIsMoved(false); // Reset the position immediately when showing } }, [hide]); const onReply = useCallback((message)=> { if(onEditMessage){ clearEditorContent() } 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(); }, []) const handleReaction = useCallback(async (reaction, chatMessage, reactionState = true)=> { try { if(isSending) return if(+balance < 4) throw new Error('You need at least 4 QORT to send a message') pauseAllQueues() setIsSending(true) const message = '' const secretKeyObject = await getSecretKey(false, true) const otherData = { specialId: uid.rnd(), type: 'reaction', content: reaction, contentState: reactionState } const objectMessage = { message, ...(otherData || {}) } const message64: any = await objectToBase64(objectMessage) const reactiontypeNumber = RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS const encryptSingle = await encryptChatMessage(message64, secretKeyObject, reactiontypeNumber) // const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle}) const sendMessageFunc = async () => { return await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle, chatReference: chatMessage.signature}) }; // Add the function to the queue const messageObj = { message: { text: message, timestamp: Date.now(), senderName: myName, sender: myAddress, ...(otherData || {}) }, chatReference: chatMessage.signature } addToQueue(sendMessageFunc, messageObj, 'chat-reaction', 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() } }, []) const openQManager = useCallback(()=> { setIsOpenQManager(true) }, []) return (