Page MenuHomePhabricator

No OneTemporary

diff --git a/web/chat/composed-message.react.js b/web/chat/composed-message.react.js
index 08bd448fc..5878a07bd 100644
--- a/web/chat/composed-message.react.js
+++ b/web/chat/composed-message.react.js
@@ -1,190 +1,190 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
import {
Circle as CircleIcon,
CheckCircle as CheckCircleIcon,
XCircle as XCircleIcon,
} from 'react-feather';
import { type ChatMessageInfoItem } from 'lib/selectors/chat-selectors';
import { stringForUser } from 'lib/shared/user-utils';
import { assertComposableMessageType } from 'lib/types/message-types';
import { type ThreadInfo } from 'lib/types/thread-types';
import { type InputState, InputStateContext } from '../input/input-state';
+import { tooltipPositions, useMessageTooltip } from '../utils/tooltip-utils';
import css from './chat-message-list.css';
import FailedSend from './failed-send.react';
import InlineSidebar from './inline-sidebar.react';
-import { tooltipPositions, useMessageTooltip } from './tooltip-utils';
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<BaseProps, typeof ComposedMessage.defaultProps>;
type Props = {
...BaseProps,
// withInputState
+inputState: ?InputState,
+onMouseLeave: ?() => mixed,
+onMouseEnter: (event: SyntheticEvent<HTMLDivElement>) => mixed,
+containsInlineSidebar: boolean,
};
class ComposedMessage extends React.PureComponent<Props> {
static defaultProps: { +borderRadius: number } = {
borderRadius: 8,
};
render(): React.Node {
assertComposableMessageType(this.props.item.messageInfo.type);
const { borderRadius, item, threadInfo } = this.props;
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,
});
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;
if (!isViewer && item.startsCluster) {
authorName = (
<span className={css.authorName}>{stringForUser(creator)}</span>
);
}
let deliveryIcon = null;
let failedSendInfo = null;
if (isViewer) {
let deliveryIconSpan;
let deliveryIconColor = threadColor;
if (id !== null && id !== undefined) {
deliveryIconSpan = <CheckCircleIcon />;
} else if (this.props.sendFailed) {
deliveryIconSpan = <XCircleIcon />;
deliveryIconColor = 'FF0000';
failedSendInfo = <FailedSend item={item} threadInfo={threadInfo} />;
} else {
deliveryIconSpan = <CircleIcon />;
}
deliveryIcon = (
<div
className={css.iconContainer}
style={{ color: `#${deliveryIconColor}` }}
>
{deliveryIconSpan}
</div>
);
}
let inlineSidebar = null;
if (this.props.containsInlineSidebar && item.threadCreatedFromMessage) {
const positioning = isViewer ? 'right' : 'left';
inlineSidebar = (
<div className={css.sidebarMarginBottom}>
<InlineSidebar
threadInfo={item.threadCreatedFromMessage}
positioning={positioning}
/>
</div>
);
}
return (
<React.Fragment>
{authorName}
<div className={contentClassName}>
<div
className={messageBoxContainerClassName}
onMouseEnter={this.props.onMouseEnter}
onMouseLeave={this.props.onMouseLeave}
>
<div className={messageBoxClassName} style={messageBoxStyle}>
{this.props.children}
</div>
</div>
{deliveryIcon}
</div>
{failedSendInfo}
{inlineSidebar}
</React.Fragment>
);
}
}
type ConnectedConfig = React.Config<
BaseProps,
typeof ComposedMessage.defaultProps,
>;
const ConnectedComposedMessage: React.ComponentType<ConnectedConfig> = React.memo<BaseConfig>(
function ConnectedComposedMessage(props) {
const { item, threadInfo } = props;
const inputState = React.useContext(InputStateContext);
const isViewer = props.item.messageInfo.creator.isViewer;
const availablePositions = isViewer
? availableTooltipPositionsForViewerMessage
: availableTooltipPositionsForNonViewerMessage;
const containsInlineSidebar = !!item.threadCreatedFromMessage;
const { onMouseLeave, onMouseEnter } = useMessageTooltip({
item,
threadInfo,
availablePositions,
});
return (
<ComposedMessage
{...props}
inputState={inputState}
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
containsInlineSidebar={containsInlineSidebar}
/>
);
},
);
export default ConnectedComposedMessage;
diff --git a/web/chat/message-tooltip.react.js b/web/chat/message-tooltip.react.js
index 42ab6cc5c..44ceb8172 100644
--- a/web/chat/message-tooltip.react.js
+++ b/web/chat/message-tooltip.react.js
@@ -1,115 +1,115 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
+import { type MessageTooltipAction } from '../utils/tooltip-utils';
import {
tooltipButtonStyle,
tooltipLabelStyle,
tooltipStyle,
} from './chat-constants';
import css from './message-tooltip.css';
-import { type MessageTooltipAction } from './tooltip-utils';
type MessageTooltipProps = {
+actions: $ReadOnlyArray<MessageTooltipAction>,
+messageTimestamp: string,
+alignment?: 'left' | 'center' | 'right',
};
function MessageTooltip(props: MessageTooltipProps): React.Node {
const { actions, messageTimestamp, alignment = 'left' } = props;
const [activeTooltipLabel, setActiveTooltipLabel] = React.useState<?string>();
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 (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
key={label}
onClick={onClick}
style={messageTooltipButtonStyle}
className={css.messageTooltipButton}
>
{actionButtonContent}
</div>
);
});
return (
<div className={messageActionButtonsContainerClassName}>{buttons}</div>
);
}, [
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 (
<div className={css.messageTooltipLabel} style={messageTooltipLabelStyle}>
{activeTooltipLabel}
</div>
);
}, [activeTooltipLabel, messageTooltipLabelStyle]);
const tooltipTimestamp = React.useMemo(() => {
if (!messageTimestamp) {
return null;
}
return (
<div className={css.messageTooltipLabel} style={messageTooltipLabelStyle}>
{messageTimestamp}
</div>
);
}, [messageTimestamp, messageTooltipLabelStyle]);
const messageTooltipContainerStyle = React.useMemo(() => tooltipStyle, []);
const containerClassNames = React.useMemo(
() =>
classNames(css.messageTooltipContainer, {
[css.leftTooltipAlign]: alignment === 'left',
[css.centerTooltipAlign]: alignment === 'center',
[css.rightTooltipAlign]: alignment === 'right',
}),
[alignment],
);
return (
<div className={containerClassNames} style={messageTooltipContainerStyle}>
<div style={messageTooltipTopLabelStyle}>{tooltipLabel}</div>
{tooltipButtons}
{tooltipTimestamp}
</div>
);
}
export default MessageTooltip;
diff --git a/web/chat/robotext-message.react.js b/web/chat/robotext-message.react.js
index 35a4b2ca2..ffa428d6d 100644
--- a/web/chat/robotext-message.react.js
+++ b/web/chat/robotext-message.react.js
@@ -1,166 +1,166 @@
// @flow
import * as React from 'react';
import { useDispatch } from 'react-redux';
import { type RobotextChatMessageInfoItem } from 'lib/selectors/chat-selectors';
import { threadInfoSelector } from 'lib/selectors/thread-selectors';
import { splitRobotext, parseRobotextEntity } from 'lib/shared/message-utils';
import type { Dispatch } from 'lib/types/redux-types';
import { type ThreadInfo } from 'lib/types/thread-types';
import Markdown from '../markdown/markdown.react';
import { linkRules } from '../markdown/rules.react';
import { updateNavInfoActionType } from '../redux/action-types';
import { useSelector } from '../redux/redux-utils';
+import { tooltipPositions, useMessageTooltip } from '../utils/tooltip-utils';
import InlineSidebar from './inline-sidebar.react';
import css from './robotext-message.css';
-import { tooltipPositions, useMessageTooltip } from './tooltip-utils';
const availableTooltipPositionsForRobotext = [
tooltipPositions.LEFT,
tooltipPositions.LEFT_TOP,
tooltipPositions.LEFT_BOTTOM,
tooltipPositions.RIGHT,
tooltipPositions.RIGHT_TOP,
tooltipPositions.RIGHT_BOTTOM,
];
type BaseProps = {
+item: RobotextChatMessageInfoItem,
+threadInfo: ThreadInfo,
};
type Props = {
...BaseProps,
+onMouseLeave: ?() => mixed,
+onMouseEnter: (event: SyntheticEvent<HTMLDivElement>) => mixed,
};
class RobotextMessage extends React.PureComponent<Props> {
render() {
let inlineSidebar;
if (this.props.item.threadCreatedFromMessage) {
inlineSidebar = (
<div className={css.sidebarMarginTop}>
<InlineSidebar
threadInfo={this.props.item.threadCreatedFromMessage}
positioning="center"
/>
</div>
);
}
return (
<div className={css.robotextContainer}>
<div
className={css.innerRobotextContainer}
onMouseEnter={this.props.onMouseEnter}
onMouseLeave={this.props.onMouseLeave}
>
<span>{this.linkedRobotext()}</span>
</div>
{inlineSidebar}
</div>
);
}
linkedRobotext() {
const { item } = this.props;
const { robotext } = item;
const robotextParts = splitRobotext(robotext);
const textParts = [];
let keyIndex = 0;
for (const splitPart of robotextParts) {
if (splitPart === '') {
continue;
}
if (splitPart.charAt(0) !== '<') {
const key = `text${keyIndex++}`;
textParts.push(
<Markdown key={key} rules={linkRules(false)}>
{decodeURI(splitPart)}
</Markdown>,
);
continue;
}
const { rawText, entityType, id } = parseRobotextEntity(splitPart);
if (entityType === 't' && id !== item.messageInfo.threadID) {
textParts.push(<ThreadEntity key={id} id={id} name={rawText} />);
} else if (entityType === 'c') {
textParts.push(<ColorEntity key={id} color={rawText} />);
} else {
textParts.push(rawText);
}
}
return textParts;
}
}
type BaseInnerThreadEntityProps = {
+id: string,
+name: string,
};
type InnerThreadEntityProps = {
...BaseInnerThreadEntityProps,
+threadInfo: ThreadInfo,
+dispatch: Dispatch,
};
class InnerThreadEntity extends React.PureComponent<InnerThreadEntityProps> {
render() {
return <a onClick={this.onClickThread}>{this.props.name}</a>;
}
onClickThread = (event: SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
const id = this.props.id;
this.props.dispatch({
type: updateNavInfoActionType,
payload: {
activeChatThreadID: id,
},
});
};
}
const ThreadEntity = React.memo<BaseInnerThreadEntityProps>(
function ConnectedInnerThreadEntity(props: BaseInnerThreadEntityProps) {
const { id } = props;
const threadInfo = useSelector(state => threadInfoSelector(state)[id]);
const dispatch = useDispatch();
return (
<InnerThreadEntity
{...props}
threadInfo={threadInfo}
dispatch={dispatch}
/>
);
},
);
function ColorEntity(props: { color: string }) {
const colorStyle = { color: props.color };
return <span style={colorStyle}>{props.color}</span>;
}
const ConnectedRobotextMessage: React.ComponentType<BaseProps> = React.memo<BaseProps>(
function ConnectedRobotextMessage(props) {
const { item, threadInfo } = props;
const { onMouseLeave, onMouseEnter } = useMessageTooltip({
item,
threadInfo,
availablePositions: availableTooltipPositionsForRobotext,
});
return (
<RobotextMessage
{...props}
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
/>
);
},
);
export default ConnectedRobotextMessage;
diff --git a/web/chat/tooltip-provider.js b/web/chat/tooltip-provider.js
index 41f17626c..7ffbdd24a 100644
--- a/web/chat/tooltip-provider.js
+++ b/web/chat/tooltip-provider.js
@@ -1,172 +1,172 @@
// @flow
import classNames from 'classnames';
import invariant from 'invariant';
import * as React from 'react';
-import type { TooltipPositionStyle } from './tooltip-utils';
+import type { TooltipPositionStyle } from '../utils/tooltip-utils';
import css from './tooltip.css';
const onMouseLeaveSourceDisappearTimeoutMs = 200;
const onMouseLeaveTooltipDisappearTimeoutMs = 100;
export type RenderTooltipParams = {
+newNode: React.Node,
+tooltipPositionStyle: TooltipPositionStyle,
};
export type RenderTooltipResult = {
+onMouseLeaveCallback: () => mixed,
+clearTooltip: () => mixed,
};
type TooltipContextType = {
+renderTooltip: (params: RenderTooltipParams) => RenderTooltipResult,
+clearTooltip: () => mixed,
};
const TooltipContext: React.Context<TooltipContextType> = React.createContext<TooltipContextType>(
{
renderTooltip: () => ({
onMouseLeaveCallback: () => {},
clearTooltip: () => {},
}),
clearTooltip: () => {},
},
);
type Props = {
+children: React.Node,
};
function TooltipProvider(props: Props): React.Node {
const { children } = props;
// eslint-disable-next-line no-unused-vars
const tooltipSymbol = React.useRef<?symbol>(null);
// eslint-disable-next-line no-unused-vars
const tooltipCancelTimer = React.useRef<?TimeoutID>(null);
// eslint-disable-next-line no-unused-vars
const [tooltipNode, setTooltipNode] = React.useState<React.Node>(null);
const [
// eslint-disable-next-line no-unused-vars
tooltipPosition,
// eslint-disable-next-line no-unused-vars
setTooltipPosition,
] = React.useState<?TooltipPositionStyle>(null);
const clearTooltip = React.useCallback((tooltipToClose: symbol) => {
if (tooltipSymbol.current !== tooltipToClose) {
return;
}
tooltipCancelTimer.current = null;
setTooltipNode(null);
setTooltipPosition(null);
tooltipSymbol.current = null;
}, []);
const clearCurrentTooltip = React.useCallback(() => {
if (tooltipSymbol.current) {
clearTooltip(tooltipSymbol.current);
}
}, [clearTooltip]);
const renderTooltip = React.useCallback(
({
newNode,
tooltipPositionStyle: newTooltipPosition,
}: RenderTooltipParams): RenderTooltipResult => {
setTooltipNode(newNode);
setTooltipPosition(newTooltipPosition);
const newNodeSymbol = Symbol();
tooltipSymbol.current = newNodeSymbol;
if (tooltipCancelTimer.current) {
clearTimeout(tooltipCancelTimer.current);
}
return {
onMouseLeaveCallback: () => {
const newTimer = setTimeout(
() => clearTooltip(newNodeSymbol),
onMouseLeaveSourceDisappearTimeoutMs,
);
tooltipCancelTimer.current = newTimer;
},
clearTooltip: () => clearTooltip(newNodeSymbol),
};
},
[clearTooltip],
);
// eslint-disable-next-line no-unused-vars
const onMouseEnterTooltip = React.useCallback(() => {
if (tooltipSymbol.current) {
clearTimeout(tooltipCancelTimer.current);
}
}, []);
// eslint-disable-next-line no-unused-vars
const onMouseLeaveTooltip = React.useCallback(() => {
const timer = setTimeout(
clearCurrentTooltip,
onMouseLeaveTooltipDisappearTimeoutMs,
);
tooltipCancelTimer.current = timer;
}, [clearCurrentTooltip]);
const tooltip = React.useMemo(() => {
if (!tooltipNode || !tooltipPosition) {
return null;
}
const tooltipContainerStyle = {
position: 'absolute',
top: tooltipPosition.yCoord,
left: tooltipPosition.xCoord,
};
const { verticalPosition, horizontalPosition } = tooltipPosition;
const tooltipClassName = classNames(css.tooltipAbsolute, {
[css.tooltipAbsoluteLeft]: horizontalPosition === 'right',
[css.tooltipAbsoluteRight]: horizontalPosition === 'left',
[css.tooltipAbsoluteTop]: verticalPosition === 'bottom',
[css.tooltipAbsoluteBottom]: verticalPosition === 'top',
});
return (
<div style={tooltipContainerStyle}>
<div
className={tooltipClassName}
onMouseEnter={onMouseEnterTooltip}
onMouseLeave={onMouseLeaveTooltip}
>
{tooltipNode}
</div>
</div>
);
}, [onMouseEnterTooltip, onMouseLeaveTooltip, tooltipNode, tooltipPosition]);
const value = React.useMemo(
() => ({
renderTooltip,
clearTooltip: clearCurrentTooltip,
}),
[renderTooltip, clearCurrentTooltip],
);
return (
<TooltipContext.Provider value={value}>
{children}
{tooltip}
</TooltipContext.Provider>
);
}
function useTooltipContext(): TooltipContextType {
const context = React.useContext(TooltipContext);
invariant(context, 'TooltipContext not found');
return context;
}
export { TooltipProvider, useTooltipContext };
diff --git a/web/chat/tooltip-utils.js b/web/utils/tooltip-utils.js
similarity index 98%
rename from web/chat/tooltip-utils.js
rename to web/utils/tooltip-utils.js
index dc6b1eaf8..c0a9d51d7 100644
--- a/web/chat/tooltip-utils.js
+++ b/web/utils/tooltip-utils.js
@@ -1,520 +1,520 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors';
import { createMessageReply } from 'lib/shared/message-utils';
import {
threadHasPermission,
useSidebarExistsOrCanBeCreated,
} from 'lib/shared/thread-utils';
import { isComposableMessageType } from 'lib/types/message-types';
import type { ThreadInfo } from 'lib/types/thread-types';
import { threadPermissions } from 'lib/types/thread-types';
import { longAbsoluteDate } from 'lib/utils/date-utils';
+import {
+ tooltipButtonStyle,
+ tooltipLabelStyle,
+ tooltipStyle,
+} from '../chat/chat-constants';
+import MessageTooltip from '../chat/message-tooltip.react';
+import type { PositionInfo } from '../chat/position-types';
+import { useTooltipContext } from '../chat/tooltip-provider';
import CommIcon from '../CommIcon.react';
import { InputStateContext } from '../input/input-state';
import { useSelector } from '../redux/redux-utils';
import {
useOnClickPendingSidebar,
useOnClickThread,
} from '../selectors/nav-selectors';
import { calculateMaxTextWidth } from '../utils/text-utils';
-import {
- tooltipButtonStyle,
- tooltipLabelStyle,
- tooltipStyle,
-} from './chat-constants';
-import MessageTooltip from './message-tooltip.react';
-import type { PositionInfo } from './position-types';
-import { useTooltipContext } from './tooltip-provider';
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',
});
type TooltipSize = {
+height: number,
+width: number,
};
export type TooltipPositionStyle = {
+xCoord: number,
+yCoord: number,
+verticalPosition: 'top' | 'bottom',
+horizontalPosition: 'left' | 'right',
+alignment: 'left' | 'center' | 'right',
};
export type TooltipPosition = $Values<typeof tooltipPositions>;
export type MessageTooltipAction = {
+label: string,
+onClick: (SyntheticEvent<HTMLDivElement>) => mixed,
+actionButtonContent: React.Node,
};
const appTopBarHeight = 65;
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<TooltipPosition>,
+defaultPosition: TooltipPosition,
+preventDisplayingBelowSource?: boolean,
};
function findTooltipPosition({
sourcePositionInfo,
tooltipSize,
availablePositions,
defaultPosition,
preventDisplayingBelowSource,
}: FindTooltipPositionArgs): TooltipPosition {
if (!window) {
return defaultPosition;
}
const appContainerPositionInfo: PositionInfo = {
height: window.innerHeight - appTopBarHeight,
width: window.innerWidth,
top: appTopBarHeight,
bottom: window.innerHeight,
left: 0,
right: window.innerWidth,
};
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 {
xCoord: sourcePositionInfo.right,
yCoord: sourcePositionInfo.top,
horizontalPosition: 'right',
verticalPosition: 'bottom',
alignment: 'left',
};
} else if (tooltipPosition === tooltipPositions.LEFT_TOP) {
return {
xCoord: sourcePositionInfo.left,
yCoord: sourcePositionInfo.top,
horizontalPosition: 'left',
verticalPosition: 'bottom',
alignment: 'right',
};
} else if (tooltipPosition === tooltipPositions.RIGHT_BOTTOM) {
return {
xCoord: sourcePositionInfo.right,
yCoord: sourcePositionInfo.bottom,
horizontalPosition: 'right',
verticalPosition: 'top',
alignment: 'left',
};
} else if (tooltipPosition === tooltipPositions.LEFT_BOTTOM) {
return {
xCoord: sourcePositionInfo.left,
yCoord: sourcePositionInfo.bottom,
horizontalPosition: 'left',
verticalPosition: 'top',
alignment: 'right',
};
} else if (tooltipPosition === tooltipPositions.LEFT) {
return {
xCoord: sourcePositionInfo.left,
yCoord:
sourcePositionInfo.top +
sourcePositionInfo.height / 2 -
tooltipSize.height / 2,
horizontalPosition: 'left',
verticalPosition: 'bottom',
alignment: 'right',
};
} else if (tooltipPosition === tooltipPositions.RIGHT) {
return {
xCoord: sourcePositionInfo.right,
yCoord:
sourcePositionInfo.top +
sourcePositionInfo.height / 2 -
tooltipSize.height / 2,
horizontalPosition: 'right',
verticalPosition: 'bottom',
alignment: 'left',
};
} else if (tooltipPosition === tooltipPositions.TOP) {
return {
xCoord:
sourcePositionInfo.left +
sourcePositionInfo.width / 2 -
tooltipSize.width / 2,
yCoord: sourcePositionInfo.top,
horizontalPosition: 'right',
verticalPosition: 'top',
alignment: 'center',
};
} else if (tooltipPosition === tooltipPositions.BOTTOM) {
return {
xCoord:
sourcePositionInfo.left +
sourcePositionInfo.width / 2 -
tooltipSize.width / 2,
yCoord: sourcePositionInfo.bottom,
horizontalPosition: 'right',
verticalPosition: 'bottom',
alignment: 'center',
};
}
invariant(false, `Unexpected tooltip position value: ${tooltipPosition}`);
}
type CalculateTooltipSizeArgs = {
+tooltipLabels: $ReadOnlyArray<string>,
+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 = <CommIcon icon="sidebar-filled" size={16} />;
const onClick = (event: SyntheticEvent<HTMLElement>) => {
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 = <CommIcon icon="reply-filled" size={18} />;
const onClick = () => {
if (!messageInfo.text) {
return;
}
addReply(createMessageReply(messageInfo.text));
};
return {
actionButtonContent: buttonContent,
onClick,
label: 'Reply',
};
}, [addReply, item.messageInfo.type, messageInfo, threadInfo]);
}
function useMessageTooltipActions(
item: ChatMessageInfoItem,
threadInfo: ThreadInfo,
): $ReadOnlyArray<MessageTooltipAction> {
const sidebarAction = useMessageTooltipSidebarAction(item, threadInfo);
const replyAction = useMessageTooltipReplyAction(item, threadInfo);
return React.useMemo(() => [replyAction, sidebarAction].filter(Boolean), [
replyAction,
sidebarAction,
]);
}
type UseMessageTooltipArgs = {
+availablePositions: $ReadOnlyArray<TooltipPosition>,
+item: ChatMessageInfoItem,
+threadInfo: ThreadInfo,
};
type UseMessageTooltipResult = {
onMouseEnter: (event: SyntheticEvent<HTMLElement>) => void,
onMouseLeave: ?() => mixed,
};
function useMessageTooltip({
availablePositions,
item,
threadInfo,
}: UseMessageTooltipArgs): UseMessageTooltipResult {
const [onMouseLeave, setOnMouseLeave] = React.useState<?() => mixed>(null);
const { renderTooltip } = useTooltipContext();
const tooltipActions = useMessageTooltipActions(item, threadInfo);
const containsInlineSidebar = !!item.threadCreatedFromMessage;
const timeZone = useSelector(state => state.timeZone);
const messageTimestamp = React.useMemo(() => {
const time = item.messageInfo.time;
return longAbsoluteDate(time, timeZone);
}, [item.messageInfo.time, timeZone]);
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 onMouseEnter = React.useCallback(
(event: SyntheticEvent<HTMLElement>) => {
if (!renderTooltip) {
return;
}
const rect = event.currentTarget.getBoundingClientRect();
const { top, bottom, left, right, height, width } = rect;
const messagePosition = { top, bottom, left, right, height, width };
const tooltipPosition = findTooltipPosition({
sourcePositionInfo: messagePosition,
tooltipSize,
availablePositions,
defaultPosition: availablePositions[0],
preventDisplayingBelowSource: containsInlineSidebar,
});
if (!tooltipPosition) {
return;
}
const tooltipPositionStyle = getMessageActionTooltipStyle({
tooltipPosition,
sourcePositionInfo: messagePosition,
tooltipSize: tooltipSize,
});
const { alignment } = tooltipPositionStyle;
const tooltip = (
<MessageTooltip
actions={tooltipActions}
messageTimestamp={messageTimestamp}
alignment={alignment}
/>
);
const renderTooltipResult = renderTooltip({
newNode: tooltip,
tooltipPositionStyle,
});
if (renderTooltipResult) {
const { onMouseLeaveCallback: callback } = renderTooltipResult;
setOnMouseLeave((() => callback: () => () => mixed));
}
},
[
availablePositions,
containsInlineSidebar,
messageTimestamp,
renderTooltip,
tooltipActions,
tooltipSize,
],
);
return {
onMouseEnter,
onMouseLeave,
};
}
export {
findTooltipPosition,
calculateTooltipSize,
getMessageActionTooltipStyle,
useMessageTooltipSidebarAction,
useMessageTooltipReplyAction,
useMessageTooltipActions,
useMessageTooltip,
};
diff --git a/web/utils/tooltip-utils.test.js b/web/utils/tooltip-utils.test.js
index 153b14ae8..21a1273c6 100644
--- a/web/utils/tooltip-utils.test.js
+++ b/web/utils/tooltip-utils.test.js
@@ -1,204 +1,204 @@
// @flow
import type { PositionInfo } from '../chat/position-types';
-import { findTooltipPosition, tooltipPositions } from '../chat/tooltip-utils';
+import { findTooltipPosition, tooltipPositions } from './tooltip-utils';
const QHDWindow = {
width: 2560,
height: 1440,
};
const tooltipSourcePositionCenter: PositionInfo = {
width: 200,
height: 300,
left: QHDWindow.width / 2 - 100,
top: QHDWindow.height / 2 - 150,
right: QHDWindow.width / 2 + 100,
bottom: QHDWindow.height / 2 + 150,
};
const tooltipSourcePositionTopRight: PositionInfo = {
width: 200,
height: 300,
left: QHDWindow.width - 200,
top: 65, // app top bar height
right: QHDWindow.width,
bottom: 300 + 65, // tooltip height + app top bar height
};
const tooltipSourcePositionBottomLeft: PositionInfo = {
width: 200,
height: 300,
left: 0,
top: QHDWindow.height - 300,
right: 200,
bottom: QHDWindow.height,
};
const tooltipSizeSmall = {
width: 100,
height: 200,
};
const tooltipSizeBig = {
width: 300,
height: 500,
};
const allTooltipPositions = [
tooltipPositions.LEFT,
tooltipPositions.LEFT_TOP,
tooltipPositions.LEFT_BOTTOM,
tooltipPositions.RIGHT,
tooltipPositions.RIGHT_TOP,
tooltipPositions.RIGHT_BOTTOM,
tooltipPositions.TOP,
tooltipPositions.BOTTOM,
];
const sidewaysTooltipPositions = [
tooltipPositions.LEFT,
tooltipPositions.LEFT_TOP,
tooltipPositions.LEFT_BOTTOM,
tooltipPositions.RIGHT,
tooltipPositions.RIGHT_TOP,
tooltipPositions.RIGHT_BOTTOM,
];
const topAndBottomTooltipPositions = [
tooltipPositions.TOP,
tooltipPositions.BOTTOM,
];
const onlyLeftTooltipPositions = [
tooltipPositions.LEFT,
tooltipPositions.LEFT_BOTTOM,
tooltipPositions.LEFT_TOP,
];
beforeAll(() => {
window.innerWidth = QHDWindow.width;
window.innerHeight = QHDWindow.height;
});
afterAll(() => {
window.innerWidth = 1024;
window.innerHeight = 768;
});
describe('findTooltipPosition', () => {
it(
'should return first position if there is enough space ' +
'in every direction',
() =>
expect(
findTooltipPosition({
sourcePositionInfo: tooltipSourcePositionCenter,
tooltipSize: tooltipSizeSmall,
availablePositions: allTooltipPositions,
defaultPosition: allTooltipPositions[0],
}),
).toMatch(allTooltipPositions[0]),
);
it(
'should return first non-left position ' +
'if there is no space on the left',
() =>
expect(
findTooltipPosition({
sourcePositionInfo: tooltipSourcePositionBottomLeft,
tooltipSize: tooltipSizeSmall,
availablePositions: sidewaysTooltipPositions,
defaultPosition: sidewaysTooltipPositions[0],
}),
).toMatch(tooltipPositions.RIGHT),
);
it('should return bottom position if there is no space on the top ', () =>
expect(
findTooltipPosition({
sourcePositionInfo: tooltipSourcePositionTopRight,
tooltipSize: tooltipSizeSmall,
availablePositions: topAndBottomTooltipPositions,
defaultPosition: topAndBottomTooltipPositions[0],
}),
).toMatch(tooltipPositions.BOTTOM));
it(
'should return top left position if the tooltip is higher than the ' +
'source object and there is no enough space on the top',
() =>
expect(
findTooltipPosition({
sourcePositionInfo: tooltipSourcePositionTopRight,
tooltipSize: tooltipSizeBig,
availablePositions: onlyLeftTooltipPositions,
defaultPosition: onlyLeftTooltipPositions[0],
}),
).toMatch(tooltipPositions.LEFT_TOP),
);
it(
'should return bottom position on left ' +
'to prevent covering element below source',
() =>
expect(
findTooltipPosition({
sourcePositionInfo: tooltipSourcePositionCenter,
tooltipSize: tooltipSizeBig,
availablePositions: onlyLeftTooltipPositions,
defaultPosition: onlyLeftTooltipPositions[0],
preventDisplayingBelowSource: true,
}),
).toMatch(tooltipPositions.LEFT_BOTTOM),
);
it(
'should return first position ' +
'that does not cover element below source ',
() =>
expect(
findTooltipPosition({
sourcePositionInfo: tooltipSourcePositionCenter,
tooltipSize: tooltipSizeBig,
availablePositions: [
tooltipPositions.BOTTOM,
tooltipPositions.RIGHT,
tooltipPositions.RIGHT_TOP,
tooltipPositions.LEFT,
tooltipPositions.LEFT_TOP,
tooltipPositions.TOP,
tooltipPositions.LEFT_BOTTOM,
],
defaultPosition: tooltipPositions.BOTTOM,
preventDisplayingBelowSource: true,
}),
).toMatch(tooltipPositions.TOP),
);
it(
'should return default position ' +
'if an empty array of available is provided',
() =>
expect(
findTooltipPosition({
sourcePositionInfo: tooltipSourcePositionCenter,
tooltipSize: tooltipSizeSmall,
availablePositions: [],
defaultPosition: tooltipPositions.LEFT_BOTTOM,
}),
).toMatch(tooltipPositions.LEFT_BOTTOM),
);
it('should return default position if an no position is available', () =>
expect(
findTooltipPosition({
sourcePositionInfo: tooltipSourcePositionTopRight,
tooltipSize: tooltipSizeBig,
availablePositions: allTooltipPositions,
defaultPosition: tooltipPositions.BOTTOM,
preventDisplayingBelowSource: true,
}),
).toMatch(tooltipPositions.BOTTOM));
});

File Metadata

Mime Type
text/x-diff
Expires
Mon, Dec 23, 12:33 AM (3 h, 15 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2690074
Default Alt Text
(41 KB)

Event Timeline