diff --git a/web/chat/chat-constants.js b/web/chat/chat-constants.js new file mode 100644 --- /dev/null +++ b/web/chat/chat-constants.js @@ -0,0 +1,18 @@ +// @flow + +export const tooltipStyle = { + paddingLeft: 5, + paddingRight: 5, + rowGap: 3, +}; + +export const tooltipLabelStyle = { + padding: 6, + height: 20, +}; +export const tooltipButtonStyle = { + paddingLeft: 6, + paddingRight: 6, + width: 30, + height: 38, +}; diff --git a/web/chat/composed-message.react.js b/web/chat/composed-message.react.js --- a/web/chat/composed-message.react.js +++ b/web/chat/composed-message.react.js @@ -124,31 +124,12 @@ this.props.mouseOverMessagePosition.item.messageInfo.id === id && (this.props.sidebarExistsOrCanBeCreated || this.props.canReply) ) { + // eslint-disable-next-line no-unused-vars const availableTooltipPositions = isViewer ? availableTooltipPositionsForViewerMessage : availableTooltipPositionsForNonViewerMessage; - const messageTooltipProps = { - threadInfo, - item, - availableTooltipPositions, - mouseOverMessagePosition: this.props.mouseOverMessagePosition, - }; - - if (this.props.canReply) { - messageTooltip = ( - - ); - } else { - messageTooltip = ( - - ); - } + messageTooltip = ; } let messageTooltipLinks; diff --git a/web/chat/message-tooltip.css b/web/chat/message-tooltip.css --- a/web/chat/message-tooltip.css +++ b/web/chat/message-tooltip.css @@ -1,9 +1,15 @@ +div.messageTooltipContainer { + display: flex; + flex-direction: column; + align-items: center; + font-size: var(--s-font-14); +} + 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; @@ -17,66 +23,38 @@ 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.messageTooltipButton { + display: flex; + align-items: center; + justify-content: center; } -div.messageActionTopRightTooltip { - bottom: 100%; - margin-bottom: 1px; - right: 0; + +div.messageTooltipLabel { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + background-color: var(--message-action-tooltip-bg); + color: var(--tool-tip-color); + border-radius: 8px; + overflow: auto; + white-space: nowrap; } -div.messageActionTopLeftTooltip { - bottom: 100%; - margin-bottom: 1px; - left: 0; + +div.leftTooltipAlign { + align-items: flex-start; } -div.messageActionLeftTooltip { - top: 50%; - right: 100%; - margin-right: 7px; + +div.centerTooltipAlign { + align-items: center; } -div.messageActionRightTooltip { - top: 50%; - left: 100%; - margin-left: 7px; + +div.rightTooltipAlign { + align-items: flex-end; } diff --git a/web/chat/message-tooltip.react.js b/web/chat/message-tooltip.react.js --- a/web/chat/message-tooltip.react.js +++ b/web/chat/message-tooltip.react.js @@ -1,269 +1,115 @@ // @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 CommIcon from '../CommIcon.react.js'; -import type { InputState } from '../input/input-state'; import { - useOnClickThread, - useOnClickPendingSidebar, -} from '../selectors/nav-selectors'; -import MessageReplyButton from './message-reply-button.react'; + tooltipButtonStyle, + tooltipLabelStyle, + tooltipStyle, +} from './chat-constants'; 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'; +import { type MessageTooltipAction } from './tooltip-utils'; -type BaseMessageTooltipProps = { - +threadInfo: ThreadInfo, - +item: ChatMessageInfoItem, - +availableTooltipPositions: $ReadOnlyArray, - +mouseOverMessagePosition: OnMessagePositionWithContainerInfo, +type MessageTooltipProps = { + +actions: $ReadOnlyArray, + +messageTimestamp: string, + +alignment?: 'left' | 'center' | 'right', }; -type MessageTooltipProps = - | { - ...BaseMessageTooltipProps, - +canReply: false, - } - | { - ...BaseMessageTooltipProps, - +canReply: true, - +inputState: ?InputState, - +setMouseOverMessagePosition: ( - messagePositionInfo: MessagePositionInfo, - ) => void, - }; function MessageTooltip(props: MessageTooltipProps): React.Node { - const { - threadInfo, - item, - availableTooltipPositions, - mouseOverMessagePosition, - canReply, - } = 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 { actions, messageTimestamp, alignment = 'left' } = props; + const [activeTooltipLabel, setActiveTooltipLabel] = React.useState(); + const messageActionButtonsContainerClassName = classNames( + css.messageActionContainer, + css.messageActionButtons, ); - const hideTooltip = React.useCallback(() => { - setActiveTooltip(null); - }, []); + const messageTooltipButtonStyle = React.useMemo(() => tooltipButtonStyle, []); - 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 setMouseOverMessagePosition = props.canReply - ? props.setMouseOverMessagePosition - : null; - - const onReplyButtonClick = React.useCallback(() => { - setMouseOverMessagePosition?.({ - type: 'off', - item: item, + const tooltipButtons = React.useMemo(() => { + if (!actions || actions.length === 0) { + return null; + } + const buttons = actions.map(({ label, onClick, actionButtonContent }) => { + const onMouseEnter = () => { + setActiveTooltipLabel(label); + }; + const onMouseLeave = () => + setActiveTooltipLabel(oldLabel => + label === oldLabel ? null : oldLabel, + ); + + return ( +
+ {actionButtonContent} +
+ ); }); - }, [item, setMouseOverMessagePosition]); - - let tooltipText = ''; - if (activeTooltip === 'reply') { - tooltipText = 'Reply'; - } else if (activeTooltip === 'sidebar') { - tooltipText = threadCreatedFromMessage - ? openSidebarText - : createSidebarText; - } - - let tooltipMenu = null; - if (pointingTo && activeTooltip) { - tooltipMenu = ( - - - + return ( +
{buttons}
); - } + }, [ + actions, + messageActionButtonsContainerClassName, + messageTooltipButtonStyle, + ]); + + const messageTooltipLabelStyle = React.useMemo(() => tooltipLabelStyle, []); + const messageTooltipTopLabelStyle = React.useMemo( + () => ({ + height: `${tooltipLabelStyle.height + 2 * tooltipLabelStyle.padding}px`, + }), + [], + ); - let replyButton; - if (canReply) { - invariant(props.inputState, 'inputState must be set if replyButton exists'); - replyButton = ( -
- - {activeTooltip === 'reply' ? tooltipMenu : null} + const tooltipLabel = React.useMemo(() => { + if (!activeTooltipLabel) { + return null; + } + return ( +
+ {activeTooltipLabel}
); - } + }, [activeTooltipLabel, messageTooltipLabelStyle]); - const sidebarExistsOrCanBeCreated = useSidebarExistsOrCanBeCreated( - threadInfo, - item, - ); - - let sidebarButton; - if (sidebarExistsOrCanBeCreated) { - sidebarButton = ( -
- - {activeTooltip === 'sidebar' ? tooltipMenu : null} + const tooltipTimestamp = React.useMemo(() => { + if (!messageTimestamp) { + return null; + } + return ( +
+ {messageTimestamp}
); - } + }, [messageTimestamp, messageTooltipLabelStyle]); + + const messageTooltipContainerStyle = React.useMemo(() => tooltipStyle, []); + + const containerClassNames = React.useMemo( + () => + classNames(css.messageTooltipContainer, { + [css.leftTooltipAlign]: alignment === 'left', + [css.centerTooltipAlign]: alignment === 'center', + [css.rightTooltipAlign]: alignment === 'right', + }), + [alignment], + ); - const { isViewer } = messageInfo.creator; - const messageActionButtonsContainerClassName = classNames({ - [css.messageActionContainer]: true, - [css.messageActionButtons]: true, - [css.messageActionButtonsViewer]: isViewer, - [css.messageActionButtonsNonViewer]: !isViewer, - }); return ( -
-
- {sidebarButton} - {replyButton} -
+
+
{tooltipLabel}
+ {tooltipButtons} + {tooltipTimestamp}
); } -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; diff --git a/web/chat/robotext-message.react.js b/web/chat/robotext-message.react.js --- a/web/chat/robotext-message.react.js +++ b/web/chat/robotext-message.react.js @@ -24,6 +24,7 @@ import css from './robotext-message.css'; import { tooltipPositions } from './tooltip-utils'; +// eslint-disable-next-line no-unused-vars const availableTooltipPositionsForRobotext = [ tooltipPositions.TOP_RIGHT, tooltipPositions.RIGHT, @@ -57,7 +58,7 @@ ); } - const { item, threadInfo, sidebarExistsOrCanBeCreated } = this.props; + const { item, sidebarExistsOrCanBeCreated } = this.props; const { id } = item.messageInfo; let messageTooltip; if ( @@ -65,15 +66,7 @@ this.props.mouseOverMessagePosition.item.messageInfo.id === id && sidebarExistsOrCanBeCreated ) { - messageTooltip = ( - - ); + messageTooltip = ; } let messageTooltipLinks; diff --git a/web/chat/tooltip-utils.js b/web/chat/tooltip-utils.js --- a/web/chat/tooltip-utils.js +++ b/web/chat/tooltip-utils.js @@ -1,6 +1,7 @@ // @flow import invariant from 'invariant'; +import * as React from 'react'; import { calculateMaxTextWidth } from '../utils/text-utils'; import type { ItemAndContainerPositionInfo } from './position-types'; @@ -24,6 +25,12 @@ export type TooltipPosition = $Values; +export type MessageTooltipAction = { + +label: string, + +onClick: (SyntheticEvent) => mixed, + +actionButtonContent: React.Node, +}; + const sizeOfTooltipArrow = 10; // 7px arrow + 3px extra const tooltipMenuItemHeight = 22; // 17px line-height + 5px padding bottom const tooltipInnerTopPadding = 5; // 5px bottom is included in last item