import * as React from 'react'; import { Animated, Dimensions, Easing, GestureResponderEvent, Linking, Platform, // Image as RNImage, Pressable, StyleSheet, View, } from 'react-native'; import { ICON_ASSETS, IconNameType } from '../../assets'; import { gCustomMessageCardEventType, gMessageAttributeQuote, gMessageAttributeTranslate, gMessageAttributeUrlPreview, } from '../../chat'; import { userInfoFromMessage } from '../../chat/utils'; import { useConfigContext } from '../../config'; import { useColors, useGetStyleProps } from '../../hook'; import { useI18nContext } from '../../i18n'; import { ChatCombineMessageBody, ChatCustomMessageBody, ChatFileMessageBody, ChatImageMessageBody, ChatMessage, ChatMessageChatType, ChatMessageType, ChatTextMessageBody, ChatVideoMessageBody, ChatVoiceMessageBody, } from '../../rename.chat'; import { usePaletteContext, useThemeContext } from '../../theme'; import { VoiceImageAnimation } from '../../ui/Animated'; import { DefaultImage, Icon, LoadingIcon } from '../../ui/Image'; import { HighUrl, SingleLineText, Text } from '../../ui/Text'; import { formatTsForConvDetail } from '../../utils'; import { Avatar } from '../Avatar'; import { gMaxVoiceDuration } from '../const'; import { useMessageSnapshot } from '../hooks/useMessageSnapshot'; import { useUrlPreview } from '../hooks/useUrlPreview'; import { gMessageAvatarSize, gTriangleWidth } from './const'; // NOTE: Do NOT use static import here to avoid circular dependency: // MessageListItem.tsx -> MessageHistoryListItem.tsx -> MessageListItem.tsx // This circular dependency can cause Hermes engine scope resolution issues, // leading to 'ReferenceError: Property MessageViewWrapper doesn\'t exist'. // Use lazy require() instead to break the cycle. import { getFileSize, getImageShowSize, getImageThumbUrl, getMessageBubblePadding, getMessageState, getPaddingWidth, getStateIcon, getStateIconColor, getVideoThumbUrl, isQuoteMessage, isSupportMessage, useSystemTip, } from './MessageListItem.hooks'; import type { AvatarViewProps, CheckViewProps, MessageBubbleProps, MessageCombineProps, MessageContentProps, MessageCustomCardProps, MessageDefaultImageProps, MessageFileProps, MessageImageProps, MessageQuoteBubbleProps, MessageReactionProps, MessageTextProps, MessageThreadBubbleProps, MessageVideoProps, MessageViewProps, MessageVoiceProps, NameViewProps, StateViewProps, SystemTipViewProps, TimeTipViewProps, TimeViewProps, } from './MessageListItem.type'; import type { MessageEditableStateType, MessageHistoryModel, MessageListItemProps, MessageModel, SystemMessageModel, TimeMessageModel, } from './types'; export function MessageText(props: MessageTextProps) { const { layoutType, msg, isSupport, onLongPress } = props; const { tr } = useI18nContext(); const { colors } = usePaletteContext(); const { getUrlListFromText } = useUrlPreview(); const { getColor } = useColors({ left_text: { light: colors.neutral[1], dark: colors.neutral[1], }, left_url_text: { light: colors.primary[5], dark: colors.primary[5], }, right_text: { light: colors.neutral[98], dark: colors.neutral[98], }, left_text_flag: { light: colors.neutralSpecial[5], dark: colors.neutralSpecial[7], }, right_text_flag: { light: colors.neutral[98], dark: colors.neutral[1], }, left_divider: { light: colors.neutralSpecial[8], dark: colors.primary[6], }, right_divider: { light: colors.primary[8], dark: colors.primary[6], }, url_text: { light: colors.neutral[1], dark: colors.neutral[98], }, url_bg: { light: colors.neutral[95], dark: colors.neutral[2], }, url_image_bg: { light: 'rgba(0, 0, 0, 0.05)', dark: 'rgba(0, 0, 0, 0.05)', }, url_parsing: { light: colors.neutral[5], dark: colors.neutral[7], }, }); const body = msg.body as ChatTextMessageBody; // const content = emoji.toCodePointText(body.content); let content = body.content; const editable = body.modifyCount !== undefined && body.modifyCount > 0 ? 'edited' : ('no-editable' as MessageEditableStateType); if (isSupport !== true) { content = tr('_uikit_msg_tip_not_support'); } // const codes = body.targetLanguageCodes; // const translated = codes && codes?.length > 0; const translatedContent = body.translations && body.targetLanguageCodes && body.targetLanguageCodes?.length > 0 ? body.translations[body.targetLanguageCodes[0]!] : undefined; const translated = msg.attributes?.[gMessageAttributeTranslate] as | boolean | undefined; const urlPreview = msg.attributes?.[gMessageAttributeUrlPreview] as { url: string; title: string | undefined; description: string | undefined; imageUrl: string | undefined; }; const urls = getUrlListFromText(content); return ( {urls && urls.length >= 1 ? ( { const _url = url.startsWith('http') ? url : `https://${url}`; Linking.openURL(_url); }} onLongPress={onLongPress} /> ) : ( {content} )} {editable === 'edited' ? ( {tr('_uikit_msg_edit')} ) : null} {translated === true ? ( ) : null} {translated === true ? ( {}}> {translatedContent} ) : null} {translated === true ? ( {tr('_uikit_msg_translate')} ) : null} {urlPreview ? ( urlPreview.title ? ( {urlPreview.imageUrl ? ( ) : ( )} {urlPreview.title} {urlPreview.description} ) : null ) : urls && urls.length === 1 ? ( {tr('_uikit_message_url_parsing')} ) : null} ); } export function MessageCombine(props: MessageCombineProps) { const { layoutType, msg } = props; const { tr } = useI18nContext(); const { colors } = usePaletteContext(); const { getColor } = useColors({ left_text: { light: colors.neutral[1], dark: colors.neutral[1], }, right_text: { light: colors.neutral[98], dark: colors.neutral[98], }, left_text_flag: { light: colors.neutralSpecial[5], dark: colors.neutralSpecial[7], }, right_text_flag: { light: colors.neutral[98], dark: colors.neutral[1], }, left_divider: { light: colors.neutralSpecial[8], dark: colors.primary[6], }, right_divider: { light: colors.primary[8], dark: colors.primary[6], }, }); const body = msg.body as ChatCombineMessageBody; // const content = emoji.toCodePointText(body.content); let content = body.summary; return ( {content} {tr('_uikit_msg_record')} ); } export function MessageDefaultImage(props: MessageDefaultImageProps) { const { url, width, height, thumbHeight, thumbWidth, iconName, onError, containerStyle, } = props; const { colors, cornerRadius } = usePaletteContext(); const { cornerRadius: corner } = useThemeContext(); const { getBorderRadius } = useGetStyleProps(); const { releaseArea } = useConfigContext(); const { getColor } = useColors({ thumb: { light: colors.neutral[7], dark: colors.neutral[2], }, border: { light: colors.neutral[9], dark: colors.neutral[3], }, }); return ( ); } export function MessageImage(props: MessageImageProps) { const { msg, maxWidth } = props; // const url1 = // '/storage/emulated/0/Android/data/com.hyphenate.rn.ChatUikitExample/1135220126133718#demo/files/asterisk003/asterisk001/53e8d540-a144-11ee-a811-ab4c303d7025.jpg'; // const url3 = // 'file:///storage/emulated/0/Android/data/com.hyphenate.rn.ChatUikitExample/1135220126133718%23demo/files/asterisk003/asterisk001/53e8d540-a144-11ee-a811-ab4c303d7025.jpg'; // const url5 = // 'file:///storage/emulated/0/Android/data/com.hyphenate.rn.ChatUikitExample/1135220126133718#demo/files/asterisk003/asterisk001/53e8d540-a144-11ee-a811-ab4c303d7025.jpg'; // const url2 = // '/var/mobile/Containers/Data/Application/CC0AD493-D627-463B-B351-44500E6FB1E2/tmp/AD1256B8-B32C-4CFE-B5F5-ECA21662B4E8.jpg'; const [thumbUrl, setThumbUrl] = React.useState(undefined); const { width, height } = getImageShowSize(msg, maxWidth); React.useEffect(() => { msg.status; getImageThumbUrl(msg) .then((url) => { setThumbUrl(url); }) .catch(); }, [msg, msg.status]); return ( ); } export function MessageVoice(props: MessageVoiceProps) { const { msg, layoutType, isPlay: propsIsPlay = false, maxWidth: propsMaxWidth, } = props; const body = msg.body as ChatVoiceMessageBody; const { duration: propsDuration } = body; const safeDuration = propsDuration > 60 ? 60 : propsDuration < 1 ? 1 : propsDuration; const duration = safeDuration * 1000; const maxWidth = propsMaxWidth ?? Dimensions.get('window').width * 0.6; const minWidth = Dimensions.get('window').width * 0.1; const width = Math.floor(((maxWidth - minWidth) * duration) / gMaxVoiceDuration) + minWidth; const { colors } = usePaletteContext(); const { getColor } = useColors({ left_voice: { light: colors.neutralSpecial[5], dark: colors.neutralSpecial[6], }, right_voice: { light: colors.neutral[98], dark: colors.neutral[95], }, left_second: { light: colors.neutral[1], dark: colors.neutral[98], }, right_second: { light: colors.neutral[98], dark: colors.neutral[1], }, }); const seconds = safeDuration; return ( {`${seconds}"`} ); } export function MessageVideo(props: MessageVideoProps) { const { msg, maxWidth } = props; const [thumbUrl, setThumbUrl] = React.useState(); const { width, height } = getImageShowSize(msg, maxWidth); const [showTriangle, setShowTriangle] = React.useState(true); const { colors } = usePaletteContext(); const { getColor } = useColors({ video: { light: colors.neutral[98], dark: colors.neutral[95], }, }); React.useEffect(() => { msg.status; getVideoThumbUrl(msg) .then((url) => { if (url) { setThumbUrl(url); } }) .catch(); }, [msg, msg.status]); return ( { setShowTriangle(false); }} /> {showTriangle === true ? ( ) : null} ); } export function MessageFile(props: MessageFileProps) { const { msg, maxWidth, layoutType } = props; const body = msg.body as ChatFileMessageBody; const fileName = body.displayName; const fileSize = React.useMemo( () => getFileSize(body.fileSize), [body.fileSize] ); const { colors, cornerRadius } = usePaletteContext(); const { cornerRadius: corner } = useThemeContext(); const { getBorderRadius } = useGetStyleProps(); const { releaseArea } = useConfigContext(); const { getColor } = useColors({ left_file_bg: { light: colors.neutral[100], dark: colors.neutral[6], }, right_file_bg: { light: colors.neutral[100], dark: colors.neutral[6], }, left_file_fg: { light: colors.neutral[7], dark: colors.neutral[6], }, right_file_fg: { light: colors.neutral[7], dark: colors.neutral[6], }, left_name: { light: colors.neutral[1], dark: colors.neutral[98], }, right_name: { light: colors.neutral[98], dark: colors.neutral[98], }, left_size: { light: colors.neutralSpecial[5], dark: colors.neutralSpecial[6], }, right_size: { light: colors.neutral[95], dark: colors.neutralSpecial[6], }, }); return ( {layoutType !== 'right' ? null : } {fileName} {fileSize} {layoutType === 'left' ? : null} ); } export function MessageCustomCard(props: MessageCustomCardProps) { const { msg, maxWidth, layoutType } = props; const body = msg.body as ChatCustomMessageBody; const avatar = body.params?.avatar; const userId = body.params?.userId; const userName = body.params?.nickname; const { tr } = useI18nContext(); const { colors } = usePaletteContext(); const { getColor } = useColors({ left_divider: { light: colors.neutralSpecial[8], dark: colors.primary[6], }, right_divider: { light: colors.primary[8], dark: colors.primary[6], }, left_name: { light: colors.neutral[1], dark: colors.neutral[98], }, right_name: { light: colors.neutral[98], dark: colors.neutral[98], }, left_name_small: { light: colors.neutralSpecial[5], dark: colors.neutralSpecial[3], }, right_name_small: { light: colors.neutral[95], dark: colors.neutralSpecial[7], }, }); return ( {userName ?? userId} {tr('_uikit_msg_custom_card')} ); } export function MessageContent(props: MessageContentProps) { const { msg, isSupport, layoutType, contentMaxWidth, isVoicePlaying, ...others } = props; if (isSupport === true) { switch (msg.body.type) { case ChatMessageType.TXT: { return ( ); } case ChatMessageType.IMAGE: { return ( ); } case ChatMessageType.VOICE: { return ( ); } case ChatMessageType.VIDEO: { return ( ); } case ChatMessageType.FILE: { return ( ); } case ChatMessageType.COMBINE: { return ( ); } case ChatMessageType.CUSTOM: { const body = msg.body as ChatCustomMessageBody; if (body.event === gCustomMessageCardEventType) { return ( ); } return ( ); } default: { return ( ); } } } else { return ( ); } } export function MessageBubble(props: MessageBubbleProps) { const { hasTriangle = true, model, containerStyle, onClicked, onLongPress, maxWidth, MessageContent: propsMessageContent, onClickedChecked, } = props; const checked = (model as MessageModel)?.checked; const _MessageContent = propsMessageContent ?? (MessageContent as any); const { layoutType, msg, isVoicePlaying, quoteMsg, thread: threadMsg, } = model; const touchRef = React.useRef>({} as any); const { releaseArea } = useConfigContext(); const { paddingHorizontal, paddingVertical, hasBorderRadius } = React.useMemo( () => getMessageBubblePadding(msg), [msg] ); const hasQuote = quoteMsg !== undefined; const hasThread = threadMsg !== undefined; const triangleWidth = releaseArea === 'china' ? gTriangleWidth : 0; const isSupport = isSupportMessage(msg); const { colors, cornerRadius } = usePaletteContext(); const { cornerRadius: corner } = useThemeContext(); const { getMessageBubbleBorderRadius, getBorderRadius } = useGetStyleProps(); const { getColor } = useColors({ left_bg: { light: colors.primary[95], dark: colors.primary[6], }, right_bg: { light: colors.primary[5], dark: colors.primary[2], }, url_bg: { light: colors.neutral[95], dark: colors.neutral[2], }, }); const isShowTriangle = React.useMemo(() => { return ( hasTriangle === true && msg.body.type !== ChatMessageType.IMAGE && msg.body.type !== ChatMessageType.VIDEO ); }, [hasTriangle, msg.body.type]); const contentMaxWidth = React.useMemo(() => { const _maxWidth = maxWidth ? maxWidth - (paddingHorizontal ?? 0) * 2 : undefined; if (isShowTriangle === true) { return _maxWidth ? _maxWidth - triangleWidth : undefined; } else { return _maxWidth; } }, [isShowTriangle, maxWidth, paddingHorizontal, triangleWidth]); const _onClicked = React.useCallback( (event?: GestureResponderEvent) => { if (checked !== undefined) { onClickedChecked?.(); } else { if (onClicked) { if (event) { const pressedX = event.nativeEvent.pageX; const pressedY = event.nativeEvent.pageY; touchRef.current?.measure((_, __, width, height, pageX, pageY) => { onClicked(msg.msgId.toString(), model, { pressedX: pressedX, pressedY: pressedY, componentHeight: height, componentWidth: width, componentX: pageX, componentY: pageY, }); }); } else { onClicked(msg.msgId.toString(), model); } } } }, [checked, model, msg.msgId, onClicked, onClickedChecked] ); const _onLongPress = React.useCallback( (event?: GestureResponderEvent) => { if (checked !== undefined) { onClickedChecked?.(); } else { if (onLongPress) { if (event) { const pressedX = event.nativeEvent.pageX; const pressedY = event.nativeEvent.pageY; touchRef.current?.measure((_, __, width, height, pageX, pageY) => { onLongPress(msg.msgId.toString(), model, { pressedX: pressedX, pressedY: pressedY, componentHeight: height, componentWidth: width, componentX: pageX, componentY: pageY, }); }); } else { onLongPress(msg.msgId.toString(), model); } } } }, [checked, model, msg.msgId, onClickedChecked, onLongPress] ); const _onClickedContent = _onClicked; const _onLongPressContent = _onLongPress; return ( {isShowTriangle ? ( ) : null} {/* */} {_MessageContent({ isSupport, msg, layoutType, isVoicePlaying, contentMaxWidth, onClicked: _onClickedContent, onLongPress: _onLongPressContent, })} ); } export function AvatarView(props: AvatarViewProps) { const { isVisible = true, layoutType, avatar, onAvatarClicked } = props; return ( ); } export function NameView(props: NameViewProps) { const { isVisible = true, layoutType, name, hasAvatar, hasTriangle } = props; const { colors } = usePaletteContext(); const { getColor } = useColors({ text: { light: colors.neutralSpecial[5], dark: colors.neutralSpecial[6], }, }); const paddingWidth = getPaddingWidth({ avatarWidth: hasAvatar ? gMessageAvatarSize : 0, triangleWidth: hasTriangle ? gTriangleWidth : 0, }); return ( {name} ); } export function TimeView(props: TimeViewProps) { const { isVisible = true, layoutType, timestamp, hasAvatar, hasTriangle, } = props; const { formatTime } = useConfigContext(); const { colors } = usePaletteContext(); const { getColor } = useColors({ text: { light: colors.neutral[7], dark: colors.neutral[6], }, }); const time = formatTime?.conversationDetailCallback ? formatTime.conversationDetailCallback(timestamp) : formatTsForConvDetail(timestamp); const paddingWidth = getPaddingWidth({ avatarWidth: hasAvatar ? gMessageAvatarSize : 0, triangleWidth: hasTriangle ? gTriangleWidth : 0, }); return ( {time} ); } export function StateView(props: StateViewProps) { const { isVisible = true, layoutType, state, onClicked } = props; const { colors } = usePaletteContext(); const { getColor } = useColors({ common: { light: colors.neutral[7], dark: colors.neutral[6], }, red: { light: colors.error[5], dark: colors.error[6], }, green: { light: colors.secondary[4], dark: colors.secondary[5], }, }); const isStop = React.useMemo(() => { return state !== 'loading-attachment' && state !== 'sending'; }, [state]); const iconName = React.useMemo(() => getStateIcon(state), [state]); const iconColor = React.useMemo(() => getStateIconColor(state), [state]); return ( {isStop === true ? ( ) : ( )} ); } export function CheckView(props: CheckViewProps) { const { isVisible = false, layoutType, children, checked, onClicked } = props; const { colors } = usePaletteContext(); const { getColor } = useColors({ uncheck: { light: colors.neutral[7], dark: colors.neutral[6], }, checked: { light: colors.primary[5], dark: colors.primary[6], }, }); return ( {children} ); } export function MessageThreadBubble(props: MessageThreadBubbleProps) { const { thread, hasAvatar, hasTriangle, layoutType, onClicked, maxWidth } = props; const { colors } = usePaletteContext(); const { getMessageBubbleBorderRadius } = useGetStyleProps(); const { fontFamily } = useConfigContext(); const { tr } = useI18nContext(); const { getMessageSnapshot } = useMessageSnapshot(); const { getColor } = useColors({ common: { light: colors.primary[5], dark: colors.primary[6], }, dis: { light: colors.neutral[3], dark: colors.neutral[7], }, text2: { light: colors.neutral[5], dark: colors.neutral[6], }, }); const paddingWidth = getPaddingWidth({ avatarWidth: hasAvatar ? gMessageAvatarSize : 0, triangleWidth: hasTriangle ? gTriangleWidth : 0, }); const onLongPress = React.useCallback(() => { if (thread) { onClicked?.(thread.threadId); } }, [onClicked, thread]); if (!thread) { return null; } return ( {thread.threadName ?? thread.threadId} {tr( '_uikit_thread_msg_count', `${thread.msgCount > 99 ? '+99' : thread.msgCount}` )} {getMessageSnapshot(thread.lastMessage)} ); } export function MessageReaction(props: MessageReactionProps) { const { reactions, hasAvatar, hasTriangle, layoutType, onClicked, onLongPress, } = props; const { colors, cornerRadius } = usePaletteContext(); const { cornerRadius: corner } = useThemeContext(); const { getBorderRadius } = useGetStyleProps(); const { fontFamily } = useConfigContext(); const { releaseArea } = useConfigContext(); const { getColor } = useColors({ common: { light: colors.primary[5], dark: colors.primary[6], }, dis: { light: colors.neutral[3], dark: colors.neutral[7], }, green: { light: colors.secondary[4], dark: colors.secondary[5], }, plus: { light: colors.neutral[5], dark: colors.neutral[6], }, }); const paddingWidth = getPaddingWidth({ avatarWidth: hasAvatar ? gMessageAvatarSize : 0, triangleWidth: hasTriangle ? gTriangleWidth : 0, }); return ( {reactions?.map((v, i) => { if (i >= 0 && i < 4) { const r = v.reaction; return ( onClicked?.(v.reaction)} onLongPress={() => onLongPress?.(v.reaction)} > {r} {v.count > 99 ? '+99' : v.count} ); } else { return null; } })} onClicked?.('faceplus')} > ); } export function MessageQuoteBubble(props: MessageQuoteBubbleProps) { const { hasAvatar, hasTriangle, model, containerStyle, maxWidth, onQuoteClicked, onClickedChecked, } = props; const checked = (model as MessageModel)?.checked; const { layoutType, quoteMsg, msg: originalMsg } = model; const { paddingHorizontal, paddingVertical } = React.useMemo(() => { return { paddingHorizontal: 12, paddingVertical: 8, }; }, []); const { tr } = useI18nContext(); const { colors } = usePaletteContext(); const { getMessageBubbleBorderRadius } = useGetStyleProps(); const { getColor } = useColors({ left_bg: { light: colors.neutral[95], dark: colors.neutral[6], }, right_bg: { light: colors.neutral[95], dark: colors.neutral[2], }, left_name: { light: colors.neutralSpecial[6], dark: colors.neutralSpecial[7], }, right_name: { light: colors.neutralSpecial[6], dark: colors.neutralSpecial[7], }, left_text: { light: colors.neutral[5], dark: colors.neutral[6], }, right_text: { light: colors.neutral[5], dark: colors.neutral[6], }, }); const marginWidth = getPaddingWidth({ avatarWidth: hasAvatar ? gMessageAvatarSize : 0, triangleWidth: hasTriangle ? gTriangleWidth : 0, }); const getContent = (originalMsg: ChatMessage, quoteMsg?: ChatMessage) => { const user = userInfoFromMessage(quoteMsg); switch (quoteMsg?.body.type) { case ChatMessageType.TXT: { const body = quoteMsg?.body as ChatTextMessageBody; return ( {user?.userName ?? user?.userId ?? quoteMsg.from} {body.content} ); } case ChatMessageType.IMAGE: { const body = quoteMsg.body as ChatImageMessageBody; return ( {user?.userName ?? user?.userId ?? quoteMsg.from} {tr('picture')} ); } case ChatMessageType.VOICE: { const body = quoteMsg?.body as ChatVoiceMessageBody; return ( {user?.userName ?? user?.userId ?? quoteMsg.from} {tr('voice')} {`: ${Math.floor(body.duration)}`} ); } case ChatMessageType.VIDEO: { const body = quoteMsg.body as ChatVideoMessageBody; return ( {user?.userName ?? user?.userId ?? quoteMsg.from} {tr('video')} ); } case ChatMessageType.FILE: { const body = quoteMsg?.body as ChatFileMessageBody; return ( {user?.userName ?? user?.userId ?? quoteMsg.from} {tr('file')} {`: ${body.displayName}`} ); } case ChatMessageType.COMBINE: { return ( {user?.userName ?? user?.userId ?? quoteMsg.from} {tr('_uikit_msg_record')} ); } case ChatMessageType.CUSTOM: { const body = quoteMsg?.body as ChatCustomMessageBody; if (body.event === gCustomMessageCardEventType) { const cardParams = body.params as { userId: string; nickname: string; avatar: string; }; return ( {user?.userName ?? user?.userId ?? quoteMsg.from} {tr('card')} {`: ${cardParams.nickname ?? cardParams.userId}`} ); } return ( {tr('_uikit_msg_tip_not_support')} ); } default: { if (originalMsg.attributes?.[gMessageAttributeQuote] && !quoteMsg) { return ( {tr('_uikit_msg_tip_msg_not_exist')} ); } else { return ( {tr('_uikit_msg_tip_not_support')} ); } } } }; const _onClicked = (msg: ChatMessage, quoteMsg?: ChatMessage) => { if (checked !== undefined) { onClickedChecked?.(); } else { if (onQuoteClicked) { const quote = msg.attributes[gMessageAttributeQuote]; onQuoteClicked?.(quoteMsg ? quoteMsg.msgId : quote.msgID, model); } } }; return ( _onClicked(originalMsg, quoteMsg)} > {getContent(originalMsg, quoteMsg)} ); } export function MessageView(props: MessageViewProps) { const { isVisible = true, model, avatarIsVisible = true, nameIsVisible = true, timeIsVisible = true, onQuoteClicked, onAvatarClicked, onStateClicked, MessageQuoteBubble: propsMessageQuoteBubble, MessageBubble: propsMessageBubble, MessageThreadBubble: propsMessageThreadBubble, MessageReaction: propsMessageReaction, onReactionClicked, onThreadClicked, onClickedChecked, onReactionLongPress, ...others } = props; const checked = (model as MessageModel)?.checked; const MessageQuoteBubbleWrapper = propsMessageQuoteBubble ?? MessageQuoteBubble; const MessageBubbleWrapper = propsMessageBubble ?? MessageBubble; const MessageThreadBubbleWrapper = propsMessageThreadBubble ?? MessageThreadBubble; const MessageReactionWrapper = propsMessageReaction ?? MessageReaction; const { layoutType, reactions, thread, isHighBackground } = model; const { enableThread, enableReaction, releaseArea } = useConfigContext(); const state = getMessageState(model.msg); const maxWidth = Dimensions.get('window').width * 0.6; const time = model.msg.localTime ?? model.msg.serverTime; const bubblePadding = 12; const hasTriangle = releaseArea === 'china' ? true : false; const isQuote = isQuoteMessage(model.msg, model.quoteMsg); const isSingleChat = React.useRef( model.msg.chatType === ChatMessageChatType.PeerChat ).current; const userName = model.userName ?? model.userId; const userAvatar = model.userAvatar; const { colors } = usePaletteContext(); const { getColor } = useColors({ h: { light: colors.neutral[95], dark: colors.neutral[6], }, }); const animatedValue = React.useRef(new Animated.Value(0)).current; const backgroundColor = animatedValue.interpolate({ inputRange: [0, 0.5, 1], outputRange: [ getColor('bg') as string, getColor('h') as string, getColor('bg') as string, ], }); const onClickedAvatar = React.useCallback(() => { if (checked !== undefined) { onClickedChecked?.(); } else { onAvatarClicked?.(model.msg.msgId, model); } }, [checked, model, onAvatarClicked, onClickedChecked]); const onClickedState = React.useCallback(() => { if (checked !== undefined) { onClickedChecked?.(); } else { onStateClicked?.(model.msg.msgId, model); } }, [checked, model, onClickedChecked, onStateClicked]); const _onReactionClicked = React.useCallback( (face: string) => { if (checked !== undefined) { onClickedChecked?.(); } else { onReactionClicked?.(model.msg.msgId, model, face); } }, [checked, model, onClickedChecked, onReactionClicked] ); const _onReactionLongPress = React.useCallback( (face: string) => { if (checked !== undefined) { } else { onReactionLongPress?.(model.msg.msgId, model, face); } }, [checked, model, onReactionLongPress] ); const _onThreadClicked = React.useCallback(() => { if (checked !== undefined) { onClickedChecked?.(); } else { onThreadClicked?.(model.msg.msgId, model); } }, [checked, model, onClickedChecked, onThreadClicked]); React.useEffect(() => { if (isHighBackground !== undefined) { if (isHighBackground === true) { Animated.loop( Animated.timing(animatedValue, { useNativeDriver: false, duration: 1000, toValue: 1, easing: Easing.linear, }) ).start(); } else { animatedValue.stopAnimation(); } } }, [isHighBackground, animatedValue]); return ( {nameIsVisible && isSingleChat !== true ? ( ) : null} {isQuote ? ( ) : null} {avatarIsVisible ? ( ) : null} {state !== 'none' ? ( ) : null} {thread && enableThread === true ? ( ) : null} {reactions && reactions?.length > 0 && enableReaction === true ? ( ) : null} {timeIsVisible ? ( ) : null} ); } export function SystemTipView(props: SystemTipViewProps) { const { isVisible = true, model } = props; const { msg } = model; const { systemTip } = useSystemTip(); const { tr } = useI18nContext(); const { colors } = usePaletteContext(); const { getColor } = useColors({ t1: { light: colors.neutral[7], dark: colors.neutral[6], }, }); return ( {systemTip(msg, tr)} ); } export function TimeTipView(props: TimeTipViewProps) { const { isVisible = true, model } = props; const { timestamp } = model; const date = new Date(timestamp); const { colors } = usePaletteContext(); const { getColor } = useColors({ t1: { light: colors.neutral[7], dark: colors.neutral[6], }, }); return ( {date.toDateString()} ); } export function MessageListItem(props: MessageListItemProps) { const { id, model, MessageView: propsMessageView, SystemTipView: propsSystemTipView, TimeTipView: propsTimeTipView, onChecked: propsOnChecked, ...others } = props; const { modelType } = model; const MessageViewWrapper = propsMessageView ?? MessageView; const SystemTipViewWrapper = propsSystemTipView ?? SystemTipView; const TimeTipViewWrapper = propsTimeTipView ?? TimeTipView; const checked = (model as MessageModel)?.checked; const _onChecked = React.useCallback(() => { if (propsOnChecked) { propsOnChecked(id, model); } }, [id, model, propsOnChecked]); return ( {modelType === 'message' ? ( checked !== undefined ? ( ) : ( ) ) : null} {modelType === 'system' ? ( ) : null} {modelType === 'time' ? ( ) : null} {modelType === 'history' ? (() => { const { MessageHistoryListItemMemo, } = require('./MessageHistoryListItem'); return ( ); })() : null} ); } export const MessageListItemMemo = React.memo(MessageListItem); const styles = StyleSheet.create({ bubble: { overflow: 'hidden', }, });