import React, { useEffect } from 'react'; import { StyleSheet, TouchableOpacity, useWindowDimensions, View, ViewStyle } from 'react-native'; import Animated, { useAnimatedStyle, useSharedValue, withDelay, withSequence, withTiming, } from 'react-native-reanimated'; import Svg, { Circle } from 'react-native-svg'; import { MessageContextValue, Reactions, useMessageContext, } from '../../../contexts/messageContext/MessageContext'; import { useMessagesContext } from '../../../contexts/messagesContext/MessagesContext'; import { useTheme } from '../../../contexts/themeContext/ThemeContext'; import { Unknown } from '../../../icons/Unknown'; import type { IconProps } from '../../../icons/utils/base'; import type { DefaultAttachmentType, DefaultChannelType, DefaultCommandType, DefaultEventType, DefaultMessageType, DefaultReactionType, DefaultUserType, UnknownType, } from '../../../types/types'; import type { ReactionData } from '../../../utils/utils'; const styles = StyleSheet.create({ container: { left: 0, position: 'absolute', top: 0, }, reactionBubble: { alignItems: 'center', flexDirection: 'row', justifyContent: 'space-evenly', position: 'absolute', }, reactionBubbleBackground: { position: 'absolute', }, }); export type MessageReactions = { reactions: Reactions; supportedReactions?: ReactionData[]; }; const Icon: React.FC< Pick & { size: number; supportedReactions: ReactionData[]; type: string; } > = ({ pathFill, size, style, supportedReactions, type }) => { const ReactionIcon = supportedReactions.find((reaction) => reaction.type === type)?.Icon || Unknown; const scale = useSharedValue(0); const showReaction = () => { 'worklet'; scale.value = withSequence( withDelay(250, withTiming(0.5, { duration: 100 })), withTiming(1.5, { duration: 400 }), withTiming(1, { duration: 500 }), ); }; useEffect(() => { showReaction(); }, []); const animatedStyle = useAnimatedStyle( () => ({ transform: [ { scale: scale.value, }, ], }), [], ); return ( ); }; export type ReactionListPropsWithContext< At extends UnknownType = DefaultAttachmentType, Ch extends UnknownType = DefaultChannelType, Co extends string = DefaultCommandType, Ev extends UnknownType = DefaultEventType, Me extends UnknownType = DefaultMessageType, Re extends UnknownType = DefaultReactionType, Us extends UnknownType = DefaultUserType, > = Pick< MessageContextValue, | 'alignment' | 'onLongPress' | 'onPress' | 'onPressIn' | 'preventPress' | 'reactions' | 'showMessageOverlay' > & { messageContentWidth: number; supportedReactions: ReactionData[]; fill?: string; radius?: number; // not recommended to change this reactionSize?: number; stroke?: string; strokeSize?: number; // not recommended to change this }; const ReactionListWithContext = < At extends UnknownType = DefaultAttachmentType, Ch extends UnknownType = DefaultChannelType, Co extends string = DefaultCommandType, Ev extends UnknownType = DefaultEventType, Me extends UnknownType = DefaultMessageType, Re extends UnknownType = DefaultReactionType, Us extends UnknownType = DefaultUserType, >( props: ReactionListPropsWithContext, ) => { const { alignment, fill: propFill, messageContentWidth, onLongPress, onPress, onPressIn, preventPress, radius: propRadius, reactions, reactionSize: propReactionSize, showMessageOverlay, stroke: propStroke, strokeSize: propStrokeSize, supportedReactions, } = props; const { theme: { colors: { accent_blue, grey, grey_gainsboro, grey_whisper, white }, messageSimple: { avatarWrapper: { leftAlign, spacer }, reactionList: { container, middleIcon, radius: themeRadius, reactionBubble, reactionBubbleBackground, reactionSize: themeReactionSize, strokeSize: themeStrokeSize, }, }, screenPadding, }, } = useTheme(); const opacity = useSharedValue(0); const width = useWindowDimensions().width; const supportedReactionTypes = supportedReactions.map( (supportedReaction) => supportedReaction.type, ); const hasSupportedReactions = reactions.some((reaction) => supportedReactionTypes.includes(reaction.type), ); const showReactions = (show: boolean) => { 'worklet'; opacity.value = show ? withDelay(250, withTiming(1, { duration: 500 })) : 0; }; useEffect(() => { showReactions(hasSupportedReactions && messageContentWidth !== 0); }, [hasSupportedReactions, messageContentWidth]); const animatedStyle = useAnimatedStyle( () => ({ opacity: opacity.value, }), [], ); if (!hasSupportedReactions || messageContentWidth === 0) { return null; } const alignmentLeft = alignment === 'left'; const fill = propFill || alignmentLeft ? grey_gainsboro : grey_whisper; const radius = propRadius || themeRadius; const reactionSize = propReactionSize || themeReactionSize; const stroke = propStroke || white; const strokeSize = propStrokeSize || themeStrokeSize; const x1 = alignmentLeft ? messageContentWidth + (Number(leftAlign.marginRight) || 0) + (Number(spacer.width) || 0) - radius * 0.5 : width - screenPadding * 2 - messageContentWidth; const x2 = x1 + radius * 2 * (alignmentLeft ? 1 : -1); const y1 = reactionSize + radius * 2; const y2 = reactionSize - radius; const insideLeftBound = x2 - (reactionSize * reactions.length) / 2 > screenPadding; const insideRightBound = x2 + strokeSize + (reactionSize * reactions.length) / 2 < width - screenPadding * 2; const left = reactions.length === 1 ? x1 + (alignmentLeft ? -radius : radius - reactionSize) : !insideLeftBound ? screenPadding : !insideRightBound ? width - screenPadding * 2 - reactionSize * reactions.length - strokeSize : x2 - (reactionSize * reactions.length) / 2 - strokeSize; return ( { if (onLongPress) { onLongPress({ emitter: 'reactionList', event, }); } }} onPress={(event) => { if (onPress) { onPress({ defaultHandler: () => showMessageOverlay(true), emitter: 'reactionList', event, }); } }} onPressIn={(event) => { if (onPressIn) { onPressIn({ defaultHandler: () => showMessageOverlay(true), emitter: 'reactionList', event, }); } }} style={[ styles.container, { height: reactionSize + radius * 5, width, }, container, ]} testID='reaction-list' > {reactions.length ? ( {reactions.map((reaction) => ( ))} ) : null} ); }; const areEqual = < At extends UnknownType = DefaultAttachmentType, Ch extends UnknownType = DefaultChannelType, Co extends string = DefaultCommandType, Ev extends UnknownType = DefaultEventType, Me extends UnknownType = DefaultMessageType, Re extends UnknownType = DefaultReactionType, Us extends UnknownType = DefaultUserType, >( prevProps: ReactionListPropsWithContext, nextProps: ReactionListPropsWithContext, ) => { const { messageContentWidth: prevMessageContentWidth, reactions: prevReactions } = prevProps; const { messageContentWidth: nextMessageContentWidth, reactions: nextReactions } = nextProps; const messageContentWidthEqual = prevMessageContentWidth === nextMessageContentWidth; if (!messageContentWidthEqual) return false; const reactionsEqual = prevReactions.length === nextReactions.length && prevReactions.every( (latestReaction, index) => nextReactions[index].own === latestReaction.own && nextReactions[index].type === latestReaction.type, ); if (!reactionsEqual) return false; return true; }; const MemoizedReactionList = React.memo( ReactionListWithContext, areEqual, ) as typeof ReactionListWithContext; export type ReactionListProps< At extends UnknownType = DefaultAttachmentType, Ch extends UnknownType = DefaultChannelType, Co extends string = DefaultCommandType, Ev extends UnknownType = DefaultEventType, Me extends UnknownType = DefaultMessageType, Re extends UnknownType = DefaultReactionType, Us extends UnknownType = DefaultUserType, > = Partial, 'messageContentWidth'>> & Pick, 'messageContentWidth'>; /** * ReactionList - A high level component which implements all the logic required for a message reaction list */ export const ReactionList = < At extends UnknownType = DefaultAttachmentType, Ch extends UnknownType = DefaultChannelType, Co extends string = DefaultCommandType, Ev extends UnknownType = DefaultEventType, Me extends UnknownType = DefaultMessageType, Re extends UnknownType = DefaultReactionType, Us extends UnknownType = DefaultUserType, >( props: ReactionListProps, ) => { const { alignment, onLongPress, onPress, onPressIn, preventPress, reactions, showMessageOverlay, } = useMessageContext(); const { supportedReactions } = useMessagesContext(); return ( ); };