diff --git a/web/chat/message-timestamp-tooltip.react.js b/web/chat/message-timestamp-tooltip.react.js
index 4bdbfddf1..815c6f5e2 100644
--- a/web/chat/message-timestamp-tooltip.react.js
+++ b/web/chat/message-timestamp-tooltip.react.js
@@ -1,146 +1,146 @@
// @flow
import classNames from 'classnames';
import * as React from 'react';
import { isComposableMessageType } from 'lib/types/message-types';
import { longAbsoluteDate } from 'lib/utils/date-utils';
import css from './chat-message-list.css';
import type { OnMessagePositionWithContainerInfo } from './position-types';
import {
type TooltipPosition,
tooltipPositions,
findTooltipPosition,
sizeOfTooltipArrow,
} from './tooltip-utils';
const availablePositionsForComposedViewerMessage = [
tooltipPositions.BOTTOM_RIGHT,
];
const availablePositionsForNonComposedOrNonViewerMessage = [
tooltipPositions.LEFT,
];
type Props = {|
+messagePositionInfo: OnMessagePositionWithContainerInfo,
+timeZone: ?string,
|};
function MessageTimestampTooltip(props: Props) {
const { messagePositionInfo, timeZone } = props;
const { time, creator, type } = messagePositionInfo.item.messageInfo;
const text = React.useMemo(() => longAbsoluteDate(time, timeZone), [
time,
timeZone,
]);
const availableTooltipPositions = React.useMemo(() => {
const { isViewer } = creator;
const isComposed = isComposableMessageType(type);
return isComposed && isViewer
? availablePositionsForComposedViewerMessage
: availablePositionsForNonComposedOrNonViewerMessage;
}, [creator, type]);
const { messagePosition, containerPosition } = messagePositionInfo;
const pointingToInfo = React.useMemo(() => {
return {
containerPosition,
itemPosition: messagePosition,
};
}, [messagePosition, containerPosition]);
const tooltipPosition = React.useMemo(
() =>
findTooltipPosition({
pointingToInfo,
- text,
+ tooltipTexts: [text],
availablePositions: availableTooltipPositions,
layoutPosition: 'absolute',
}),
[availableTooltipPositions, pointingToInfo, text],
);
const { style, className } = React.useMemo(
() => getTimestampTooltipStyle(messagePositionInfo, tooltipPosition),
[messagePositionInfo, tooltipPosition],
);
return (
{text}
);
}
function getTimestampTooltipStyle(
messagePositionInfo: OnMessagePositionWithContainerInfo,
tooltipPosition: TooltipPosition,
) {
const { messagePosition, containerPosition } = messagePositionInfo;
const { height: containerHeight, width: containerWidth } = containerPosition;
let style, className;
if (tooltipPosition === tooltipPositions.LEFT) {
const centerOfMessage = messagePosition.top + messagePosition.height / 2;
const tooltipPointing = Math.max(
Math.min(centerOfMessage, containerHeight),
0,
);
style = {
right: containerWidth - messagePosition.left + sizeOfTooltipArrow,
top: tooltipPointing,
};
className = css.messageLeftTooltip;
} else if (tooltipPosition === tooltipPositions.RIGHT) {
const centerOfMessage = messagePosition.top + messagePosition.height / 2;
const tooltipPointing = Math.max(
Math.min(centerOfMessage, containerHeight),
0,
);
style = {
left: messagePosition.right + sizeOfTooltipArrow,
top: tooltipPointing,
};
className = css.messageRightTooltip;
} else if (tooltipPosition === tooltipPositions.TOP_LEFT) {
const tooltipPointing = Math.min(
containerHeight - messagePosition.top,
containerHeight,
);
style = {
left: messagePosition.left,
bottom: tooltipPointing + sizeOfTooltipArrow,
};
className = css.messageTopLeftTooltip;
} else if (tooltipPosition === tooltipPositions.TOP_RIGHT) {
const tooltipPointing = Math.min(
containerHeight - messagePosition.top,
containerHeight,
);
style = {
right: containerWidth - messagePosition.right,
bottom: tooltipPointing + sizeOfTooltipArrow,
};
className = css.messageTopRightTooltip;
} else if (tooltipPosition === tooltipPositions.BOTTOM_LEFT) {
const tooltipPointing = Math.min(messagePosition.bottom, containerHeight);
style = {
left: messagePosition.left,
top: tooltipPointing + sizeOfTooltipArrow,
};
className = css.messageBottomLeftTooltip;
} else if (tooltipPosition === tooltipPositions.BOTTOM_RIGHT) {
const tooltipPointing = Math.min(messagePosition.bottom, containerHeight);
style = {
right: containerWidth - messagePosition.right,
top: tooltipPointing + sizeOfTooltipArrow,
};
className = css.messageBottomRightTooltip;
}
return { style, className };
}
export default MessageTimestampTooltip;
diff --git a/web/chat/sidebar-tooltip.react.js b/web/chat/sidebar-tooltip.react.js
index a3af480b1..d83abf310 100644
--- a/web/chat/sidebar-tooltip.react.js
+++ b/web/chat/sidebar-tooltip.react.js
@@ -1,287 +1,287 @@
// @flow
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classNames from 'classnames';
import invariant from 'invariant';
import * as React from 'react';
import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors';
import {
type ComposableMessageInfo,
type RobotextMessageInfo,
} from 'lib/types/message-types';
import type { ThreadInfo } from 'lib/types/thread-types';
import {
useOnClickThread,
useOnClickPendingSidebar,
} from '../selectors/nav-selectors';
import css from './chat-message-list.css';
import type {
ItemAndContainerPositionInfo,
PositionInfo,
} from './position-types';
import {
findTooltipPosition,
tooltipPositions,
type TooltipPosition,
} from './tooltip-utils';
const ellipsisIconExcessVerticalWhitespace = 10;
type Props = {|
+onLeave: () => void,
+onButtonClick: (event: SyntheticEvent) => void,
+buttonText: string,
+containerPosition: PositionInfo,
+availableTooltipPositions: $ReadOnlyArray,
|};
function SidebarTooltipButton(props: Props) {
const {
onLeave,
onButtonClick,
buttonText,
containerPosition,
availableTooltipPositions,
} = props;
const [tooltipVisible, setTooltipVisible] = React.useState(false);
const [pointingTo, setPointingTo] = React.useState();
const toggleMenu = React.useCallback(
(event: SyntheticEvent) => {
setTooltipVisible(!tooltipVisible);
if (tooltipVisible) {
return;
}
const rect = event.currentTarget.getBoundingClientRect();
const { top, bottom, left, right, width, height } = rect;
const dotsPosition: ItemAndContainerPositionInfo = {
containerPosition,
itemPosition: {
top:
top - containerPosition.top + ellipsisIconExcessVerticalWhitespace,
bottom:
bottom -
containerPosition.top -
ellipsisIconExcessVerticalWhitespace,
left: left - containerPosition.left,
right: right - containerPosition.left,
width,
height: height - ellipsisIconExcessVerticalWhitespace * 2,
},
};
setPointingTo(dotsPosition);
},
[containerPosition, tooltipVisible],
);
const toggleSidebar = React.useCallback(
(event: SyntheticEvent) => {
onButtonClick(event);
onLeave();
},
[onLeave, onButtonClick],
);
const hideMenu = React.useCallback(() => {
setTooltipVisible(false);
}, []);
const tooltipPosition = React.useMemo(() => {
if (!pointingTo) {
return null;
}
return findTooltipPosition({
pointingToInfo: pointingTo,
- text: buttonText,
+ tooltipTexts: [buttonText],
availablePositions: availableTooltipPositions,
layoutPosition: 'relative',
});
}, [availableTooltipPositions, pointingTo, buttonText]);
const sidebarTooltip = React.useMemo(() => {
if (!tooltipVisible || !tooltipPosition) {
return null;
}
return (
);
}, [buttonText, toggleSidebar, tooltipPosition, tooltipVisible]);
return (
);
}
type TooltipButtonProps = {|
+onButtonClick: (event: SyntheticEvent) => void,
+buttonText: string,
+tooltipPosition: TooltipPosition,
|};
function TooltipButton(props: TooltipButtonProps) {
const { onButtonClick, buttonText, tooltipPosition } = props;
const sidebarStyle = React.useMemo(
() => getSidebarTooltipStyle(tooltipPosition),
[tooltipPosition],
);
const sidebarMenuClassName = React.useMemo(
() => classNames(css.menuSidebarContent, sidebarStyle),
[sidebarStyle],
);
return (
);
}
const openSidebarText = 'Go to sidebar';
type OpenSidebarProps = {|
+threadCreatedFromMessage: ThreadInfo,
+onLeave: () => void,
+containerPosition: PositionInfo,
+availableTooltipPositions: $ReadOnlyArray,
|};
function OpenSidebar(props: OpenSidebarProps) {
const {
threadCreatedFromMessage,
onLeave,
containerPosition,
availableTooltipPositions,
} = props;
const onButtonClick = useOnClickThread(threadCreatedFromMessage.id);
return (
);
}
const createSidebarText = 'Create sidebar';
type CreateSidebarProps = {|
+threadInfo: ThreadInfo,
+messageInfo: ComposableMessageInfo | RobotextMessageInfo,
+onLeave: () => void,
+containerPosition: PositionInfo,
+availableTooltipPositions: $ReadOnlyArray,
|};
function CreateSidebar(props: CreateSidebarProps) {
const {
threadInfo,
messageInfo,
containerPosition,
availableTooltipPositions,
} = props;
const onButtonClick = useOnClickPendingSidebar(messageInfo, threadInfo);
return (
);
}
type MessageActionTooltipProps = {|
+threadInfo: ThreadInfo,
+item: ChatMessageInfoItem,
+onLeave: () => void,
+containerPosition: PositionInfo,
+availableTooltipPositions: $ReadOnlyArray,
|};
function MessageActionTooltip(props: MessageActionTooltipProps) {
const {
threadInfo,
item,
onLeave,
containerPosition,
availableTooltipPositions,
} = props;
if (item.threadCreatedFromMessage) {
return (
);
} else {
return (
);
}
}
function getSidebarTooltipStyle(tooltipPosition: TooltipPosition): string {
let className;
if (tooltipPosition === tooltipPositions.TOP_RIGHT) {
className = classNames(
css.menuSidebarTopRightTooltip,
css.messageTopRightTooltip,
css.menuSidebarExtraAreaTop,
css.menuSidebarExtraAreaTopRight,
);
} else if (tooltipPosition === tooltipPositions.TOP_LEFT) {
className = classNames(
css.menuSidebarTopLeftTooltip,
css.messageTopLeftTooltip,
css.menuSidebarExtraAreaTop,
css.menuSidebarExtraAreaTopLeft,
);
} else if (tooltipPosition === tooltipPositions.RIGHT) {
className = classNames(
css.menuSidebarRightTooltip,
css.messageRightTooltip,
css.menuSidebarExtraArea,
css.menuSidebarExtraAreaRight,
);
} else if (tooltipPosition === tooltipPositions.LEFT) {
className = classNames(
css.menuSidebarLeftTooltip,
css.messageLeftTooltip,
css.menuSidebarExtraArea,
css.menuSidebarExtraAreaLeft,
);
}
invariant(className, `${tooltipPosition} is not valid for sidebar tooltip`);
return className;
}
export default MessageActionTooltip;
diff --git a/web/chat/tooltip-utils.js b/web/chat/tooltip-utils.js
index f1e2816e1..7dfcebd5b 100644
--- a/web/chat/tooltip-utils.js
+++ b/web/chat/tooltip-utils.js
@@ -1,132 +1,136 @@
// @flow
-import { calculateTextWidth } from '../utils/text-utils';
+import invariant from 'invariant';
+
+import { calculateMaxTextWidth } from '../utils/text-utils';
import type { ItemAndContainerPositionInfo } from './position-types';
export const tooltipPositions = Object.freeze({
LEFT: 'left',
RIGHT: 'right',
BOTTOM_LEFT: 'bottom-left',
BOTTOM_RIGHT: 'bottom-right',
TOP_LEFT: 'top-left',
TOP_RIGHT: 'top-right',
});
export type TooltipPosition = $Values;
const sizeOfTooltipArrow = 10; // 7px arrow + 3px extra
-const tooltipHeight = 27; // 17px line-height + 10px padding
-const heightWithArrow = tooltipHeight + sizeOfTooltipArrow;
+const tooltipMenuItemHeight = 27; // 17px line-height + 10px padding
const tooltipInnerPadding = 10;
const font =
'14px -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", ' +
'"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", ' +
'"Helvetica Neue", ui-sans-serif';
type FindTooltipPositionArgs = {|
+pointingToInfo: ItemAndContainerPositionInfo,
- +text: string,
+ +tooltipTexts: $ReadOnlyArray,
+availablePositions: $ReadOnlyArray,
+layoutPosition: 'relative' | 'absolute',
|};
function findTooltipPosition({
pointingToInfo,
- text,
+ tooltipTexts,
availablePositions,
layoutPosition,
}: FindTooltipPositionArgs) {
const { itemPosition: pointingTo, containerPosition } = pointingToInfo;
const {
height: containerHeight,
top: containerTop,
width: containerWidth,
left: containerLeft,
} = containerPosition;
- const textWidth = calculateTextWidth(text, font);
+ const textWidth = calculateMaxTextWidth(tooltipTexts, font);
const width = textWidth + tooltipInnerPadding + sizeOfTooltipArrow;
+ const numberOfTooltipItems = tooltipTexts.length;
+ const tooltipHeight = numberOfTooltipItems * tooltipMenuItemHeight;
+ const heightWithArrow = tooltipHeight + sizeOfTooltipArrow;
const absolutePositionedTooltip = layoutPosition === 'absolute';
let canBeDisplayedInLeftPosition,
canBeDisplayedInRightPosition,
canBeDisplayedInTopPosition,
canBeDisplayedInBottomPosition;
if (absolutePositionedTooltip) {
const pointingCenter = pointingTo.top + pointingTo.height / 2;
const currentTop = Math.max(pointingTo.top, 0);
const currentBottom = Math.min(pointingTo.bottom, containerHeight);
const currentPointing = Math.max(
Math.min(pointingCenter, containerHeight),
0,
);
const canBeDisplayedSideways =
currentPointing - tooltipHeight / 2 + containerTop >= 0 &&
currentPointing + tooltipHeight / 2 + containerTop <= window.innerHeight;
canBeDisplayedInLeftPosition =
pointingTo.left - width + containerLeft >= 0 && canBeDisplayedSideways;
canBeDisplayedInRightPosition =
pointingTo.right + width + containerLeft <= window.innerWidth &&
canBeDisplayedSideways;
canBeDisplayedInTopPosition =
currentTop - heightWithArrow + containerTop >= 0;
canBeDisplayedInBottomPosition =
currentBottom + heightWithArrow + containerTop <= window.innerHeight;
} else {
const canBeDisplayedSideways =
pointingTo.top - (tooltipHeight - pointingTo.height) / 2 >= 0 &&
pointingTo.bottom + (tooltipHeight - pointingTo.height) / 2 <=
containerHeight;
canBeDisplayedInLeftPosition =
pointingTo.left - width >= 0 && canBeDisplayedSideways;
canBeDisplayedInRightPosition =
pointingTo.right + width <= containerWidth && canBeDisplayedSideways;
canBeDisplayedInTopPosition = pointingTo.top - heightWithArrow >= 0;
canBeDisplayedInBottomPosition =
pointingTo.bottom + heightWithArrow <= containerHeight;
}
for (const tooltipPosition of availablePositions) {
+ invariant(
+ numberOfTooltipItems === 1 ||
+ (tooltipPosition !== tooltipPositions.RIGHT &&
+ tooltipPosition !== tooltipPositions.LEFT),
+ `${tooltipPosition} position can be used only for single element tooltip`,
+ );
if (
tooltipPosition === tooltipPositions.RIGHT &&
canBeDisplayedInRightPosition
) {
return tooltipPosition;
} else if (
tooltipPosition === tooltipPositions.BOTTOM_RIGHT &&
canBeDisplayedInBottomPosition
) {
return tooltipPosition;
} else if (
tooltipPosition === tooltipPositions.LEFT &&
canBeDisplayedInLeftPosition
) {
return tooltipPosition;
} else if (
tooltipPosition === tooltipPositions.BOTTOM_LEFT &&
canBeDisplayedInBottomPosition
) {
return tooltipPosition;
} else if (
tooltipPosition === tooltipPositions.TOP_LEFT &&
canBeDisplayedInTopPosition
) {
return tooltipPosition;
} else if (
tooltipPosition === tooltipPositions.TOP_RIGHT &&
canBeDisplayedInTopPosition
) {
return tooltipPosition;
}
}
return availablePositions[availablePositions.length - 1];
}
-export {
- findTooltipPosition,
- sizeOfTooltipArrow,
- tooltipHeight,
- heightWithArrow,
- tooltipInnerPadding,
-};
+export { findTooltipPosition, sizeOfTooltipArrow, tooltipInnerPadding };
diff --git a/web/utils/text-utils.js b/web/utils/text-utils.js
index 3ec872f5f..e344db152 100644
--- a/web/utils/text-utils.js
+++ b/web/utils/text-utils.js
@@ -1,15 +1,19 @@
// @flow
let canvas;
-function calculateTextWidth(text: string, font: string): number {
+function calculateMaxTextWidth(
+ texts: $ReadOnlyArray,
+ font: string,
+): number {
if (!canvas) {
canvas = document.createElement('canvas');
}
const context = canvas.getContext('2d');
context.font = font;
- const { width } = context.measureText(text);
- return width;
+
+ const widths = texts.map((text) => context.measureText(text).width);
+ return Math.max(...widths);
}
-export { calculateTextWidth };
+export { calculateMaxTextWidth };