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,18 +1,9 @@ -div.container { - display: flex; - flex-direction: row; -} - -div.containerLeftAlign, -div.containerCenterAlign { - flex-direction: row-reverse; -} - div.messageTooltipContainer { display: flex; flex-direction: column; align-items: center; font-size: var(--s-font-14); + padding: 8px 0; } div.messageActionContainer { @@ -71,3 +62,7 @@ div.rightTooltipAlign { align-items: flex-end; } + +div.emojiKeyboard { + position: absolute; +} 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 @@ -15,15 +15,23 @@ tooltipStyle, } from './chat-constants.js'; import css from './message-tooltip.css'; -import { useSendReaction } from './reaction-message-utils.js'; +import { + useSendReaction, + getEmojiKeyboardPosition, +} from './reaction-message-utils.js'; import { useTooltipContext } from './tooltip-provider.js'; import { useSelector } from '../redux/redux-utils.js'; -import { type MessageTooltipAction } from '../utils/tooltip-utils.js'; +import type { + MessageTooltipAction, + TooltipSize, + TooltipPositionStyle, +} from '../utils/tooltip-utils.js'; type MessageTooltipProps = { +actions: $ReadOnlyArray, +messageTimestamp: string, - +alignment?: 'left' | 'center' | 'right', + +tooltipPositionStyle: TooltipPositionStyle, + +tooltipSize: TooltipSize, +item: ChatMessageInfoItem, +threadInfo: ThreadInfo, }; @@ -31,15 +39,43 @@ const { actions, messageTimestamp, - alignment = 'left', + tooltipPositionStyle, + tooltipSize, item, threadInfo, } = props; const { messageInfo, reactions } = item; + const { alignment = 'left' } = tooltipPositionStyle; + const [activeTooltipLabel, setActiveTooltipLabel] = React.useState(); - const { renderEmojiKeyboard } = useTooltipContext(); + const { shouldRenderEmojiKeyboard } = useTooltipContext(); + + // emoji-mart actually doesn't render its contents until a useEffect runs: + // https://github.com/missive/emoji-mart/blob/d29728f7b4e295e46f9b64aa80335aa4a3c15b8e/packages/emoji-mart-react/react.tsx#L13-L19 + // We need to measure the width/height of the picker, but because of this we + // need to do the measurement in our own useEffect, in order to guarantee it + // runs after emoji-mart's useEffect. To do this, we have to define two pieces + // of React state: + // - emojiKeyboardNode, which will get set by the emoji keyboard's ref and + // will trigger our useEffect + // - emojiKeyboardRenderedNode, which will get set in that useEffect and will + // trigger the rerendering of this component with the correct height/width + + const [emojiKeyboardNode, setEmojiKeyboardNode] = React.useState(null); + const [emojiKeyboardRenderedNode, setEmojiKeyboardRenderedNode] = + React.useState(null); + + React.useEffect(() => { + if (emojiKeyboardNode) { + // It would be more simple to just call getEmojiKeyboardPosition + // immediately here, but some quirk of emoji-mart causes the width of the + // node to be 0 here. If instead we wait until the next render of this + // component to check the width, it ends up being set correctly. + setEmojiKeyboardRenderedNode(emojiKeyboardNode); + } + }, [emojiKeyboardNode]); const messageActionButtonsContainerClassName = classNames( css.messageActionContainer, @@ -113,6 +149,27 @@ ); }, [messageTimestamp, messageTooltipLabelStyle]); + const emojiKeyboardPosition = React.useMemo( + () => + getEmojiKeyboardPosition( + emojiKeyboardRenderedNode, + tooltipPositionStyle, + tooltipSize, + ), + [emojiKeyboardRenderedNode, tooltipPositionStyle, tooltipSize], + ); + + const emojiKeyboardPositionStyle = React.useMemo(() => { + if (!emojiKeyboardPosition) { + return null; + } + + return { + bottom: emojiKeyboardPosition.bottom, + left: emojiKeyboardPosition.left, + }; + }, [emojiKeyboardPosition]); + const nextLocalID = useSelector(state => state.nextLocalID); const localID = `${localIDPrefix}${nextLocalID}`; @@ -133,21 +190,24 @@ ); const emojiKeyboard = React.useMemo(() => { - if (!renderEmojiKeyboard) { + if (!shouldRenderEmojiKeyboard) { return null; } - return ; - }, [onEmojiSelect, renderEmojiKeyboard]); + + return ( +
+ +
+ ); + }, [emojiKeyboardPositionStyle, onEmojiSelect, shouldRenderEmojiKeyboard]); const messageTooltipContainerStyle = React.useMemo(() => tooltipStyle, []); const containerClassName = classNames({ - [css.container]: true, - [css.containerLeftAlign]: alignment === 'left', - [css.containerCenterAlign]: alignment === 'center', - }); - - const messageTooltipContainerClassNames = classNames({ [css.messageTooltipContainer]: true, [css.leftTooltipAlign]: alignment === 'left', [css.centerTooltipAlign]: alignment === 'center', @@ -155,17 +215,14 @@ }); return ( -
+ <> {emojiKeyboard} -
+
{tooltipLabel}
{tooltipButtons} {tooltipTimestamp}
-
+ ); } diff --git a/web/chat/tooltip-provider.js b/web/chat/tooltip-provider.js --- a/web/chat/tooltip-provider.js +++ b/web/chat/tooltip-provider.js @@ -26,8 +26,8 @@ type TooltipContextType = { +renderTooltip: (params: RenderTooltipParams) => RenderTooltipResult, +clearTooltip: () => mixed, - +renderEmojiKeyboard: boolean, - +setRenderEmojiKeyboard: SetState, + +shouldRenderEmojiKeyboard: boolean, + +setShouldRenderEmojiKeyboard: SetState, }; const TooltipContext: React.Context = @@ -38,8 +38,8 @@ updateTooltip: () => {}, }), clearTooltip: () => {}, - renderEmojiKeyboard: false, - setRenderEmojiKeyboard: () => {}, + shouldRenderEmojiKeyboard: false, + setShouldRenderEmojiKeyboard: () => {}, }); type Props = { @@ -53,7 +53,7 @@ const [tooltipNode, setTooltipNode] = React.useState(null); const [tooltipPosition, setTooltipPosition] = React.useState(null); - const [renderEmojiKeyboard, setRenderEmojiKeyboard] = + const [shouldRenderEmojiKeyboard, setShouldRenderEmojiKeyboard] = React.useState(false); const clearTooltip = React.useCallback((tooltipToClose: symbol) => { @@ -63,7 +63,7 @@ tooltipCancelTimer.current = null; setTooltipNode(null); setTooltipPosition(null); - setRenderEmojiKeyboard(false); + setShouldRenderEmojiKeyboard(false); tooltipSymbol.current = null; }, []); @@ -156,10 +156,10 @@ () => ({ renderTooltip, clearTooltip: clearCurrentTooltip, - renderEmojiKeyboard, - setRenderEmojiKeyboard, + shouldRenderEmojiKeyboard, + setShouldRenderEmojiKeyboard, }), - [renderTooltip, clearCurrentTooltip, renderEmojiKeyboard], + [renderTooltip, clearCurrentTooltip, shouldRenderEmojiKeyboard], ); return ( diff --git a/web/utils/tooltip-utils.js b/web/utils/tooltip-utils.js --- a/web/utils/tooltip-utils.js +++ b/web/utils/tooltip-utils.js @@ -452,7 +452,7 @@ ): ?MessageTooltipAction { const { messageInfo } = item; - const { setRenderEmojiKeyboard } = useTooltipContext(); + const { setShouldRenderEmojiKeyboard } = useTooltipContext(); const canCreateReactionFromMessage = useCanCreateReactionFromMessage( threadInfo, @@ -467,10 +467,10 @@ const buttonContent = ; const onClickReact = () => { - if (!setRenderEmojiKeyboard) { + if (!setShouldRenderEmojiKeyboard) { return; } - setRenderEmojiKeyboard(true); + setShouldRenderEmojiKeyboard(true); }; return { @@ -478,7 +478,7 @@ onClick: onClickReact, label: 'React', }; - }, [canCreateReactionFromMessage, setRenderEmojiKeyboard]); + }, [canCreateReactionFromMessage, setShouldRenderEmojiKeyboard]); } function useMessageTooltipActions( @@ -548,13 +548,12 @@ tooltipSize, }); - const { alignment } = tooltipPositionStyle; - const tooltip = (