Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3509020
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
41 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment