const ChatContainer = styled.div` background-color: #0e0e10; padding-left: 4px; height: calc(100vh - 169px); `; //HELPER FUNCTIONS function parseHexString(hexString) { const result = []; while (hexString.length >= 2) { result.push(parseInt(hexString.substring(0, 2), 16)); hexString = hexString.substring(2, hexString.length); } return result; } function toHexString(byteArray) { const result = ""; for (let byte of byteArray) { result += ("0" + (byte & 0xff).toString(16)).slice(-2); } return result; } function decryptMessages(messages, key) { if (!key || key.length === 0) { return []; } const decryptedMessages = messages.map((msg) => { return { ...msg, text: decrypt(msg.text, key, msg.nonce), thread: msg.thread.map((threadMsg) => { return { ...threadMsg, text: decrypt(threadMsg.text, key, threadMsg.nonce), }; }), }; }); return decryptedMessages; } function decrypt(text, key, nonce) { if (!key) { return null; } const byteNonce = []; let hexString = nonce; while (hexString.length >= 2) { byteNonce.push(parseInt(hexString.substring(0, 2), 16)); hexString = hexString.substring(2, hexString.length); } try { const decipher = Crypto.createDecipheriv( "aes-256-cbc", parseHexString(key), byteNonce ); let decrypted = decipher.update(text, "base64", "utf8"); decrypted += decipher.final("utf8"); return decrypted; } catch (e) { console.log("Calimero.decrypt error: ", e.toString()); return null; } } const fetchMessages = () => props.fetchMessages({ selectedDM: props.selectedDM, selectedChannel: props.selectedChannel, }).then((messages) => { setEncryptedMessages(messages); }); const pollMessages = () => { const interval = setInterval(() => { fetchMessages(); }, 200); return () => clearInterval(interval); }; const [encryptedMessages, setEncryptedMessages] = useState([]); const [decryptedMessages, setDecryptedMessages] = useState([]); const [encryptionKey, setEncryptionKey] = useState(undefined); const [openThreadId, setOpenThreadId] = useState(undefined); useEffect(() => { if (!props.selectedDM && !props.selectedChannel) { return; } setEncryptionKey(undefined); setOpenThreadId(undefined); setDecryptedMessages([]); props .fetchKey({ selectedDM: props.selectedDM, selectedChannel: props.selectedChannel, }) .then((key) => { setEncryptionKey(key); }); const stopPolling = pollMessages(); return () => { stopPolling(); }; }, [props.selectedDM, props.selectedChannel]); useEffect(() => { if (encryptedMessages.length > 0 && encryptionKey && encryptionKey.length > 0) { const decryptedNewMessages = decryptMessages(encryptedMessages, encryptionKey); setDecryptedMessages(decryptedNewMessages); } }, [encryptedMessages, encryptionKey]); const handleMessageSent = useCallback( (text, img) => { const temporalMessage = { id: Date.now(), // we need to generate a unique id for the message in the client nonce: Date.now(), // just mocking a nonce here, probably can be removed from the temporal message reactions: {}, // empty reactions as it's a new message sender: context.accountId, // the sender is the current user text: img ? `${text}$?$https://ipfs.near.social/ipfs/${img.cid}` : text, thread: [], // No thread as it's a new message timestamp: Date.now(), isTemporal: true, // marking the message as temporal }; if (!openThreadId) { setDecryptedMessages(decryptedMessages.concat(temporalMessage)); } else { const messageIndex = decryptedMessages.findIndex( (message) => message.id === openThreadId ); setDecryptedMessages( decryptedMessages .slice(0, messageIndex) .concat([ { ...decryptedMessages[messageIndex], thread: decryptedMessages[messageIndex].thread.concat(temporalMessage), }, ]) .concat(decryptedMessages.slice(messageIndex + 1)) ); } props.sendMessage({ message: text, toAccount: props.selectedDM, toChannel: props.selectedChannel, img, key: encryptionKey, threadId: openThreadId, }); }, [ decryptedMessages, props.sendMessage, openThreadId, props.selectedDM, props.selectedChannel, encryptionKey, openThreadId, ] ); return ( <ChatContainer> <Widget src={`${props.componentOwnerId}/widget/Calimero.Curb.Chat.ChatDisplay`} props={{ componentOwnerId: props.componentOwnerId, messages: decryptedMessages, readMessage: props.readMessage, addMessageReaction: props.addMessageReaction, openThreadId, setOpenThreadId, }} /> <Widget src={`${props.componentOwnerId}/widget/Calimero.Curb.Chat.MessageInput`} props={{ componentOwnerId: props.componentOwnerId, selectedChat: props.selectedDM || props.selectedChannel, sendMessage: handleMessageSent, resetImage: props.resetImage, threadReply: openThreadId, }} /> </ChatContainer> );