diff --git a/native/chat/composed-message.react.js b/native/chat/composed-message.react.js index ddc7ff885..d1621e77b 100644 --- a/native/chat/composed-message.react.js +++ b/native/chat/composed-message.react.js @@ -1,299 +1,270 @@ // @flow import Icon from '@expo/vector-icons/Feather.js'; import invariant from 'invariant'; import * as React from 'react'; import { StyleSheet, View } from 'react-native'; import Animated from 'react-native-reanimated'; import { getAvatarForUser } from 'lib/shared/avatar-utils.js'; import { createMessageReply } from 'lib/shared/message-utils.js'; import { assertComposableMessageType } from 'lib/types/message-types.js'; import { clusterEndHeight, - inlineEngagementStyle, - inlineEngagementLeftStyle, - inlineEngagementRightStyle, composedMessageStyle, avatarOffset, } from './chat-constants.js'; import { useComposedMessageMaxWidth } from './composed-message-width.js'; import { FailedSend } from './failed-send.react.js'; import { InlineEngagement } from './inline-engagement.react.js'; import { MessageHeader } from './message-header.react.js'; import { useNavigateToSidebar } from './sidebar-navigation.js'; import SwipeableMessage from './swipeable-message.react.js'; import { useContentAndHeaderOpacity, useDeliveryIconOpacity } from './utils.js'; import Avatar from '../components/avatar.react.js'; import { type InputState, InputStateContext } from '../input/input-state.js'; import { type Colors, useColors } from '../themes/colors.js'; import type { ChatMessageInfoItemWithHeight } from '../types/chat-types.js'; import { type AnimatedStyleObj, AnimatedView } from '../types/styles.js'; import { useShouldRenderAvatars } from '../utils/avatar-utils.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 = { ...React.ElementConfig, +item: ChatMessageInfoItemWithHeight, +sendFailed: boolean, +focused: boolean, +swipeOptions: SwipeOptions, +children: React.Node, }; type Props = { ...BaseProps, // Redux state +composedMessageMaxWidth: number, +colors: Colors, +contentAndHeaderOpacity: number | Node, +deliveryIconOpacity: number | Node, // withInputState +inputState: ?InputState, +navigateToSidebar: () => mixed, +shouldRenderAvatars: boolean, }; class ComposedMessage extends React.PureComponent { render() { assertComposableMessageType(this.props.item.messageInfo.type); const { item, sendFailed, focused, swipeOptions, children, composedMessageMaxWidth, colors, inputState, navigateToSidebar, contentAndHeaderOpacity, deliveryIconOpacity, shouldRenderAvatars, ...viewProps } = this.props; 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 = [ styles.alignment, { marginBottom: containerMarginBottom }, ]; const swipeableMessageBoxStyle = [ styles.swipeableContainer, { maxWidth: composedMessageMaxWidth }, ]; const messageBoxStyleContainerStyle = [styles.messageBoxContainer]; const positioningStyle = isViewer ? { alignItems: 'flex-end' } : { alignItems: 'flex-start' }; messageBoxStyleContainerStyle.push(positioningStyle); let deliveryIcon = null; let failedSendInfo = null; if (isViewer) { 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 = ( ); } const triggerReply = swipeOptions === 'reply' || swipeOptions === 'both' ? this.reply : undefined; const triggerSidebar = swipeOptions === 'sidebar' || swipeOptions === 'both' ? navigateToSidebar : undefined; let avatar; if (!isViewer && item.endsCluster && shouldRenderAvatars) { const avatarInfo = getAvatarForUser(item.messageInfo.creator); avatar = ( ); } else if (!isViewer && shouldRenderAvatars) { avatar = ; } const messageBox = ( {avatar} {children} ); let inlineEngagement = null; if ( item.threadCreatedFromMessage || Object.keys(item.reactions).length > 0 ) { const positioning = isViewer ? 'right' : 'left'; - - const inlineEngagementPositionStyle = []; - if (positioning === 'left') { - inlineEngagementPositionStyle.push(styles.leftInlineEngagement); - } else { - inlineEngagementPositionStyle.push(styles.rightInlineEngagement); - } - if (this.props.shouldRenderAvatars) { - inlineEngagementPositionStyle.push({ marginLeft: avatarOffset }); - } - inlineEngagement = ( - - - + ); } return ( {deliveryIcon} {messageBox} {failedSendInfo} {inlineEngagement} ); } 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.addReply(createMessageReply(item.messageInfo.text)); }; } const styles = StyleSheet.create({ alignment: { marginLeft: composedMessageStyle.marginLeft, marginRight: composedMessageStyle.marginRight, }, avatarContainer: { marginRight: 8, }, avatarOffset: { width: avatarOffset, }, content: { alignItems: 'center', flexDirection: 'row-reverse', }, icon: { fontSize: 16, textAlign: 'center', }, iconContainer: { marginLeft: 2, width: 16, }, - inlineEngagement: { - marginBottom: inlineEngagementStyle.marginBottom, - marginTop: inlineEngagementStyle.marginTop, - }, leftChatBubble: { justifyContent: 'flex-end', }, - leftInlineEngagement: { - justifyContent: 'flex-start', - position: 'relative', - top: inlineEngagementLeftStyle.topOffset, - }, messageBoxContainer: { flex: 1, marginRight: 5, }, rightChatBubble: { justifyContent: 'flex-start', }, - rightInlineEngagement: { - alignSelf: 'flex-end', - position: 'relative', - right: inlineEngagementRightStyle.marginRight, - top: inlineEngagementRightStyle.topOffset, - }, swipeableContainer: { alignItems: 'flex-end', flexDirection: 'row', }, }); 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 shouldRenderAvatars = useShouldRenderAvatars(); return ( ); }); export default ConnectedComposedMessage; diff --git a/native/chat/inline-engagement.react.js b/native/chat/inline-engagement.react.js index 2c933b85e..e436eab6c 100644 --- a/native/chat/inline-engagement.react.js +++ b/native/chat/inline-engagement.react.js @@ -1,247 +1,294 @@ // @flow import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; import { Text, View } from 'react-native'; import Animated, { Extrapolate, interpolateNode, } from 'react-native-reanimated'; import useInlineEngagementText from 'lib/hooks/inline-engagement-text.react.js'; import type { ReactionInfo } from 'lib/selectors/chat-selectors.js'; import { stringForReactionList } from 'lib/shared/reaction-utils.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import { inlineEngagementStyle, inlineEngagementCenterStyle, inlineEngagementRightStyle, + inlineEngagementLeftStyle, composedMessageStyle, + avatarOffset, } from './chat-constants.js'; import { useNavigateToThread } from './message-list-types.js'; import CommIcon from '../components/comm-icon.react.js'; import GestureTouchableOpacity from '../components/gesture-touchable-opacity.react.js'; import { MessageReactionsModalRouteName } from '../navigation/route-names.js'; import { useStyles } from '../themes/colors.js'; import type { ChatMessageInfoItemWithHeight } from '../types/chat-types.js'; type Props = { +threadInfo: ?ThreadInfo, +reactions?: ReactionInfo, +disabled?: boolean, + +positioning?: 'left' | 'right', + +shouldRenderAvatars?: boolean, }; function InlineEngagement(props: Props): React.Node { - const { disabled = false, reactions, threadInfo } = props; + const { + disabled = false, + reactions, + threadInfo, + positioning, + shouldRenderAvatars, + } = props; const repliesText = useInlineEngagementText(threadInfo); const navigateToThread = useNavigateToThread(); const { navigate } = useNavigation(); const styles = useStyles(unboundStyles); const unreadStyle = threadInfo?.currentUser.unread ? styles.unread : null; const repliesStyles = React.useMemo( () => [styles.repliesText, unreadStyle], [styles.repliesText, unreadStyle], ); const onPressThread = React.useCallback(() => { if (threadInfo && !disabled) { navigateToThread({ threadInfo }); } }, [disabled, navigateToThread, threadInfo]); const sidebarItem = React.useMemo(() => { if (!threadInfo) { return null; } return ( {repliesText} ); }, [ threadInfo, onPressThread, styles.sidebar, styles.icon, repliesStyles, repliesText, ]); const onPressReactions = React.useCallback(() => { navigate<'MessageReactionsModal'>({ name: MessageReactionsModalRouteName, params: { reactions }, }); }, [navigate, reactions]); const marginLeft = React.useMemo( () => (sidebarItem ? styles.reactionMarginLeft : null), [sidebarItem, styles.reactionMarginLeft], ); const reactionList = React.useMemo(() => { if (!reactions || Object.keys(reactions).length === 0) { return null; } const reactionText = stringForReactionList(reactions); const reactionItems = {reactionText}; return ( {reactionItems} ); }, [ marginLeft, onPressReactions, reactions, styles.reaction, styles.reactionsContainer, ]); + const container = React.useMemo(() => { + return ( + + {sidebarItem} + {reactionList} + + ); + }, [reactionList, sidebarItem, styles.container]); + + const inlineEngagementPositionStyle = []; + if (positioning === 'left') { + inlineEngagementPositionStyle.push(styles.leftInlineEngagement); + } else { + inlineEngagementPositionStyle.push(styles.rightInlineEngagement); + } + if (shouldRenderAvatars) { + inlineEngagementPositionStyle.push({ marginLeft: avatarOffset }); + } + return ( - - {sidebarItem} - {reactionList} + + {container} ); } const unboundStyles = { container: { flexDirection: 'row', height: inlineEngagementStyle.height, borderRadius: 16, backgroundColor: 'inlineEngagementBackground', alignSelf: 'baseline', alignItems: 'center', padding: 8, }, unread: { color: 'listForegroundLabel', fontWeight: 'bold', }, + rightInlineEngagement: { + alignSelf: 'flex-end', + position: 'relative', + right: inlineEngagementRightStyle.marginRight, + top: inlineEngagementRightStyle.topOffset, + }, + leftInlineEngagement: { + justifyContent: 'flex-start', + position: 'relative', + top: inlineEngagementLeftStyle.topOffset, + }, sidebar: { flexDirection: 'row', alignItems: 'center', }, + inlineEngagement: { + flexDirection: 'row', + marginBottom: inlineEngagementStyle.marginBottom, + marginTop: inlineEngagementStyle.marginTop, + alignItems: 'center', + }, icon: { color: 'inlineEngagementLabel', marginRight: 4, }, repliesText: { color: 'inlineEngagementLabel', fontSize: 14, lineHeight: 22, }, reaction: { color: 'inlineEngagementLabel', fontSize: 14, lineHeight: 22, }, reactionMarginLeft: { marginLeft: 12, }, reactionsContainer: { display: 'flex', flexDirection: 'row', alignItems: 'center', }, + avatarOffset: { + width: avatarOffset, + }, }; type TooltipInlineEngagementProps = { +item: ChatMessageInfoItemWithHeight, +isOpeningSidebar: boolean, +progress: Animated.Node, +windowWidth: number, +positioning: 'left' | 'right' | 'center', +initialCoordinates: { +x: number, +y: number, +width: number, +height: number, }, }; function TooltipInlineEngagement( props: TooltipInlineEngagementProps, ): React.Node { const { item, isOpeningSidebar, progress, windowWidth, initialCoordinates, positioning, } = props; const inlineEngagementStyles = React.useMemo(() => { if (positioning === 'left') { return { position: 'absolute', top: - inlineEngagementStyle.marginTop + - inlineEngagementRightStyle.topOffset, + inlineEngagementStyle.marginTop + inlineEngagementLeftStyle.topOffset, left: composedMessageStyle.marginLeft, }; } else if (positioning === 'right') { return { position: 'absolute', right: inlineEngagementRightStyle.marginRight + composedMessageStyle.marginRight, top: inlineEngagementStyle.marginTop + inlineEngagementRightStyle.topOffset, }; } else if (positioning === 'center') { return { alignSelf: 'center', top: inlineEngagementCenterStyle.topOffset, }; } }, [positioning]); const inlineEngagementContainer = React.useMemo(() => { const opacity = isOpeningSidebar ? 0 : interpolateNode(progress, { inputRange: [0, 1], outputRange: [1, 0], extrapolate: Extrapolate.CLAMP, }); return { position: 'absolute', width: windowWidth, top: initialCoordinates.height, left: -initialCoordinates.x, opacity, }; }, [ initialCoordinates.height, initialCoordinates.x, isOpeningSidebar, progress, windowWidth, ]); return ( ); } export { InlineEngagement, TooltipInlineEngagement };