import React, { useCallback, useMemo, useState, useRef } from "react"; import { Avatar, Box, Hide, HStack, Icon, Skeleton, Text, Tooltip, useColorModeValue, VStack, useOutsideClick, Flex, } from "@chakra-ui/react"; import { useWallet } from "@solana/wallet-adapter-react"; import { MessageType } from "@strata-foundation/chat"; import { useErrorHandler, useMint, useTokenMetadata, } from "@strata-foundation/react"; import { humanReadable, toNumber } from "@strata-foundation/spl-utils"; import moment from "moment"; import { useAsync } from "react-async-hook"; import { BsLockFill } from "react-icons/bs"; import { BuyMoreButton } from "../BuyMoreButton"; import { useEmojis } from "../../contexts/emojis"; import { useSendMessage } from "../../contexts/sendMessage"; import { useReply } from "../../contexts/reply"; import { IMessageWithPendingAndReacts } from "../../hooks/useMessages"; import { useWalletProfile } from "../../hooks/useWalletProfile"; import { useChatOwnedAmounts } from "../../hooks/useChatOwnedAmounts"; import { useChatPermissionsFromChat } from "../../hooks/useChatPermissionsFromChat"; import { MessageBody } from "./MessageBody"; import { MessageToolbar } from "./MessageToolbar"; import { DisplayReply } from "./DisplayReply"; import { MessageHeader } from "./MessageHeader"; import { Reacts } from "./Reacts"; import { MessageStatus } from "./MessageStatus"; const defaultOptions = { allowedTags: ["b", "i", "em", "strong", "a", "code", "ul", "li", "p"], allowedAttributes: { a: ["href", "target"], }, }; export function Message( props: Partial & { htmlAllowlist?: any; pending?: boolean; showUser: boolean; scrollToMessage: (id: string) => void; } ) { const ref = useRef(); const [isActive, setIsActive] = useState(false); useOutsideClick({ ref: ref, handler: () => setIsActive(false), }); const { id: messageId, getDecodedMessage, sender, readPermissionAmount, chatKey, txids, startBlockTime, htmlAllowlist = defaultOptions, reacts, type: messageType, showUser = true, pending = false, reply, scrollToMessage, } = props; const { publicKey } = useWallet(); const { referenceMessageId: emojiReferenceMessageId, showPicker } = useEmojis(); const { info: profile } = useWalletProfile(sender); const { info: chatPermissions } = useChatPermissionsFromChat(chatKey); const time = useMemo(() => { if (startBlockTime) { const t = new Date(0); t.setUTCSeconds(startBlockTime); return t; } }, [startBlockTime]); const readMint = chatPermissions?.readPermissionKey; const mintAcc = useMint(readMint); const { metadata } = useTokenMetadata(readMint); const tokenAmount = mintAcc && readPermissionAmount && humanReadable(readPermissionAmount, mintAcc); const { ownedReadAmount, ownedPostAmount } = useChatOwnedAmounts( publicKey || undefined, chatKey ); const notEnoughTokens = useMemo(() => { return ( readPermissionAmount && mintAcc && (ownedReadAmount || 0) < toNumber(readPermissionAmount, mintAcc) ); }, [readPermissionAmount, mintAcc, ownedReadAmount]); // Re decode if not enough tokens changes const getDecodedMessageOrIdentity = (_: boolean) => getDecodedMessage ? getDecodedMessage() : Promise.resolve(undefined); const { result: message, loading: decoding, error: decodeError, } = useAsync(getDecodedMessageOrIdentity, [notEnoughTokens]); const lockedColor = useColorModeValue("gray.400", "gray.600"); const highlightedBg = useColorModeValue("gray.200", "gray.800"); const { handleErrors } = useErrorHandler(); const { sendMessage, error } = useSendMessage(); handleErrors(error, decodeError); const handleOnReaction = useCallback(() => { showPicker(messageId); }, [showPicker, messageId]); const { replyMessage } = useReply(); const bg = useMemo( () => messageId === emojiReferenceMessageId || messageId === replyMessage?.id || isActive ? highlightedBg : "initial", [ highlightedBg, emojiReferenceMessageId, messageId, replyMessage?.id, isActive, ] ); const textColor = useColorModeValue("black", "white"); const loadingSkeleton = useMemo(() => { return ( {Array.from({ length: genLength(messageId || "") }, () => ".").join()} ); }, [messageId, lockedColor]); const buyMoreTrigger = useCallback( (props: any) => { return ( {Array.from( { length: genLength(messageId || "") }, () => "." ).join()} ); }, [tokenAmount, metadata, lockedColor, messageId] ); // LEGACY: If this is a reaction before message types were stored on the top level instead of json if (message?.type === MessageType.React) { return null; } return ( setIsActive(true)} onMouseLeave={() => setIsActive(false)} onClick={() => setIsActive(true)} position="relative" > {isActive && ( e.preventDefault()} > )} {reply && ( )} {showUser ? ( ) : ( )} {showUser && ( )} {!notEnoughTokens && message && messageType ? ( ) : decoding ? ( loadingSkeleton ) : notEnoughTokens ? ( ) : ( {Array.from( { length: genLength(messageId || "") }, () => "." ).join()} )} {reacts && reacts.length > 0 && ( { if (!mine) sendMessage({ message: { type: MessageType.React, emoji: emoji, referenceMessageId: messageId, }, }); }} /> )} {showUser && ( {moment(time).format("LT")} )} ); } const lengths: Record = {}; function genLength(id: string): number { if (!lengths[id]) { lengths[id] = 10 + Math.random() * 100; } return lengths[id]; } export const MemodMessage = React.memo(Message);