import React, { useMemo } from 'react'; import { AnimatableNumericValue, ColorValue, Pressable, StyleSheet, View } from 'react-native'; import { MessageTextContainer } from './MessageTextContainer'; import { useChatContext } from '../../../contexts'; import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext'; import { MessageContextValue, useMessageContext, } from '../../../contexts/messageContext/MessageContext'; import { useMessageListItemContext } from '../../../contexts/messageListItemContext/MessageListItemContext'; import { MessagesContextValue, useMessagesContext, } from '../../../contexts/messagesContext/MessagesContext'; import { useTheme } from '../../../contexts/themeContext/ThemeContext'; import { TranslationContextValue, useTranslationContext, } from '../../../contexts/translationContext/TranslationContext'; import { components, primitives } from '../../../theme'; import { FileTypes } from '../../../types/types'; import { checkMessageEquality, checkQuotedMessageEquality } from '../../../utils/utils'; import { Poll } from '../../Poll/Poll'; const useReplyStyles = () => { const { theme: { semantics }, } = useTheme(); const { isMyMessage } = useMessageContext(); return useMemo(() => { return StyleSheet.create({ container: { minWidth: 256, // TODO: Not sure how to fix this backgroundColor: isMyMessage ? semantics.chatBgAttachmentOutgoing : semantics.chatBgAttachmentIncoming, paddingLeft: primitives.spacingSm, }, leftContainer: { borderLeftColor: isMyMessage ? semantics.chatReplyIndicatorOutgoing : semantics.chatReplyIndicatorIncoming, }, }); }, [semantics, isMyMessage]); }; export type MessageContentPropsWithContext = Pick< MessageContextValue, | 'alignment' | 'goToMessage' | 'groupStyles' | 'isMyMessage' | 'message' | 'messageContentOrder' | 'onLongPress' | 'onPress' | 'onPressIn' | 'otherAttachments' | 'preventPress' | 'threadList' | 'isMessageAIGenerated' > & Pick< MessagesContextValue, | 'additionalPressableProps' | 'enableMessageGroupingByUser' | 'isAttachmentEqual' | 'myMessageTheme' > & Pick & { /** * Background color for the message content */ backgroundColor?: ColorValue; /** * If the message is the very last message in the message list */ isVeryLastMessage?: boolean; /** * If the message has no border radius */ noBorder?: boolean; /** * If the message is grouped in a single or bottom container */ messageGroupedSingleOrBottom?: boolean; /** * If the message has a single file */ isSingleFile?: boolean; hidePaddingTop?: boolean; hidePaddingHorizontal?: boolean; hidePaddingBottom?: boolean; }; /** * Child of MessageItemView that displays a message's content */ const MessageContentWithContext = (props: MessageContentPropsWithContext) => { const { additionalPressableProps, alignment, backgroundColor, enableMessageGroupingByUser, groupStyles, goToMessage, isMessageAIGenerated, isMyMessage, isVeryLastMessage, message, messageContentOrder, messageGroupedSingleOrBottom = false, noBorder, onLongPress, onPress, onPressIn, otherAttachments, preventPress, hidePaddingTop, hidePaddingHorizontal, hidePaddingBottom, } = props; const { client } = useChatContext(); const { Attachment, FileAttachmentGroup, Gallery, MessageContentBottomView, MessageContentLeadingView, MessageContentTopView, MessageContentTrailingView, MessageLocation, Reply, StreamingMessageView, } = useComponentsContext(); const replyStyles = useReplyStyles(); const { theme: { messageItemView: { content: { container: { borderBottomLeftRadius, borderBottomRightRadius, borderRadius, borderTopLeftRadius, borderTopRightRadius, ...container }, containerInner, contentContainer, lastMessageContainer, messageGroupedSingleOrBottomContainer, messageGroupedTopContainer, replyContainer, }, }, }, } = useTheme(); const isAIGenerated = useMemo( () => isMessageAIGenerated(message), [message, isMessageAIGenerated], ); const getBorderRadius = () => { // enum('top', 'middle', 'bottom', 'single') const groupPosition = groupStyles?.[0]; const isBottomOrSingle = groupPosition === 'single' || groupPosition === 'bottom'; let borderBottomLeftRadius = components.messageBubbleRadiusGroupBottom; let borderBottomRightRadius = components.messageBubbleRadiusGroupBottom; if (isBottomOrSingle) { // add relevant sharp corner if (isMyMessage) { borderBottomRightRadius = components.messageBubbleRadiusTail; } else { borderBottomLeftRadius = components.messageBubbleRadiusTail; } } return { borderBottomLeftRadius, borderBottomRightRadius, }; }; const getBorderRadiusFromTheme = () => { const bordersFromTheme: Record = { borderBottomLeftRadius, borderBottomRightRadius, borderRadius, borderTopLeftRadius, borderTopRightRadius, }; // filter out undefined values for (const key in bordersFromTheme) { if (bordersFromTheme[key] === undefined) { delete bordersFromTheme[key]; } } return bordersFromTheme; }; const { setNativeScrollability } = useMessageListItemContext(); const hasContentSideViews = !!(MessageContentLeadingView || MessageContentTrailingView); const contentBody = ( <> {messageContentOrder.map((messageContentType, messageContentOrderIndex) => { switch (messageContentType) { case 'quoted_reply': return ( message.quoted_message && ( { if (onLongPress) { onLongPress({ emitter: 'messageContent', event, }); } }} onPress={(event) => { if (!message.quoted_message || !goToMessage) { return; } if (onPress) { onPress({ defaultHandler: () => goToMessage(message.quoted_message!.id), emitter: 'messageContent', event, }); return; } goToMessage(message.quoted_message.id); }} onPressIn={(event) => { if (onPressIn) { onPressIn({ emitter: 'messageContent', event, }); } }} style={[styles.replyContainer, replyContainer]} > ) ); case 'attachments': return otherAttachments.map((attachment, attachmentIndex) => ( )); case 'files': return ( ); case 'gallery': return ( ); case 'poll': { const pollId = message.poll_id; const poll = pollId && client.polls.fromState(pollId); return pollId && poll ? ( ) : null; } case 'location': return MessageLocation ? ( ) : null; case 'ai_text': return isAIGenerated ? ( ) : null; default: return null; } })} {(otherAttachments.length && otherAttachments[0].actions) || isAIGenerated ? null : ( )} ); return ( { if (onLongPress) { onLongPress({ emitter: 'messageContent', event, }); } }} onPress={(event) => { if (onPress) { onPress({ emitter: 'messageContent', event, }); } }} onPressIn={(event) => { if (onPressIn) { onPressIn({ emitter: 'messageContent', event, }); } }} style={container} {...additionalPressableProps} onPressOut={(event) => { setNativeScrollability(true); if (additionalPressableProps?.onPressOut) { additionalPressableProps.onPressOut(event); } }} > {MessageContentTopView ? : null} {hasContentSideViews ? ( {MessageContentLeadingView ? : null} {contentBody} {MessageContentTrailingView ? : null} ) : ( contentBody )} {MessageContentBottomView ? : null} ); }; const areEqual = ( prevProps: MessageContentPropsWithContext, nextProps: MessageContentPropsWithContext, ) => { const { alignment: prevAlignment, backgroundColor: prevBackgroundColor, preventPress: prevPreventPress, goToMessage: prevGoToMessage, groupStyles: prevGroupStyles, isAttachmentEqual, message: prevMessage, messageContentOrder: prevMessageContentOrder, myMessageTheme: prevMyMessageTheme, otherAttachments: prevOtherAttachments, t: prevT, } = prevProps; const { alignment: nextAlignment, backgroundColor: nextBackgroundColor, preventPress: nextPreventPress, goToMessage: nextGoToMessage, groupStyles: nextGroupStyles, message: nextMessage, messageContentOrder: nextMessageContentOrder, myMessageTheme: nextMyMessageTheme, otherAttachments: nextOtherAttachments, t: nextT, } = nextProps; if (prevBackgroundColor !== nextBackgroundColor) { return false; } if (prevAlignment !== nextAlignment) { return false; } if (prevPreventPress !== nextPreventPress) { return false; } const goToMessageChangedAndMatters = nextMessage.quoted_message_id && prevGoToMessage !== nextGoToMessage; if (goToMessageChangedAndMatters) { return false; } const otherAttachmentsEqual = prevOtherAttachments.length === nextOtherAttachments.length && prevOtherAttachments?.[0]?.actions?.length === nextOtherAttachments?.[0]?.actions?.length; if (!otherAttachmentsEqual) { return false; } const groupStylesEqual = prevGroupStyles === nextGroupStyles; if (!groupStylesEqual) { return false; } const messageEqual = checkMessageEquality(prevMessage, nextMessage); if (!messageEqual) { return false; } const quotedMessageEqual = checkQuotedMessageEquality( prevMessage.quoted_message, nextMessage.quoted_message, ); if (!quotedMessageEqual) { return false; } const prevMessageAttachments = prevMessage.attachments; const nextMessageAttachments = nextMessage.attachments; const attachmentsEqual = Array.isArray(prevMessageAttachments) && Array.isArray(nextMessageAttachments) ? prevMessageAttachments.length === nextMessageAttachments.length && prevMessageAttachments.every((attachment, index) => { const attachmentKeysEqual = attachment.image_url === nextMessageAttachments[index].image_url && attachment.og_scrape_url === nextMessageAttachments[index].og_scrape_url && attachment.thumb_url === nextMessageAttachments[index].thumb_url && attachment.type === nextMessageAttachments[index].type; if (isAttachmentEqual) { return ( attachmentKeysEqual && !!isAttachmentEqual(attachment, nextMessageAttachments[index]) ); } return attachmentKeysEqual; }) : prevMessageAttachments === nextMessageAttachments; if (!attachmentsEqual) { return false; } const quotedMessageAttachmentsEqual = prevMessage.quoted_message?.attachments?.length === nextMessage.quoted_message?.attachments?.length; if (!quotedMessageAttachmentsEqual) { return false; } const latestReactionsEqual = Array.isArray(prevMessage.latest_reactions) && Array.isArray(nextMessage.latest_reactions) ? prevMessage.latest_reactions.length === nextMessage.latest_reactions.length && prevMessage.latest_reactions.every( ({ type }, index) => type === nextMessage.latest_reactions?.[index].type, ) : prevMessage.latest_reactions === nextMessage.latest_reactions; if (!latestReactionsEqual) { return false; } const messageContentOrderEqual = prevMessageContentOrder.length === nextMessageContentOrder.length && prevMessageContentOrder.every( (messageContentType, index) => messageContentType === nextMessageContentOrder[index], ); if (!messageContentOrderEqual) { return false; } const tEqual = prevT === nextT; if (!tEqual) { return false; } const messageThemeEqual = JSON.stringify(prevMyMessageTheme) === JSON.stringify(nextMyMessageTheme); if (!messageThemeEqual) { return false; } const prevSharedLocation = prevMessage.shared_location; const nextSharedLocation = nextMessage.shared_location; const sharedLocationEqual = prevSharedLocation?.latitude === nextSharedLocation?.latitude && prevSharedLocation?.longitude === nextSharedLocation?.longitude && prevSharedLocation?.end_at === nextSharedLocation?.end_at; if (!sharedLocationEqual) { return false; } return true; }; const MemoizedMessageContent = React.memo( MessageContentWithContext, areEqual, ) as typeof MessageContentWithContext; export type MessageContentProps = Partial; /** * Child of MessageItemView that displays a message's content */ export const MessageContent = (props: MessageContentProps) => { const { alignment, goToMessage, groupStyles, isMessageAIGenerated, isMyMessage, message, messageContentOrder, onLongPress, onPress, onPressIn, otherAttachments, preventPress, threadList, files, images, videos, } = useMessageContext(); const { additionalPressableProps, enableMessageGroupingByUser, isAttachmentEqual, myMessageTheme, } = useMessagesContext(); const { t } = useTranslationContext(); const isSingleFile = files.length === 1; const messageHasPoll = messageContentOrder.includes('poll'); const messageHasSingleMedia = messageContentOrder.length === 1 && messageContentOrder.includes('gallery') && images.length + videos.length === 1; const messageHasSingleFile = messageContentOrder.length === 1 && messageContentOrder[0] === 'files' && isSingleFile; const messageHasOnlyText = messageContentOrder.length === 1 && messageContentOrder[0] === 'text'; const messageHasStandaloneGiphyOrImgur = !message.quoted_message && otherAttachments.filter( (file) => file.type === FileTypes.Giphy || file.type === FileTypes.Imgur, ).length > 0; const hidePaddingTop = messageHasPoll || messageHasSingleMedia || messageHasSingleFile || messageHasOnlyText || messageHasStandaloneGiphyOrImgur; const hidePaddingHorizontal = messageHasPoll || messageHasSingleMedia || messageHasSingleFile || messageHasStandaloneGiphyOrImgur; const hidePaddingBottom = messageHasPoll || messageHasSingleMedia || messageHasSingleFile || messageHasOnlyText || messageHasStandaloneGiphyOrImgur || (messageContentOrder.length > 1 && messageContentOrder[messageContentOrder.length - 1] === 'text'); return ( ); }; const styles = StyleSheet.create({ container: { flexShrink: 1, }, containerInner: { borderTopLeftRadius: components.messageBubbleRadiusGroupBottom, borderTopRightRadius: components.messageBubbleRadiusGroupBottom, overflow: 'hidden', }, contentBody: { flexShrink: 1, minWidth: 0, }, contentRow: { flexDirection: 'row', }, rightAlignContentRow: { flexDirection: 'row-reverse', }, leftAlignContent: { justifyContent: 'flex-start', }, leftAlignItems: { alignItems: 'flex-start', }, replyBorder: { borderLeftWidth: 1, bottom: 0, position: 'absolute', }, replyContainer: { alignSelf: 'center', }, galleryContainer: {}, rightAlignContent: { justifyContent: 'flex-end', }, rightAlignItems: { alignItems: 'flex-end', }, textWrapper: {}, });