import React, { useMemo } from 'react';
import { Dimensions, StyleSheet, View, ViewStyle } from 'react-native';
import { SwipableMessageWrapper } from './MessageBubble';
import { useComponentsContext } from '../../../contexts/componentsContext/ComponentsContext';
import {
Alignment,
MessageContextValue,
useMessageContext,
} from '../../../contexts/messageContext/MessageContext';
import {
MessagesContextValue,
useMessagesContext,
} from '../../../contexts/messagesContext/MessagesContext';
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
import { useStableCallback } from '../../../hooks/useStableCallback';
import { primitives } from '../../../theme';
import { FileTypes } from '../../../types/types';
import { checkMessageEquality, checkQuotedMessageEquality } from '../../../utils/utils';
import { useMessageData } from '../hooks/useMessageData';
type GroupType = 'single' | 'top' | 'middle' | 'bottom' | undefined;
const useStyles = ({
alignment,
isVeryLastMessage,
messageGroupedSingle,
messageGroupedBottom,
messageGroupedTop,
messageGroupedMiddle,
enableMessageGroupingByUser,
}: {
alignment: Alignment;
isVeryLastMessage: boolean;
messageGroupedSingle: boolean;
messageGroupedBottom: boolean;
messageGroupedTop: boolean;
messageGroupedMiddle: boolean;
enableMessageGroupingByUser: boolean;
}) => {
const {
theme: {
messageItemView: {
container,
bubbleContentContainer,
bubbleErrorContainer,
bubbleReactionListTopContainer,
bubbleWrapper,
contentContainer,
repliesContainer,
leftAlignItems,
rightAlignItems,
messageGroupedSingleStyles,
messageGroupedBottomStyles,
messageGroupedTopStyles,
messageGroupedMiddleStyles,
lastMessageContainer,
},
},
} = useTheme();
const groupType: GroupType = useMemo(() => {
if (messageGroupedSingle) return 'single';
if (messageGroupedTop) return 'top';
if (messageGroupedMiddle) return 'middle';
if (messageGroupedBottom) return 'bottom';
return undefined;
}, [messageGroupedSingle, messageGroupedTop, messageGroupedMiddle, messageGroupedBottom]);
const styles = useMemo(
() =>
StyleSheet.create({
baseContainer: {
alignItems: 'flex-end',
gap: primitives.spacingXs,
flexDirection: alignment === 'left' ? 'row' : 'row-reverse',
width: '100%',
...container,
},
contentContainer: {
gap: primitives.spacingXxs,
...contentContainer,
},
bubbleContentContainer: {
alignSelf: alignment === 'left' ? 'flex-start' : 'flex-end',
...bubbleContentContainer,
},
bubbleErrorContainer: {
position: 'absolute',
top: 8,
right: -12,
...bubbleErrorContainer,
},
bubbleReactionListTopContainer: {
alignSelf: alignment === 'left' ? 'flex-end' : 'flex-start',
...bubbleReactionListTopContainer,
},
bubbleWrapper: {
zIndex: 1,
...bubbleWrapper,
},
repliesContainer: {
marginTop: -primitives.spacingXxs, // Reducing the margin to account the gap added in the content container
zIndex: 0,
...repliesContainer,
},
leftAlignItems: {
alignItems: 'flex-start',
...leftAlignItems,
},
rightAlignItems: {
alignItems: 'flex-end',
...rightAlignItems,
},
}),
[
alignment,
bubbleContentContainer,
bubbleErrorContainer,
bubbleReactionListTopContainer,
bubbleWrapper,
container,
contentContainer,
leftAlignItems,
repliesContainer,
rightAlignItems,
],
);
const groupStylesMap = useMemo(() => {
return {
single: {
paddingVertical: primitives.spacingXs,
...messageGroupedSingleStyles,
},
top: {
paddingTop: primitives.spacingXs,
paddingBottom: primitives.spacingXxs,
...messageGroupedTopStyles,
},
middle: {
paddingBottom: primitives.spacingXxs,
...messageGroupedMiddleStyles,
},
bottom: {
paddingBottom: primitives.spacingXs,
...messageGroupedBottomStyles,
},
};
}, [
messageGroupedBottomStyles,
messageGroupedMiddleStyles,
messageGroupedSingleStyles,
messageGroupedTopStyles,
]);
const containerStyle = useMemo(() => {
let results: ViewStyle = styles.baseContainer;
if (groupType) {
results = {
...results,
...groupStylesMap[groupType],
};
}
if (isVeryLastMessage && enableMessageGroupingByUser) {
results = {
...results,
marginBottom: primitives.spacingSm,
...lastMessageContainer,
};
}
return results;
}, [
styles.baseContainer,
groupStylesMap,
groupType,
isVeryLastMessage,
enableMessageGroupingByUser,
lastMessageContainer,
]);
return {
container: containerStyle,
bubbleContentContainer: styles.bubbleContentContainer,
bubbleErrorContainer: styles.bubbleErrorContainer,
bubbleReactionListTopContainer: styles.bubbleReactionListTopContainer,
bubbleWrapper: styles.bubbleWrapper,
contentContainer: styles.contentContainer,
repliesContainer: styles.repliesContainer,
leftAlignItems: styles.leftAlignItems,
rightAlignItems: styles.rightAlignItems,
};
};
export type MessageItemViewPropsWithContext = Pick<
MessageContextValue,
| 'alignment'
| 'channel'
| 'groupStyles'
| 'hasAttachmentActions'
| 'isMyMessage'
| 'message'
| 'onlyEmojis'
| 'otherAttachments'
| 'setQuotedMessage'
| 'lastGroupMessage'
| 'contextMenuAnchorRef'
| 'members'
> &
Pick<
MessagesContextValue,
| 'customMessageSwipeAction'
| 'enableMessageGroupingByUser'
| 'enableSwipeToReply'
| 'myMessageTheme'
| 'messageSwipeToReplyHitSlop'
| 'reactionListPosition'
| 'reactionListType'
>;
const MessageItemViewWithContext = (props: MessageItemViewPropsWithContext) => {
const { width } = Dimensions.get('screen');
const {
alignment,
channel,
contextMenuAnchorRef,
customMessageSwipeAction,
enableMessageGroupingByUser,
enableSwipeToReply,
groupStyles,
hasAttachmentActions,
isMyMessage,
message,
messageSwipeToReplyHitSlop = { left: width, right: width },
onlyEmojis,
otherAttachments,
reactionListPosition,
reactionListType,
setQuotedMessage,
} = props;
const {
MessageAuthor,
MessageContent,
MessageDeleted,
MessageError,
MessageFooter,
MessageHeader,
MessageReplies,
MessageSpacer,
ReactionListBottom,
ReactionListTop,
} = useComponentsContext();
const {
theme: {
semantics,
messageItemView: {
content: { errorContainer },
},
},
} = useTheme();
const {
isMessageErrorType,
isMessageReceivedOrErrorType,
isMessageTypeDeleted,
isVeryLastMessage,
messageGroupedSingle,
messageGroupedBottom,
messageGroupedTop,
messageGroupedSingleOrBottom,
messageGroupedMiddle,
} = useMessageData({});
const styles = useStyles({
alignment,
isVeryLastMessage,
messageGroupedSingle,
messageGroupedBottom,
messageGroupedTop,
messageGroupedMiddle,
enableMessageGroupingByUser,
});
const groupStyle = `${alignment}_${groupStyles?.[0]?.toLowerCase?.()}`;
const hasVisibleQuotedReply = !!message.quoted_message && !hasAttachmentActions;
const hasStandaloneGiphyOrImgur =
!hasVisibleQuotedReply &&
otherAttachments.length > 0 &&
(otherAttachments[0].type === FileTypes.Giphy || otherAttachments[0].type === FileTypes.Imgur);
let noBorder = onlyEmojis && !hasVisibleQuotedReply;
if (otherAttachments.length) {
if (hasStandaloneGiphyOrImgur && !isMyMessage) {
noBorder = false;
} else {
noBorder = true;
}
}
let backgroundColor = semantics.chatBgOutgoing;
if (onlyEmojis && !hasVisibleQuotedReply) {
backgroundColor = 'transparent';
} else if (hasStandaloneGiphyOrImgur) {
backgroundColor = 'transparent';
} else if (isMessageReceivedOrErrorType) {
backgroundColor = semantics.chatBgIncoming;
}
const onSwipeActionHandler = useStableCallback(() => {
if (customMessageSwipeAction) {
customMessageSwipeAction({ channel, message });
return;
}
setQuotedMessage(message);
});
const itemViewContent = (
{alignment === 'left' ? : null}
{isMessageTypeDeleted ? (
) : (
{reactionListPosition === 'top' && ReactionListTop ? (
) : null}
{isMessageErrorType ? (
) : null}
{reactionListPosition === 'bottom' && ReactionListBottom ? (
) : null}
)}
{MessageSpacer ? : null}
);
return enableSwipeToReply && !isMessageTypeDeleted ? (
{itemViewContent}
) : (
itemViewContent
);
};
const areEqual = (
prevProps: MessageItemViewPropsWithContext,
nextProps: MessageItemViewPropsWithContext,
) => {
const {
channel: prevChannel,
groupStyles: prevGroupStyles,
message: prevMessage,
myMessageTheme: prevMyMessageTheme,
onlyEmojis: prevOnlyEmojis,
otherAttachments: prevOtherAttachments,
lastGroupMessage: prevLastGroupMessage,
members: prevMembers,
} = prevProps;
const {
channel: nextChannel,
groupStyles: nextGroupStyles,
message: nextMessage,
myMessageTheme: nextMyMessageTheme,
onlyEmojis: nextOnlyEmojis,
otherAttachments: nextOtherAttachments,
lastGroupMessage: nextLastGroupMessage,
members: nextMembers,
} = nextProps;
const groupStylesEqual = prevGroupStyles === nextGroupStyles;
if (!groupStylesEqual) {
return false;
}
const lastGroupMessageEqual = prevLastGroupMessage === nextLastGroupMessage;
if (!lastGroupMessageEqual) {
return false;
}
const membersEqual = Object.keys(prevMembers).length === Object.keys(nextMembers).length;
if (!membersEqual) {
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 channelEqual = prevChannel?.state.messages.length === nextChannel?.state.messages.length;
if (!channelEqual) {
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;
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 messageThemeEqual =
JSON.stringify(prevMyMessageTheme) === JSON.stringify(nextMyMessageTheme);
if (!messageThemeEqual) {
return false;
}
const onlyEmojisEqual = prevOnlyEmojis === nextOnlyEmojis;
if (!onlyEmojisEqual) {
return false;
}
const otherAttachmentsEqual =
prevOtherAttachments.length === nextOtherAttachments.length &&
prevOtherAttachments?.[0]?.actions?.length === nextOtherAttachments?.[0]?.actions?.length;
if (!otherAttachmentsEqual) {
return false;
}
return true;
};
const MemoizedMessageItemView = React.memo(
MessageItemViewWithContext,
areEqual,
) as typeof MessageItemViewWithContext;
export type MessageItemViewProps = Partial;
/**
*
* Message UI component
*/
export const MessageItemView = (props: MessageItemViewProps) => {
const {
alignment,
channel,
groupStyles,
hasAttachmentActions,
isMyMessage,
message,
contextMenuAnchorRef,
onlyEmojis,
otherAttachments,
setQuotedMessage,
lastGroupMessage,
members,
} = useMessageContext();
const {
customMessageSwipeAction,
enableMessageGroupingByUser,
enableSwipeToReply,
messageSwipeToReplyHitSlop,
myMessageTheme,
reactionListPosition,
reactionListType,
} = useMessagesContext();
return (
);
};
MessageItemView.displayName = 'MessageItemView{messageItemView{container}}';