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',
},
});