diff --git a/web/chat/composed-message.react.js b/web/chat/composed-message.react.js index 5878a07bd..f61afca1b 100644 --- a/web/chat/composed-message.react.js +++ b/web/chat/composed-message.react.js @@ -1,190 +1,194 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import { Circle as CircleIcon, CheckCircle as CheckCircleIcon, XCircle as XCircleIcon, } from 'react-feather'; import { type ChatMessageInfoItem } from 'lib/selectors/chat-selectors'; import { stringForUser } from 'lib/shared/user-utils'; import { assertComposableMessageType } from 'lib/types/message-types'; import { type ThreadInfo } from 'lib/types/thread-types'; import { type InputState, InputStateContext } from '../input/input-state'; import { tooltipPositions, useMessageTooltip } from '../utils/tooltip-utils'; import css from './chat-message-list.css'; import FailedSend from './failed-send.react'; import InlineSidebar from './inline-sidebar.react'; const availableTooltipPositionsForViewerMessage = [ tooltipPositions.LEFT, tooltipPositions.LEFT_BOTTOM, tooltipPositions.LEFT_TOP, tooltipPositions.RIGHT, tooltipPositions.RIGHT_BOTTOM, tooltipPositions.RIGHT_TOP, tooltipPositions.BOTTOM, tooltipPositions.TOP, ]; const availableTooltipPositionsForNonViewerMessage = [ tooltipPositions.RIGHT, tooltipPositions.RIGHT_BOTTOM, tooltipPositions.RIGHT_TOP, tooltipPositions.LEFT, tooltipPositions.LEFT_BOTTOM, tooltipPositions.LEFT_TOP, tooltipPositions.BOTTOM, tooltipPositions.TOP, ]; type BaseProps = { +item: ChatMessageInfoItem, +threadInfo: ThreadInfo, +sendFailed: boolean, +children: React.Node, +fixedWidth?: boolean, +borderRadius: number, }; type BaseConfig = React.Config; type Props = { ...BaseProps, // withInputState +inputState: ?InputState, +onMouseLeave: ?() => mixed, +onMouseEnter: (event: SyntheticEvent) => mixed, +containsInlineSidebar: boolean, }; class ComposedMessage extends React.PureComponent { static defaultProps: { +borderRadius: number } = { borderRadius: 8, }; render(): React.Node { assertComposableMessageType(this.props.item.messageInfo.type); const { borderRadius, item, threadInfo } = this.props; const { id, creator } = item.messageInfo; const threadColor = threadInfo.color; const { isViewer } = creator; const contentClassName = classNames({ [css.content]: true, [css.viewerContent]: isViewer, [css.nonViewerContent]: !isViewer, }); const messageBoxContainerClassName = classNames({ [css.messageBoxContainer]: true, [css.fixedWidthMessageBoxContainer]: this.props.fixedWidth, }); const messageBoxClassName = classNames({ [css.messageBox]: true, [css.fixedWidthMessageBox]: this.props.fixedWidth, }); const messageBoxStyle = { borderTopRightRadius: isViewer && !item.startsCluster ? 0 : borderRadius, borderBottomRightRadius: isViewer && !item.endsCluster ? 0 : borderRadius, borderTopLeftRadius: !isViewer && !item.startsCluster ? 0 : borderRadius, borderBottomLeftRadius: !isViewer && !item.endsCluster ? 0 : borderRadius, }; let authorName = null; if (!isViewer && item.startsCluster) { authorName = ( {stringForUser(creator)} ); } let deliveryIcon = null; let failedSendInfo = null; if (isViewer) { let deliveryIconSpan; let deliveryIconColor = threadColor; if (id !== null && id !== undefined) { deliveryIconSpan = ; } else if (this.props.sendFailed) { deliveryIconSpan = ; deliveryIconColor = 'FF0000'; failedSendInfo = ; } else { deliveryIconSpan = ; } deliveryIcon = (
{deliveryIconSpan}
); } let inlineSidebar = null; - if (this.props.containsInlineSidebar && item.threadCreatedFromMessage) { + if ( + (this.props.containsInlineSidebar && item.threadCreatedFromMessage) || + item.reactions.size > 0 + ) { const positioning = isViewer ? 'right' : 'left'; inlineSidebar = (
); } return ( {authorName}
{this.props.children}
{deliveryIcon}
{failedSendInfo} {inlineSidebar}
); } } type ConnectedConfig = React.Config< BaseProps, typeof ComposedMessage.defaultProps, >; const ConnectedComposedMessage: React.ComponentType = React.memo( function ConnectedComposedMessage(props) { const { item, threadInfo } = props; const inputState = React.useContext(InputStateContext); const isViewer = props.item.messageInfo.creator.isViewer; const availablePositions = isViewer ? availableTooltipPositionsForViewerMessage : availableTooltipPositionsForNonViewerMessage; const containsInlineSidebar = !!item.threadCreatedFromMessage; const { onMouseLeave, onMouseEnter } = useMessageTooltip({ item, threadInfo, availablePositions, }); return ( ); }, ); export default ConnectedComposedMessage; diff --git a/web/chat/inline-sidebar.react.js b/web/chat/inline-sidebar.react.js index 71cd3172a..c5dcbb3d6 100644 --- a/web/chat/inline-sidebar.react.js +++ b/web/chat/inline-sidebar.react.js @@ -1,75 +1,73 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import useInlineSidebarText from 'lib/hooks/inline-sidebar-text.react'; +import type { MessageReactionInfo } from 'lib/selectors/chat-selectors'; +import { stringForReactionList } from 'lib/shared/reaction-utils'; import type { ThreadInfo } from 'lib/types/thread-types'; import CommIcon from '../CommIcon.react'; import { useOnClickThread } from '../selectors/thread-selectors'; import css from './inline-sidebar.css'; type Props = { +threadInfo: ?ThreadInfo, - +reactions?: $ReadOnlyArray, + +reactions?: $ReadOnlyMap, +positioning: 'left' | 'center' | 'right', }; function InlineSidebar(props: Props): React.Node { const { threadInfo, positioning, reactions } = props; const inlineSidebarText = useInlineSidebarText(threadInfo); const containerClasses = classNames([ css.inlineSidebarContainer, { [css.leftContainer]: positioning === 'left', [css.centerContainer]: positioning === 'center', [css.rightContainer]: positioning === 'right', }, ]); const reactionsList = React.useMemo(() => { - if (!reactions || reactions.length === 0) { + if (!reactions || reactions.size === 0) { return null; } - const reactionsItems = reactions.map(reaction => { - return ( -
- {reaction} -
- ); - }); - return
{reactionsItems}
; + + const reactionText = stringForReactionList(reactions); + + return
{reactionText}
; }, [reactions]); const onClick = useOnClickThread(threadInfo); const threadInfoExists = !!threadInfo; const sidebarItem = React.useMemo(() => { if (!threadInfoExists || !inlineSidebarText) { return null; } return (
{inlineSidebarText.repliesText}
); }, [threadInfoExists, inlineSidebarText]); return ( ); } export default InlineSidebar; diff --git a/web/chat/robotext-message.react.js b/web/chat/robotext-message.react.js index ffa428d6d..b8a12f95d 100644 --- a/web/chat/robotext-message.react.js +++ b/web/chat/robotext-message.react.js @@ -1,166 +1,170 @@ // @flow import * as React from 'react'; import { useDispatch } from 'react-redux'; import { type RobotextChatMessageInfoItem } from 'lib/selectors/chat-selectors'; import { threadInfoSelector } from 'lib/selectors/thread-selectors'; import { splitRobotext, parseRobotextEntity } from 'lib/shared/message-utils'; import type { Dispatch } from 'lib/types/redux-types'; import { type ThreadInfo } from 'lib/types/thread-types'; import Markdown from '../markdown/markdown.react'; import { linkRules } from '../markdown/rules.react'; import { updateNavInfoActionType } from '../redux/action-types'; import { useSelector } from '../redux/redux-utils'; import { tooltipPositions, useMessageTooltip } from '../utils/tooltip-utils'; import InlineSidebar from './inline-sidebar.react'; import css from './robotext-message.css'; const availableTooltipPositionsForRobotext = [ tooltipPositions.LEFT, tooltipPositions.LEFT_TOP, tooltipPositions.LEFT_BOTTOM, tooltipPositions.RIGHT, tooltipPositions.RIGHT_TOP, tooltipPositions.RIGHT_BOTTOM, ]; type BaseProps = { +item: RobotextChatMessageInfoItem, +threadInfo: ThreadInfo, }; type Props = { ...BaseProps, +onMouseLeave: ?() => mixed, +onMouseEnter: (event: SyntheticEvent) => mixed, }; class RobotextMessage extends React.PureComponent { render() { let inlineSidebar; - if (this.props.item.threadCreatedFromMessage) { + if ( + this.props.item.threadCreatedFromMessage || + this.props.item.reactions.size > 0 + ) { inlineSidebar = (
); } return (
{this.linkedRobotext()}
{inlineSidebar}
); } linkedRobotext() { const { item } = this.props; const { robotext } = item; const robotextParts = splitRobotext(robotext); const textParts = []; let keyIndex = 0; for (const splitPart of robotextParts) { if (splitPart === '') { continue; } if (splitPart.charAt(0) !== '<') { const key = `text${keyIndex++}`; textParts.push( {decodeURI(splitPart)} , ); continue; } const { rawText, entityType, id } = parseRobotextEntity(splitPart); if (entityType === 't' && id !== item.messageInfo.threadID) { textParts.push(); } else if (entityType === 'c') { textParts.push(); } else { textParts.push(rawText); } } return textParts; } } type BaseInnerThreadEntityProps = { +id: string, +name: string, }; type InnerThreadEntityProps = { ...BaseInnerThreadEntityProps, +threadInfo: ThreadInfo, +dispatch: Dispatch, }; class InnerThreadEntity extends React.PureComponent { render() { return {this.props.name}; } onClickThread = (event: SyntheticEvent) => { event.preventDefault(); const id = this.props.id; this.props.dispatch({ type: updateNavInfoActionType, payload: { activeChatThreadID: id, }, }); }; } const ThreadEntity = React.memo( function ConnectedInnerThreadEntity(props: BaseInnerThreadEntityProps) { const { id } = props; const threadInfo = useSelector(state => threadInfoSelector(state)[id]); const dispatch = useDispatch(); return ( ); }, ); function ColorEntity(props: { color: string }) { const colorStyle = { color: props.color }; return {props.color}; } const ConnectedRobotextMessage: React.ComponentType = React.memo( function ConnectedRobotextMessage(props) { const { item, threadInfo } = props; const { onMouseLeave, onMouseEnter } = useMessageTooltip({ item, threadInfo, availablePositions: availableTooltipPositionsForRobotext, }); return ( ); }, ); export default ConnectedRobotextMessage;