diff --git a/lib/hooks/inline-engagement-text.react.js b/lib/shared/inline-engagement-utils.js similarity index 66% rename from lib/hooks/inline-engagement-text.react.js rename to lib/shared/inline-engagement-utils.js index d052d2bd5..67b2cc686 100644 --- a/lib/hooks/inline-engagement-text.react.js +++ b/lib/shared/inline-engagement-utils.js @@ -1,13 +1,13 @@ // @flow import type { ThreadInfo } from '../types/thread-types.js'; -function useInlineEngagementText(threadInfo: ?ThreadInfo): string { +function getInlineEngagementSidebarText(threadInfo: ?ThreadInfo): string { if (!threadInfo) { return ''; } const repliesCount = threadInfo.repliesCount || 1; return `${repliesCount} ${repliesCount > 1 ? 'replies' : 'reply'}`; } -export default useInlineEngagementText; +export { getInlineEngagementSidebarText }; diff --git a/native/chat/inline-engagement.react.js b/native/chat/inline-engagement.react.js index 05092f568..f2f90d076 100644 --- a/native/chat/inline-engagement.react.js +++ b/native/chat/inline-engagement.react.js @@ -1,422 +1,422 @@ // @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 { getInlineEngagementSidebarText } from 'lib/shared/inline-engagement-utils.js'; import { localIDPrefix } from 'lib/shared/message-utils.js'; import type { MessageInfo } from 'lib/types/message-types.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import { inlineEngagementLabelStyle, inlineEngagementStyle, inlineEngagementCenterStyle, inlineEngagementRightStyle, composedMessageStyle, avatarOffset, } from './chat-constants.js'; import { useNavigateToThread } from './message-list-types.js'; import { useSendReaction } from './reaction-message-utils.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 { useSelector } from '../redux/redux-utils.js'; import { useStyles } from '../themes/colors.js'; import type { ChatMessageInfoItemWithHeight } from '../types/chat-types.js'; type Props = { +messageInfo: MessageInfo, +threadInfo: ThreadInfo, +sidebarThreadInfo: ?ThreadInfo, +reactions: ReactionInfo, +disabled?: boolean, +positioning?: 'left' | 'right' | 'center', +label?: ?string, }; function InlineEngagement(props: Props): React.Node { const { messageInfo, threadInfo, sidebarThreadInfo, reactions, disabled = false, positioning, label, } = props; 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 repliesText = getInlineEngagementSidebarText(sidebarThreadInfo); const sidebarStyle = React.useMemo(() => { const stylesResult = [styles.sidebar]; if (Object.keys(reactions).length === 0) { return stylesResult; } if (isRight) { stylesResult.push(styles.sidebarMarginLeft); } else { stylesResult.push(styles.sidebarMarginRight); } return stylesResult; }, [ isRight, reactions, styles.sidebar, styles.sidebarMarginLeft, styles.sidebarMarginRight, ]); const sidebarItem = React.useMemo(() => { if (!sidebarThreadInfo) { return null; } return ( {repliesText} ); }, [ sidebarThreadInfo, onPressSidebar, sidebarStyle, styles.icon, repliesStyles, repliesText, ]); const nextLocalID = useSelector(state => state.nextLocalID); const localID = `${localIDPrefix}${nextLocalID}`; const sendReaction = useSendReaction( messageInfo.id, localID, threadInfo.id, reactions, ); const onPressReaction = React.useCallback( (reaction: string) => sendReaction(reaction), [sendReaction], ); const onLongPressReaction = React.useCallback(() => { navigate<'MessageReactionsModal'>({ name: MessageReactionsModalRouteName, params: { reactions }, }); }, [navigate, reactions]); const reactionStyle = React.useMemo(() => { const stylesResult = [styles.reactionsContainer]; if (isRight) { stylesResult.push(styles.reactionsContainerMarginLeft); } else { stylesResult.push(styles.reactionsContainerMarginRight); } return stylesResult; }, [ isRight, styles.reactionsContainer, styles.reactionsContainerMarginLeft, styles.reactionsContainerMarginRight, ]); const reactionList = React.useMemo(() => { if (Object.keys(reactions).length === 0) { return null; } return Object.keys(reactions).map(reaction => { const reactionInfo = reactions[reaction]; const numOfReacts = reactionInfo.users.length; const style = reactionInfo.viewerReacted ? [...reactionStyle, styles.reactionsContainerSelected] : reactionStyle; return ( onPressReaction(reaction)} onLongPress={onLongPressReaction} activeOpacity={0.7} key={reaction} > {`${reaction} ${numOfReacts}`} ); }); }, [ onLongPressReaction, onPressReaction, reactionStyle, reactions, styles.reaction, styles.reactionsContainerSelected, ]); const inlineEngagementPositionStyle = React.useMemo(() => { const styleResult = [styles.inlineEngagement]; if (isRight) { styleResult.push(styles.rightInlineEngagement); } else if (isCenter) { styleResult.push(styles.centerInlineEngagement); } return styleResult; }, [ isCenter, isRight, styles.centerInlineEngagement, styles.inlineEngagement, styles.rightInlineEngagement, ]); return ( {editedLabel} {sidebarItem} {reactionList} ); } const unboundStyles = { inlineEngagement: { flexDirection: 'row', marginBottom: inlineEngagementStyle.marginBottom, marginLeft: avatarOffset, flexWrap: 'wrap', top: inlineEngagementStyle.topOffset, }, centerInlineEngagement: { marginLeft: 20, marginRight: 20, justifyContent: 'center', }, rightInlineEngagement: { flexDirection: 'row-reverse', marginLeft: inlineEngagementRightStyle.marginLeft, }, sidebar: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'inlineEngagementBackground', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 8, marginTop: inlineEngagementStyle.marginTop, }, sidebarMarginLeft: { marginLeft: 4, }, sidebarMarginRight: { marginRight: 4, }, 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, }, reactionsContainerSelected: { borderWidth: 1, borderColor: 'inlineEngagementLabel', paddingHorizontal: 7, paddingVertical: 3, }, reactionsContainerMarginLeft: { marginLeft: 4, }, reactionsContainerMarginRight: { marginRight: 4, }, reaction: { color: 'inlineEngagementLabel', fontSize: 14, lineHeight: 22, }, 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 + inlineEngagementStyle.topOffset, left: composedMessageStyle.marginLeft, }; } else if (positioning === 'right') { return { position: 'absolute', right: inlineEngagementRightStyle.marginLeft + composedMessageStyle.marginRight, 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/web/chat/inline-engagement.react.js b/web/chat/inline-engagement.react.js index 9f20ebb58..c56bc8986 100644 --- a/web/chat/inline-engagement.react.js +++ b/web/chat/inline-engagement.react.js @@ -1,138 +1,138 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import { useModalContext } from 'lib/components/modal-provider.react.js'; -import useInlineEngagementText from 'lib/hooks/inline-engagement-text.react.js'; import type { ReactionInfo } from 'lib/selectors/chat-selectors.js'; +import { getInlineEngagementSidebarText } from 'lib/shared/inline-engagement-utils.js'; import { stringForReactionList } from 'lib/shared/reaction-utils.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import css from './inline-engagement.css'; import CommIcon from '../CommIcon.react.js'; import MessageReactionsModal from '../modals/chat/message-reactions-modal.react.js'; import { useOnClickThread } from '../selectors/thread-selectors.js'; type Props = { +threadInfo: ?ThreadInfo, +reactions?: ReactionInfo, +positioning: 'left' | 'center' | 'right', +label?: ?string, }; function InlineEngagement(props: Props): React.Node { const { threadInfo, reactions, positioning, label } = props; const { pushModal, popModal } = useModalContext(); - const repliesText = useInlineEngagementText(threadInfo); + const repliesText = getInlineEngagementSidebarText(threadInfo); const containerClasses = classNames([ css.inlineEngagementContainer, { [css.leftContainer]: positioning === 'left', [css.centerContainer]: positioning === 'center', [css.rightContainer]: positioning === 'right', }, ]); const reactionsExist = reactions && Object.keys(reactions).length > 0; const threadsContainerClasses = classNames({ [css.threadsContainer]: threadInfo && !reactionsExist, [css.threadsSplitContainer]: threadInfo && reactionsExist, }); const reactionsContainerClasses = classNames({ [css.reactionsContainer]: reactionsExist && !threadInfo, [css.reactionsSplitContainer]: reactionsExist && threadInfo, }); const onClickThreadInner = useOnClickThread(threadInfo); const onClickThread = React.useCallback( (event: SyntheticEvent) => { popModal(); onClickThreadInner(event); }, [popModal, onClickThreadInner], ); const sidebarItem = React.useMemo(() => { if (!threadInfo || !repliesText) { return null; } return ( {repliesText} ); }, [threadInfo, repliesText, onClickThread, threadsContainerClasses]); const onClickReactions = React.useCallback( (event: SyntheticEvent) => { event.preventDefault(); if (!reactions) { return; } pushModal( , ); }, [popModal, pushModal, reactions], ); const reactionsList = React.useMemo(() => { if (!reactions || Object.keys(reactions).length === 0) { return null; } const reactionText = stringForReactionList(reactions); return ( {reactionText} ); }, [reactions, onClickReactions, reactionsContainerClasses]); const isLeft = positioning === 'left'; const labelClasses = classNames({ [css.messageLabel]: true, [css.messageLabelLeft]: isLeft, [css.messageLabelRight]: !isLeft, [css.onlyMessageLabel]: !sidebarItem && !reactionsList, }); const messageLabel = React.useMemo(() => { if (!label) { return null; } return (
{label}
); }, [label, labelClasses]); let body; if (isLeft) { body = ( <> {messageLabel} {sidebarItem} {reactionsList} ); } else { body = ( <> {sidebarItem} {reactionsList} {messageLabel} ); } return
{body}
; } export default InlineEngagement;