diff --git a/web/chat/chat-message-list.react.js b/web/chat/chat-message-list.react.js index cc68636ee..f7fa0f672 100644 --- a/web/chat/chat-message-list.react.js +++ b/web/chat/chat-message-list.react.js @@ -1,401 +1,414 @@ // @flow import classNames from 'classnames'; import { detect as detectBrowser } from 'detect-browser'; import invariant from 'invariant'; import * as React from 'react'; import { fetchMessagesBeforeCursorActionTypes, fetchMessagesBeforeCursor, fetchMostRecentMessagesActionTypes, fetchMostRecentMessages, } from 'lib/actions/message-actions'; import { registerFetchKey } from 'lib/reducers/loading-reducer'; import { type ChatMessageItem, useMessageListData, } from 'lib/selectors/chat-selectors'; import { messageKey } from 'lib/shared/message-utils'; import { threadIsPending } from 'lib/shared/thread-utils'; import type { FetchMessageInfosPayload } from 'lib/types/message-types'; import { type ThreadInfo } from 'lib/types/thread-types'; import { type DispatchActionPromise, useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils'; import { type InputState, InputStateContext } from '../input/input-state'; import LoadingIndicator from '../loading-indicator.react'; import { useTextMessageRulesFunc } from '../markdown/rules.react'; import { useSelector } from '../redux/redux-utils'; import css from './chat-message-list.css'; import { MessageListContext } from './message-list-types'; +import MessageTimestampTooltip from './message-timestamp-tooltip.react'; import Message from './message.react'; import type { OnMessagePositionWithContainerInfo, MessagePositionInfo, } from './position-types'; import RelationshipPrompt from './relationship-prompt/relationship-prompt'; type BaseProps = { +threadInfo: ThreadInfo, }; type Props = { ...BaseProps, // Redux state +activeChatThreadID: ?string, +messageListData: ?$ReadOnlyArray, +startReached: boolean, +timeZone: ?string, +supportsReverseFlex: boolean, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +fetchMessagesBeforeCursor: ( threadID: string, beforeMessageID: string, ) => Promise, +fetchMostRecentMessages: ( threadID: string, ) => Promise, // withInputState +inputState: ?InputState, }; type State = { +mouseOverMessagePosition: ?OnMessagePositionWithContainerInfo, }; type Snapshot = { +scrollTop: number, +scrollHeight: number, }; class ChatMessageList extends React.PureComponent { state: State = { mouseOverMessagePosition: null, }; container: ?HTMLDivElement; messageContainer: ?HTMLDivElement; loadingFromScroll = false; componentDidMount() { this.scrollToBottom(); } getSnapshotBeforeUpdate(prevProps: Props) { if ( ChatMessageList.hasNewMessage(this.props, prevProps) && this.messageContainer ) { const { scrollTop, scrollHeight } = this.messageContainer; return { scrollTop, scrollHeight }; } return null; } static hasNewMessage(props: Props, prevProps: Props) { const { messageListData } = props; if (!messageListData || messageListData.length === 0) { return false; } const prevMessageListData = prevProps.messageListData; if (!prevMessageListData || prevMessageListData.length === 0) { return true; } return ( ChatMessageList.keyExtractor(prevMessageListData[0]) !== ChatMessageList.keyExtractor(messageListData[0]) ); } componentDidUpdate(prevProps: Props, prevState: State, snapshot: ?Snapshot) { const { messageListData } = this.props; const prevMessageListData = prevProps.messageListData; if ( this.loadingFromScroll && messageListData && (!prevMessageListData || messageListData.length > prevMessageListData.length || this.props.startReached) ) { this.loadingFromScroll = false; } const { messageContainer } = this; if (messageContainer && prevMessageListData !== messageListData) { this.onScroll(); } // We'll scroll to the bottom if the user was already scrolled to the bottom // before the new message, or if the new message was composed locally const hasNewMessage = ChatMessageList.hasNewMessage(this.props, prevProps); if ( this.props.activeChatThreadID !== prevProps.activeChatThreadID || (hasNewMessage && messageListData && messageListData[0].itemType === 'message' && messageListData[0].messageInfo.localID) || (hasNewMessage && snapshot && Math.abs(snapshot.scrollTop) <= 1) ) { this.scrollToBottom(); } else if (hasNewMessage && messageContainer && snapshot) { const { scrollTop, scrollHeight } = messageContainer; if ( scrollHeight > snapshot.scrollHeight && scrollTop === snapshot.scrollTop ) { const newHeight = scrollHeight - snapshot.scrollHeight; const newScrollTop = Math.abs(scrollTop) + newHeight; if (this.props.supportsReverseFlex) { messageContainer.scrollTop = -1 * newScrollTop; } else { messageContainer.scrollTop = newScrollTop; } } } } scrollToBottom() { if (this.messageContainer) { this.messageContainer.scrollTop = 0; } } static keyExtractor(item: ChatMessageItem) { if (item.itemType === 'loader') { return 'loader'; } return messageKey(item.messageInfo); } renderItem = item => { if (item.itemType === 'loader') { return (
); } const { threadInfo } = this.props; invariant(threadInfo, 'ThreadInfo should be set if messageListData is'); return ( ); }; setMouseOverMessagePosition = (messagePositionInfo: MessagePositionInfo) => { if (!this.messageContainer) { return; } if (messagePositionInfo.type === 'off') { this.setState({ mouseOverMessagePosition: null }); return; } const { top: containerTop, bottom: containerBottom, left: containerLeft, right: containerRight, height: containerHeight, width: containerWidth, } = this.messageContainer.getBoundingClientRect(); const mouseOverMessagePosition = { ...messagePositionInfo, messagePosition: { ...messagePositionInfo.messagePosition, top: messagePositionInfo.messagePosition.top - containerTop, bottom: messagePositionInfo.messagePosition.bottom - containerTop, left: messagePositionInfo.messagePosition.left - containerLeft, right: messagePositionInfo.messagePosition.right - containerLeft, }, containerPosition: { top: containerTop, bottom: containerBottom, left: containerLeft, right: containerRight, height: containerHeight, width: containerWidth, }, }; this.setState({ mouseOverMessagePosition }); }; render() { const { messageListData, threadInfo, inputState } = this.props; if (!messageListData) { return
; } invariant(inputState, 'InputState should be set'); const messages = messageListData.map(this.renderItem); + let tooltip; + if (this.state.mouseOverMessagePosition) { + const messagePositionInfo = this.state.mouseOverMessagePosition; + tooltip = ( + + ); + } + let relationshipPrompt; if (threadInfo) { relationshipPrompt = ; } const messageContainerStyle = classNames({ [css.messageContainer]: true, [css.mirroredMessageContainer]: !this.props.supportsReverseFlex, }); return (
{relationshipPrompt}
{messages}
+ {tooltip}
); } messageContainerRef = (messageContainer: ?HTMLDivElement) => { this.messageContainer = messageContainer; // In case we already have all the most recent messages, // but they're not enough this.possiblyLoadMoreMessages(); if (messageContainer) { messageContainer.addEventListener('scroll', this.onScroll); } }; onScroll = () => { if (!this.messageContainer) { return; } if (this.state.mouseOverMessagePosition) { this.setState({ mouseOverMessagePosition: null }); } this.possiblyLoadMoreMessages(); }; possiblyLoadMoreMessages() { if (!this.messageContainer) { return; } const { scrollTop, scrollHeight, clientHeight } = this.messageContainer; if ( this.props.startReached || Math.abs(scrollTop) + clientHeight + 55 < scrollHeight ) { return; } if (this.loadingFromScroll) { return; } this.loadingFromScroll = true; const threadID = this.props.activeChatThreadID; invariant(threadID, 'should be set'); const oldestMessageServerID = this.oldestMessageServerID(); if (oldestMessageServerID) { this.props.dispatchActionPromise( fetchMessagesBeforeCursorActionTypes, this.props.fetchMessagesBeforeCursor(threadID, oldestMessageServerID), ); } else { this.props.dispatchActionPromise( fetchMostRecentMessagesActionTypes, this.props.fetchMostRecentMessages(threadID), ); } } oldestMessageServerID(): ?string { const data = this.props.messageListData; invariant(data, 'should be set'); for (let i = data.length - 1; i >= 0; i--) { if (data[i].itemType === 'message' && data[i].messageInfo.id) { return data[i].messageInfo.id; } } return null; } } registerFetchKey(fetchMessagesBeforeCursorActionTypes); registerFetchKey(fetchMostRecentMessagesActionTypes); const ConnectedChatMessageList: React.ComponentType = React.memo( function ConnectedChatMessageList(props: BaseProps): React.Node { const { threadInfo } = props; const userAgent = useSelector(state => state.userAgent); const supportsReverseFlex = React.useMemo(() => { const browser = detectBrowser(userAgent); return ( !browser || browser.name !== 'firefox' || parseInt(browser.version) >= 81 ); }, [userAgent]); const timeZone = useSelector(state => state.timeZone); const messageListData = useMessageListData({ threadInfo, searching: false, userInfoInputArray: [], }); const startReached = !!useSelector(state => { const activeID = threadInfo.id; if (!activeID) { return null; } if (threadIsPending(activeID)) { return true; } const threadMessageInfo = state.messageStore.threads[activeID]; if (!threadMessageInfo) { return null; } return threadMessageInfo.startReached; }); const dispatchActionPromise = useDispatchActionPromise(); const callFetchMessagesBeforeCursor = useServerCall( fetchMessagesBeforeCursor, ); const callFetchMostRecentMessages = useServerCall(fetchMostRecentMessages); const inputState = React.useContext(InputStateContext); const getTextMessageMarkdownRules = useTextMessageRulesFunc(threadInfo.id); const messageListContext = React.useMemo(() => { if (!getTextMessageMarkdownRules) { return undefined; } return { getTextMessageMarkdownRules }; }, [getTextMessageMarkdownRules]); return ( ); }, ); export default ConnectedChatMessageList; diff --git a/web/chat/message-timestamp-tooltip.css b/web/chat/message-timestamp-tooltip.css new file mode 100644 index 000000000..3a45decb1 --- /dev/null +++ b/web/chat/message-timestamp-tooltip.css @@ -0,0 +1,53 @@ +div.messageLeftTooltip:after { + top: 7px; + right: -14px; + border-color: transparent transparent transparent var(--tool-tip-bg); +} +div.messageRightTooltip:after { + top: 7px; + left: -14px; + border-color: transparent var(--tool-tip-bg) transparent transparent; +} +div.messageTopLeftTooltip:after { + bottom: -14px; + left: 4px; + border-color: var(--tool-tip-bg) transparent transparent transparent; +} +div.messageTopRightTooltip:after { + bottom: -14px; + right: 4px; + border-color: var(--tool-tip-bg) transparent transparent transparent; +} +div.messageBottomLeftTooltip:after { + top: -14px; + left: 4px; + border-color: transparent transparent var(--tool-tip-bg) transparent; +} +div.messageBottomRightTooltip:after { + top: -14px; + right: 4px; + border-color: transparent transparent var(--tool-tip-bg) transparent; +} + +div.messageActionActiveArea { + position: absolute; + display: flex; + top: 0; + bottom: 0; + align-items: center; + padding: 0 12px; +} + +div.viewerMessageActionActiveArea { + right: 100%; +} +div.nonViewerMessageActiveArea { + left: 100%; +} +div.messageActionActiveArea > div + div { + margin-left: 4px; +} + +div.messageActionLinkIcon:hover { + cursor: pointer; +} diff --git a/web/chat/message-timestamp-tooltip.react.js b/web/chat/message-timestamp-tooltip.react.js new file mode 100644 index 000000000..50c636c5a --- /dev/null +++ b/web/chat/message-timestamp-tooltip.react.js @@ -0,0 +1,148 @@ +// @flow + +import invariant from 'invariant'; +import * as React from 'react'; + +import { isComposableMessageType } from 'lib/types/message-types'; +import { longAbsoluteDate } from 'lib/utils/date-utils'; + +import css from './message-timestamp-tooltip.css'; +import type { OnMessagePositionWithContainerInfo } from './position-types'; +import { + type TooltipPosition, + tooltipPositions, + sizeOfTooltipArrow, +} from './tooltip-utils'; +import { + TooltipMenu, + type TooltipStyle, + TooltipTextItem, +} from './tooltip.react'; + +const availablePositionsForComposedViewerMessage = [ + tooltipPositions.BOTTOM_RIGHT, +]; +const availablePositionsForNonComposedOrNonViewerMessage = [ + tooltipPositions.LEFT, +]; + +type Props = { + +messagePositionInfo: OnMessagePositionWithContainerInfo, + +timeZone: ?string, +}; +function MessageTimestampTooltip(props: Props): React.Node { + const { messagePositionInfo, timeZone } = props; + const { time, creator, type } = messagePositionInfo.item.messageInfo; + console.log(messagePositionInfo); + + const text = React.useMemo(() => longAbsoluteDate(time, timeZone), [ + time, + timeZone, + ]); + const availableTooltipPositions = React.useMemo(() => { + const { isViewer } = creator; + const isComposed = isComposableMessageType(type); + return isComposed && isViewer + ? availablePositionsForComposedViewerMessage + : availablePositionsForNonComposedOrNonViewerMessage; + }, [creator, type]); + + const { messagePosition, containerPosition } = messagePositionInfo; + const pointingToInfo = React.useMemo(() => { + return { + containerPosition, + itemPosition: messagePosition, + }; + }, [messagePosition, containerPosition]); + + const getTooltipStyle = React.useCallback( + (tooltipPosition: TooltipPosition) => + getTimestampTooltipStyle(messagePositionInfo, tooltipPosition), + [messagePositionInfo], + ); + return ( + + + + ); +} + +function getTimestampTooltipStyle( + messagePositionInfo: OnMessagePositionWithContainerInfo, + tooltipPosition: TooltipPosition, +): TooltipStyle { + const { messagePosition, containerPosition } = messagePositionInfo; + const { height: containerHeight, width: containerWidth } = containerPosition; + + let style, className; + if (tooltipPosition === tooltipPositions.LEFT) { + const centerOfMessage = messagePosition.top + messagePosition.height / 2; + const tooltipPointing = Math.max( + Math.min(centerOfMessage, containerHeight), + 0, + ); + style = { + right: containerWidth - messagePosition.left + sizeOfTooltipArrow, + top: tooltipPointing, + }; + className = css.messageLeftTooltip; + } else if (tooltipPosition === tooltipPositions.RIGHT) { + const centerOfMessage = messagePosition.top + messagePosition.height / 2; + const tooltipPointing = Math.max( + Math.min(centerOfMessage, containerHeight), + 0, + ); + style = { + left: messagePosition.right + sizeOfTooltipArrow, + top: tooltipPointing, + }; + className = css.messageRightTooltip; + } else if (tooltipPosition === tooltipPositions.TOP_LEFT) { + const tooltipPointing = Math.min( + containerHeight - messagePosition.top, + containerHeight, + ); + style = { + left: messagePosition.left, + bottom: tooltipPointing + sizeOfTooltipArrow, + }; + className = css.messageTopLeftTooltip; + } else if (tooltipPosition === tooltipPositions.TOP_RIGHT) { + const tooltipPointing = Math.min( + containerHeight - messagePosition.top, + containerHeight, + ); + style = { + right: containerWidth - messagePosition.right, + bottom: tooltipPointing + sizeOfTooltipArrow, + }; + className = css.messageTopRightTooltip; + } else if (tooltipPosition === tooltipPositions.BOTTOM_LEFT) { + const tooltipPointing = Math.min(messagePosition.bottom, containerHeight); + style = { + left: messagePosition.left, + top: tooltipPointing + sizeOfTooltipArrow, + }; + className = css.messageBottomLeftTooltip; + } else if (tooltipPosition === tooltipPositions.BOTTOM_RIGHT) { + const tooltipPointing = Math.min(messagePosition.bottom, containerHeight); + style = { + right: containerWidth - messagePosition.right, + top: tooltipPointing + sizeOfTooltipArrow, + }; + className = css.messageBottomRightTooltip; + console.log(`container height:${containerHeight}`); + } + invariant( + className && style, + `${tooltipPosition} is not valid for timestamp tooltip`, + ); + return { className, style }; +} + +export default MessageTimestampTooltip; diff --git a/web/chat/message-tooltip.css b/web/chat/message-tooltip.css index 5c18e019d..b451ef762 100644 --- a/web/chat/message-tooltip.css +++ b/web/chat/message-tooltip.css @@ -1,100 +1,82 @@ div.messageActionContainer { display: flex; flex-direction: row; align-items: center; justify-content: center; padding: 0 6px; background-color: var(--message-action-tooltip-bg); border-radius: 8px; width: fit-content; } -div.timestampContainer { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - padding: 0 6px; - margin-top: 6px; - background-color: var(--message-action-tooltip-bg); - border-radius: 8px; - color: var(--tool-tip-color); -} - -div.timestampContainer p { - white-space: nowrap; - padding: 5px; - font-size: var(--s-font-14); -} - div.messageActionButtons { display: flex; font-size: 16px; } div.messageActionButtons svg { padding: 10px 6px 6px; color: var(--color-disabled); } div.messageActionButtons svg:hover { cursor: pointer; color: var(--fg); } div.messageActionButtonsViewer { flex-direction: row; margin-left: auto; margin-right: 0; } div.messageActionButtonsNonViewer { flex-direction: row-reverse; margin-left: 0; margin-right: auto; } div.messageActionLinkIcon { margin: 0 3px; position: relative; } div.messageActionExtraAreaTop:before { height: 15px; width: 55px; content: ''; position: absolute; bottom: -15px; } div.messageActionExtraAreaTopRight:before { right: 0; } div.messageActionExtraAreaTopLeft:before { left: 0; } div.messageActionExtraArea:before { height: 30px; width: 20px; content: ''; position: absolute; } div.messageActionExtraAreaRight:before { left: -20px; } div.messageActionExtraAreaLeft:before { right: -20px; } div.messageActionTopRightTooltip { bottom: 100%; margin-bottom: 1px; right: 0; } div.messageActionTopLeftTooltip { bottom: 100%; margin-bottom: 1px; left: 0; } div.messageActionLeftTooltip { top: 50%; right: 100%; margin-right: 7px; } div.messageActionRightTooltip { top: 50%; left: 100%; margin-left: 7px; } diff --git a/web/chat/message-tooltip.react.js b/web/chat/message-tooltip.react.js index aece3fd61..1cf32e00f 100644 --- a/web/chat/message-tooltip.react.js +++ b/web/chat/message-tooltip.react.js @@ -1,275 +1,264 @@ // @flow import classNames from 'classnames'; import invariant from 'invariant'; import * as React from 'react'; import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors'; import { useSidebarExistsOrCanBeCreated } from 'lib/shared/thread-utils'; import type { ThreadInfo } from 'lib/types/thread-types'; -import { longAbsoluteDate } from 'lib/utils/date-utils'; import type { InputState } from '../input/input-state'; -import { useSelector } from '../redux/redux-utils'; import { useOnClickThread, useOnClickPendingSidebar, } from '../selectors/nav-selectors'; import SWMansionIcon from '../SWMansionIcon.react'; import MessageReplyButton from './message-reply-button.react'; import css from './message-tooltip.css'; import type { ItemAndContainerPositionInfo, MessagePositionInfo, OnMessagePositionWithContainerInfo, PositionInfo, } from './position-types'; import { tooltipPositions, type TooltipPosition } from './tooltip-utils'; import { TooltipMenu, type TooltipStyle, TooltipTextItem, } from './tooltip.react'; const messageActionIconExcessVerticalWhitespace = 10; const openSidebarText = 'Go to thread'; const createSidebarText = 'Create thread'; type TooltipType = 'sidebar' | 'reply'; type MessageTooltipProps = { +threadInfo: ThreadInfo, +item: ChatMessageInfoItem, +availableTooltipPositions: $ReadOnlyArray, +setMouseOverMessagePosition?: ( messagePositionInfo: MessagePositionInfo, ) => void, +mouseOverMessagePosition: OnMessagePositionWithContainerInfo, +canReply?: boolean, +inputState?: ?InputState, }; function MessageTooltip(props: MessageTooltipProps): React.Node { const { threadInfo, item, availableTooltipPositions, setMouseOverMessagePosition, mouseOverMessagePosition, canReply, inputState, } = props; const { containerPosition } = mouseOverMessagePosition; const [activeTooltip, setActiveTooltip] = React.useState(); const [pointingTo, setPointingTo] = React.useState(); const showTooltip = React.useCallback( (tooltipType: TooltipType, iconPosition: ItemAndContainerPositionInfo) => { if (activeTooltip) { return; } setActiveTooltip(tooltipType); setPointingTo(iconPosition); }, [activeTooltip], ); const hideTooltip = React.useCallback(() => { setActiveTooltip(null); }, []); const showSidebarTooltip = React.useCallback( (event: SyntheticEvent) => { const rect = event.currentTarget.getBoundingClientRect(); const iconPosition = getIconPosition(rect, containerPosition); showTooltip('sidebar', iconPosition); }, [containerPosition, showTooltip], ); const showReplyTooltip = React.useCallback( (event: SyntheticEvent) => { const rect = event.currentTarget.getBoundingClientRect(); const iconPosition = getIconPosition(rect, containerPosition); showTooltip('reply', iconPosition); }, [containerPosition, showTooltip], ); const { threadCreatedFromMessage, messageInfo } = item; const onThreadOpen = useOnClickThread(threadCreatedFromMessage); const onPendingSidebarOpen = useOnClickPendingSidebar( messageInfo, threadInfo, ); const onSidebarButtonClick = React.useCallback( (event: SyntheticEvent) => { if (threadCreatedFromMessage) { onThreadOpen(event); } else { onPendingSidebarOpen(event); } }, [onPendingSidebarOpen, onThreadOpen, threadCreatedFromMessage], ); const onReplyButtonClick = React.useCallback(() => { invariant( setMouseOverMessagePosition, 'setMouseOverMessagePosition should be set if replyButton exists', ); setMouseOverMessagePosition({ type: 'off', item: item }); }, [item, setMouseOverMessagePosition]); let tooltipText = ''; if (activeTooltip === 'reply') { tooltipText = 'Reply'; } else if (activeTooltip === 'sidebar') { tooltipText = threadCreatedFromMessage ? openSidebarText : createSidebarText; } let tooltipMenu = null; if (pointingTo && activeTooltip) { tooltipMenu = ( ); } let replyButton; if (canReply) { invariant(inputState, 'inputState must be set if replyButton exists'); invariant( mouseOverMessagePosition, 'mouseOverMessagePosition must be set if replyButton exists', ); replyButton = (
{activeTooltip === 'reply' ? tooltipMenu : null}
); } const sidebarExistsOrCanBeCreated = useSidebarExistsOrCanBeCreated( threadInfo, item, ); let sidebarButton; if (sidebarExistsOrCanBeCreated) { sidebarButton = (
{activeTooltip === 'sidebar' ? tooltipMenu : null}
); } - const timezone = useSelector(state => state.timeZone); - const timestampText = React.useMemo( - () => longAbsoluteDate(messageInfo.time, timezone), - [messageInfo.time, timezone], - ); - const { isViewer } = messageInfo.creator; const messageActionButtonsContainerClassName = classNames({ [css.messageActionContainer]: true, [css.messageActionButtons]: true, [css.messageActionButtonsViewer]: isViewer, [css.messageActionButtonsNonViewer]: !isViewer, }); return (
{sidebarButton} {replyButton}
-
-

{timestampText}

-
); } function getIconPosition( rect: ClientRect, containerPosition: PositionInfo, ): ItemAndContainerPositionInfo { const { top, bottom, left, right, width, height } = rect; return { containerPosition, itemPosition: { top: top - containerPosition.top + messageActionIconExcessVerticalWhitespace, bottom: bottom - containerPosition.top - messageActionIconExcessVerticalWhitespace, left: left - containerPosition.left, right: right - containerPosition.left, width, height: height - messageActionIconExcessVerticalWhitespace * 2, }, }; } function getMessageActionTooltipStyle( tooltipPosition: TooltipPosition, ): TooltipStyle { let className; if (tooltipPosition === tooltipPositions.TOP_RIGHT) { className = classNames( css.messageActionTopRightTooltip, css.messageActionExtraAreaTop, css.messageActionExtraAreaTopRight, ); } else if (tooltipPosition === tooltipPositions.TOP_LEFT) { className = classNames( css.messageActionTopLeftTooltip, css.messageActionExtraAreaTop, css.messageActionExtraAreaTopLeft, ); } else if (tooltipPosition === tooltipPositions.RIGHT) { className = classNames( css.messageActionRightTooltip, css.messageActionExtraArea, css.messageActionExtraAreaRight, ); } else if (tooltipPosition === tooltipPositions.LEFT) { className = classNames( css.messageActionLeftTooltip, css.messageActionExtraArea, css.messageActionExtraAreaLeft, ); } invariant(className, `${tooltipPosition} is not valid for message tooltip`); return { className }; } export default MessageTooltip;