diff --git a/web/chat/composed-message.react.js b/web/chat/composed-message.react.js index e441e679b..32ea28f35 100644 --- a/web/chat/composed-message.react.js +++ b/web/chat/composed-message.react.js @@ -1,247 +1,247 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import { Circle as CircleIcon, CheckCircle as CheckCircleIcon, XCircle as XCircleIcon, } from 'react-feather'; import { useStringForUser } from 'lib/hooks/ens-cache.js'; import { type ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js'; import { getMessageLabel } from 'lib/shared/edit-messages-utils.js'; import { assertComposableMessageType } from 'lib/types/message-types.js'; import { type ThreadInfo } from 'lib/types/thread-types.js'; import { getComposedMessageID } from './chat-constants.js'; import css from './chat-message-list.css'; import FailedSend from './failed-send.react.js'; import InlineEngagement from './inline-engagement.react.js'; import UserAvatar from '../avatars/user-avatar.react.js'; import CommIcon from '../CommIcon.react.js'; import { type InputState, InputStateContext } from '../input/input-state.js'; import { useMessageTooltip } from '../utils/tooltip-action-utils.js'; import { tooltipPositions } from '../utils/tooltip-utils.js'; export type ComposedMessageID = string; 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, +shouldDisplayPinIndicator: boolean, +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, +containsInlineEngagement: boolean, +stringForUser: ?string, }; class ComposedMessage extends React.PureComponent { static defaultProps: { +borderRadius: number } = { borderRadius: 8, }; render(): React.Node { assertComposableMessageType(this.props.item.messageInfo.type); const { borderRadius, item, threadInfo, shouldDisplayPinIndicator } = this.props; const { hasBeenEdited, isPinned } = item; 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; const { stringForUser } = this.props; if (stringForUser) { authorName = {stringForUser}; } 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 inlineEngagement = null; const label = getMessageLabel(hasBeenEdited, threadInfo.id); if ( (this.props.containsInlineEngagement && item.threadCreatedFromMessage) || Object.keys(item.reactions).length > 0 || label ) { const positioning = isViewer ? 'right' : 'left'; inlineEngagement = (
); } let avatar; if (!isViewer && item.endsCluster) { avatar = (
); } else if (!isViewer) { avatar =
; } const pinIconPositioning = isViewer ? 'left' : 'right'; const pinIconName = pinIconPositioning === 'left' ? 'pin-mirror' : 'pin'; const pinIconContainerClassName = classNames({ [css.pinIconContainer]: true, [css.pinIconLeft]: pinIconPositioning === 'left', [css.pinIconRight]: pinIconPositioning === 'right', }); let pinIcon; if (isPinned && shouldDisplayPinIndicator) { pinIcon = (
); } return ( {authorName}
{avatar}
{pinIcon}
{this.props.children}
{deliveryIcon}
{failedSendInfo} {inlineEngagement}
); } } 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 { creator } = props.item.messageInfo; const { isViewer } = creator; const availablePositions = isViewer ? availableTooltipPositionsForViewerMessage : availableTooltipPositionsForNonViewerMessage; const containsInlineEngagement = !!item.threadCreatedFromMessage; const { onMouseLeave, onMouseEnter } = useMessageTooltip({ item, threadInfo, availablePositions, }); const shouldShowUsername = !isViewer && item.startsCluster; const stringForUser = useStringForUser(shouldShowUsername ? creator : null); return ( ); }); export default ConnectedComposedMessage; diff --git a/web/chat/inline-engagement.css b/web/chat/inline-engagement.css index 34961a756..c7fbc7e9e 100644 --- a/web/chat/inline-engagement.css +++ b/web/chat/inline-engagement.css @@ -1,89 +1,89 @@ div.inlineEngagementContainer { display: flex; flex-direction: row; align-items: center; } div.centerContainer { justify-content: center; } div.leftContainer { justify-content: flex-start; position: relative; top: -10px; left: 44px; margin-right: 44px; } div.rightContainer { justify-content: flex-end; position: relative; top: -10px; right: 30px; margin-left: 31px; } -a.threadsContainer, -a.threadsSplitContainer, +a.sidebarContainer, +a.sidebarSplitContainer, a.reactionsContainer, a.reactionsSplitContainer { background: var(--inline-engagement-bg); color: var(--inline-engagement-color); font-size: var(--s-font-14); line-height: var(--line-height-text); transition: background 0.2s ease-in-out; padding: 8px; gap: 4px; flex-direction: row; display: flex; align-items: center; } -a.threadsContainer, +a.sidebarContainer, a.reactionsContainer { border-radius: 16px; } -a.threadsSplitContainer { +a.sidebarSplitContainer { border-radius: 16px 0 0 16px; } a.reactionsSplitContainer { border-radius: 0 16px 16px 0; } -a.threadsContainer:hover, -a.threadsSplitContainer:hover, +a.sidebarContainer:hover, +a.sidebarSplitContainer:hover, a.reactionsContainer:hover, a.reactionsSplitContainer:hover { background: var(--inline-engagement-bg-hover); } div.unread { font-weight: bold; } svg.inlineEngagementIcon { color: #666666; } div.messageLabel { display: flex; flex-shrink: 0; } div.messageLabel > span { font-size: 12px; padding: 0 3px; color: var(--message-label-color); } div.messageLabelLeft { margin-left: 8px; margin-right: 4px; } div.messageLabelRight { margin-right: 12px; margin-left: 4px; } div.onlyMessageLabel { margin-top: 8px; } diff --git a/web/chat/inline-engagement.react.js b/web/chat/inline-engagement.react.js index c56bc8986..8c426f737 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 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, + +sidebarThreadInfo: ?ThreadInfo, +reactions?: ReactionInfo, +positioning: 'left' | 'center' | 'right', +label?: ?string, }; function InlineEngagement(props: Props): React.Node { - const { threadInfo, reactions, positioning, label } = props; + const { sidebarThreadInfo, reactions, positioning, label } = props; const { pushModal, popModal } = useModalContext(); - const repliesText = getInlineEngagementSidebarText(threadInfo); + const repliesText = getInlineEngagementSidebarText(sidebarThreadInfo); 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 sidebarContainerClasses = classNames({ + [css.sidebarContainer]: sidebarThreadInfo && !reactionsExist, + [css.sidebarSplitContainer]: sidebarThreadInfo && reactionsExist, }); const reactionsContainerClasses = classNames({ - [css.reactionsContainer]: reactionsExist && !threadInfo, - [css.reactionsSplitContainer]: reactionsExist && threadInfo, + [css.reactionsContainer]: reactionsExist && !sidebarThreadInfo, + [css.reactionsSplitContainer]: reactionsExist && sidebarThreadInfo, }); - const onClickThreadInner = useOnClickThread(threadInfo); + const onClickSidebarInner = useOnClickThread(sidebarThreadInfo); - const onClickThread = React.useCallback( + const onClickSidebar = React.useCallback( (event: SyntheticEvent) => { popModal(); - onClickThreadInner(event); + onClickSidebarInner(event); }, - [popModal, onClickThreadInner], + [popModal, onClickSidebarInner], ); const sidebarItem = React.useMemo(() => { - if (!threadInfo || !repliesText) { + if (!sidebarThreadInfo || !repliesText) { return null; } return ( - + {repliesText} ); - }, [threadInfo, repliesText, onClickThread, threadsContainerClasses]); + }, [sidebarThreadInfo, repliesText, onClickSidebar, sidebarContainerClasses]); 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(() => { + const editedLabel = React.useMemo(() => { if (!label) { return null; } return (
{label}
); }, [label, labelClasses]); let body; if (isLeft) { body = ( <> - {messageLabel} + {editedLabel} {sidebarItem} {reactionsList} ); } else { body = ( <> {sidebarItem} {reactionsList} - {messageLabel} + {editedLabel} ); } return
{body}
; } export default InlineEngagement; diff --git a/web/chat/robotext-message.react.js b/web/chat/robotext-message.react.js index b0fff181c..ee6899d59 100644 --- a/web/chat/robotext-message.react.js +++ b/web/chat/robotext-message.react.js @@ -1,144 +1,144 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { useDispatch } from 'react-redux'; import { type RobotextChatMessageInfoItem } from 'lib/selectors/chat-selectors.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; import type { Dispatch } from 'lib/types/redux-types.js'; import { type ThreadInfo } from 'lib/types/thread-types.js'; import { entityTextToReact, useENSNamesForEntityText, } from 'lib/utils/entity-text.js'; import InlineEngagement from './inline-engagement.react.js'; import css from './robotext-message.css'; import Markdown from '../markdown/markdown.react.js'; import { linkRules } from '../markdown/rules.react.js'; import { updateNavInfoActionType } from '../redux/action-types.js'; import { useSelector } from '../redux/redux-utils.js'; import { useMessageTooltip } from '../utils/tooltip-action-utils.js'; import { tooltipPositions } from '../utils/tooltip-utils.js'; const availableTooltipPositionsForRobotext = [ tooltipPositions.LEFT, tooltipPositions.LEFT_TOP, tooltipPositions.LEFT_BOTTOM, tooltipPositions.RIGHT, tooltipPositions.RIGHT_TOP, tooltipPositions.RIGHT_BOTTOM, ]; type Props = { +item: RobotextChatMessageInfoItem, +threadInfo: ThreadInfo, }; function RobotextMessage(props: Props): React.Node { let inlineEngagement; const { item } = props; const { threadCreatedFromMessage, reactions } = item; if (threadCreatedFromMessage || Object.keys(reactions).length > 0) { inlineEngagement = (
); } const { messageInfo, robotext } = item; const { threadID } = messageInfo; const robotextWithENSNames = useENSNamesForEntityText(robotext); invariant( robotextWithENSNames, 'useENSNamesForEntityText only returns falsey when passed falsey', ); const textParts = React.useMemo(() => { return entityTextToReact(robotextWithENSNames, threadID, { // eslint-disable-next-line react/display-name renderText: ({ text }) => ( {text} ), // eslint-disable-next-line react/display-name renderThread: ({ id, name }) => , // eslint-disable-next-line react/display-name renderColor: ({ hex }) => , }); }, [robotextWithENSNames, threadID]); const { threadInfo } = props; const { onMouseEnter, onMouseLeave } = useMessageTooltip({ item, threadInfo, availablePositions: availableTooltipPositionsForRobotext, }); return (
{textParts}
{inlineEngagement}
); } 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 MemoizedRobotextMessage: React.ComponentType = React.memo(RobotextMessage); export default MemoizedRobotextMessage;