import { useMemo, useCallback } from 'react'; import { Toast, useUIKit } from '@tencentcloud/uikit-base-component-react'; import cs from 'classnames'; import { MessageStatus, MessageType, View, useUIContext } from 'tuikit-atomicx-react/chat'; import { emojiUrlMap, emojiBaseUrl } from '../../../../constant/emoji'; import { useChatUIState } from '../../../../context/useChatUIState'; import { useMessageNavigation } from '../../../../hooks/useMessageNavigation'; import { transformTextWithEmojiKeyToName } from '../../../../utils'; import { markdownToPlainText } from '../../../../utils/markdown'; import classes from './TextMessage.module.scss'; import type { CustomMessagePayload, MessageInfo, StreamMessagePayload, TextMessagePayload, } from 'tuikit-atomicx-react/chat'; interface TextMessageProps { message: MessageInfo; } type TextNode = { name: 'text'; text: string }; type EmojiNode = { name: 'img'; src: string; emojiKey: string }; type RenderNode = TextNode | EmojiNode; const EMOJI_PATTERN = /(\[TUIEmoji_\w+\])/; function TextMessage(props: TextMessageProps) { const { message } = props; const { t } = useUIKit(); const { recalledMessageIDSet } = useChatUIState(); const { messageList } = useUIContext(); const { navigateToMessage } = useMessageNavigation(); const messagePayload = message.messagePayload as TextMessagePayload; const { quoteInfo } = message; // Render text content with inline emoji const renderContent = useMemo((): RenderNode[] => { const raw = messagePayload?.text ?? ''; return raw .split(EMOJI_PATTERN) .filter(segment => segment !== '') .map((segment): RenderNode => { const url = emojiUrlMap[segment]; if (url) { return { name: 'img', src: `${emojiBaseUrl}${url}`, emojiKey: segment }; } return { name: 'text', text: segment }; }); }, [messagePayload?.text]); // Quote preview text const quotePreview = useMemo(() => { if (!quoteInfo) { return ''; } switch (quoteInfo.messageType) { case MessageType.Text: return transformTextWithEmojiKeyToName((quoteInfo.messagePayload as TextMessagePayload)?.text || ''); case MessageType.Image: return `[${t('MessageList.image')}]`; case MessageType.Audio: return `[${t('MessageList.audio')}]`; case MessageType.Video: return `[${t('MessageList.video')}]`; case MessageType.File: return `[${t('MessageList.file')}]`; case MessageType.Face: return `[${t('MessageList.face')}]`; case MessageType.Custom: return (quoteInfo.messagePayload as CustomMessagePayload)?.description || `[${t('MessageList.custom')}]`; case MessageType.Merged: return `[${t('MessageList.merged')}]`; case MessageType.Stream: return markdownToPlainText((quoteInfo.messagePayload as StreamMessagePayload)?.markdown || '') || `[${t('MessageList.unknown')}]`; default: return `[${t('MessageList.unknown')}]`; } }, [quoteInfo, t]); const isOriginMessageRecalled = quoteInfo?.msgID ? recalledMessageIDSet.has(quoteInfo.msgID) : false; const handleReferenceClick = useCallback(() => { if (!quoteInfo?.msgID) { return; } const targetMessage = messageList?.find(item => item.msgID === quoteInfo.msgID); if (isOriginMessageRecalled || targetMessage?.status === MessageStatus.Deleted || targetMessage?.status === MessageStatus.Recalled) { Toast.error({ message: t('MessageList.origin_message_has_been_recalled'), }); return; } navigateToMessage({ messageID: quoteInfo.msgID, cursor: quoteInfo as unknown as MessageInfo, }); }, [quoteInfo, messageList, isOriginMessageRecalled, t, navigateToMessage]); return ( {quoteInfo && ( {(isOriginMessageRecalled || quoteInfo.messageType === MessageType.Unknown) ? t('MessageList.origin_message_has_been_recalled') : ( <> {quoteInfo.sender?.nickname || quoteInfo.sender?.userID} {quotePreview} )} )} {renderContent.map((item, index) => ( item.name === 'text' ? {item.text} : ( {item.emojiKey ) ))} ); } export default TextMessage;