diff --git a/web/chat/message-tooltip.react.js b/web/chat/message-tooltip.react.js index 0ad173944..587ba6923 100644 --- a/web/chat/message-tooltip.react.js +++ b/web/chat/message-tooltip.react.js @@ -1,234 +1,233 @@ // @flow import data from '@emoji-mart/data'; import Picker from '@emoji-mart/react'; import classNames from 'classnames'; import * as React from 'react'; import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js'; import { localIDPrefix } from 'lib/shared/message-utils.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import { tooltipButtonStyle, tooltipLabelStyle, tooltipStyle, } from './chat-constants.js'; import css from './message-tooltip.css'; import { useSendReaction, getEmojiKeyboardPosition, } from './reaction-message-utils.js'; import { useTooltipContext } from './tooltip-provider.js'; import { useSelector } from '../redux/redux-utils.js'; import type { MessageTooltipAction, TooltipSize, TooltipPositionStyle, } from '../utils/tooltip-utils.js'; type MessageTooltipProps = { +actions: $ReadOnlyArray, +messageTimestamp: string, +tooltipPositionStyle: TooltipPositionStyle, +tooltipSize: TooltipSize, +item: ChatMessageInfoItem, +threadInfo: ThreadInfo, }; function MessageTooltip(props: MessageTooltipProps): React.Node { const { actions, messageTimestamp, tooltipPositionStyle, tooltipSize, item, threadInfo, } = props; const { messageInfo, reactions } = item; const { alignment = 'left' } = tooltipPositionStyle; const [activeTooltipLabel, setActiveTooltipLabel] = React.useState(); 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, css.messageActionButtons, ); const messageTooltipButtonStyle = React.useMemo(() => tooltipButtonStyle, []); 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}
); }); return (
{buttons}
); }, [ actions, messageActionButtonsContainerClassName, messageTooltipButtonStyle, ]); const messageTooltipLabelStyle = React.useMemo(() => tooltipLabelStyle, []); const messageTooltipTopLabelStyle = React.useMemo( () => ({ height: `${tooltipLabelStyle.height + 2 * tooltipLabelStyle.padding}px`, }), [], ); const tooltipLabel = React.useMemo(() => { if (!activeTooltipLabel) { return null; } return (
{activeTooltipLabel}
); }, [activeTooltipLabel, messageTooltipLabelStyle]); const tooltipTimestamp = React.useMemo(() => { if (!messageTimestamp) { return null; } return (
{messageTimestamp}
); }, [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}`; - const sendReaction = useSendReaction(messageInfo.id, localID, threadInfo.id); + const sendReaction = useSendReaction( + messageInfo.id, + localID, + threadInfo.id, + reactions, + ); const onEmojiSelect = React.useCallback( emoji => { const reactionInput = emoji.native; - - const viewerReacted = reactions[reactionInput] - ? reactions[reactionInput].viewerReacted - : false; - const action = viewerReacted ? 'remove_reaction' : 'add_reaction'; - - sendReaction(reactionInput, action); + sendReaction(reactionInput); }, - [sendReaction, reactions], + [sendReaction], ); const emojiKeyboard = React.useMemo(() => { if (!shouldRenderEmojiKeyboard) { return null; } return (
); }, [emojiKeyboardPositionStyle, onEmojiSelect, shouldRenderEmojiKeyboard]); const messageTooltipContainerStyle = React.useMemo(() => tooltipStyle, []); const containerClassName = classNames({ [css.messageTooltipContainer]: true, [css.leftTooltipAlign]: alignment === 'left', [css.centerTooltipAlign]: alignment === 'center', [css.rightTooltipAlign]: alignment === 'right', }); return ( <> {emojiKeyboard}
{tooltipLabel}
{tooltipButtons} {tooltipTimestamp}
); } export default MessageTooltip; diff --git a/web/chat/reaction-message-utils.js b/web/chat/reaction-message-utils.js index 48fcfbca6..66169abd0 100644 --- a/web/chat/reaction-message-utils.js +++ b/web/chat/reaction-message-utils.js @@ -1,209 +1,217 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { sendReactionMessage, sendReactionMessageActionTypes, } from 'lib/actions/message-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; +import type { ReactionInfo } from 'lib/selectors/chat-selectors'; import { messageTypes } from 'lib/types/message-types-enum.js'; import type { RawReactionMessageInfo } from 'lib/types/messages/reaction.js'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils.js'; import { cloneError } from 'lib/utils/errors.js'; import Alert from '../modals/alert.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { type TooltipSize, type TooltipPositionStyle, } from '../utils/tooltip-utils.js'; import { getAppContainerPositionInfo } from '../utils/window-utils.js'; function useSendReaction( messageID: ?string, localID: string, threadID: string, -): (reaction: string, action: 'add_reaction' | 'remove_reaction') => mixed { + reactions: ReactionInfo, +): (reaction: string) => mixed { const { pushModal } = useModalContext(); const viewerID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const callSendReactionMessage = useServerCall(sendReactionMessage); const dispatchActionPromise = useDispatchActionPromise(); return React.useCallback( - (reaction, action) => { + reaction => { if (!messageID) { return; } invariant(viewerID, 'viewerID should be set'); + const viewerReacted = reactions[reaction] + ? reactions[reaction].viewerReacted + : false; + const action = viewerReacted ? 'remove_reaction' : 'add_reaction'; + const reactionMessagePromise = (async () => { try { const result = await callSendReactionMessage({ threadID, localID, targetMessageID: messageID, reaction, action, }); return { localID, serverID: result.id, threadID, time: result.time, interface: result.interface, }; } catch (e) { pushModal( Please try again later , ); const copy = cloneError(e); copy.localID = localID; copy.threadID = threadID; throw copy; } })(); const startingPayload: RawReactionMessageInfo = { type: messageTypes.REACTION, threadID, localID, creatorID: viewerID, time: Date.now(), targetMessageID: messageID, reaction, action, }; dispatchActionPromise( sendReactionMessageActionTypes, reactionMessagePromise, undefined, startingPayload, ); }, [ messageID, viewerID, + reactions, threadID, localID, dispatchActionPromise, callSendReactionMessage, pushModal, ], ); } type EmojiKeyboardPosition = { +bottom: number, +left: number, }; function getEmojiKeyboardPosition( emojiKeyboard: ?HTMLDivElement, tooltipPositionStyle: TooltipPositionStyle, tooltipSize: TooltipSize, ): ?EmojiKeyboardPosition { const { alignment, anchorPoint } = tooltipPositionStyle; const tooltipAnchorX = anchorPoint.x; const tooltipAnchorY = anchorPoint.y; const tooltipWidth = tooltipSize.width; const tooltipHeight = tooltipSize.height; const appContainerPositionInfo = getAppContainerPositionInfo(); if (!appContainerPositionInfo) { return null; } let emojiKeyboardWidth = 352; let emojiKeyboardHeight = 435; if (emojiKeyboard) { const { width, height } = emojiKeyboard.getBoundingClientRect(); emojiKeyboardWidth = width; emojiKeyboardHeight = height; } const { top: containerTop, left: containerLeft, right: containerRight, bottom: containerBottom, } = appContainerPositionInfo; const padding = 16; const canBeDisplayedOnRight = tooltipAnchorX + tooltipWidth + emojiKeyboardWidth <= containerRight; const canBeDisplayedOnLeft = tooltipAnchorX - emojiKeyboardWidth >= containerLeft; const canBeDisplayedOnTop = tooltipAnchorY - emojiKeyboardHeight - padding >= containerTop; const canBeDisplayedOnBottom = tooltipAnchorY + tooltipHeight + emojiKeyboardHeight + padding <= containerBottom; const emojiKeyboardOverflowTop = containerTop - (tooltipAnchorY + tooltipHeight - emojiKeyboardHeight); const emojiKeyboardOverflowTopCorrection = emojiKeyboardOverflowTop > 0 ? -emojiKeyboardOverflowTop - padding : 0; const emojiKeyboardOverflowRight = tooltipAnchorX + emojiKeyboardWidth - containerRight; const emojiKeyboardOverflowRightCorrection = emojiKeyboardOverflowRight > 0 ? -emojiKeyboardOverflowRight - padding : 0; if (alignment === 'left' && canBeDisplayedOnRight) { return { left: tooltipWidth, bottom: emojiKeyboardOverflowTopCorrection, }; } if (alignment === 'right' && canBeDisplayedOnLeft) { return { left: -emojiKeyboardWidth, bottom: emojiKeyboardOverflowTopCorrection, }; } if (canBeDisplayedOnTop) { return { bottom: tooltipHeight + padding, left: emojiKeyboardOverflowRightCorrection, }; } if (canBeDisplayedOnBottom) { return { bottom: -emojiKeyboardHeight - padding, left: emojiKeyboardOverflowRightCorrection, }; } return { left: alignment === 'left' ? -emojiKeyboardWidth : tooltipWidth, bottom: emojiKeyboardOverflowTopCorrection, }; } export { useSendReaction, getEmojiKeyboardPosition };