diff --git a/native/chat/chat-constants.js b/native/chat/chat-constants.js index 61e69a597..0a78171ab 100644 --- a/native/chat/chat-constants.js +++ b/native/chat/chat-constants.js @@ -1,33 +1,30 @@ // @flow export const composedMessageStyle = { marginLeft: 12, marginRight: 7, }; export const inlineEngagementStyle = { height: 38, marginTop: 5, marginBottom: 3, -}; -export const inlineEngagementLeftStyle = { topOffset: -10, }; export const inlineEngagementCenterStyle = { topOffset: -5, }; export const inlineEngagementRightStyle = { - marginRight: 22, - topOffset: -10, + marginLeft: 22, }; export const inlineEngagementLabelStyle = { height: 16, topOffset: 8, }; export const clusterEndHeight = 7; export const avatarOffset = 32; diff --git a/native/chat/inline-engagement.react.js b/native/chat/inline-engagement.react.js index ea8d4ca78..84f733a93 100644 --- a/native/chat/inline-engagement.react.js +++ b/native/chat/inline-engagement.react.js @@ -1,333 +1,325 @@ // @flow import { useNavigation } from '@react-navigation/native'; import invariant from 'invariant'; 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 { inlineEngagementLabelStyle, 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 = { +sidebarThreadInfo: ?ThreadInfo, +reactions?: ReactionInfo, +disabled?: boolean, - +positioning?: 'left' | 'right', + +positioning?: 'left' | 'right' | 'center', +label?: ?string, }; function InlineEngagement(props: Props): React.Node { const { disabled = false, reactions, sidebarThreadInfo, positioning, label, } = props; - const repliesText = useInlineEngagementText(sidebarThreadInfo); + + const isLeft = positioning === 'left'; + const isRight = positioning === 'right'; + const isCenter = positioning === 'center'; const navigateToThread = useNavigateToThread(); const { navigate } = useNavigation(); const styles = useStyles(unboundStyles); + const editedLabel = React.useMemo(() => { + if (!label) { + return null; + } + + const labelLeftRight = isLeft + ? styles.messageLabelLeft + : styles.messageLabelRight; + + return ( + + {label} + + ); + }, [isLeft, label, styles]); + const unreadStyle = sidebarThreadInfo?.currentUser.unread ? styles.unread : null; const repliesStyles = React.useMemo( () => [styles.repliesText, unreadStyle], [styles.repliesText, unreadStyle], ); const onPressSidebar = React.useCallback(() => { if (sidebarThreadInfo && !disabled) { navigateToThread({ threadInfo: sidebarThreadInfo }); } }, [disabled, navigateToThread, sidebarThreadInfo]); + const repliesText = useInlineEngagementText(sidebarThreadInfo); + const sidebarItem = React.useMemo(() => { if (!sidebarThreadInfo) { return null; } return ( {repliesText} ); }, [ sidebarThreadInfo, onPressSidebar, 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 isLeft = positioning === 'left'; - - const editedLabel = React.useMemo(() => { - if (!label) { - return null; - } - - const labelLeftRight = isLeft - ? styles.messageLabelLeft - : styles.messageLabelRight; - - return {label}; - }, [isLeft, label, styles]); + return Object.keys(reactions).map(reaction => { + const numOfReacts = reactions[reaction].users.length; + return ( + + {`${reaction} ${numOfReacts}`} + + ); + }); + }, [onPressReactions, reactions, styles.reaction, styles.reactionsContainer]); const inlineEngagementPositionStyle = React.useMemo(() => { const styleResult = [styles.inlineEngagement]; - if (!isLeft) { + if (isRight) { styleResult.push(styles.rightInlineEngagement); + } else if (isCenter) { + styleResult.push(styles.centerInlineEngagement); } return styleResult; - }, [isLeft, styles.inlineEngagement, styles.rightInlineEngagement]); - - let body; - if (isLeft) { - body = ( - <> - {editedLabel} - {sidebarItem} - {reactionList} - - ); - } else { - body = ( - <> - {sidebarItem} - {reactionList} - {editedLabel} - - ); - } + }, [ + isCenter, + isRight, + styles.centerInlineEngagement, + styles.inlineEngagement, + styles.rightInlineEngagement, + ]); - return {body}; + return ( + + {editedLabel} + {sidebarItem} + {reactionList} + + ); } const unboundStyles = { inlineEngagement: { flexDirection: 'row', marginBottom: inlineEngagementStyle.marginBottom, - marginTop: inlineEngagementStyle.marginTop, marginLeft: avatarOffset, - top: inlineEngagementLeftStyle.topOffset, + flexWrap: 'wrap', + top: inlineEngagementStyle.topOffset, + }, + centerInlineEngagement: { + marginLeft: 20, + marginRight: 20, + justifyContent: 'center', }, rightInlineEngagement: { - alignSelf: 'flex-end', - right: inlineEngagementRightStyle.marginRight, + flexDirection: 'row-reverse', + marginLeft: inlineEngagementRightStyle.marginLeft, }, sidebar: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'inlineEngagementBackground', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 8, + marginTop: inlineEngagementStyle.marginTop, }, icon: { color: 'inlineEngagementLabel', marginRight: 4, }, repliesText: { color: 'inlineEngagementLabel', fontSize: 14, lineHeight: 22, }, unread: { color: 'listForegroundLabel', fontWeight: 'bold', }, reactionsContainer: { display: 'flex', flexDirection: 'row', alignItems: 'center', backgroundColor: 'inlineEngagementBackground', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 8, + marginTop: inlineEngagementStyle.marginTop, }, reaction: { color: 'inlineEngagementLabel', fontSize: 14, lineHeight: 22, }, - reactionMarginLeft: { - marginLeft: 12, - }, messageLabel: { color: 'messageLabel', paddingHorizontal: 3, fontSize: 13, top: inlineEngagementLabelStyle.topOffset, height: inlineEngagementLabelStyle.height, + marginTop: inlineEngagementStyle.marginTop, }, messageLabelLeft: { marginLeft: 9, marginRight: 4, }, messageLabelRight: { marginRight: 10, marginLeft: 4, }, 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; // ESLint doesn't recognize that invariant always throws // eslint-disable-next-line consistent-return const inlineEngagementStyles = React.useMemo(() => { if (positioning === 'left') { return { position: 'absolute', - top: - inlineEngagementStyle.marginTop + inlineEngagementLeftStyle.topOffset, + top: inlineEngagementStyle.marginTop + inlineEngagementStyle.topOffset, left: composedMessageStyle.marginLeft, }; } else if (positioning === 'right') { return { position: 'absolute', right: - inlineEngagementRightStyle.marginRight + + inlineEngagementRightStyle.marginLeft + composedMessageStyle.marginRight, - top: - inlineEngagementStyle.marginTop + - inlineEngagementRightStyle.topOffset, + top: inlineEngagementStyle.marginTop + inlineEngagementStyle.topOffset, }; } else if (positioning === 'center') { return { alignSelf: 'center', top: inlineEngagementCenterStyle.topOffset, }; } invariant( false, `${positioning} is not a valid positioning value for InlineEngagement`, ); }, [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 }; diff --git a/native/chat/robotext-message.react.js b/native/chat/robotext-message.react.js index 992d006bf..68be611b9 100644 --- a/native/chat/robotext-message.react.js +++ b/native/chat/robotext-message.react.js @@ -1,223 +1,224 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View } from 'react-native'; import { messageKey } from 'lib/shared/message-utils.js'; import { useCanCreateSidebarFromMessage } from 'lib/shared/thread-utils.js'; import { inlineEngagementCenterStyle } from './chat-constants.js'; import type { ChatNavigationProp } from './chat.react.js'; import { InlineEngagement } from './inline-engagement.react.js'; import { InnerRobotextMessage } from './inner-robotext-message.react.js'; import { Timestamp } from './timestamp.react.js'; import { getMessageTooltipKey, useContentAndHeaderOpacity } from './utils.js'; import { ChatContext } from '../chat/chat-context.js'; import { KeyboardContext } from '../keyboard/keyboard-state.js'; import type { AppNavigationProp } from '../navigation/app-navigator.react'; import { OverlayContext } from '../navigation/overlay-context.js'; import { RobotextMessageTooltipModalRouteName } from '../navigation/route-names.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useStyles } from '../themes/colors.js'; import { fixedTooltipHeight } from '../tooltip/tooltip.react.js'; import type { ChatRobotextMessageInfoItemWithHeight } from '../types/chat-types.js'; import type { VerticalBounds } from '../types/layout-types.js'; import { AnimatedView } from '../types/styles.js'; type Props = { ...React.ElementConfig, +item: ChatRobotextMessageInfoItemWithHeight, +navigation: | ChatNavigationProp<'MessageList'> | AppNavigationProp<'TogglePinModal'> | ChatNavigationProp<'MessageResultsScreen'> | ChatNavigationProp<'MessageSearch'>, +route: | NavigationRoute<'MessageList'> | NavigationRoute<'TogglePinModal'> | NavigationRoute<'MessageResultsScreen'> | NavigationRoute<'MessageSearch'>, +focused: boolean, +toggleFocus: (messageKey: string) => void, +verticalBounds: ?VerticalBounds, }; function RobotextMessage(props: Props): React.Node { const { item, navigation, route, focused, toggleFocus, verticalBounds, ...viewProps } = props; let timestamp = null; if (focused || item.startsConversation) { timestamp = ( ); } const styles = useStyles(unboundStyles); let inlineEngagement = null; if (item.threadCreatedFromMessage || Object.keys(item.reactions).length > 0) { inlineEngagement = ( ); } const chatContext = React.useContext(ChatContext); const keyboardState = React.useContext(KeyboardContext); const key = messageKey(item.messageInfo); const onPress = React.useCallback(() => { const didDismiss = keyboardState && keyboardState.dismissKeyboardIfShowing(); if (!didDismiss) { toggleFocus(key); } }, [keyboardState, toggleFocus, key]); const overlayContext = React.useContext(OverlayContext); const viewRef = React.useRef>(); const canCreateSidebarFromMessage = useCanCreateSidebarFromMessage( item.threadInfo, item.messageInfo, ); const visibleEntryIDs = React.useMemo(() => { const result = []; if (item.threadCreatedFromMessage || canCreateSidebarFromMessage) { result.push('sidebar'); } return result; }, [item.threadCreatedFromMessage, canCreateSidebarFromMessage]); const openRobotextTooltipModal = React.useCallback( (x, y, width, height, pageX, pageY) => { invariant( verticalBounds, 'verticalBounds should be present in openRobotextTooltipModal', ); const coordinates = { x: pageX, y: pageY, width, height }; const messageTop = pageY; const messageBottom = pageY + height; const boundsTop = verticalBounds.y; const boundsBottom = verticalBounds.y + verticalBounds.height; const belowMargin = 20; const belowSpace = fixedTooltipHeight + belowMargin; const { isViewer } = item.messageInfo.creator; const aboveMargin = isViewer ? 30 : 50; const aboveSpace = fixedTooltipHeight + aboveMargin; let margin = 0; if ( messageBottom + belowSpace > boundsBottom && messageTop - aboveSpace > boundsTop ) { margin = aboveMargin; } const currentInputBarHeight = chatContext?.chatInputBarHeights.get(item.threadInfo.id) ?? 0; props.navigation.navigate<'RobotextMessageTooltipModal'>({ name: RobotextMessageTooltipModalRouteName, params: { presentedFrom: props.route.key, initialCoordinates: coordinates, verticalBounds, visibleEntryIDs, tooltipLocation: 'fixed', margin, item, chatInputBarHeight: currentInputBarHeight, }, key: getMessageTooltipKey(item), }); }, [ item, props.navigation, props.route.key, verticalBounds, visibleEntryIDs, chatContext, ], ); const onLongPress = React.useCallback(() => { if (keyboardState && keyboardState.dismissKeyboardIfShowing()) { return; } if (visibleEntryIDs.length === 0) { return; } if (!viewRef.current || !verticalBounds) { return; } if (!focused) { toggleFocus(messageKey(item.messageInfo)); } invariant(overlayContext, 'RobotextMessage should have OverlayContext'); overlayContext.setScrollBlockingModalStatus('open'); viewRef.current?.measure(openRobotextTooltipModal); }, [ focused, item, keyboardState, overlayContext, toggleFocus, verticalBounds, viewRef, visibleEntryIDs, openRobotextTooltipModal, ]); const onLayout = React.useCallback(() => {}, []); const contentAndHeaderOpacity = useContentAndHeaderOpacity(item); return ( {timestamp} {inlineEngagement} ); } const unboundStyles = { sidebar: { marginTop: inlineEngagementCenterStyle.topOffset, marginBottom: -inlineEngagementCenterStyle.topOffset, alignSelf: 'center', }, }; export { RobotextMessage };