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 @@ -20,7 +20,8 @@ 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, 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 @@ -19,7 +19,8 @@ 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, diff --git a/web/components/pinned-message.css b/web/components/pinned-message.css new file mode 100644 --- /dev/null +++ b/web/components/pinned-message.css @@ -0,0 +1,28 @@ +.messageContainer { + overflow-y: scroll; + border: 1px solid var(--pin-message-modal-border-color); + border-radius: 7px; + max-height: 400px; + margin: 16px 32px; +} + +.messageDate { + color: var(--chat-timestamp-color); + font-size: var(--xs-font-12); + padding: 0px 0px 6px 0px; + line-height: var(--line-height-text); + text-align: left; + margin-left: 16px; +} + +.creator { + font-size: small; + color: var(--shades-white-60); + font-size: var(--s-font-14); + padding: 4px 24px; + text-align: left; +} + +.messageContent { + margin-bottom: 1px; +} diff --git a/web/components/pinned-message.react.js b/web/components/pinned-message.react.js new file mode 100644 --- /dev/null +++ b/web/components/pinned-message.react.js @@ -0,0 +1,57 @@ +// @flow + +import * as React from 'react'; + +import { useStringForUser } from 'lib/hooks/ens-cache.js'; +import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js'; +import type { ThreadInfo } from 'lib/types/thread-types.js'; +import { longAbsoluteDate } from 'lib/utils/date-utils.js'; + +import css from './pinned-message.css'; +import { MessageListContext } from '../chat/message-list-types.js'; +import Message from '../chat/message.react.js'; +import { useTextMessageRulesFunc } from '../markdown/rules.react.js'; + +type PinnedMessageProps = { + +item: ChatMessageInfoItem, + +threadInfo: ThreadInfo, +}; + +function PinnedMessage(props: PinnedMessageProps): React.Node { + const { item, threadInfo } = props; + + const getTextMessageMarkdownRules = useTextMessageRulesFunc(threadInfo); + const messageListContext = React.useMemo(() => { + if (!getTextMessageMarkdownRules) { + return undefined; + } + return { getTextMessageMarkdownRules }; + }, [getTextMessageMarkdownRules]); + + const shouldShowUsername = !item.startsConversation && !item.startsCluster; + const username = useStringForUser( + shouldShowUsername ? item.messageInfo.creator : null, + ); + + return ( +
+
+
{username}
+
+ + + +
+
+ {longAbsoluteDate(item.messageInfo.time)} +
+
+
+ ); +} + +export default PinnedMessage; diff --git a/web/modals/chat/toggle-pin-modal.css b/web/modals/chat/toggle-pin-modal.css --- a/web/modals/chat/toggle-pin-modal.css +++ b/web/modals/chat/toggle-pin-modal.css @@ -0,0 +1,30 @@ +.confirmationText { + color: var(--pin-message-information-text-color); + padding: 16px 32px 0 32px; + font-size: small; +} + +.buttonContainer { + width: 100%; + display: flex; + flex-direction: column; + align-self: center; + align-items: stretch; + margin-bottom: 16px; +} + +.togglePinButton { + margin: 0 32px 0 32px; +} + +.cancelButton { + color: white; + display: flex; + justify-content: center; + margin-top: 16px; +} + +.cancelButton:hover { + cursor: pointer; + text-decoration: underline; +} diff --git a/web/modals/chat/toggle-pin-modal.react.js b/web/modals/chat/toggle-pin-modal.react.js --- a/web/modals/chat/toggle-pin-modal.react.js +++ b/web/modals/chat/toggle-pin-modal.react.js @@ -1,18 +1,129 @@ // @flow +import invariant from 'invariant'; import * as React from 'react'; +import { + toggleMessagePin, + toggleMessagePinActionTypes, +} from 'lib/actions/thread-actions.js'; +import { useModalContext } from 'lib/components/modal-provider.react.js'; import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; +import { + useServerCall, + useDispatchActionPromise, +} from 'lib/utils/action-utils.js'; + +import css from './toggle-pin-modal.css'; +import Button, { buttonThemes } from '../../components/button.react.js'; +import PinnedMessage from '../../components/pinned-message.react.js'; +import Modal from '../modal.react.js'; type TogglePinModalProps = { +item: ChatMessageInfoItem, +threadInfo: ThreadInfo, }; -// eslint-disable-next-line no-unused-vars function TogglePinModal(props: TogglePinModalProps): React.Node { - return <>; + const { item, threadInfo } = props; + const { messageInfo, isPinned } = item; + const { popModal } = useModalContext(); + + const callToggleMessagePin = useServerCall(toggleMessagePin); + const dispatchActionPromise = useDispatchActionPromise(); + + const modalInfo = React.useMemo(() => { + if (isPinned) { + return { + name: 'Remove Pinned Message', + action: 'unpin', + confirmationText: + 'Are you sure you want to remove this pinned message?', + buttonText: 'Remove Pinned Message', + buttonColor: buttonThemes.danger, + }; + } + + return { + name: 'Pin Message', + action: 'pin', + confirmationText: `You may pin this message to the channel + you are currently viewing. To unpin a message, select the pinned + messages icon in the channel.`, + buttonText: 'Pin Message', + buttonColor: buttonThemes.standard, + }; + }, [isPinned]); + + // We want to remove inline engagement (threadCreatedFromMessage / reactions) + // and the message header (startsConversation). We also want to set isViewer + // to false so that the message is left-aligned and uncolored. + const modifiedItem = React.useMemo(() => { + if (item.messageInfoType !== 'composable') { + return item; + } + + return { + ...item, + threadCreatedFromMessage: undefined, + reactions: {}, + startsConversation: false, + messageInfo: { + ...item.messageInfo, + creator: { + ...item.messageInfo.creator, + isViewer: false, + }, + }, + }; + }, [item]); + + const onClick = React.useCallback(() => { + const createToggleMessagePinPromise = async () => { + invariant(messageInfo.id, 'messageInfo.id should be defined'); + const result = await callToggleMessagePin({ + messageID: messageInfo.id, + action: modalInfo.action, + }); + return { + newMessageInfos: result.newMessageInfos, + threadID: result.threadID, + }; + }; + + dispatchActionPromise( + toggleMessagePinActionTypes, + createToggleMessagePinPromise(), + ); + popModal(); + }, [ + modalInfo, + callToggleMessagePin, + dispatchActionPromise, + messageInfo.id, + popModal, + ]); + + return ( + +
{modalInfo.confirmationText}
+ +
+ +
+ Cancel +
+
+
+ ); } export default TogglePinModal; diff --git a/web/theme.css b/web/theme.css --- a/web/theme.css +++ b/web/theme.css @@ -212,4 +212,6 @@ --topbar-button-fg: var(--shades-white-60); --message-label-color: var(--shades-black-60); --topbar-lines: rgba(255, 255, 255, 0.08); + --pin-message-information-text-color: var(--shades-white-60); + --pin-message-modal-border-color: var(--shades-black-80); } diff --git a/web/utils/tooltip-utils.js b/web/utils/tooltip-action-utils.js copy from web/utils/tooltip-utils.js copy to web/utils/tooltip-action-utils.js --- a/web/utils/tooltip-utils.js +++ b/web/utils/tooltip-action-utils.js @@ -20,12 +20,14 @@ 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'; @@ -36,310 +38,6 @@ 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, @@ -494,6 +192,8 @@ isComposableMessageType(messageInfo.type) && threadHasPermission(threadInfo, threadPermissions.MANAGE_PINS); + const inputState = React.useContext(InputStateContext); + return React.useMemo(() => { if (!canTogglePin) { return null; @@ -504,7 +204,11 @@ const buttonContent = ; const onClickTogglePin = () => { - pushModal(); + pushModal( + + + , + ); }; return { @@ -512,7 +216,7 @@ onClick: onClickTogglePin, label: isPinned ? 'Unpin' : 'Pin', }; - }, [canTogglePin, isPinned, pushModal, item, threadInfo]); + }, [canTogglePin, inputState, isPinned, pushModal, item, threadInfo]); } function useMessageTooltipActions( @@ -723,9 +427,6 @@ } export { - findTooltipPosition, - calculateTooltipSize, - getMessageActionTooltipStyle, useMessageTooltipSidebarAction, useMessageTooltipReplyAction, useMessageReactAction, 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 @@ -1,41 +1,15 @@ // @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'; -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({ @@ -341,394 +315,8 @@ }; } -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, - 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, };