diff --git a/web/CommIcon.react.js b/web/CommIcon.react.js index 273487eba..9877f3c9f 100644 --- a/web/CommIcon.react.js +++ b/web/CommIcon.react.js @@ -1,38 +1,40 @@ // @flow import * as React from 'react'; import IcomoonReact from 'react-icomoon'; import iconSet from 'lib/shared/comm-icon-config.json'; const IcomoonIcon = IcomoonReact.default; export type CommIcons = | 'cloud-filled' | 'sidebar' | 'sidebar-filled' | 'reply' | 'reply-filled' | 'megaphone' | 'copy-filled' - | 'emote-smile-filled'; + | 'emote-smile-filled' + | 'pin' + | 'unpin'; type CommIconProps = { +icon: CommIcons, +size: number | string, +color?: string, +title?: string, +className?: string, +disableFill?: boolean, +removeInlineStyle?: boolean, }; const iconStyle = { stroke: 'none', }; function CommIcon(props: CommIconProps): React.Node { return ; } export default CommIcon; diff --git a/web/chat/composed-message.react.js b/web/chat/composed-message.react.js index 29b371aa2..bff43aa1e 100644 --- a/web/chat/composed-message.react.js +++ b/web/chat/composed-message.react.js @@ -1,224 +1,225 @@ // @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 css from './chat-message-list.css'; import FailedSend from './failed-send.react.js'; import InlineEngagement from './inline-engagement.react.js'; import UserAvatar from '../components/user-avatar.react.js'; import { type InputState, InputStateContext } from '../input/input-state.js'; import { shouldRenderAvatars } from '../utils/avatar-utils.js'; -import { tooltipPositions, useMessageTooltip } from '../utils/tooltip-utils.js'; +import { useMessageTooltip } from '../utils/tooltip-action-utils.js'; +import { tooltipPositions } from '../utils/tooltip-utils.js'; 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, +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 } = this.props; const { hasBeenEdited } = 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, [css.messageBoxContainerPositionAvatar]: shouldRenderAvatars, [css.messageBoxContainerPositionNoAvatar]: !shouldRenderAvatars, }); 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; const authorNameClassName = classNames({ [css.authorName]: true, [css.authorNamePositionAvatar]: shouldRenderAvatars, [css.authorNamePositionNoAvatar]: !shouldRenderAvatars, }); 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); 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 && shouldRenderAvatars) { avatar = (
); } else if (!isViewer && shouldRenderAvatars) { avatar =
; } return ( {authorName}
{avatar}
{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/robotext-message.react.js b/web/chat/robotext-message.react.js index 43f2a29b0..b0fff181c 100644 --- a/web/chat/robotext-message.react.js +++ b/web/chat/robotext-message.react.js @@ -1,143 +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 { tooltipPositions, useMessageTooltip } from '../utils/tooltip-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; diff --git a/web/modals/chat/toggle-pin-modal.css b/web/modals/chat/toggle-pin-modal.css new file mode 100644 index 000000000..e69de29bb diff --git a/web/modals/chat/toggle-pin-modal.react.js b/web/modals/chat/toggle-pin-modal.react.js new file mode 100644 index 000000000..8ccc23139 --- /dev/null +++ b/web/modals/chat/toggle-pin-modal.react.js @@ -0,0 +1,18 @@ +// @flow + +import * as React from 'react'; + +import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js'; +import type { ThreadInfo } from 'lib/types/thread-types.js'; + +type TogglePinModalProps = { + +item: ChatMessageInfoItem, + +threadInfo: ThreadInfo, +}; + +// eslint-disable-next-line no-unused-vars +function TogglePinModal(props: TogglePinModalProps): React.Node { + return <>; +} + +export default TogglePinModal; diff --git a/web/utils/tooltip-utils.js b/web/utils/tooltip-action-utils.js similarity index 53% copy from web/utils/tooltip-utils.js copy to web/utils/tooltip-action-utils.js index e3abeb60c..e77fb0750 100644 --- a/web/utils/tooltip-utils.js +++ b/web/utils/tooltip-action-utils.js @@ -1,692 +1,429 @@ // @flow import invariant from 'invariant'; import _debounce from 'lodash/debounce.js'; import * as React from 'react'; +import { useModalContext } from 'lib/components/modal-provider.react.js'; import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js'; import { createMessageReply } from 'lib/shared/message-utils.js'; import { useCanCreateReactionFromMessage } from 'lib/shared/reaction-utils.js'; import { threadHasPermission, useSidebarExistsOrCanBeCreated, } from 'lib/shared/thread-utils.js'; import { isComposableMessageType, messageTypes, } from 'lib/types/message-types.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import { threadPermissions } from 'lib/types/thread-types.js'; import { longAbsoluteDate } from 'lib/utils/date-utils.js'; -import { getAppContainerPositionInfo } from './window-utils.js'; import { - tooltipButtonStyle, - tooltipLabelStyle, - tooltipStyle, -} from '../chat/chat-constants.js'; + type MessageTooltipAction, + findTooltipPosition, + getMessageActionTooltipStyle, + calculateTooltipSize, + type TooltipSize, + type TooltipPosition, +} from './tooltip-utils.js'; import MessageTooltip from '../chat/message-tooltip.react.js'; import type { PositionInfo } from '../chat/position-types.js'; import { useTooltipContext } from '../chat/tooltip-provider.js'; import CommIcon from '../CommIcon.react.js'; import { InputStateContext } from '../input/input-state.js'; +import TogglePinModal from '../modals/chat/toggle-pin-modal.react.js'; import { useOnClickPendingSidebar, useOnClickThread, } from '../selectors/thread-selectors.js'; -import { calculateMaxTextWidth } from '../utils/text-utils.js'; - -export const tooltipPositions = Object.freeze({ - LEFT: 'left', - RIGHT: 'right', - LEFT_BOTTOM: 'left-bottom', - RIGHT_BOTTOM: 'right-bottom', - LEFT_TOP: 'left-top', - RIGHT_TOP: 'right-top', - TOP: 'top', - BOTTOM: 'bottom', -}); - -export type TooltipSize = { - +height: number, - +width: number, -}; - -export type TooltipPositionStyle = { - +anchorPoint: { - +x: number, - +y: number, - }, - +verticalPosition: 'top' | 'bottom', - +horizontalPosition: 'left' | 'right', - +alignment: 'left' | 'center' | 'right', -}; - -export type TooltipPosition = $Values; - -export type MessageTooltipAction = { - +label: string, - +onClick: (SyntheticEvent) => mixed, - +actionButtonContent: React.Node, -}; - -const font = - '14px "Inter", -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", ' + - '"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", ui-sans-serif'; - -type FindTooltipPositionArgs = { - +sourcePositionInfo: PositionInfo, - +tooltipSize: TooltipSize, - +availablePositions: $ReadOnlyArray, - +defaultPosition: TooltipPosition, - +preventDisplayingBelowSource?: boolean, -}; - -function findTooltipPosition({ - sourcePositionInfo, - tooltipSize, - availablePositions, - defaultPosition, - preventDisplayingBelowSource, -}: FindTooltipPositionArgs): TooltipPosition { - const appContainerPositionInfo = getAppContainerPositionInfo(); - - if (!appContainerPositionInfo) { - return defaultPosition; - } - - const pointingTo = sourcePositionInfo; - const { - top: containerTop, - left: containerLeft, - right: containerRight, - bottom: containerBottom, - } = appContainerPositionInfo; - - const tooltipWidth = tooltipSize.width; - const tooltipHeight = tooltipSize.height; - - const canBeDisplayedOnLeft = containerLeft + tooltipWidth <= pointingTo.left; - const canBeDisplayedOnRight = - tooltipWidth + pointingTo.right <= containerRight; - - const willCoverSidebarOnTopSideways = - preventDisplayingBelowSource && - pointingTo.top + tooltipHeight > pointingTo.bottom; - - const canBeDisplayedOnTopSideways = - pointingTo.top >= containerTop && - pointingTo.top + tooltipHeight <= containerBottom && - !willCoverSidebarOnTopSideways; - - const canBeDisplayedOnBottomSideways = - pointingTo.bottom <= containerBottom && - pointingTo.bottom - tooltipHeight >= containerTop; - - const verticalCenterOfPointingTo = pointingTo.top + pointingTo.height / 2; - const horizontalCenterOfPointingTo = pointingTo.left + pointingTo.width / 2; - - const willCoverSidebarInTheMiddleSideways = - preventDisplayingBelowSource && - verticalCenterOfPointingTo + tooltipHeight / 2 > pointingTo.bottom; - - const canBeDisplayedInTheMiddleSideways = - verticalCenterOfPointingTo - tooltipHeight / 2 >= containerTop && - verticalCenterOfPointingTo + tooltipHeight / 2 <= containerBottom && - !willCoverSidebarInTheMiddleSideways; - - const canBeDisplayedOnTop = - pointingTo.top - tooltipHeight >= containerTop && - horizontalCenterOfPointingTo - tooltipWidth / 2 >= containerLeft && - horizontalCenterOfPointingTo + tooltipWidth / 2 <= containerRight; - - const canBeDisplayedOnBottom = - pointingTo.bottom + tooltipHeight <= containerBottom && - horizontalCenterOfPointingTo - tooltipWidth / 2 >= containerLeft && - horizontalCenterOfPointingTo + tooltipWidth / 2 <= containerRight && - !preventDisplayingBelowSource; - - for (const tooltipPosition of availablePositions) { - if ( - tooltipPosition === tooltipPositions.RIGHT && - canBeDisplayedOnRight && - canBeDisplayedInTheMiddleSideways - ) { - return tooltipPosition; - } else if ( - tooltipPosition === tooltipPositions.RIGHT_BOTTOM && - canBeDisplayedOnRight && - canBeDisplayedOnBottomSideways - ) { - return tooltipPosition; - } else if ( - tooltipPosition === tooltipPositions.LEFT && - canBeDisplayedOnLeft && - canBeDisplayedInTheMiddleSideways - ) { - return tooltipPosition; - } else if ( - tooltipPosition === tooltipPositions.LEFT_BOTTOM && - canBeDisplayedOnLeft && - canBeDisplayedOnBottomSideways - ) { - return tooltipPosition; - } else if ( - tooltipPosition === tooltipPositions.LEFT_TOP && - canBeDisplayedOnLeft && - canBeDisplayedOnTopSideways - ) { - return tooltipPosition; - } else if ( - tooltipPosition === tooltipPositions.RIGHT_TOP && - canBeDisplayedOnRight && - canBeDisplayedOnTopSideways - ) { - return tooltipPosition; - } else if ( - tooltipPosition === tooltipPositions.TOP && - canBeDisplayedOnTop - ) { - return tooltipPosition; - } else if ( - tooltipPosition === tooltipPositions.BOTTOM && - canBeDisplayedOnBottom - ) { - return tooltipPosition; - } - } - return defaultPosition; -} - -type GetMessageActionTooltipStyleParams = { - +sourcePositionInfo: PositionInfo, - +tooltipSize: TooltipSize, - +tooltipPosition: TooltipPosition, -}; - -function getMessageActionTooltipStyle({ - sourcePositionInfo, - tooltipSize, - tooltipPosition, -}: GetMessageActionTooltipStyleParams): TooltipPositionStyle { - if (tooltipPosition === tooltipPositions.RIGHT_TOP) { - return { - anchorPoint: { - x: sourcePositionInfo.right, - y: sourcePositionInfo.top, - }, - horizontalPosition: 'right', - verticalPosition: 'bottom', - alignment: 'left', - }; - } else if (tooltipPosition === tooltipPositions.LEFT_TOP) { - return { - anchorPoint: { - x: sourcePositionInfo.left, - y: sourcePositionInfo.top, - }, - horizontalPosition: 'left', - verticalPosition: 'bottom', - alignment: 'right', - }; - } else if (tooltipPosition === tooltipPositions.RIGHT_BOTTOM) { - return { - anchorPoint: { - x: sourcePositionInfo.right, - y: sourcePositionInfo.bottom, - }, - horizontalPosition: 'right', - verticalPosition: 'top', - alignment: 'left', - }; - } else if (tooltipPosition === tooltipPositions.LEFT_BOTTOM) { - return { - anchorPoint: { - x: sourcePositionInfo.left, - y: sourcePositionInfo.bottom, - }, - horizontalPosition: 'left', - verticalPosition: 'top', - alignment: 'right', - }; - } else if (tooltipPosition === tooltipPositions.LEFT) { - return { - anchorPoint: { - x: sourcePositionInfo.left, - y: - sourcePositionInfo.top + - sourcePositionInfo.height / 2 - - tooltipSize.height / 2, - }, - horizontalPosition: 'left', - verticalPosition: 'bottom', - alignment: 'right', - }; - } else if (tooltipPosition === tooltipPositions.RIGHT) { - return { - anchorPoint: { - x: sourcePositionInfo.right, - y: - sourcePositionInfo.top + - sourcePositionInfo.height / 2 - - tooltipSize.height / 2, - }, - horizontalPosition: 'right', - verticalPosition: 'bottom', - alignment: 'left', - }; - } else if (tooltipPosition === tooltipPositions.TOP) { - return { - anchorPoint: { - x: - sourcePositionInfo.left + - sourcePositionInfo.width / 2 - - tooltipSize.width / 2, - y: sourcePositionInfo.top, - }, - horizontalPosition: 'right', - verticalPosition: 'top', - alignment: 'center', - }; - } else if (tooltipPosition === tooltipPositions.BOTTOM) { - return { - anchorPoint: { - x: - sourcePositionInfo.left + - sourcePositionInfo.width / 2 - - tooltipSize.width / 2, - y: sourcePositionInfo.bottom, - }, - horizontalPosition: 'right', - verticalPosition: 'bottom', - alignment: 'center', - }; - } - invariant(false, `Unexpected tooltip position value: ${tooltipPosition}`); -} - -type CalculateTooltipSizeArgs = { - +tooltipLabels: $ReadOnlyArray, - +timestamp: string, -}; - -function calculateTooltipSize({ - tooltipLabels, - timestamp, -}: CalculateTooltipSizeArgs): { - +width: number, - +height: number, -} { - const textWidth = - calculateMaxTextWidth([...tooltipLabels, timestamp], font) + - 2 * tooltipLabelStyle.padding; - const buttonsWidth = - tooltipLabels.length * - (tooltipButtonStyle.width + - tooltipButtonStyle.paddingLeft + - tooltipButtonStyle.paddingRight); - const width = - Math.max(textWidth, buttonsWidth) + - tooltipStyle.paddingLeft + - tooltipStyle.paddingRight; - const height = - (tooltipLabelStyle.height + 2 * tooltipLabelStyle.padding) * 2 + - tooltipStyle.rowGap * 2 + - tooltipButtonStyle.height; - return { - width, - height, - }; -} function useMessageTooltipSidebarAction( item: ChatMessageInfoItem, threadInfo: ThreadInfo, ): ?MessageTooltipAction { const { threadCreatedFromMessage, messageInfo } = item; const sidebarExists = !!threadCreatedFromMessage; const sidebarExistsOrCanBeCreated = useSidebarExistsOrCanBeCreated( threadInfo, item, ); const openThread = useOnClickThread(threadCreatedFromMessage); const openPendingSidebar = useOnClickPendingSidebar(messageInfo, threadInfo); return React.useMemo(() => { if (!sidebarExistsOrCanBeCreated) { return null; } const buttonContent = ; const onClick = (event: SyntheticEvent) => { if (threadCreatedFromMessage) { openThread(event); } else { openPendingSidebar(event); } }; return { actionButtonContent: buttonContent, onClick, label: sidebarExists ? 'Go to thread' : 'Create thread', }; }, [ openPendingSidebar, openThread, sidebarExists, sidebarExistsOrCanBeCreated, threadCreatedFromMessage, ]); } function useMessageTooltipReplyAction( item: ChatMessageInfoItem, threadInfo: ThreadInfo, ): ?MessageTooltipAction { const { messageInfo } = item; const inputState = React.useContext(InputStateContext); invariant(inputState, 'inputState is required'); const { addReply } = inputState; return React.useMemo(() => { if ( !isComposableMessageType(item.messageInfo.type) || !threadHasPermission(threadInfo, threadPermissions.VOICED) ) { return null; } const buttonContent = ; const onClick = () => { if (!messageInfo.text) { return; } addReply(createMessageReply(messageInfo.text)); }; return { actionButtonContent: buttonContent, onClick, label: 'Reply', }; }, [addReply, item.messageInfo.type, messageInfo, threadInfo]); } const copiedMessageDurationMs = 2000; function useMessageCopyAction( item: ChatMessageInfoItem, ): ?MessageTooltipAction { const { messageInfo } = item; const [successful, setSuccessful] = React.useState(false); const resetStatusAfterTimeout = React.useRef( _debounce(() => setSuccessful(false), copiedMessageDurationMs), ); const onSuccess = React.useCallback(() => { setSuccessful(true); resetStatusAfterTimeout.current(); }, []); React.useEffect(() => resetStatusAfterTimeout.current.cancel, []); return React.useMemo(() => { if (messageInfo.type !== messageTypes.TEXT) { return null; } const buttonContent = ; const onClick = async () => { try { await navigator.clipboard.writeText(messageInfo.text); onSuccess(); } catch (e) { setSuccessful(false); } }; return { actionButtonContent: buttonContent, onClick, label: successful ? 'Copied!' : 'Copy', }; }, [messageInfo.text, messageInfo.type, onSuccess, successful]); } function useMessageReactAction( item: ChatMessageInfoItem, threadInfo: ThreadInfo, ): ?MessageTooltipAction { const { messageInfo } = item; const { setShouldRenderEmojiKeyboard } = useTooltipContext(); const canCreateReactionFromMessage = useCanCreateReactionFromMessage( threadInfo, messageInfo, ); return React.useMemo(() => { if (!canCreateReactionFromMessage) { return null; } const buttonContent = ; const onClickReact = () => { if (!setShouldRenderEmojiKeyboard) { return; } setShouldRenderEmojiKeyboard(true); }; return { actionButtonContent: buttonContent, onClick: onClickReact, label: 'React', }; }, [canCreateReactionFromMessage, setShouldRenderEmojiKeyboard]); } +function useMessageTogglePinAction( + item: ChatMessageInfoItem, + threadInfo: ThreadInfo, +): ?MessageTooltipAction { + const { pushModal } = useModalContext(); + const { messageInfo, isPinned } = item; + + const canTogglePin = + isComposableMessageType(messageInfo.type) && + threadHasPermission(threadInfo, threadPermissions.MANAGE_PINS); + + return React.useMemo(() => { + if (!canTogglePin) { + return null; + } + + const iconName = isPinned ? 'unpin' : 'pin'; + + const buttonContent = ; + + const onClickTogglePin = () => { + pushModal(); + }; + + return { + actionButtonContent: buttonContent, + onClick: onClickTogglePin, + label: isPinned ? 'Unpin' : 'Pin', + }; + }, [canTogglePin, isPinned, pushModal, item, threadInfo]); +} + function useMessageTooltipActions( item: ChatMessageInfoItem, threadInfo: ThreadInfo, ): $ReadOnlyArray { const sidebarAction = useMessageTooltipSidebarAction(item, threadInfo); const replyAction = useMessageTooltipReplyAction(item, threadInfo); const copyAction = useMessageCopyAction(item); const reactAction = useMessageReactAction(item, threadInfo); + const togglePinAction = useMessageTogglePinAction(item, threadInfo); return React.useMemo( - () => [replyAction, sidebarAction, copyAction, reactAction].filter(Boolean), - [replyAction, sidebarAction, copyAction, reactAction], + () => + [ + replyAction, + sidebarAction, + copyAction, + reactAction, + togglePinAction, + ].filter(Boolean), + [replyAction, sidebarAction, copyAction, reactAction, togglePinAction], ); } type UseMessageTooltipArgs = { +availablePositions: $ReadOnlyArray, +item: ChatMessageInfoItem, +threadInfo: ThreadInfo, }; type UseMessageTooltipResult = { onMouseEnter: (event: SyntheticEvent) => void, onMouseLeave: ?() => mixed, }; type CreateTooltipParams = { +tooltipMessagePosition: ?PositionInfo, +tooltipSize: TooltipSize, +availablePositions: $ReadOnlyArray, +containsInlineEngagement: boolean, +tooltipActions: $ReadOnlyArray, +messageTimestamp: string, +item: ChatMessageInfoItem, +threadInfo: ThreadInfo, }; function createTooltip(params: CreateTooltipParams) { const { tooltipMessagePosition, tooltipSize, availablePositions, containsInlineEngagement, tooltipActions, messageTimestamp, item, threadInfo, } = params; if (!tooltipMessagePosition) { return; } const tooltipPosition = findTooltipPosition({ sourcePositionInfo: tooltipMessagePosition, tooltipSize, availablePositions, defaultPosition: availablePositions[0], preventDisplayingBelowSource: containsInlineEngagement, }); if (!tooltipPosition) { return; } const tooltipPositionStyle = getMessageActionTooltipStyle({ tooltipPosition, sourcePositionInfo: tooltipMessagePosition, tooltipSize, }); const tooltip = ( ); return { tooltip, tooltipPositionStyle }; } function useMessageTooltip({ availablePositions, item, threadInfo, }: UseMessageTooltipArgs): UseMessageTooltipResult { const [onMouseLeave, setOnMouseLeave] = React.useState mixed>(null); const { renderTooltip } = useTooltipContext(); const tooltipActions = useMessageTooltipActions(item, threadInfo); const containsInlineEngagement = !!item.threadCreatedFromMessage; const messageTimestamp = React.useMemo(() => { const time = item.messageInfo.time; return longAbsoluteDate(time); }, [item.messageInfo.time]); const tooltipSize = React.useMemo(() => { if (typeof document === 'undefined') { return { width: 0, height: 0, }; } const tooltipLabels = tooltipActions.map(action => action.label); return calculateTooltipSize({ tooltipLabels, timestamp: messageTimestamp, }); }, [messageTimestamp, tooltipActions]); const updateTooltip = React.useRef(); const [tooltipMessagePosition, setTooltipMessagePosition] = React.useState(); const onMouseEnter = React.useCallback( (event: SyntheticEvent) => { if (!renderTooltip) { return; } const rect = event.currentTarget.getBoundingClientRect(); const { top, bottom, left, right, height, width } = rect; const messagePosition = { top, bottom, left, right, height, width }; setTooltipMessagePosition(messagePosition); const tooltipResult = createTooltip({ tooltipMessagePosition, tooltipSize, availablePositions, containsInlineEngagement, tooltipActions, messageTimestamp, item, threadInfo, }); if (!tooltipResult) { return; } const { tooltip, tooltipPositionStyle } = tooltipResult; const renderTooltipResult = renderTooltip({ newNode: tooltip, tooltipPositionStyle, }); if (renderTooltipResult) { const { onMouseLeaveCallback: callback } = renderTooltipResult; setOnMouseLeave((() => callback: () => () => mixed)); updateTooltip.current = renderTooltipResult.updateTooltip; } }, [ availablePositions, containsInlineEngagement, item, messageTimestamp, renderTooltip, threadInfo, tooltipActions, tooltipMessagePosition, tooltipSize, ], ); React.useEffect(() => { if (!updateTooltip.current) { return; } const tooltipResult = createTooltip({ tooltipMessagePosition, tooltipSize, availablePositions, containsInlineEngagement, tooltipActions, messageTimestamp, item, threadInfo, }); if (!tooltipResult) { return; } updateTooltip.current?.(tooltipResult.tooltip); }, [ availablePositions, containsInlineEngagement, item, messageTimestamp, threadInfo, tooltipActions, tooltipMessagePosition, tooltipSize, ]); return { onMouseEnter, onMouseLeave, }; } export { - findTooltipPosition, - calculateTooltipSize, - getMessageActionTooltipStyle, useMessageTooltipSidebarAction, useMessageTooltipReplyAction, useMessageReactAction, useMessageTooltipActions, useMessageTooltip, }; diff --git a/web/utils/tooltip-utils.js b/web/utils/tooltip-utils.js index e3abeb60c..904a3f81f 100644 --- a/web/utils/tooltip-utils.js +++ b/web/utils/tooltip-utils.js @@ -1,692 +1,322 @@ // @flow import invariant from 'invariant'; -import _debounce from 'lodash/debounce.js'; import * as React from 'react'; -import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js'; -import { createMessageReply } from 'lib/shared/message-utils.js'; -import { useCanCreateReactionFromMessage } from 'lib/shared/reaction-utils.js'; -import { - threadHasPermission, - useSidebarExistsOrCanBeCreated, -} from 'lib/shared/thread-utils.js'; -import { - isComposableMessageType, - messageTypes, -} from 'lib/types/message-types.js'; -import type { ThreadInfo } from 'lib/types/thread-types.js'; -import { threadPermissions } from 'lib/types/thread-types.js'; -import { longAbsoluteDate } from 'lib/utils/date-utils.js'; - import { getAppContainerPositionInfo } from './window-utils.js'; import { tooltipButtonStyle, tooltipLabelStyle, tooltipStyle, } from '../chat/chat-constants.js'; -import MessageTooltip from '../chat/message-tooltip.react.js'; import type { PositionInfo } from '../chat/position-types.js'; -import { useTooltipContext } from '../chat/tooltip-provider.js'; -import CommIcon from '../CommIcon.react.js'; -import { InputStateContext } from '../input/input-state.js'; -import { - useOnClickPendingSidebar, - useOnClickThread, -} from '../selectors/thread-selectors.js'; import { calculateMaxTextWidth } from '../utils/text-utils.js'; export const tooltipPositions = Object.freeze({ LEFT: 'left', RIGHT: 'right', LEFT_BOTTOM: 'left-bottom', RIGHT_BOTTOM: 'right-bottom', LEFT_TOP: 'left-top', RIGHT_TOP: 'right-top', TOP: 'top', BOTTOM: 'bottom', }); export type TooltipSize = { +height: number, +width: number, }; export type TooltipPositionStyle = { +anchorPoint: { +x: number, +y: number, }, +verticalPosition: 'top' | 'bottom', +horizontalPosition: 'left' | 'right', +alignment: 'left' | 'center' | 'right', }; export type TooltipPosition = $Values; export type MessageTooltipAction = { +label: string, +onClick: (SyntheticEvent) => mixed, +actionButtonContent: React.Node, }; const font = '14px "Inter", -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", ' + '"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", ui-sans-serif'; type FindTooltipPositionArgs = { +sourcePositionInfo: PositionInfo, +tooltipSize: TooltipSize, +availablePositions: $ReadOnlyArray, +defaultPosition: TooltipPosition, +preventDisplayingBelowSource?: boolean, }; function findTooltipPosition({ sourcePositionInfo, tooltipSize, availablePositions, defaultPosition, preventDisplayingBelowSource, }: FindTooltipPositionArgs): TooltipPosition { const appContainerPositionInfo = getAppContainerPositionInfo(); if (!appContainerPositionInfo) { return defaultPosition; } const pointingTo = sourcePositionInfo; const { top: containerTop, left: containerLeft, right: containerRight, bottom: containerBottom, } = appContainerPositionInfo; const tooltipWidth = tooltipSize.width; const tooltipHeight = tooltipSize.height; const canBeDisplayedOnLeft = containerLeft + tooltipWidth <= pointingTo.left; const canBeDisplayedOnRight = tooltipWidth + pointingTo.right <= containerRight; const willCoverSidebarOnTopSideways = preventDisplayingBelowSource && pointingTo.top + tooltipHeight > pointingTo.bottom; const canBeDisplayedOnTopSideways = pointingTo.top >= containerTop && pointingTo.top + tooltipHeight <= containerBottom && !willCoverSidebarOnTopSideways; const canBeDisplayedOnBottomSideways = pointingTo.bottom <= containerBottom && pointingTo.bottom - tooltipHeight >= containerTop; const verticalCenterOfPointingTo = pointingTo.top + pointingTo.height / 2; const horizontalCenterOfPointingTo = pointingTo.left + pointingTo.width / 2; const willCoverSidebarInTheMiddleSideways = preventDisplayingBelowSource && verticalCenterOfPointingTo + tooltipHeight / 2 > pointingTo.bottom; const canBeDisplayedInTheMiddleSideways = verticalCenterOfPointingTo - tooltipHeight / 2 >= containerTop && verticalCenterOfPointingTo + tooltipHeight / 2 <= containerBottom && !willCoverSidebarInTheMiddleSideways; const canBeDisplayedOnTop = pointingTo.top - tooltipHeight >= containerTop && horizontalCenterOfPointingTo - tooltipWidth / 2 >= containerLeft && horizontalCenterOfPointingTo + tooltipWidth / 2 <= containerRight; const canBeDisplayedOnBottom = pointingTo.bottom + tooltipHeight <= containerBottom && horizontalCenterOfPointingTo - tooltipWidth / 2 >= containerLeft && horizontalCenterOfPointingTo + tooltipWidth / 2 <= containerRight && !preventDisplayingBelowSource; for (const tooltipPosition of availablePositions) { if ( tooltipPosition === tooltipPositions.RIGHT && canBeDisplayedOnRight && canBeDisplayedInTheMiddleSideways ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.RIGHT_BOTTOM && canBeDisplayedOnRight && canBeDisplayedOnBottomSideways ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.LEFT && canBeDisplayedOnLeft && canBeDisplayedInTheMiddleSideways ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.LEFT_BOTTOM && canBeDisplayedOnLeft && canBeDisplayedOnBottomSideways ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.LEFT_TOP && canBeDisplayedOnLeft && canBeDisplayedOnTopSideways ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.RIGHT_TOP && canBeDisplayedOnRight && canBeDisplayedOnTopSideways ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.TOP && canBeDisplayedOnTop ) { return tooltipPosition; } else if ( tooltipPosition === tooltipPositions.BOTTOM && canBeDisplayedOnBottom ) { return tooltipPosition; } } return defaultPosition; } type GetMessageActionTooltipStyleParams = { +sourcePositionInfo: PositionInfo, +tooltipSize: TooltipSize, +tooltipPosition: TooltipPosition, }; function getMessageActionTooltipStyle({ sourcePositionInfo, tooltipSize, tooltipPosition, }: GetMessageActionTooltipStyleParams): TooltipPositionStyle { if (tooltipPosition === tooltipPositions.RIGHT_TOP) { return { anchorPoint: { x: sourcePositionInfo.right, y: sourcePositionInfo.top, }, horizontalPosition: 'right', verticalPosition: 'bottom', alignment: 'left', }; } else if (tooltipPosition === tooltipPositions.LEFT_TOP) { return { anchorPoint: { x: sourcePositionInfo.left, y: sourcePositionInfo.top, }, horizontalPosition: 'left', verticalPosition: 'bottom', alignment: 'right', }; } else if (tooltipPosition === tooltipPositions.RIGHT_BOTTOM) { return { anchorPoint: { x: sourcePositionInfo.right, y: sourcePositionInfo.bottom, }, horizontalPosition: 'right', verticalPosition: 'top', alignment: 'left', }; } else if (tooltipPosition === tooltipPositions.LEFT_BOTTOM) { return { anchorPoint: { x: sourcePositionInfo.left, y: sourcePositionInfo.bottom, }, horizontalPosition: 'left', verticalPosition: 'top', alignment: 'right', }; } else if (tooltipPosition === tooltipPositions.LEFT) { return { anchorPoint: { x: sourcePositionInfo.left, y: sourcePositionInfo.top + sourcePositionInfo.height / 2 - tooltipSize.height / 2, }, horizontalPosition: 'left', verticalPosition: 'bottom', alignment: 'right', }; } else if (tooltipPosition === tooltipPositions.RIGHT) { return { anchorPoint: { x: sourcePositionInfo.right, y: sourcePositionInfo.top + sourcePositionInfo.height / 2 - tooltipSize.height / 2, }, horizontalPosition: 'right', verticalPosition: 'bottom', alignment: 'left', }; } else if (tooltipPosition === tooltipPositions.TOP) { return { anchorPoint: { x: sourcePositionInfo.left + sourcePositionInfo.width / 2 - tooltipSize.width / 2, y: sourcePositionInfo.top, }, horizontalPosition: 'right', verticalPosition: 'top', alignment: 'center', }; } else if (tooltipPosition === tooltipPositions.BOTTOM) { return { anchorPoint: { x: sourcePositionInfo.left + sourcePositionInfo.width / 2 - tooltipSize.width / 2, y: sourcePositionInfo.bottom, }, horizontalPosition: 'right', verticalPosition: 'bottom', alignment: 'center', }; } invariant(false, `Unexpected tooltip position value: ${tooltipPosition}`); } type CalculateTooltipSizeArgs = { +tooltipLabels: $ReadOnlyArray, +timestamp: string, }; function calculateTooltipSize({ tooltipLabels, timestamp, }: CalculateTooltipSizeArgs): { +width: number, +height: number, } { const textWidth = calculateMaxTextWidth([...tooltipLabels, timestamp], font) + 2 * tooltipLabelStyle.padding; const buttonsWidth = tooltipLabels.length * (tooltipButtonStyle.width + tooltipButtonStyle.paddingLeft + tooltipButtonStyle.paddingRight); const width = Math.max(textWidth, buttonsWidth) + tooltipStyle.paddingLeft + tooltipStyle.paddingRight; const height = (tooltipLabelStyle.height + 2 * tooltipLabelStyle.padding) * 2 + tooltipStyle.rowGap * 2 + tooltipButtonStyle.height; return { width, height, }; } -function useMessageTooltipSidebarAction( - item: ChatMessageInfoItem, - threadInfo: ThreadInfo, -): ?MessageTooltipAction { - const { threadCreatedFromMessage, messageInfo } = item; - const sidebarExists = !!threadCreatedFromMessage; - const sidebarExistsOrCanBeCreated = useSidebarExistsOrCanBeCreated( - threadInfo, - item, - ); - const openThread = useOnClickThread(threadCreatedFromMessage); - const openPendingSidebar = useOnClickPendingSidebar(messageInfo, threadInfo); - return React.useMemo(() => { - if (!sidebarExistsOrCanBeCreated) { - return null; - } - const buttonContent = ; - const onClick = (event: SyntheticEvent) => { - if (threadCreatedFromMessage) { - openThread(event); - } else { - openPendingSidebar(event); - } - }; - return { - actionButtonContent: buttonContent, - onClick, - label: sidebarExists ? 'Go to thread' : 'Create thread', - }; - }, [ - openPendingSidebar, - openThread, - sidebarExists, - sidebarExistsOrCanBeCreated, - threadCreatedFromMessage, - ]); -} - -function useMessageTooltipReplyAction( - item: ChatMessageInfoItem, - threadInfo: ThreadInfo, -): ?MessageTooltipAction { - const { messageInfo } = item; - const inputState = React.useContext(InputStateContext); - invariant(inputState, 'inputState is required'); - const { addReply } = inputState; - return React.useMemo(() => { - if ( - !isComposableMessageType(item.messageInfo.type) || - !threadHasPermission(threadInfo, threadPermissions.VOICED) - ) { - return null; - } - const buttonContent = ; - const onClick = () => { - if (!messageInfo.text) { - return; - } - addReply(createMessageReply(messageInfo.text)); - }; - return { - actionButtonContent: buttonContent, - onClick, - label: 'Reply', - }; - }, [addReply, item.messageInfo.type, messageInfo, threadInfo]); -} - -const copiedMessageDurationMs = 2000; -function useMessageCopyAction( - item: ChatMessageInfoItem, -): ?MessageTooltipAction { - const { messageInfo } = item; - - const [successful, setSuccessful] = React.useState(false); - const resetStatusAfterTimeout = React.useRef( - _debounce(() => setSuccessful(false), copiedMessageDurationMs), - ); - - const onSuccess = React.useCallback(() => { - setSuccessful(true); - resetStatusAfterTimeout.current(); - }, []); - - React.useEffect(() => resetStatusAfterTimeout.current.cancel, []); - - return React.useMemo(() => { - if (messageInfo.type !== messageTypes.TEXT) { - return null; - } - const buttonContent = ; - const onClick = async () => { - try { - await navigator.clipboard.writeText(messageInfo.text); - onSuccess(); - } catch (e) { - setSuccessful(false); - } - }; - return { - actionButtonContent: buttonContent, - onClick, - label: successful ? 'Copied!' : 'Copy', - }; - }, [messageInfo.text, messageInfo.type, onSuccess, successful]); -} - -function useMessageReactAction( - item: ChatMessageInfoItem, - threadInfo: ThreadInfo, -): ?MessageTooltipAction { - const { messageInfo } = item; - - const { setShouldRenderEmojiKeyboard } = useTooltipContext(); - - const canCreateReactionFromMessage = useCanCreateReactionFromMessage( - threadInfo, - messageInfo, - ); - - return React.useMemo(() => { - if (!canCreateReactionFromMessage) { - return null; - } - - const buttonContent = ; - - const onClickReact = () => { - if (!setShouldRenderEmojiKeyboard) { - return; - } - setShouldRenderEmojiKeyboard(true); - }; - - return { - actionButtonContent: buttonContent, - onClick: onClickReact, - label: 'React', - }; - }, [canCreateReactionFromMessage, setShouldRenderEmojiKeyboard]); -} - -function useMessageTooltipActions( - item: ChatMessageInfoItem, - threadInfo: ThreadInfo, -): $ReadOnlyArray { - const sidebarAction = useMessageTooltipSidebarAction(item, threadInfo); - const replyAction = useMessageTooltipReplyAction(item, threadInfo); - const copyAction = useMessageCopyAction(item); - const reactAction = useMessageReactAction(item, threadInfo); - return React.useMemo( - () => [replyAction, sidebarAction, copyAction, reactAction].filter(Boolean), - [replyAction, sidebarAction, copyAction, reactAction], - ); -} - -type UseMessageTooltipArgs = { - +availablePositions: $ReadOnlyArray, - +item: ChatMessageInfoItem, - +threadInfo: ThreadInfo, -}; - -type UseMessageTooltipResult = { - onMouseEnter: (event: SyntheticEvent) => void, - onMouseLeave: ?() => mixed, -}; - -type CreateTooltipParams = { - +tooltipMessagePosition: ?PositionInfo, - +tooltipSize: TooltipSize, - +availablePositions: $ReadOnlyArray, - +containsInlineEngagement: boolean, - +tooltipActions: $ReadOnlyArray, - +messageTimestamp: string, - +item: ChatMessageInfoItem, - +threadInfo: ThreadInfo, -}; - -function createTooltip(params: CreateTooltipParams) { - const { - tooltipMessagePosition, - tooltipSize, - availablePositions, - containsInlineEngagement, - tooltipActions, - messageTimestamp, - item, - threadInfo, - } = params; - if (!tooltipMessagePosition) { - return; - } - const tooltipPosition = findTooltipPosition({ - sourcePositionInfo: tooltipMessagePosition, - tooltipSize, - availablePositions, - defaultPosition: availablePositions[0], - preventDisplayingBelowSource: containsInlineEngagement, - }); - if (!tooltipPosition) { - return; - } - - const tooltipPositionStyle = getMessageActionTooltipStyle({ - tooltipPosition, - sourcePositionInfo: tooltipMessagePosition, - tooltipSize, - }); - - const tooltip = ( - - ); - return { tooltip, tooltipPositionStyle }; -} - -function useMessageTooltip({ - availablePositions, - item, - threadInfo, -}: UseMessageTooltipArgs): UseMessageTooltipResult { - const [onMouseLeave, setOnMouseLeave] = React.useState mixed>(null); - - const { renderTooltip } = useTooltipContext(); - const tooltipActions = useMessageTooltipActions(item, threadInfo); - - const containsInlineEngagement = !!item.threadCreatedFromMessage; - - const messageTimestamp = React.useMemo(() => { - const time = item.messageInfo.time; - return longAbsoluteDate(time); - }, [item.messageInfo.time]); - - const tooltipSize = React.useMemo(() => { - if (typeof document === 'undefined') { - return { - width: 0, - height: 0, - }; - } - const tooltipLabels = tooltipActions.map(action => action.label); - return calculateTooltipSize({ - tooltipLabels, - timestamp: messageTimestamp, - }); - }, [messageTimestamp, tooltipActions]); - - const updateTooltip = React.useRef(); - const [tooltipMessagePosition, setTooltipMessagePosition] = React.useState(); - - const onMouseEnter = React.useCallback( - (event: SyntheticEvent) => { - if (!renderTooltip) { - return; - } - const rect = event.currentTarget.getBoundingClientRect(); - const { top, bottom, left, right, height, width } = rect; - const messagePosition = { top, bottom, left, right, height, width }; - setTooltipMessagePosition(messagePosition); - - const tooltipResult = createTooltip({ - tooltipMessagePosition, - tooltipSize, - availablePositions, - containsInlineEngagement, - tooltipActions, - messageTimestamp, - item, - threadInfo, - }); - if (!tooltipResult) { - return; - } - - const { tooltip, tooltipPositionStyle } = tooltipResult; - const renderTooltipResult = renderTooltip({ - newNode: tooltip, - tooltipPositionStyle, - }); - if (renderTooltipResult) { - const { onMouseLeaveCallback: callback } = renderTooltipResult; - setOnMouseLeave((() => callback: () => () => mixed)); - updateTooltip.current = renderTooltipResult.updateTooltip; - } - }, - [ - availablePositions, - containsInlineEngagement, - item, - messageTimestamp, - renderTooltip, - threadInfo, - tooltipActions, - tooltipMessagePosition, - tooltipSize, - ], - ); - - React.useEffect(() => { - if (!updateTooltip.current) { - return; - } - - const tooltipResult = createTooltip({ - tooltipMessagePosition, - tooltipSize, - availablePositions, - containsInlineEngagement, - tooltipActions, - messageTimestamp, - item, - threadInfo, - }); - if (!tooltipResult) { - return; - } - - updateTooltip.current?.(tooltipResult.tooltip); - }, [ - availablePositions, - containsInlineEngagement, - item, - messageTimestamp, - threadInfo, - tooltipActions, - tooltipMessagePosition, - tooltipSize, - ]); - - return { - onMouseEnter, - onMouseLeave, - }; -} - export { findTooltipPosition, calculateTooltipSize, getMessageActionTooltipStyle, - useMessageTooltipSidebarAction, - useMessageTooltipReplyAction, - useMessageReactAction, - useMessageTooltipActions, - useMessageTooltip, };