diff --git a/web/navigation-sidebar/account-settings-button.react.js b/web/navigation-sidebar/account-settings-button.react.js
index 50a401cd4..0d79ad41f 100644
--- a/web/navigation-sidebar/account-settings-button.react.js
+++ b/web/navigation-sidebar/account-settings-button.react.js
@@ -1,43 +1,47 @@
// @flow
import * as React from 'react';
import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
import { useDispatch } from 'lib/utils/redux-utils.js';
import css from './account-settings-button.css';
+import { navigationSidebarLabelTooltipMargin } from './navigation-sidebar-constants.js';
import { updateNavInfoActionType } from '../redux/action-types.js';
import { useNavigationSidebarTooltip } from '../utils/tooltip-action-utils.js';
+import { tooltipPositions } from '../utils/tooltip-utils.js';
function AccountSettingsButton(): React.Node {
const dispatch = useDispatch();
const openAccountSettings = React.useCallback(
() =>
dispatch({
type: updateNavInfoActionType,
payload: {
tab: 'settings',
settingsSection: 'account',
},
}),
[dispatch],
);
const { onMouseEnter, onMouseLeave } = useNavigationSidebarTooltip({
tooltipLabel: 'Settings',
+ position: tooltipPositions.RIGHT,
+ tooltipMargin: navigationSidebarLabelTooltipMargin,
});
return (
);
}
export default AccountSettingsButton;
diff --git a/web/navigation-sidebar/community-creation-button.react.js b/web/navigation-sidebar/community-creation-button.react.js
index 8af5962a3..09be25d18 100644
--- a/web/navigation-sidebar/community-creation-button.react.js
+++ b/web/navigation-sidebar/community-creation-button.react.js
@@ -1,36 +1,40 @@
// @flow
import * as React from 'react';
import { useModalContext } from 'lib/components/modal-provider.react.js';
import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
import css from './community-creation-button.css';
+import { navigationSidebarLabelTooltipMargin } from './navigation-sidebar-constants.js';
import CommunityCreationModal from '../sidebar/community-creation/community-creation-modal.react.js';
import { useNavigationSidebarTooltip } from '../utils/tooltip-action-utils.js';
+import { tooltipPositions } from '../utils/tooltip-utils.js';
function CommunityCreationButton(): React.Node {
const { pushModal } = useModalContext();
const onPressCommunityCreationButton = React.useCallback(
() => pushModal(),
[pushModal],
);
const { onMouseEnter, onMouseLeave } = useNavigationSidebarTooltip({
tooltipLabel: 'Create community',
+ position: tooltipPositions.RIGHT,
+ tooltipMargin: navigationSidebarLabelTooltipMargin,
});
return (
);
}
export default CommunityCreationButton;
diff --git a/web/navigation-sidebar/community-list-item.react.js b/web/navigation-sidebar/community-list-item.react.js
index cafe6b995..8cdb38bd4 100644
--- a/web/navigation-sidebar/community-list-item.react.js
+++ b/web/navigation-sidebar/community-list-item.react.js
@@ -1,53 +1,57 @@
// @flow
import * as React from 'react';
import { unreadCountSelectorForCommunity } from 'lib/selectors/thread-selectors.js';
import type { ResolvedThreadInfo } from 'lib/types/thread-types.js';
import css from './community-list-item.css';
+import { navigationSidebarLabelTooltipMargin } from './navigation-sidebar-constants.js';
import ThreadAvatar from '../avatars/thread-avatar.react.js';
import UnreadBadge from '../components/unread-badge.react.js';
import { useSelector } from '../redux/redux-utils.js';
import { useNavigationSidebarTooltip } from '../utils/tooltip-action-utils.js';
+import { tooltipPositions } from '../utils/tooltip-utils.js';
type Props = {
+threadInfo: ResolvedThreadInfo,
};
function CommunityListItem(props: Props): React.Node {
const { threadInfo } = props;
const { id: threadID } = threadInfo;
const communityUnreadCountSelector =
unreadCountSelectorForCommunity(threadID);
const unreadCountValue = useSelector(communityUnreadCountSelector);
const unreadBadge = React.useMemo(() => {
if (unreadCountValue === 0) {
return null;
}
return (
);
}, [unreadCountValue]);
const { onMouseEnter, onMouseLeave } = useNavigationSidebarTooltip({
tooltipLabel: threadInfo.uiName,
+ position: tooltipPositions.RIGHT,
+ tooltipMargin: navigationSidebarLabelTooltipMargin,
});
return (
{unreadBadge}
);
}
export default CommunityListItem;
diff --git a/web/navigation-sidebar/navigation-sidebar-constants.js b/web/navigation-sidebar/navigation-sidebar-constants.js
index bf2270e53..3a5b89a14 100644
--- a/web/navigation-sidebar/navigation-sidebar-constants.js
+++ b/web/navigation-sidebar/navigation-sidebar-constants.js
@@ -1,13 +1,11 @@
// @flow
-export const navigationSidebarTooltipContainerStyle = {
- marginLeft: 24,
-};
+export const navigationSidebarLabelTooltipMargin = 24;
export const navigationSidebarTooltipStyle = {
paddingTop: 8,
paddingBottom: 8,
paddingLeft: 8,
paddingRight: 8,
height: 17,
};
diff --git a/web/navigation-sidebar/navigation-sidebar-home-button.react.js b/web/navigation-sidebar/navigation-sidebar-home-button.react.js
index 93dc3bb83..c7220dd8c 100644
--- a/web/navigation-sidebar/navigation-sidebar-home-button.react.js
+++ b/web/navigation-sidebar/navigation-sidebar-home-button.react.js
@@ -1,45 +1,49 @@
// @flow
import * as React from 'react';
import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
import { unreadCount } from 'lib/selectors/thread-selectors.js';
+import { navigationSidebarLabelTooltipMargin } from './navigation-sidebar-constants.js';
import css from './navigation-sidebar-home-button.css';
import UnreadBadge from '../components/unread-badge.react.js';
import { useSelector } from '../redux/redux-utils.js';
import { useNavigationSidebarTooltip } from '../utils/tooltip-action-utils.js';
+import { tooltipPositions } from '../utils/tooltip-utils.js';
function NavigationSidebarHomeButton(): React.Node {
const unreadCountValue = useSelector(unreadCount);
const { onMouseEnter, onMouseLeave } = useNavigationSidebarTooltip({
tooltipLabel: 'Home',
+ position: tooltipPositions.RIGHT,
+ tooltipMargin: navigationSidebarLabelTooltipMargin,
});
const unreadBadge = React.useMemo(() => {
if (unreadCountValue === 0) {
return null;
}
return (
);
}, [unreadCountValue]);
return (
);
}
export default NavigationSidebarHomeButton;
diff --git a/web/navigation-sidebar/navigation-sidebar-toolitp.react.js b/web/navigation-sidebar/navigation-sidebar-toolitp.react.js
index 32a1e46aa..1141dfb41 100644
--- a/web/navigation-sidebar/navigation-sidebar-toolitp.react.js
+++ b/web/navigation-sidebar/navigation-sidebar-toolitp.react.js
@@ -1,31 +1,49 @@
// @flow
+import classNames from 'classnames';
import * as React from 'react';
-import {
- navigationSidebarTooltipContainerStyle,
- navigationSidebarTooltipStyle,
-} from './navigation-sidebar-constants.js';
+import { navigationSidebarTooltipStyle } from './navigation-sidebar-constants.js';
import css from './navigation-sidebar-tooltip.css';
+import {
+ tooltipPositions,
+ type TooltipPosition,
+} from '../utils/tooltip-utils.js';
type Props = {
+tooltipLabel: string,
+ +position: TooltipPosition,
+ +tooltipMargin: number,
};
function NavigationSidebarTooltip(props: Props): React.Node {
- const { tooltipLabel } = props;
+ const { tooltipLabel, position, tooltipMargin } = props;
+
+ const tooltipMarginStyle = React.useMemo(
+ () => ({
+ marginLeft: position === tooltipPositions.RIGHT ? tooltipMargin : 0,
+ marginRight: position === tooltipPositions.LEFT ? tooltipMargin : 0,
+ marginTop: position === tooltipPositions.BOTTOM ? tooltipMargin : 0,
+ marginBottom: position === tooltipPositions.TOP ? tooltipMargin : 0,
+ }),
+ [position, tooltipMargin],
+ );
+
+ const arrowClassName = classNames(css.arrow, {
+ [css.arrowLeft]: position === tooltipPositions.RIGHT,
+ [css.arrowRight]: position === tooltipPositions.LEFT,
+ [css.arrowTop]: position === tooltipPositions.BOTTOM,
+ [css.arrowBottom]: position === tooltipPositions.TOP,
+ });
return (
-
-
+
);
}
export default NavigationSidebarTooltip;
diff --git a/web/navigation-sidebar/navigation-sidebar-tooltip.css b/web/navigation-sidebar/navigation-sidebar-tooltip.css
index d9a66a805..54a38fbad 100644
--- a/web/navigation-sidebar/navigation-sidebar-tooltip.css
+++ b/web/navigation-sidebar/navigation-sidebar-tooltip.css
@@ -1,23 +1,53 @@
.container {
position: relative;
}
-.arrowLeft {
+.arrow {
position: absolute;
- top: 50%;
- transform: translateY(-50%);
- right: 100%;
width: 0;
height: 0;
+}
+
+.arrowLeft,
+.arrowRight {
+ top: 50%;
+ transform: translateY(-50%);
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
+}
+
+.arrowTop,
+.arrowBottom {
+ left: 50%;
+ transform: translateX(-50%);
+ border-left: 10px solid transparent;
+ border-right: 10px solid transparent;
+}
+
+.arrowLeft {
+ right: 100%;
border-right: 10px solid var(--tooltip-background-primary-default);
}
+.arrowRight {
+ left: 100%;
+ border-left: 10px solid var(--tooltip-background-primary-default);
+}
+
+.arrowTop {
+ bottom: 100%;
+ border-bottom: 10px solid var(--tooltip-background-primary-default);
+}
+
+.arrowBottom {
+ top: 100%;
+ border-top: 10px solid var(--tooltip-background-primary-default);
+}
+
.tooltipLabel {
border-radius: 4px;
background-color: var(--tooltip-background-primary-default);
color: var(--tooltip-label-primary-default);
font-size: var(--s-font-14);
white-space: nowrap;
}
diff --git a/web/utils/tooltip-action-utils.js b/web/utils/tooltip-action-utils.js
index 0b706fceb..a63ea6414 100644
--- a/web/utils/tooltip-action-utils.js
+++ b/web/utils/tooltip-action-utils.js
@@ -1,525 +1,539 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { useModalContext } from 'lib/components/modal-provider.react.js';
import { useResettingState } from 'lib/hooks/use-resetting-state.js';
import type {
ReactionInfo,
ChatMessageInfoItem,
} from 'lib/selectors/chat-selectors.js';
import { useCanEditMessage } from 'lib/shared/edit-messages-utils.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 { messageTypes } from 'lib/types/message-types-enum.js';
import { threadPermissions } from 'lib/types/thread-permission-types.js';
import type { ThreadInfo } from 'lib/types/thread-types.js';
import { longAbsoluteDate } from 'lib/utils/date-utils.js';
import { canToggleMessagePin } from 'lib/utils/toggle-pin-utils.js';
import {
type MessageTooltipAction,
getTooltipPositionStyle,
calculateMessageTooltipSize,
calculateReactionTooltipSize,
calculateNavigationSidebarTooltipSize,
type TooltipPosition,
type TooltipPositionStyle,
type TooltipSize,
} from './tooltip-utils.js';
-import { tooltipPositions } from './tooltip-utils.js';
import { getComposedMessageID } from '../chat/chat-constants.js';
import { useEditModalContext } from '../chat/edit-message-provider.js';
import MessageTooltip from '../chat/message-tooltip.react.js';
import type { PositionInfo } from '../chat/position-types.js';
import ReactionTooltip from '../chat/reaction-tooltip.react.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 NavigationSidebarTooltip from '../navigation-sidebar/navigation-sidebar-toolitp.react.js';
import {
useOnClickPendingSidebar,
useOnClickThread,
} from '../selectors/thread-selectors.js';
type UseTooltipArgs = {
+createTooltip: (tooltipPositionStyle: TooltipPositionStyle) => React.Node,
+tooltipSize: TooltipSize,
+availablePositions: $ReadOnlyArray
,
};
type UseTooltipResult = {
+onMouseEnter: (event: SyntheticEvent) => mixed,
+onMouseLeave: ?() => mixed,
};
function useTooltip({
createTooltip,
tooltipSize,
availablePositions,
}: UseTooltipArgs): UseTooltipResult {
const [onMouseLeave, setOnMouseLeave] = React.useState() => mixed>(null);
const [tooltipSourcePosition, setTooltipSourcePosition] =
React.useState();
const { renderTooltip } = useTooltipContext();
const updateTooltip = React.useRef(React.Node) => mixed>();
const onMouseEnter = React.useCallback(
(event: SyntheticEvent) => {
if (!renderTooltip) {
return;
}
const rect = event.currentTarget.getBoundingClientRect();
const { top, bottom, left, right, height, width } = rect;
const sourcePosition = { top, bottom, left, right, height, width };
setTooltipSourcePosition(sourcePosition);
const tooltipPositionStyle = getTooltipPositionStyle({
tooltipSourcePosition: sourcePosition,
tooltipSize,
availablePositions,
});
if (!tooltipPositionStyle) {
return;
}
const tooltip = createTooltip(tooltipPositionStyle);
const renderTooltipResult = renderTooltip({
newNode: tooltip,
tooltipPositionStyle,
});
if (renderTooltipResult) {
const { onMouseLeaveCallback: callback } = renderTooltipResult;
setOnMouseLeave((() => callback: () => () => mixed));
updateTooltip.current = renderTooltipResult.updateTooltip;
}
},
[availablePositions, createTooltip, renderTooltip, tooltipSize],
);
React.useEffect(() => {
if (!updateTooltip.current) {
return;
}
const tooltipPositionStyle = getTooltipPositionStyle({
tooltipSourcePosition,
tooltipSize,
availablePositions,
});
if (!tooltipPositionStyle) {
return;
}
const tooltip = createTooltip(tooltipPositionStyle);
updateTooltip.current?.(tooltip);
}, [availablePositions, createTooltip, tooltipSize, tooltipSourcePosition]);
return {
onMouseEnter,
onMouseLeave,
};
}
function useMessageTooltipSidebarAction(
item: ChatMessageInfoItem,
threadInfo: ThreadInfo,
): ?MessageTooltipAction {
const { threadCreatedFromMessage, messageInfo } = item;
const { popModal } = useModalContext();
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) => {
popModal();
if (threadCreatedFromMessage) {
openThread(event);
} else {
openPendingSidebar(event);
}
};
return {
actionButtonContent: buttonContent,
onClick,
label: sidebarExists ? 'Go to thread' : 'Create thread',
};
}, [
popModal,
openPendingSidebar,
openThread,
sidebarExists,
sidebarExistsOrCanBeCreated,
threadCreatedFromMessage,
]);
}
function useMessageTooltipReplyAction(
item: ChatMessageInfoItem,
threadInfo: ThreadInfo,
): ?MessageTooltipAction {
const { messageInfo } = item;
const { popModal } = useModalContext();
const inputState = React.useContext(InputStateContext);
invariant(inputState, 'inputState is required');
const { addReply } = inputState;
return React.useMemo(() => {
if (
item.messageInfo.type !== messageTypes.TEXT ||
!threadHasPermission(threadInfo, threadPermissions.VOICED)
) {
return null;
}
const buttonContent = ;
const onClick = () => {
popModal();
if (!messageInfo.text) {
return;
}
addReply(createMessageReply(messageInfo.text));
};
return {
actionButtonContent: buttonContent,
onClick,
label: 'Reply',
};
}, [popModal, addReply, item.messageInfo.type, messageInfo, threadInfo]);
}
const copiedMessageDurationMs = 2000;
function useMessageCopyAction(
item: ChatMessageInfoItem,
): ?MessageTooltipAction {
const { messageInfo } = item;
const [successful, setSuccessful] = useResettingState(
false,
copiedMessageDurationMs,
);
return React.useMemo(() => {
if (messageInfo.type !== messageTypes.TEXT) {
return null;
}
const buttonContent = ;
const onClick = async () => {
try {
await navigator.clipboard.writeText(messageInfo.text);
setSuccessful(true);
} catch (e) {
setSuccessful(false);
}
};
return {
actionButtonContent: buttonContent,
onClick,
label: successful ? 'Copied!' : 'Copy',
};
}, [messageInfo.text, messageInfo.type, setSuccessful, 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 = canToggleMessagePin(messageInfo, threadInfo);
const inputState = React.useContext(InputStateContext);
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, inputState, isPinned, pushModal, item, threadInfo]);
}
function useMessageEditAction(
item: ChatMessageInfoItem,
threadInfo: ThreadInfo,
): ?MessageTooltipAction {
const { messageInfo } = item;
const canEditMessage = useCanEditMessage(threadInfo, messageInfo);
const { renderEditModal, scrollToMessage } = useEditModalContext();
const { clearTooltip } = useTooltipContext();
return React.useMemo(() => {
if (!canEditMessage) {
return null;
}
const buttonContent = ;
const onClickEdit = () => {
const callback = (maxHeight: number) =>
renderEditModal({
messageInfo: item,
threadInfo,
isError: false,
editedMessageDraft: messageInfo.text,
maxHeight: maxHeight,
});
clearTooltip();
scrollToMessage(getComposedMessageID(messageInfo), callback);
};
return {
actionButtonContent: buttonContent,
onClick: onClickEdit,
label: 'Edit',
};
}, [
canEditMessage,
clearTooltip,
item,
messageInfo,
renderEditModal,
scrollToMessage,
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);
const editAction = useMessageEditAction(item, threadInfo);
return React.useMemo(
() =>
[
replyAction,
sidebarAction,
copyAction,
reactAction,
togglePinAction,
editAction,
].filter(Boolean),
[
replyAction,
sidebarAction,
copyAction,
reactAction,
togglePinAction,
editAction,
],
);
}
const undefinedTooltipSize = {
width: 0,
height: 0,
};
type UseMessageTooltipArgs = {
+availablePositions: $ReadOnlyArray,
+item: ChatMessageInfoItem,
+threadInfo: ThreadInfo,
};
function useMessageTooltip({
availablePositions,
item,
threadInfo,
}: UseMessageTooltipArgs): UseTooltipResult {
const tooltipActions = useMessageTooltipActions(item, threadInfo);
const messageTimestamp = React.useMemo(() => {
const time = item.messageInfo.time;
return longAbsoluteDate(time);
}, [item.messageInfo.time]);
const tooltipSize = React.useMemo(() => {
if (typeof document === 'undefined') {
return undefinedTooltipSize;
}
const tooltipLabels = tooltipActions.map(action => action.label);
return calculateMessageTooltipSize({
tooltipLabels,
timestamp: messageTimestamp,
});
}, [messageTimestamp, tooltipActions]);
const createMessageTooltip = React.useCallback(
(tooltipPositionStyle: TooltipPositionStyle) => (
),
[item, messageTimestamp, threadInfo, tooltipActions, tooltipSize],
);
const { onMouseEnter, onMouseLeave } = useTooltip({
createTooltip: createMessageTooltip,
tooltipSize,
availablePositions,
});
return {
onMouseEnter,
onMouseLeave,
};
}
type UseReactionTooltipArgs = {
+reaction: string,
+reactions: ReactionInfo,
+availablePositions: $ReadOnlyArray,
};
function useReactionTooltip({
reaction,
reactions,
availablePositions,
}: UseReactionTooltipArgs): UseTooltipResult {
const { users } = reactions[reaction];
const tooltipSize = React.useMemo(() => {
if (typeof document === 'undefined') {
return undefinedTooltipSize;
}
const usernames = users.map(user => user.username).filter(Boolean);
return calculateReactionTooltipSize(usernames);
}, [users]);
const createReactionTooltip = React.useCallback(
() => ,
[reaction, reactions],
);
const { onMouseEnter, onMouseLeave } = useTooltip({
createTooltip: createReactionTooltip,
tooltipSize,
availablePositions,
});
return {
onMouseEnter,
onMouseLeave,
};
}
-const availableNavigationSidebarTooltipPositions = [tooltipPositions.RIGHT];
-
type UseNavigationSidebarTooltipArgs = {
+tooltipLabel: string,
+ +position: TooltipPosition,
+ // The margin size should be between the point of origin and
+ // the base of the tooltip. The arrow is a "decoration" and
+ // should not be considered when measuring the margin size.
+ +tooltipMargin: number,
};
function useNavigationSidebarTooltip({
tooltipLabel,
+ position,
+ tooltipMargin,
}: UseNavigationSidebarTooltipArgs): UseTooltipResult {
const tooltipSize = React.useMemo(() => {
if (typeof document === 'undefined') {
return undefinedTooltipSize;
}
- return calculateNavigationSidebarTooltipSize(tooltipLabel);
- }, [tooltipLabel]);
+ return calculateNavigationSidebarTooltipSize(
+ tooltipLabel,
+ position,
+ tooltipMargin,
+ );
+ }, [position, tooltipLabel, tooltipMargin]);
const createNavigationSidebarTooltip = React.useCallback(
- () => ,
- [tooltipLabel],
+ () => (
+
+ ),
+ [position, tooltipLabel, tooltipMargin],
);
const { onMouseEnter, onMouseLeave } = useTooltip({
createTooltip: createNavigationSidebarTooltip,
tooltipSize,
- availablePositions: availableNavigationSidebarTooltipPositions,
+ availablePositions: [position],
});
return {
onMouseEnter,
onMouseLeave,
};
}
export {
useMessageTooltipSidebarAction,
useMessageTooltipReplyAction,
useMessageReactAction,
useMessageTooltipActions,
useMessageTooltip,
useReactionTooltip,
useNavigationSidebarTooltip,
};
diff --git a/web/utils/tooltip-utils.js b/web/utils/tooltip-utils.js
index 47ea03d8f..c1fa14f57 100644
--- a/web/utils/tooltip-utils.js
+++ b/web/utils/tooltip-utils.js
@@ -1,443 +1,450 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { getAppContainerPositionInfo } from './window-utils.js';
import {
tooltipButtonStyle,
tooltipLabelStyle,
tooltipStyle,
reactionTooltipStyle,
reactionSeeMoreLabel,
} from '../chat/chat-constants.js';
import type { PositionInfo } from '../chat/position-types.js';
-import {
- navigationSidebarTooltipContainerStyle,
- navigationSidebarTooltipStyle,
-} from '../navigation-sidebar/navigation-sidebar-constants.js';
+import { navigationSidebarTooltipStyle } from '../navigation-sidebar/navigation-sidebar-constants.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,
};
function getTooltipScreenOverflowRightCorrection(
xAnchor: number,
tooltipWidth: number,
): number {
const appContainerPositionInfo = getAppContainerPositionInfo();
if (!appContainerPositionInfo) {
return 0;
}
const { right: containerRight } = appContainerPositionInfo;
const padding = 8;
const tooltipRightEdge = xAnchor + tooltipWidth;
const screenRightOverflow = tooltipRightEdge - containerRight;
if (screenRightOverflow <= 0) {
return 0;
}
return screenRightOverflow + padding;
}
type FindTooltipPositionArgs = {
+sourcePositionInfo: PositionInfo,
+tooltipSize: TooltipSize,
+availablePositions: $ReadOnlyArray,
+defaultPosition: TooltipPosition,
};
function findTooltipPosition({
sourcePositionInfo,
tooltipSize,
availablePositions,
defaultPosition,
}: 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 canBeDisplayedOnTopSideways =
pointingTo.top >= containerTop &&
pointingTo.top + tooltipHeight <= containerBottom;
const canBeDisplayedOnBottomSideways =
pointingTo.bottom <= containerBottom &&
pointingTo.bottom - tooltipHeight >= containerTop;
const verticalCenterOfPointingTo = pointingTo.top + pointingTo.height / 2;
const horizontalCenterOfPointingTo = pointingTo.left + pointingTo.width / 2;
const canBeDisplayedInTheMiddleSideways =
verticalCenterOfPointingTo - tooltipHeight / 2 >= containerTop &&
verticalCenterOfPointingTo + tooltipHeight / 2 <= containerBottom;
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;
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 GetTooltipStyleParams = {
+sourcePositionInfo: PositionInfo,
+tooltipSize: TooltipSize,
+tooltipPosition: TooltipPosition,
};
// ESLint doesn't recognize that invariant always throws
// eslint-disable-next-line consistent-return
function getTooltipStyle({
sourcePositionInfo,
tooltipSize,
tooltipPosition,
}: GetTooltipStyleParams): 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) {
const xAnchor =
sourcePositionInfo.left +
sourcePositionInfo.width / 2 -
tooltipSize.width / 2;
const tooltipOverflowRightCorrection =
getTooltipScreenOverflowRightCorrection(xAnchor, tooltipSize.width);
return {
anchorPoint: {
x: xAnchor - tooltipOverflowRightCorrection,
y: sourcePositionInfo.top,
},
horizontalPosition: 'right',
verticalPosition: 'top',
alignment: 'center',
};
} else if (tooltipPosition === tooltipPositions.BOTTOM) {
const xAnchor =
sourcePositionInfo.left +
sourcePositionInfo.width / 2 -
tooltipSize.width / 2;
const tooltipOverflowRightCorrection =
getTooltipScreenOverflowRightCorrection(xAnchor, tooltipSize.width);
return {
anchorPoint: {
x: xAnchor - tooltipOverflowRightCorrection,
y: sourcePositionInfo.bottom,
},
horizontalPosition: 'right',
verticalPosition: 'bottom',
alignment: 'center',
};
}
invariant(false, `Unexpected tooltip position value: ${tooltipPosition}`);
}
type GetTooltipPositionStyleParams = {
+tooltipSourcePosition: ?PositionInfo,
+tooltipSize: TooltipSize,
+availablePositions: $ReadOnlyArray,
};
function getTooltipPositionStyle(
params: GetTooltipPositionStyleParams,
): ?TooltipPositionStyle {
const { tooltipSourcePosition, tooltipSize, availablePositions } = params;
if (!tooltipSourcePosition) {
return undefined;
}
const tooltipPosition = findTooltipPosition({
sourcePositionInfo: tooltipSourcePosition,
tooltipSize,
availablePositions,
defaultPosition: availablePositions[0],
});
if (!tooltipPosition) {
return undefined;
}
const tooltipPositionStyle = getTooltipStyle({
tooltipPosition,
sourcePositionInfo: tooltipSourcePosition,
tooltipSize,
});
return tooltipPositionStyle;
}
type CalculateMessageTooltipSizeArgs = {
+tooltipLabels: $ReadOnlyArray,
+timestamp: string,
};
function calculateMessageTooltipSize({
tooltipLabels,
timestamp,
}: CalculateMessageTooltipSizeArgs): TooltipSize {
const textWidth =
calculateMaxTextWidth([...tooltipLabels, timestamp], 14) +
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 calculateReactionTooltipSize(
usernames: $ReadOnlyArray,
): TooltipSize {
const showMoreTextIsShown = usernames.length > 5;
const {
maxWidth,
maxHeight,
paddingLeft,
paddingRight,
paddingTop,
paddingBottom,
rowGap,
} = reactionTooltipStyle;
const maxTooltipContentWidth = maxWidth;
const maxTooltipContentHeight = maxHeight;
const usernamesTextWidth = calculateMaxTextWidth(usernames, 14);
const seeMoreTextWidth = calculateMaxTextWidth([reactionSeeMoreLabel], 12);
let textWidth = usernamesTextWidth;
if (showMoreTextIsShown) {
textWidth = Math.max(usernamesTextWidth, seeMoreTextWidth);
}
const width =
Math.min(maxTooltipContentWidth, textWidth) + paddingLeft + paddingRight;
let height =
usernames.length * tooltipLabelStyle.height +
(usernames.length - 1) * rowGap;
if (showMoreTextIsShown) {
height = maxTooltipContentHeight;
}
height += paddingTop + paddingBottom;
return {
width,
height,
};
}
function calculateNavigationSidebarTooltipSize(
tooltipLabel: string,
+ position: TooltipPosition,
+ tooltipMargin: number,
): TooltipSize {
- const { marginLeft } = navigationSidebarTooltipContainerStyle;
const {
paddingLeft,
paddingRight,
paddingTop,
paddingBottom,
height: contentHeight,
} = navigationSidebarTooltipStyle;
const tooltipLabelTextWidth = calculateMaxTextWidth([tooltipLabel], 14);
- const width = marginLeft + paddingLeft + tooltipLabelTextWidth + paddingRight;
- const height = paddingTop + contentHeight + paddingBottom;
+ const marginIsHorizontal =
+ position === tooltipPositions.RIGHT || position === tooltipPositions.LEFT;
+ const marginIsVertical =
+ position === tooltipPositions.TOP || position === tooltipPositions.BOTTOM;
+
+ const horizontalMargin = marginIsHorizontal ? tooltipMargin : 0;
+ const verticalMargin = marginIsVertical ? tooltipMargin : 0;
+
+ const width =
+ paddingLeft + tooltipLabelTextWidth + paddingRight + horizontalMargin;
+ const height = paddingTop + contentHeight + paddingBottom + verticalMargin;
return {
width,
height,
};
}
export {
findTooltipPosition,
getTooltipPositionStyle,
calculateMessageTooltipSize,
calculateReactionTooltipSize,
calculateNavigationSidebarTooltipSize,
};