import { memo, useCallback, useContext, useEffect, useMemo, useState, } from 'react'; import { useInView } from 'react-intersection-observer'; import { MessageDisplay } from './MessageDisplay'; import { Avatar, Box, Button, ButtonBase, List, ListItem, ListItemText, Popover, Tooltip, Typography, useTheme, } from '@mui/material'; import { formatTimestamp } from '../../utils/time'; import { QORTAL_APP_CONTEXT, getBaseApiReact } from '../../App'; import { generateHTML } from '@tiptap/react'; import Highlight from '@tiptap/extension-highlight'; import Mention from '@tiptap/extension-mention'; import StarterKit from '@tiptap/starter-kit'; import Underline from '@tiptap/extension-underline'; import { WrapperUserAction } from '../WrapperUserAction'; import ReplyIcon from '@mui/icons-material/Reply'; import { Spacer } from '../../common/Spacer'; import { ReactionPicker } from '../ReactionPicker'; import KeyOffIcon from '@mui/icons-material/KeyOff'; import EditIcon from '@mui/icons-material/Edit'; import TextStyle from '@tiptap/extension-text-style'; import level0Img from '../../assets/badges/level-0.png'; import level1Img from '../../assets/badges/level-1.png'; import level2Img from '../../assets/badges/level-2.png'; import level3Img from '../../assets/badges/level-3.png'; import level4Img from '../../assets/badges/level-4.png'; import level5Img from '../../assets/badges/level-5.png'; import level6Img from '../../assets/badges/level-6.png'; import level7Img from '../../assets/badges/level-7.png'; import level8Img from '../../assets/badges/level-8.png'; import level9Img from '../../assets/badges/level-9.png'; import level10Img from '../../assets/badges/level-10.png'; import { Embed } from '../Embeds/Embed'; import CommentsDisabledIcon from '@mui/icons-material/CommentsDisabled'; import { buildImageEmbedLink, isHtmlString, messageHasImage, } from '../../utils/chat'; import { useTranslation } from 'react-i18next'; import { ReactionsMap } from './ChatList'; const getBadgeImg = (level) => { switch (level?.toString()) { case '0': return level0Img; case '1': return level1Img; case '2': return level2Img; case '3': return level3Img; case '4': return level4Img; case '5': return level5Img; case '6': return level6Img; case '7': return level7Img; case '8': return level8Img; case '9': return level9Img; case '10': return level10Img; default: return level0Img; } }; const UserBadge = memo(({ userInfo }) => { return ( ); }); type MessageItemProps = { handleReaction: (reaction: string, messageId: string) => void; isLast: boolean; isPrivate: boolean; isShowingAsReply?: boolean; isTemp: boolean; isUpdating: boolean; lastSignature: string; message: string; myAddress: string; onEdit: (messageId: string) => void; onReply: (messageId: string) => void; onSeen: () => void; reactions: ReactionsMap | null; reply: string | null; replyIndex: number; scrollToItem: (index: number) => void; }; export const MessageItemComponent = ({ handleReaction, isLast, isPrivate, isShowingAsReply, isTemp, isUpdating, lastSignature, message, myAddress, onEdit, onReply, onSeen, reactions, reply, replyIndex, scrollToItem, }: MessageItemProps) => { const { getIndividualUserInfo } = useContext(QORTAL_APP_CONTEXT); const [anchorEl, setAnchorEl] = useState(null); const [selectedReaction, setSelectedReaction] = useState(null); const [userInfo, setUserInfo] = useState(null); useEffect(() => { const getInfo = async () => { if (!message?.sender) return; try { const res = await getIndividualUserInfo(message?.sender); if (!res) return null; setUserInfo(res); } catch (error) { // } }; getInfo(); }, [message?.sender, getIndividualUserInfo]); const htmlText = useMemo(() => { if (message?.messageText) { const isHtml = isHtmlString(message?.messageText); if (isHtml) return message?.messageText; return generateHTML(message?.messageText, [ StarterKit, Underline, Highlight, Mention, TextStyle, ]); } }, [message?.editTimestamp]); const htmlReply = useMemo(() => { if (reply?.messageText) { const isHtml = isHtmlString(reply?.messageText); if (isHtml) return reply?.messageText; return generateHTML(reply?.messageText, [ StarterKit, Underline, Highlight, Mention, TextStyle, ]); } }, [reply?.editTimestamp]); const userAvatarUrl = useMemo(() => { return message?.senderName ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${ message?.senderName }/qortal_avatar?async=true` : ''; }, []); const onSeenFunc = useCallback(() => { onSeen(message.id); }, [message?.id]); const theme = useTheme(); const { t } = useTranslation([ 'auth', 'core', 'group', 'question', 'tutorial', ]); const hasNoMessage = (!message.decryptedData?.data?.message || message.decryptedData?.data?.message === '

') && (message?.images || [])?.length === 0 && (!message?.messageText || message?.messageText === '

') && (!message?.text || message?.text === '

'); return ( <> {message?.divide && (
{t('core:message.generic.unread_messages', { postProcess: 'capitalizeFirstChar', })}
)}
{isShowingAsReply ? ( ) : ( {message?.senderName?.charAt(0)} )} {message?.senderName || message?.sender} {message?.sender === myAddress && (!message?.isNotEncrypted || isPrivate === false) && ( { onEdit(message); }} > )} {!isShowingAsReply && ( { onReply(message); }} > )} {!isShowingAsReply && handleReaction && ( { if ( reactions && reactions[val] && reactions[val]?.find( (item) => item?.sender === myAddress ) ) { handleReaction(val, message, false); } else { handleReaction(val, message, true); } }} /> )} {reply && ( <> { scrollToItem(replyIndex); }} > {t('core:message.generic.replied_to', { person: reply?.senderName || reply?.senderAddress, postProcess: 'capitalizeFirstChar', })} {reply?.messageText && ( )} {reply?.decryptedData?.type === 'notification' ? ( ) : ( )} )} {htmlText && !hasNoMessage && ( )} {message?.decryptedData?.type === 'notification' ? ( ) : hasNoMessage ? null : ( )} {hasNoMessage && ( {t('core:message.generic.no_message', { postProcess: 'capitalizeFirstChar', })} )} {message?.images && messageHasImage(message) && ( )} {reactions && Object.keys(reactions).map((reaction) => { const numberOfReactions = reactions[reaction]?.length; if (numberOfReactions === 0) return null; return ( { event.stopPropagation(); // Prevent event bubbling setAnchorEl(event.currentTarget); setSelectedReaction(reaction); }} >
{reaction}
{' '} {numberOfReactions > 1 && ( {numberOfReactions} )}
); })}
{selectedReaction && ( { setAnchorEl(null); setSelectedReaction(null); }} anchorOrigin={{ vertical: 'top', horizontal: 'center', }} transformOrigin={{ vertical: 'bottom', horizontal: 'center', }} slotProps={{ paper: { style: { backgroundColor: theme.palette.background.default, color: theme.palette.text.primary, }, }, }} > {t('core:message.generic.people_reaction', { reaction: selectedReaction, postProcess: 'capitalizeFirstChar', })} {reactions[selectedReaction]?.map((reactionItem) => ( ))} )} {message?.isNotEncrypted && isPrivate && ( )} {isUpdating ? ( {message?.status === 'failed-permanent' ? t('core:message.error.update_failed', { postProcess: 'capitalizeFirstChar', }) : t('core:message.generic.updating', { postProcess: 'capitalizeFirstChar', })} ) : isTemp ? ( {message?.status === 'failed-permanent' ? t('core:message.error.send_failed', { postProcess: 'capitalizeFirstChar', }) : t('core:message.generic.sending', { postProcess: 'capitalizeFirstChar', })} ) : ( <> {message?.isEdit && ( {t('core:message.generic.edited', { postProcess: 'capitalizeFirstChar', })} )} {formatTimestamp(message.timestamp)} )}
); }; const MemoizedMessageItem = memo(MessageItemComponent); MemoizedMessageItem.displayName = 'MessageItem'; // It ensures React DevTools shows MessageItem as the name (instead of "Anonymous" or "Memo") export const MessageItem = MemoizedMessageItem; export const ReplyPreview = ({ message, isEdit = false }) => { const theme = useTheme(); const { t } = useTranslation([ 'auth', 'core', 'group', 'question', 'tutorial', ]); const replyMessageText = useMemo(() => { if (!message?.messageText) return null; const isHtml = isHtmlString(message?.messageText); if (isHtml) return message?.messageText; return generateHTML(message?.messageText, [ StarterKit, Underline, Highlight, Mention, TextStyle, ]); }, [message?.messageText]); return ( {isEdit ? ( {t('core:message.generic.editing_message', { postProcess: 'capitalizeFirstChar', })} ) : ( {t('core:message.generic.replied_to', { person: message?.senderName || message?.senderAddress, postProcess: 'capitalizeFirstChar', })} )} {replyMessageText && } {message?.decryptedData?.type === 'notification' ? ( ) : ( )} ); }; const MessageWragger = ({ lastMessage, onSeen, isLast, children }) => { if (lastMessage) { return ( {children} ); } return children; }; const WatchComponent = ({ onSeen, isLast, children }) => { const { ref, inView } = useInView({ threshold: 0.7, // Fully visible triggerOnce: true, // Only trigger once when it becomes visible delay: 100, trackVisibility: false, }); useEffect(() => { if (inView && isLast && onSeen) { setTimeout(() => { onSeen(); }, 100); } }, [inView, isLast, onSeen]); return (
{children}
); };