const MessageContainer = styled.div` padding-top: 16px; position: relative; width: 100%; display: flex; flex-direction: column; justify-content: flex-start; ${({ isTemporal }) => isTemporal && "opacity: 0.5;"} @media (max-width: 1024px) { font-size: 14px; font-style: normal; font-weight: 400; padding-left: 18px; padding-right: 18px; } .ReactionsContainer { display: none; } &:hover .ReactionsContainer { display: flex; } `; const SenderInfoContainer = styled.div` width: 100%; display: flex; align-items: center; column-gap: 0.5rem; display: flex; justify-content: flex-start; `; const ProfileIconContainerMsg = styled.div` display: flex; justify-content: center; align-items: center; width: 32px; height: 32px; border-radius: 50%; ${({ id }) => id && `background-color: #111;`} text-align: center; /* Body/Small */ font-family: Helvetica Neue; font-size: 14px; font-style: normal; font-weight: 400; line-height: 150%; /* 21px */ margin-left: -8px; `; const NameContainerSender = styled.div` display: flex; justify-content: start; align-items: center; width: 100%; color: #6c757d; font-size: 14px; font-style: normal; font-weight: 400; line-height: 100%; `; const TimeText = styled.p` position: absolute; bottom: -4px; right: 0px; color: #777583; font-family: Helvetica Neue; font-size: 12px; font-style: normal; font-weight: 400; line-height: 100%; @media (max-width: 1024px) { bottom: -1rem; right: 4px; } `; const ReactionsContainer = styled.div` display: none; position: absolute; height: 26px; z-index: 30; top: -2rem; right: 1rem; column-gap: 0.5rem; font-size: 1.5rem; line-height: 1.75rem; cursor: pointer; background: #0e0e10; border-radius: 4px; padding-left: 2px; padding-right: 2px; padding-top: 2px; padding-bottom: 0px; `; const MessageText = styled.div` width: 100%; position: relative; word-wrap: break-word; display: flex; flex-direction: column; padding-top: 0px; padding-left: 2rem; padding-right: 1rem; padding-bottom: 1rem; color: #fff; font-family: Helvetica Neue; font-size: 16px; font-style: normal; font-weight: 400; line-height: 150%; @media (max-width: 1024px) { max-width: 320px; font-size: 14px; font-style: normal; font-weight: 400; } iframe { height: 38px; } `; const EmojiContainer = styled.p` height: 24px; width: 24px; border-radius: 4px; :hover { background-color: #5765f2; } text-align: center; display: flex; justify-content: center; align-items: center; `; const ReactedContainer = styled.div` display: flex; position: relative; bottom: 0rem; left: 0rem; column-gap: 0.5rem; font-size: 1rem; line-height: 1rem; cursor: pointer; border-radius: 4px; padding-left: 2px; padding-right: 2px; padding-top: 2px; padding-bottom: 2px; `; const EmojiReactedContainer = styled.div` position: relative; `; const EmojiCountContainer = styled.div` font-size: 0.75rem; line-height: 1rem; position: absolute; bottom: -0.7rem; left: 0.5rem; background-color: #1d1d21; width: 1rem; height: 1rem; border-radius: 50%; text-align: center; `; const ImageContainer = styled.img` padding-top: 8px; border-radius: 4px; max-height: 400px; max-width: 400px; object-fit: contain; @media (max-width: 1024px) { max-height: 300px; max-width: 300px; } `; const reactionsArray = [ { emoji: "😠", title: "Angry Face", }, { emoji: "❤️", title: "Red Heart", }, { emoji: "😀", title: "Grinning Face", }, { emoji: "😂", title: "Face with Tears of Joy", }, { emoji: "👍", title: "Thumbs Up", }, { emoji: "👎", title: "Thumbs Down", }, { emoji: "✅", title: "Check Mark Button", }, ]; const formatTimeAgo = (timestampInSeconds, flag) => { const now = new Date(); const timestamp = new Date(timestampInSeconds * 1000); // Convert seconds to milliseconds // Check if the timestamp is from today if ( timestamp.getDate() === now.getDate() && timestamp.getMonth() === now.getMonth() && timestamp.getFullYear() === now.getFullYear() ) { // Format as HH:mm (24-hour format) const hours = timestamp.getHours().toString().padStart(2, "0"); const minutes = timestamp.getMinutes().toString().padStart(2, "0"); return flag ? `today at ${hours}:${minutes}` : `${hours}:${minutes}`; } // Check if the timestamp is from yesterday const yesterday = new Date(now); yesterday.setDate(now.getDate() - 1); if ( timestamp.getDate() === yesterday.getDate() && timestamp.getMonth() === yesterday.getMonth() && timestamp.getFullYear() === yesterday.getFullYear() ) { return "yesterday"; } // If not today or yesterday, format as DD/MM/YYYY const day = timestamp.getDate().toString().padStart(2, "0"); const month = (timestamp.getMonth() + 1).toString().padStart(2, "0"); // Month is 0-based const year = timestamp.getFullYear(); return `${day}/${month}/${year}`; }; const parseReactions = (reactions) => { if (!reactions) { return []; } return Object.keys(reactions).map((reaction) => { const accountsForReaction = Array.isArray(reactions[reaction]) ? reactions[reaction] : []; return { reaction, accounts: accountsForReaction, }; }); }; const [messageReactions, setMessageReactions] = useState( parseReactions(props.message.reactions) ); useEffect(() => { const newReactions = parseReactions(props.message.reactions); if (newReactions !== messageReactions) { setMessageReactions(newReactions); } }, [props.message.reactions]); const handleNewReaction = (reaction) => { const emoji = reaction.emoji; let newMessageReactions = [...messageReactions]; const existingReactionIndex = newMessageReactions.findIndex( (r) => r.reaction === emoji ); if (existingReactionIndex !== -1) { const existingReaction = newMessageReactions[existingReactionIndex]; const hasUserReacted = existingReaction.accounts.includes( context.accountId ); if (hasUserReacted) { existingReaction.accounts = existingReaction.accounts.filter( (account) => account !== context.accountId ); if (!existingReaction.accounts.length) { newMessageReactions.splice(existingReactionIndex, 1); } } else { existingReaction.accounts.push(context.accountId); } } else { newMessageReactions.push({ reaction: emoji, accounts: [context.accountId], }); } setMessageReactions(newMessageReactions); props.addMessageReaction({ message_id: props.message.id, reaction: emoji, }); }; const text = props.message.text; const isTemporal = props.message.isTemporal; const overLayContainer = styled.div` z-index: 40; display: flex; text-align: center; padding-left: 0.5rem; padding-right: 0.5rem; padding-top: 0.25rem; padding-bottom: 0.25rem; background-color: #0e0e10; border: 1px solid #777583; border-radius: 0.375rem; font-size: 16px; padding-left: 24px; margin-top: 0.725rem; color: #fff; `; const MessageUrls = styled.a` cursor: pointer; text-decoration: none; color: #ffdd1d; cursor: pointer; :hover { color: #ffdd1d; text-decoration: underline; } :visited { color: #d0fc42; } `; const markdownParser = (text) => { const toHTML = text .replace(/^##### (.*$)/gim, "$1") // h5 tag .replace(/^#### (.*$)/gim, "$1") // h4 tag .replace(/^### (.*$)/gim, "$1") // h3 tag .replace(/^## (.*$)/gim, "$1") // h2 tag .replace(/^# (.*$)/gim, "$1") // h1 tag .replace(/\*\*(.*)\*\*/gim, "<b>$1</b>") // bold text .replace(/\*(.*)\*/gim, "<i>$1</i>") // italic text .replace(/^- (.*)$/gim, "<li>$1</li>"); // list item return toHTML.trim(); // using trim method to remove whitespace }; const storageKey = "lastReportedMessage"; const oldReportedId = Storage.privateGet(storageKey); let finalText = ""; let ipfsImage = null; let ipfsFile = null; let ipfsFileName = null; if (text) { let normalText = ""; if (text.split("$?$").length > 1) { normalText = text.split("$?$")[0]; } else if (text.split("@?$@").length > 1) { normalText = text.split("@?$@")[0]; } else { normalText = text; } ipfsImage = text.split("$?$").length > 1 ? text.split("$?$")[1] : null; ipfsFile = text.split("@?$@").length > 1 ? text.split("@?$@")[1].split("?fileName=")[0] : null; if (ipfsFile) { ipfsFileName = text.split("@?$@")[1].split("?fileName=")[1]; } const mentionRegex = /@([a-zA-Z0-9_.-]+(?<![-.0-9])(?:\.testnet|\.near))/g; const urlRegex = /(https?:\/\/[^\s]+)/g; const replacedMentionsText = normalText .replace(mentionRegex, (_match, username) => { return `<span class="mention" style="${ username === context.accountId ? "color: #FEAE37; background-color: #83522F" : "color: #fff; background-color: #343C93" }">@${username}</span>`; }) .replace( "@here", "<span class='mention' style='color: #fff; background-color: #343C93'>@here</span>" ); const replacedUrlText = replacedMentionsText.replace( urlRegex, (_match, url) => { return `<a href=${url} class="url-link" target="_blank">${url}</a>`; } ); finalText = ` <style> .url-link { cursor: pointer; text-decoration: none; color: #ffdd1d; cursor: pointer; } .url-link:hover { color: #ffdd1d; text-decoration: underline; } .url-link:visited { color: #d0fc42; } .text-container { color: #fff; font-family: Helvetica Neue; font-size: 16px; font-style: normal; font-weight: 400; line-height: 150%; } </style> <script> const handleMessage = (text) => { document.getElementById("text-container").innerHTML = text; }; window.iFrameResizer = { onMessage: handleMessage } </script> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.6/iframeResizer.contentWindow.js"></script> <div class="text-container"> ${markdownParser(replacedUrlText)} </div> `; } const overlay = <overLayContainer>Reply in thread</overLayContainer>; return ( text && ( <> <MessageContainer isTemporal={!!props.message.isTemporal}> <SenderInfoContainer> <div> <ProfileIconContainerMsg> <Widget src={`${props.componentOwnerId}/widget/Calimero.Curb.ProfileIcon.UserProfileIcon`} props={{ accountId: props.message.sender, showStatus: false, componentOwnerId: props.componentOwnerId, }} /> </ProfileIconContainerMsg> </div> <div> <NameContainerSender>{props.message.sender}</NameContainerSender> </div> <TimeText> {formatTimeAgo(props.message.timestamp / 1000, false)} </TimeText> </SenderInfoContainer> <MessageText ownMessage={props.message.sender === context.accountId} clickable={text?.split("$?$").length > 1} > <iframe iframeResizer srcDoc={finalText} class="styled-iframe" className="w-100" /> <ReactionsContainer className="ReactionsContainer"> {reactionsArray && ( <> {reactionsArray?.map((reaction, id) => ( <EmojiContainer onClick={() => handleNewReaction(reaction)} key={id} > {reaction.emoji} </EmojiContainer> ))} {!props.isThread && ( <EmojiContainer onClick={() => props.setThread(props.message.id)} > <OverlayTrigger show={state.show} trigger={["hover", "focus"]} delay={{ show: 250, hide: 300 }} placement="auto" overlay={overlay} > <i className="bi bi-reply text-light"></i> </OverlayTrigger> </EmojiContainer> )} </> )} </ReactionsContainer> {ipfsImage && ( <ImageContainer src={ipfsImage} alt="uploaded" onClick={() => { if (ipfsImage) { props.setImage(ipfsImage); } }} /> )} {ipfsFile && ( <div className="d-flex gap-1"> <i class="bi bi-file-earmark-arrow-down-fill text-light"></i> <MessageUrls href={ipfsFile} target="_blank"> {ipfsFileName} </MessageUrls> </div> )} {messageReactions.length > 0 && ( <ReactedContainer> {messageReactions?.map((reaction, id) => ( <> {reaction.accounts.length > 0 && ( <EmojiReactedContainer key={id}> {reaction.reaction} <EmojiCountContainer> {reaction.accounts.length.toString()} </EmojiCountContainer> </EmojiReactedContainer> )} </> ))} </ReactedContainer> )} </MessageText> </MessageContainer> {props.message.thread.length > 0 && ( <Widget src={`${props.componentOwnerId}/widget/Calimero.Curb.Chat.ReplyContainerButton`} props={{ replyCount: props.message.thread.length, onClick: () => { props.setThread(props.message.id); }, time: formatTimeAgo( props.message.thread[props.message.thread.length - 1].timestamp / 1000, true ), }} /> )} {oldReportedId === props.message.id && props.lastMessageId !== oldReportedId && props.message.sender !== context.accountId && ( <Widget src={`${props.componentOwnerId}/widget/Calimero.Curb.Chat.UnreadBadge`} /> )} </> ) );