diff --git a/native/chat/composed-message.react.js b/native/chat/composed-message.react.js --- a/native/chat/composed-message.react.js +++ b/native/chat/composed-message.react.js @@ -4,7 +4,7 @@ import invariant from 'invariant'; import * as React from 'react'; import { StyleSheet, View } from 'react-native'; -import Animated, { +import { useDerivedValue, withTiming, interpolateColor, @@ -30,17 +30,13 @@ import { useContentAndHeaderOpacity, useDeliveryIconOpacity } from './utils.js'; import UserAvatar from '../avatars/user-avatar.react.js'; import CommIcon from '../components/comm-icon.react.js'; -import { type InputState, InputStateContext } from '../input/input-state.js'; -import { type Colors, useColors } from '../themes/colors.js'; +import { InputStateContext } from '../input/input-state.js'; +import { useColors } from '../themes/colors.js'; import type { ChatMessageInfoItemWithHeight } from '../types/chat-types.js'; import { type AnimatedStyleObj, AnimatedView } from '../types/styles.js'; -/* eslint-disable import/no-named-as-default-member */ -const { Node } = Animated; -/* eslint-enable import/no-named-as-default-member */ - type SwipeOptions = 'reply' | 'sidebar' | 'both' | 'none'; -type BaseProps = { +type Props = { ...React.ElementConfig, +item: ChatMessageInfoItemWithHeight, +sendFailed: boolean, @@ -49,74 +45,93 @@ +shouldDisplayPinIndicator: boolean, +children: React.Node, }; -type Props = { - ...BaseProps, - // Redux state - +composedMessageMaxWidth: number, - +colors: Colors, - +contentAndHeaderOpacity: number | Node, - +deliveryIconOpacity: number | Node, - // withInputState - +inputState: ?InputState, - +navigateToSidebar: () => mixed, - +editedMessageStyle: AnimatedStyleObj, -}; -class ComposedMessage extends React.PureComponent { - render() { - assertComposableMessageType(this.props.item.messageInfo.type); + +const ConnectedComposedMessage: React.ComponentType = React.memo( + function ConnectedComposedMessage(props: Props) { + const composedMessageMaxWidth = useComposedMessageMaxWidth(); + const colors = useColors(); + const inputState = React.useContext(InputStateContext); + const navigateToSidebar = useNavigateToSidebar(props.item); + const contentAndHeaderOpacity = useContentAndHeaderOpacity(props.item); + const deliveryIconOpacity = useDeliveryIconOpacity(props.item); + + const messageEditingContext = React.useContext(MessageEditingContext); + const progress = useDerivedValue(() => { + const isThisThread = + messageEditingContext?.editState.editedMessage?.threadID === + props.item.threadInfo.id; + const isHighlighted = + messageEditingContext?.editState.editedMessage?.id === + props.item.messageInfo.id && isThisThread; + return withTiming(isHighlighted ? 1 : 0); + }); + + const editedMessageStyle = useAnimatedStyle(() => { + const backgroundColor = interpolateColor( + progress.value, + [0, 1], + ['transparent', `#${props.item.threadInfo.color}40`], + ); + return { + backgroundColor, + }; + }); + + assertComposableMessageType(props.item.messageInfo.type); const { item, sendFailed, - focused, swipeOptions, shouldDisplayPinIndicator, children, - composedMessageMaxWidth, - colors, - inputState, - navigateToSidebar, - contentAndHeaderOpacity, - deliveryIconOpacity, - editedMessageStyle, + focused, ...viewProps - } = this.props; - const { id, creator } = item.messageInfo; + } = props; + const { hasBeenEdited, isPinned } = item; + const { id, creator } = item.messageInfo; const { isViewer } = creator; const alignStyle = isViewer ? styles.rightChatBubble : styles.leftChatBubble; - let containerMarginBottom = 5; - if (item.endsCluster) { - containerMarginBottom += clusterEndHeight; - } - const containerStyle = { marginBottom: containerMarginBottom }; - - const messageBoxContainerStyle = [styles.messageBoxContainer]; - const positioningStyle = isViewer - ? styles.rightChatContainer - : styles.leftChatContainer; - messageBoxContainerStyle.push(positioningStyle); - - let deliveryIcon = null; - let failedSendInfo = null; - if (isViewer) { + const containerStyle = React.useMemo(() => { + let containerMarginBottom = 5; + if (item.endsCluster) { + containerMarginBottom += clusterEndHeight; + } + return { marginBottom: containerMarginBottom }; + }, [item.endsCluster]); + + const messageBoxContainerStyle = React.useMemo( + () => [ + styles.messageBoxContainer, + isViewer ? styles.rightChatContainer : styles.leftChatContainer, + ], + [isViewer], + ); + + const deliveryIcon = React.useMemo(() => { + if (!isViewer) { + return undefined; + } + let deliveryIconName; let deliveryIconColor = `#${item.threadInfo.color}`; + if (id !== null && id !== undefined) { deliveryIconName = 'check-circle'; } else if (sendFailed) { deliveryIconName = 'x-circle'; deliveryIconColor = colors.redText; - failedSendInfo = ; } else { deliveryIconName = 'circle'; } const animatedStyle: AnimatedStyleObj = { opacity: deliveryIconOpacity }; - deliveryIcon = ( + + return ( ); - } + }, [ + colors.redText, + deliveryIconOpacity, + id, + isViewer, + item.threadInfo.color, + sendFailed, + ]); + + const reply = React.useCallback(() => { + invariant(inputState, 'inputState should be set in reply'); + invariant(item.messageInfo.text, 'text should be set in reply'); + inputState.editInputMessage({ + message: createMessageReply(item.messageInfo.text), + mode: 'prepend', + }); + }, [inputState, item.messageInfo.text]); const triggerReply = - swipeOptions === 'reply' || swipeOptions === 'both' - ? this.reply - : undefined; + swipeOptions === 'reply' || swipeOptions === 'both' ? reply : undefined; + const triggerSidebar = swipeOptions === 'sidebar' || swipeOptions === 'both' ? navigateToSidebar : undefined; - let avatar; - if (!isViewer && item.endsCluster) { - avatar = ( - - - - ); - } else if (!isViewer) { - avatar = ; - } + const avatar = React.useMemo(() => { + if (!isViewer && item.endsCluster) { + return ( + + + + ); + } else if (!isViewer) { + return ; + } else { + return undefined; + } + }, [isViewer, item.endsCluster, item.messageInfo.creator.id]); const pinIconPositioning = isViewer ? 'left' : 'right'; const pinIconName = pinIconPositioning === 'left' ? 'pin-mirror' : 'pin'; @@ -153,9 +186,11 @@ ? styles.rightMessageBoxTopLevelContainerStyle : styles.leftMessageBoxTopLevelContainerStyle; - let pinIcon; - if (isPinned && shouldDisplayPinIndicator) { - pinIcon = ( + const pinIcon = React.useMemo(() => { + if (!isPinned || !shouldDisplayPinIndicator) { + return undefined; + } + return ( ); - } - - const messageBoxStyle = { - opacity: contentAndHeaderOpacity, - maxWidth: composedMessageMaxWidth, - }; - - const messageBox = ( - - {pinIcon} - - - {avatar} - {children} - - - + }, [ + isPinned, + item.threadInfo.color, + pinIconName, + shouldDisplayPinIndicator, + ]); + + const messageBoxStyle = React.useMemo( + () => ({ + opacity: contentAndHeaderOpacity, + maxWidth: composedMessageMaxWidth, + }), + [composedMessageMaxWidth, contentAndHeaderOpacity], ); - let inlineEngagement = null; - const label = getMessageLabel(hasBeenEdited, item.threadInfo.id); - if ( - item.threadCreatedFromMessage || - Object.keys(item.reactions).length > 0 || - label - ) { + const messageBox = React.useMemo(() => { + return ( + + {pinIcon} + + + {avatar} + {children} + + + + ); + }, [ + avatar, + children, + isViewer, + item.threadInfo.color, + messageBoxContainerStyle, + messageBoxStyle, + messageBoxTopLevelContainerStyle, + pinIcon, + triggerReply, + triggerSidebar, + ]); + + const inlineEngagement = React.useMemo(() => { + const label = getMessageLabel(hasBeenEdited, item.threadInfo.id); + if ( + !item.threadCreatedFromMessage && + Object.keys(item.reactions).length <= 0 && + !label + ) { + return undefined; + } const positioning = isViewer ? 'right' : 'left'; - inlineEngagement = ( + return ( ); - } + }, [ + hasBeenEdited, + isViewer, + item.messageInfo, + item.reactions, + item.threadCreatedFromMessage, + item.threadInfo, + ]); - const viewStyle = [styles.alignment]; - if (!__DEV__) { - // We don't force view height in dev mode because we - // want to measure it in Message to see if it's correct + const viewStyle = React.useMemo(() => { + const baseStyle = [styles.alignment]; + if (__DEV__) { + return baseStyle; + } if (item.messageShapeType === 'text') { - viewStyle.push({ height: item.contentHeight }); + baseStyle.push({ height: item.contentHeight }); } else if (item.messageShapeType === 'multimedia') { const height = item.inlineEngagementHeight ? item.contentHeight + item.inlineEngagementHeight : item.contentHeight; - viewStyle.push({ height }); + baseStyle.push({ height }); } - } + return baseStyle; + }, [ + item.contentHeight, + item.inlineEngagementHeight, + item.messageShapeType, + ]); - return ( - - - - - - - - {deliveryIcon} - {messageBox} - - {inlineEngagement} - - {failedSendInfo} - - + const messageHeaderStyle = React.useMemo( + () => ({ + opacity: contentAndHeaderOpacity, + }), + [contentAndHeaderOpacity], ); - } - - reply = () => { - const { inputState, item } = this.props; - invariant(inputState, 'inputState should be set in reply'); - invariant(item.messageInfo.text, 'text should be set in reply'); - inputState.editInputMessage({ - message: createMessageReply(item.messageInfo.text), - mode: 'prepend', - }); - }; -} + + const animatedContainerStyle = React.useMemo( + () => [containerStyle, editedMessageStyle], + [containerStyle, editedMessageStyle], + ); + + const contentStyle = React.useMemo( + () => [styles.content, alignStyle], + [alignStyle], + ); + + const failedSend = React.useMemo( + () => (sendFailed ? : undefined), + [item, sendFailed], + ); + + const composedMessage = React.useMemo(() => { + return ( + + + + + + + + {deliveryIcon} + {messageBox} + + {inlineEngagement} + + {failedSend} + + + ); + }, [ + animatedContainerStyle, + contentStyle, + deliveryIcon, + failedSend, + focused, + inlineEngagement, + item, + messageBox, + messageHeaderStyle, + viewProps, + viewStyle, + ]); + + return composedMessage; + }, +); const styles = StyleSheet.create({ alignment: { @@ -307,49 +410,4 @@ }, }); -const ConnectedComposedMessage: React.ComponentType = - React.memo(function ConnectedComposedMessage(props: BaseProps) { - const composedMessageMaxWidth = useComposedMessageMaxWidth(); - const colors = useColors(); - const inputState = React.useContext(InputStateContext); - const navigateToSidebar = useNavigateToSidebar(props.item); - const contentAndHeaderOpacity = useContentAndHeaderOpacity(props.item); - const deliveryIconOpacity = useDeliveryIconOpacity(props.item); - - const messageEditingContext = React.useContext(MessageEditingContext); - const progress = useDerivedValue(() => { - const isThisThread = - messageEditingContext?.editState.editedMessage?.threadID === - props.item.threadInfo.id; - const isHighlighted = - messageEditingContext?.editState.editedMessage?.id === - props.item.messageInfo.id && isThisThread; - return withTiming(isHighlighted ? 1 : 0); - }); - - const editedMessageStyle = useAnimatedStyle(() => { - const backgroundColor = interpolateColor( - progress.value, - [0, 1], - ['transparent', `#${props.item.threadInfo.color}40`], - ); - return { - backgroundColor, - }; - }); - - return ( - - ); - }); - export default ConnectedComposedMessage;