diff --git a/web/CommIcon.react.js b/web/CommIcon.react.js
index 273487eba..9877f3c9f 100644
--- a/web/CommIcon.react.js
+++ b/web/CommIcon.react.js
@@ -1,38 +1,40 @@
// @flow
import * as React from 'react';
import IcomoonReact from 'react-icomoon';
import iconSet from 'lib/shared/comm-icon-config.json';
const IcomoonIcon = IcomoonReact.default;
export type CommIcons =
| 'cloud-filled'
| 'sidebar'
| 'sidebar-filled'
| 'reply'
| 'reply-filled'
| 'megaphone'
| 'copy-filled'
- | 'emote-smile-filled';
+ | 'emote-smile-filled'
+ | 'pin'
+ | 'unpin';
type CommIconProps = {
+icon: CommIcons,
+size: number | string,
+color?: string,
+title?: string,
+className?: string,
+disableFill?: boolean,
+removeInlineStyle?: boolean,
};
const iconStyle = {
stroke: 'none',
};
function CommIcon(props: CommIconProps): React.Node {
return ;
}
export default CommIcon;
diff --git a/web/chat/composed-message.react.js b/web/chat/composed-message.react.js
index 29b371aa2..bff43aa1e 100644
--- a/web/chat/composed-message.react.js
+++ b/web/chat/composed-message.react.js
@@ -1,224 +1,225 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
import {
Circle as CircleIcon,
CheckCircle as CheckCircleIcon,
XCircle as XCircleIcon,
} from 'react-feather';
import { useStringForUser } from 'lib/hooks/ens-cache.js';
import { type ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js';
import { getMessageLabel } from 'lib/shared/edit-messages-utils.js';
import { assertComposableMessageType } from 'lib/types/message-types.js';
import { type ThreadInfo } from 'lib/types/thread-types.js';
import css from './chat-message-list.css';
import FailedSend from './failed-send.react.js';
import InlineEngagement from './inline-engagement.react.js';
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,
tooltipPositions.LEFT_BOTTOM,
tooltipPositions.LEFT_TOP,
tooltipPositions.RIGHT,
tooltipPositions.RIGHT_BOTTOM,
tooltipPositions.RIGHT_TOP,
tooltipPositions.BOTTOM,
tooltipPositions.TOP,
];
const availableTooltipPositionsForNonViewerMessage = [
tooltipPositions.RIGHT,
tooltipPositions.RIGHT_BOTTOM,
tooltipPositions.RIGHT_TOP,
tooltipPositions.LEFT,
tooltipPositions.LEFT_BOTTOM,
tooltipPositions.LEFT_TOP,
tooltipPositions.BOTTOM,
tooltipPositions.TOP,
];
type BaseProps = {
+item: ChatMessageInfoItem,
+threadInfo: ThreadInfo,
+sendFailed: boolean,
+children: React.Node,
+fixedWidth?: boolean,
+borderRadius: number,
};
type BaseConfig = React.Config;
type Props = {
...BaseProps,
// withInputState
+inputState: ?InputState,
+onMouseLeave: ?() => mixed,
+onMouseEnter: (event: SyntheticEvent) => mixed,
+containsInlineEngagement: boolean,
+stringForUser: ?string,
};
class ComposedMessage extends React.PureComponent {
static defaultProps: { +borderRadius: number } = {
borderRadius: 8,
};
render(): React.Node {
assertComposableMessageType(this.props.item.messageInfo.type);
const { borderRadius, item, threadInfo } = this.props;
const { hasBeenEdited } = item;
const { id, creator } = item.messageInfo;
const threadColor = threadInfo.color;
const { isViewer } = creator;
const contentClassName = classNames({
[css.content]: true,
[css.viewerContent]: isViewer,
[css.nonViewerContent]: !isViewer,
});
const messageBoxContainerClassName = classNames({
[css.messageBoxContainer]: true,
[css.fixedWidthMessageBoxContainer]: this.props.fixedWidth,
[css.messageBoxContainerPositionAvatar]: shouldRenderAvatars,
[css.messageBoxContainerPositionNoAvatar]: !shouldRenderAvatars,
});
const messageBoxClassName = classNames({
[css.messageBox]: true,
[css.fixedWidthMessageBox]: this.props.fixedWidth,
});
const messageBoxStyle = {
borderTopRightRadius: isViewer && !item.startsCluster ? 0 : borderRadius,
borderBottomRightRadius: isViewer && !item.endsCluster ? 0 : borderRadius,
borderTopLeftRadius: !isViewer && !item.startsCluster ? 0 : borderRadius,
borderBottomLeftRadius: !isViewer && !item.endsCluster ? 0 : borderRadius,
};
let authorName = null;
const { stringForUser } = this.props;
const authorNameClassName = classNames({
[css.authorName]: true,
[css.authorNamePositionAvatar]: shouldRenderAvatars,
[css.authorNamePositionNoAvatar]: !shouldRenderAvatars,
});
if (stringForUser) {
authorName = {stringForUser};
}
let deliveryIcon = null;
let failedSendInfo = null;
if (isViewer) {
let deliveryIconSpan;
let deliveryIconColor = threadColor;
if (id !== null && id !== undefined) {
deliveryIconSpan = ;
} else if (this.props.sendFailed) {
deliveryIconSpan = ;
deliveryIconColor = 'FF0000';
failedSendInfo = ;
} else {
deliveryIconSpan = ;
}
deliveryIcon = (
{deliveryIconSpan}
);
}
let inlineEngagement = null;
const label = getMessageLabel(hasBeenEdited, threadInfo);
if (
(this.props.containsInlineEngagement && item.threadCreatedFromMessage) ||
Object.keys(item.reactions).length > 0 ||
label
) {
const positioning = isViewer ? 'right' : 'left';
inlineEngagement = (
);
}
let avatar;
if (!isViewer && item.endsCluster && shouldRenderAvatars) {
avatar = (
);
} else if (!isViewer && shouldRenderAvatars) {
avatar = ;
}
return (
{authorName}
{failedSendInfo}
{inlineEngagement}
);
}
}
type ConnectedConfig = React.Config<
BaseProps,
typeof ComposedMessage.defaultProps,
>;
const ConnectedComposedMessage: React.ComponentType =
React.memo(function ConnectedComposedMessage(props) {
const { item, threadInfo } = props;
const inputState = React.useContext(InputStateContext);
const { creator } = props.item.messageInfo;
const { isViewer } = creator;
const availablePositions = isViewer
? availableTooltipPositionsForViewerMessage
: availableTooltipPositionsForNonViewerMessage;
const containsInlineEngagement = !!item.threadCreatedFromMessage;
const { onMouseLeave, onMouseEnter } = useMessageTooltip({
item,
threadInfo,
availablePositions,
});
const shouldShowUsername = !isViewer && item.startsCluster;
const stringForUser = useStringForUser(shouldShowUsername ? creator : null);
return (
);
});
export default ConnectedComposedMessage;
diff --git a/web/chat/robotext-message.react.js b/web/chat/robotext-message.react.js
index 43f2a29b0..b0fff181c 100644
--- a/web/chat/robotext-message.react.js
+++ b/web/chat/robotext-message.react.js
@@ -1,143 +1,144 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { useDispatch } from 'react-redux';
import { type RobotextChatMessageInfoItem } from 'lib/selectors/chat-selectors.js';
import { threadInfoSelector } from 'lib/selectors/thread-selectors.js';
import type { Dispatch } from 'lib/types/redux-types.js';
import { type ThreadInfo } from 'lib/types/thread-types.js';
import {
entityTextToReact,
useENSNamesForEntityText,
} from 'lib/utils/entity-text.js';
import InlineEngagement from './inline-engagement.react.js';
import css from './robotext-message.css';
import Markdown from '../markdown/markdown.react.js';
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,
tooltipPositions.LEFT_TOP,
tooltipPositions.LEFT_BOTTOM,
tooltipPositions.RIGHT,
tooltipPositions.RIGHT_TOP,
tooltipPositions.RIGHT_BOTTOM,
];
type Props = {
+item: RobotextChatMessageInfoItem,
+threadInfo: ThreadInfo,
};
function RobotextMessage(props: Props): React.Node {
let inlineEngagement;
const { item } = props;
const { threadCreatedFromMessage, reactions } = item;
if (threadCreatedFromMessage || Object.keys(reactions).length > 0) {
inlineEngagement = (
);
}
const { messageInfo, robotext } = item;
const { threadID } = messageInfo;
const robotextWithENSNames = useENSNamesForEntityText(robotext);
invariant(
robotextWithENSNames,
'useENSNamesForEntityText only returns falsey when passed falsey',
);
const textParts = React.useMemo(() => {
return entityTextToReact(robotextWithENSNames, threadID, {
// eslint-disable-next-line react/display-name
renderText: ({ text }) => (
{text}
),
// eslint-disable-next-line react/display-name
renderThread: ({ id, name }) => ,
// eslint-disable-next-line react/display-name
renderColor: ({ hex }) => ,
});
}, [robotextWithENSNames, threadID]);
const { threadInfo } = props;
const { onMouseEnter, onMouseLeave } = useMessageTooltip({
item,
threadInfo,
availablePositions: availableTooltipPositionsForRobotext,
});
return (
{textParts}
{inlineEngagement}
);
}
type BaseInnerThreadEntityProps = {
+id: string,
+name: string,
};
type InnerThreadEntityProps = {
...BaseInnerThreadEntityProps,
+threadInfo: ThreadInfo,
+dispatch: Dispatch,
};
class InnerThreadEntity extends React.PureComponent {
render() {
return {this.props.name};
}
onClickThread = (event: SyntheticEvent) => {
event.preventDefault();
const id = this.props.id;
this.props.dispatch({
type: updateNavInfoActionType,
payload: {
activeChatThreadID: id,
},
});
};
}
const ThreadEntity = React.memo(
function ConnectedInnerThreadEntity(props: BaseInnerThreadEntityProps) {
const { id } = props;
const threadInfo = useSelector(state => threadInfoSelector(state)[id]);
const dispatch = useDispatch();
return (
);
},
);
function ColorEntity(props: { color: string }) {
const colorStyle = { color: props.color };
return {props.color};
}
const MemoizedRobotextMessage: React.ComponentType =
React.memo(RobotextMessage);
export default MemoizedRobotextMessage;
diff --git a/web/modals/chat/toggle-pin-modal.css b/web/modals/chat/toggle-pin-modal.css
new file mode 100644
index 000000000..e69de29bb
diff --git a/web/modals/chat/toggle-pin-modal.react.js b/web/modals/chat/toggle-pin-modal.react.js
new file mode 100644
index 000000000..8ccc23139
--- /dev/null
+++ b/web/modals/chat/toggle-pin-modal.react.js
@@ -0,0 +1,18 @@
+// @flow
+
+import * as React from 'react';
+
+import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js';
+import type { ThreadInfo } from 'lib/types/thread-types.js';
+
+type TogglePinModalProps = {
+ +item: ChatMessageInfoItem,
+ +threadInfo: ThreadInfo,
+};
+
+// eslint-disable-next-line no-unused-vars
+function TogglePinModal(props: TogglePinModalProps): React.Node {
+ return <>>;
+}
+
+export default TogglePinModal;
diff --git a/web/utils/tooltip-utils.js b/web/utils/tooltip-action-utils.js
similarity index 53%
copy from web/utils/tooltip-utils.js
copy to web/utils/tooltip-action-utils.js
index e3abeb60c..e77fb0750 100644
--- a/web/utils/tooltip-utils.js
+++ b/web/utils/tooltip-action-utils.js
@@ -1,692 +1,429 @@
// @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';
+ 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';
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({
- 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,
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].filter(Boolean),
- [replyAction, sidebarAction, copyAction, reactAction],
+ () =>
+ [
+ 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,
};
diff --git a/web/utils/tooltip-utils.js b/web/utils/tooltip-utils.js
index e3abeb60c..904a3f81f 100644
--- a/web/utils/tooltip-utils.js
+++ b/web/utils/tooltip-utils.js
@@ -1,692 +1,322 @@
// @flow
import invariant from 'invariant';
-import _debounce from 'lodash/debounce.js';
import * as React from 'react';
-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 {
- 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,
- 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 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);
- return React.useMemo(
- () => [replyAction, sidebarAction, copyAction, reactAction].filter(Boolean),
- [replyAction, sidebarAction, copyAction, reactAction],
- );
-}
-
-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,
};