const componentOwnerId = props.componentOwnerId; const curbApi = props.curbApi; const activeChat = props.activeChat; const accountId = props.accountId; const wsApi = props.wsApi; const isWsConnectionActive = props.isWsConnectionActive; const ChatContainer = styled.div` background-color: #0e0e10; @media (min-width: 1025px) { padding-left: 4px; height: calc(100vh - 169px); } @media (max-width: 1024px) { display: flex; flex-direction: column; } `; const [openThread, setOpenThread] = useState(undefined); const [incomingMessages, setIncomingMessages] = useState([]); const [updatedMessages, setUpdatedMessages] = useState([]); const [incomingThreadMessages, setIncomingThreadMessages] = useState([]); const [updatedThreadMessages, setUpdatedThreadMessages] = useState([]); const activeChatRef = useRef(activeChat); const openThreadRef = useRef(openThread); useEffect(() => { if (activeChat.type === "channel" && isWsConnectionActive) { // todo! move this to page initialization // todo! basically, once the page has loaded, // todo! and we've fetched channels, subscribe to them wsApi.methods.subscribe([activeChat.name], (err, result) => { console.log("subscribed to", activeChat, err, result); }); } activeChatRef.current = activeChat; setOpenThread(undefined); }, [activeChat, isWsConnectionActive]); useEffect(() => { openThreadRef.current = openThread; }, [openThread]); const computeReaction = useCallback((message, reaction, sender) => { const accounts = message.reactions[reaction] ?? []; let update; if (accounts.includes(sender)) { update = accounts.filter((a) => a !== sender); } else { update = [...accounts, sender]; } return { ...message, reactions: { ...message.reactions, [reaction]: update}}; }, []); useEffect(() => { const messageListener = (event) => { if (activeChat.type === "direct_message" && event.sender === activeChatRef.current.id || activeChat.type === "channel" && event.receiver.type === "channel" && event.receiver.name === activeChatRef.current.name) { curbApi.fetchKey({ chat: activeChatRef.current }).then((key) => { const decrypted = curbApi.decryptMessages([event], key); if (event.parent_message === openThreadRef.current.id) { setOpenThread({ ...openThreadRef.current, threadCount: openThreadRef.current.threadCount + 1 }); setIncomingThreadMessages(decrypted); } else { setIncomingMessages(decrypted); } }); } }; const reactionListener = (event) => { const { message_id, reaction, sender } = event; const updateFunction = (message) => computeReaction(message, reaction, sender); setUpdatedMessages([{id: message_id, descriptor: { updateFunction }}]); } wsApi?.notifications?.on("ChannelMessage", messageListener); wsApi?.notifications?.on("MessageReaction", reactionListener); return () => { wsApi?.notifications?.off("ChannelMessage", messageListener); } }, [wsApi, curbApi]); const readMessage = useCallback( (message) => { if (message?.id && message?.sender !== accountId) { // we don't report our own messages curbApi.readMessage({ chat: activeChatRef.current, messageId: message.id }); } }, [curbApi, accountId] ); const handleReaction = useCallback((message, reaction) => { const updates = [{id: message.id, descriptor: { updateFunction: (message) => computeReaction(message, reaction, accountId) }}]; setUpdatedMessages(updates); setUpdatedThreadMessages(updates); curbApi.toggleReaction({ messageId: message.id, reaction }); }, [curbApi]); const convertCidToUrl = (file) => file ? { ...file, ipfs_cid: `https://ipfs.near.social/ipfs/${file.cid}` } : null; const handleMessageSent = useCallback( (text, img, uploadedFile) => { const images = img ? [convertCidToUrl(img)] : []; const files = uploadedFile?.file ? [convertCidToUrl(uploadedFile.file)] : []; const temporalId = curbApi.utils.toHexString(Crypto.randomBytes(16)); const temporalMessage = { // reduced to 16 bytes to be different from a real, 32-byte message id id: temporalId, // we need to generate a unique id for the message in the client nonce: curbApi.utils.toHexString(Crypto.randomBytes(16)), // just mocking a nonce here, probably can be removed from the temporal message reactions: [], // empty reactions as it's a new message images, files, sender: accountId, // the sender is the current user text, timestamp: Date.now(), temporalId, }; const incomingCall = (openThread) ? setIncomingThreadMessages : setIncomingMessages; incomingCall([temporalMessage]); curbApi.sendMessage({ message: text, chat: activeChat, images, files, threadId: openThread.id, }, (err, result) => { if (result) { const update = [{ id: temporalId, descriptor: { updatedFields: { id: result.result.id, temporalId: undefined } }, }]; if(openThread) { const parentUpdate = [{ id: openThread.id, descriptor: { updateFunction: (message) => ({ threadCount: message.threadCount + 1 }) } }]; setUpdatedThreadMessages(update); setUpdatedMessages(parentUpdate); } else { setUpdatedMessages(update); } } else { console.log(err); } }); }, [activeChat, curbApi, openThread, messages] ); return ( <ChatContainer> <Widget src={`${componentOwnerId}/widget/Calimero.Curb.Chat.ChatDisplay`} props={{ componentOwnerId, readMessage, handleReaction, openThread, setOpenThread, activeChat, accountId, curbApi, incomingMessages, updatedMessages, incomingThreadMessages, updatedThreadMessages }} /> <Widget src={`${componentOwnerId}/widget/Calimero.Curb.Chat.MessageInput`} props={{ componentOwnerId, selectedChat: activeChat.type === "channel" ? activeChat.name : activeChat.id, sendMessage: handleMessageSent, resetImage: props.resetImage, threadReply: openThread?.id, }} /> </ChatContainer> );