diff --git a/package-lock.json b/package-lock.json index 5cb6ac6..fe43916 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "buffer": "6.0.3", "compressorjs": "^1.2.1", "dompurify": "^3.1.6", + "emoji-picker-react": "^4.12.0", "file-saver": "^2.0.5", "jssha": "3.3.1", "lodash": "^4.17.21", @@ -4128,6 +4129,20 @@ "integrity": "sha512-w+9yAVHoHhysCa+gln7AzbO9CdjFcL/wN/5dd+XW/Msl2d/4+WisEaCF1nty0xbAKaxdaJfgLB2296U7zZB7BA==", "dev": true }, + "node_modules/emoji-picker-react": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.12.0.tgz", + "integrity": "sha512-q2c8UcZH0eRIMj41bj0k1akTjk69tsu+E7EzkW7giN66iltF6H9LQvQvw6ugscsxdC+1lmt3WZpQkkY65J95tg==", + "dependencies": { + "flairup": "1.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -4725,6 +4740,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flairup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/flairup/-/flairup-1.0.0.tgz", + "integrity": "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==" + }, "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", diff --git a/package.json b/package.json index 915aa55..4d0fa0c 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "buffer": "6.0.3", "compressorjs": "^1.2.1", "dompurify": "^3.1.6", + "emoji-picker-react": "^4.12.0", "file-saver": "^2.0.5", "jssha": "3.3.1", "lodash": "^4.17.21", diff --git a/src/background.ts b/src/background.ts index 431fc98..bdb1862 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1541,6 +1541,7 @@ async function sendChatGroup({ chatReference, messageText, }) { + let _reference = new Uint8Array(64); self.crypto.getRandomValues(_reference); @@ -1557,18 +1558,23 @@ async function sendChatGroup({ // const hasEnoughBalance = +balance < 4 ? false : true; const difficulty = 8; - const tx = await createTransaction(181, keyPair, { + const txBody = { timestamp: Date.now(), groupID: Number(groupId), hasReceipient: 0, - hasChatReference: typeMessage === "edit" ? 1 : 0, - // chatReference: chatReference, + hasChatReference: chatReference ? 1 : 0, message: messageText, lastReference: reference, proofOfWorkNonce: 0, isEncrypted: 0, // Set default to not encrypted for groups isText: 1, - }); + } + + if(chatReference){ + txBody['chatReference'] = chatReference + } + + const tx = await createTransaction(181, keyPair, txBody); // if (!hasEnoughBalance) { // throw new Error("Must have at least 4 QORT to send a chat message"); diff --git a/src/components/Chat/ChatGroup.tsx b/src/components/Chat/ChatGroup.tsx index b2e477f..1022fa7 100644 --- a/src/components/Chat/ChatGroup.tsx +++ b/src/components/Chat/ChatGroup.tsx @@ -29,6 +29,7 @@ const uid = new ShortUniqueId({ length: 5 }); export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, myAddress, handleNewEncryptionNotification, hide, handleSecretKeyCreationInProgress, triedToFetchSecretKey, myName, balance}) => { const [messages, setMessages] = useState([]) + const [chatReferences, setChatReferences] = useState({}) const [isSending, setIsSending] = useState(false) const [isLoading, setIsLoading] = useState(false) const [isMoved, setIsMoved] = useState(false); @@ -56,7 +57,14 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, const tempMessages = useMemo(()=> { if(!selectedGroup) return [] if(queueChats[selectedGroup]){ - return 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]) @@ -91,6 +99,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, }) } + const decryptMessages = (encryptedMessages: any[])=> { try { @@ -113,7 +122,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, res(response) if(hasInitialized.current){ - const formatted = response.map((item: any)=> { + const formatted = response.filter((rawItem)=> !rawItem?.chatReference).map((item: any)=> { return { ...item, id: item.signature, @@ -123,8 +132,66 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, } } ) setMessages((prev)=> [...prev, ...formatted]) + + + setChatReferences((prev) => { + let organizedChatReferences = { ...prev }; + + response + .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; + } + + // Initialize chat reference and reactions if not present + organizedChatReferences[item.chatReference] = { + ...(organizedChatReferences[item.chatReference] || {}), + reactions: organizedChatReferences[item.chatReference]?.reactions || {} + }; + + organizedChatReferences[item.chatReference].reactions[content] = + organizedChatReferences[item.chatReference].reactions[content] || []; + + const existingReactionIndex = organizedChatReferences[item.chatReference].reactions[content] + .findIndex(reaction => reaction.sender === sender); + + // Handle contentState: if false, remove the reaction + if (contentState === false) { + if (existingReactionIndex !== -1) { + organizedChatReferences[item.chatReference].reactions[content].splice(existingReactionIndex, 1); + } + } else { + // Add or update reaction + if (existingReactionIndex !== -1) { + const existingReaction = organizedChatReferences[item.chatReference].reactions[content][existingReactionIndex]; + const existingTimestamp = existingReaction.timestamp; + + if (newTimestamp > existingTimestamp) { + organizedChatReferences[item.chatReference].reactions[content][existingReactionIndex] = item; + } + } else { + organizedChatReferences[item.chatReference].reactions[content].push(item); + } + } + } catch (error) { + console.error("Error processing reaction item:", error, item); + } + }); + + return organizedChatReferences; + }); + + } else { - const formatted = response.map((item: any)=> { + const formatted = response.filter((rawItem)=> !rawItem?.chatReference).map((item: any)=> { return { ...item, id: item.signature, @@ -135,7 +202,64 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, } ) setMessages(formatted) hasInitialized.current = true - + setChatReferences((prev) => { + let organizedChatReferences = { ...prev }; + + response + .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; + } + + // Initialize chat reference and reactions if not present + organizedChatReferences[item.chatReference] = { + ...(organizedChatReferences[item.chatReference] || {}), + reactions: organizedChatReferences[item.chatReference]?.reactions || {} + }; + + organizedChatReferences[item.chatReference].reactions[content] = + organizedChatReferences[item.chatReference].reactions[content] || []; + + const existingReactionIndex = organizedChatReferences[item.chatReference].reactions[content] + .findIndex(reaction => reaction.sender === sender); + + // Handle contentState: if false, remove the reaction + if (contentState === false) { + if (existingReactionIndex !== -1) { + organizedChatReferences[item.chatReference].reactions[content].splice(existingReactionIndex, 1); + } + } else { + // Add or update reaction + if (existingReactionIndex !== -1) { + const existingReaction = organizedChatReferences[item.chatReference].reactions[content][existingReactionIndex]; + const existingTimestamp = existingReaction.timestamp; + + if (newTimestamp > existingTimestamp) { + organizedChatReferences[item.chatReference].reactions[content][existingReactionIndex] = item; + } + } else { + organizedChatReferences[item.chatReference].reactions[content].push(item); + } + } + } catch (error) { + console.error("Error processing reaction item:", error, item); + } + }); + + return organizedChatReferences; + }); + + + + } } rej(response.error) @@ -386,6 +510,73 @@ const clearEditorContent = () => { const onReply = useCallback((message)=> { setReplyMessage(message) }, []) + + 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 encryptSingle = await encryptChatMessage(message64, secretKeyObject) + // const res = await sendChatGroup({groupId: selectedGroup,messageText: encryptSingle}) + + const sendMessageFunc = async () => { + 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() + } + }, []) + return (