diff --git a/web/components/message-result.css b/web/components/message-result.css
new file mode 100644
index 000000000..2032beeb7
--- /dev/null
+++ b/web/components/message-result.css
@@ -0,0 +1,28 @@
+.messageContainer {
+ overflow-y: scroll;
+ border: 1px solid var(--pin-message-modal-border-color);
+ border-radius: 7px;
+ max-height: 400px;
+ margin: 16px 32px;
+}
+
+.messageDate {
+ color: var(--chat-timestamp-color);
+ font-size: var(--xs-font-12);
+ padding: 0px 0px 6px 0px;
+ line-height: var(--line-height-text);
+ text-align: left;
+ margin-left: 16px;
+}
+
+.creator {
+ font-size: small;
+ color: var(--shades-white-60);
+ font-size: var(--s-font-14);
+ padding: 4px 24px;
+ text-align: left;
+}
+
+.messageContent {
+ margin-bottom: 1px;
+}
diff --git a/web/components/message-result.react.js b/web/components/message-result.react.js
new file mode 100644
index 000000000..716098693
--- /dev/null
+++ b/web/components/message-result.react.js
@@ -0,0 +1,57 @@
+// @flow
+
+import * as React from 'react';
+
+import { useStringForUser } from 'lib/hooks/ens-cache.js';
+import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js';
+import type { ThreadInfo } from 'lib/types/thread-types.js';
+import { longAbsoluteDate } from 'lib/utils/date-utils.js';
+
+import css from './message-result.css';
+import { MessageListContext } from '../chat/message-list-types.js';
+import Message from '../chat/message.react.js';
+import { useTextMessageRulesFunc } from '../markdown/rules.react.js';
+
+type MessageResultProps = {
+ +item: ChatMessageInfoItem,
+ +threadInfo: ThreadInfo,
+};
+
+function MessageResult(props: MessageResultProps): React.Node {
+ const { item, threadInfo } = props;
+
+ const getTextMessageMarkdownRules = useTextMessageRulesFunc(threadInfo);
+ const messageListContext = React.useMemo(() => {
+ if (!getTextMessageMarkdownRules) {
+ return undefined;
+ }
+ return { getTextMessageMarkdownRules };
+ }, [getTextMessageMarkdownRules]);
+
+ const shouldShowUsername = !item.startsConversation && !item.startsCluster;
+ const username = useStringForUser(
+ shouldShowUsername ? item.messageInfo.creator : null,
+ );
+
+ return (
+
+
+
{username}
+
+
+
+
+
+
+ {longAbsoluteDate(item.messageInfo.time)}
+
+
+
+ );
+}
+
+export default MessageResult;
diff --git a/web/modals/chat/toggle-pin-modal.css b/web/modals/chat/toggle-pin-modal.css
index e69de29bb..59cc2dd9b 100644
--- a/web/modals/chat/toggle-pin-modal.css
+++ b/web/modals/chat/toggle-pin-modal.css
@@ -0,0 +1,30 @@
+.confirmationText {
+ color: var(--pin-message-information-text-color);
+ padding: 16px 32px 0 32px;
+ font-size: small;
+}
+
+.buttonContainer {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-self: center;
+ align-items: stretch;
+ margin-bottom: 16px;
+}
+
+.togglePinButton {
+ margin: 0 32px 0 32px;
+}
+
+.cancelButton {
+ color: white;
+ display: flex;
+ justify-content: center;
+ margin-top: 16px;
+}
+
+.cancelButton:hover {
+ cursor: pointer;
+ text-decoration: underline;
+}
diff --git a/web/modals/chat/toggle-pin-modal.react.js b/web/modals/chat/toggle-pin-modal.react.js
index 8ccc23139..a20e3d077 100644
--- a/web/modals/chat/toggle-pin-modal.react.js
+++ b/web/modals/chat/toggle-pin-modal.react.js
@@ -1,18 +1,129 @@
// @flow
+import invariant from 'invariant';
import * as React from 'react';
+import {
+ toggleMessagePin,
+ toggleMessagePinActionTypes,
+} from 'lib/actions/thread-actions.js';
+import { useModalContext } from 'lib/components/modal-provider.react.js';
import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js';
import type { ThreadInfo } from 'lib/types/thread-types.js';
+import {
+ useServerCall,
+ useDispatchActionPromise,
+} from 'lib/utils/action-utils.js';
+
+import css from './toggle-pin-modal.css';
+import Button, { buttonThemes } from '../../components/button.react.js';
+import MessageResult from '../../components/message-result.react.js';
+import Modal from '../modal.react.js';
type TogglePinModalProps = {
+item: ChatMessageInfoItem,
+threadInfo: ThreadInfo,
};
-// eslint-disable-next-line no-unused-vars
function TogglePinModal(props: TogglePinModalProps): React.Node {
- return <>>;
+ const { item, threadInfo } = props;
+ const { messageInfo, isPinned } = item;
+ const { popModal } = useModalContext();
+
+ const callToggleMessagePin = useServerCall(toggleMessagePin);
+ const dispatchActionPromise = useDispatchActionPromise();
+
+ const modalInfo = React.useMemo(() => {
+ if (isPinned) {
+ return {
+ name: 'Remove Pinned Message',
+ action: 'unpin',
+ confirmationText:
+ 'Are you sure you want to remove this pinned message?',
+ buttonText: 'Remove Pinned Message',
+ buttonColor: buttonThemes.danger,
+ };
+ }
+
+ return {
+ name: 'Pin Message',
+ action: 'pin',
+ confirmationText: `You may pin this message to the channel
+ you are currently viewing. To unpin a message, select the pinned
+ messages icon in the channel.`,
+ buttonText: 'Pin Message',
+ buttonColor: buttonThemes.standard,
+ };
+ }, [isPinned]);
+
+ // We want to remove inline engagement (threadCreatedFromMessage / reactions)
+ // and the message header (startsConversation). We also want to set isViewer
+ // to false so that the message is left-aligned and uncolored.
+ const modifiedItem = React.useMemo(() => {
+ if (item.messageInfoType !== 'composable') {
+ return item;
+ }
+
+ return {
+ ...item,
+ threadCreatedFromMessage: undefined,
+ reactions: {},
+ startsConversation: false,
+ messageInfo: {
+ ...item.messageInfo,
+ creator: {
+ ...item.messageInfo.creator,
+ isViewer: false,
+ },
+ },
+ };
+ }, [item]);
+
+ const onClick = React.useCallback(() => {
+ const createToggleMessagePinPromise = async () => {
+ invariant(messageInfo.id, 'messageInfo.id should be defined');
+ const result = await callToggleMessagePin({
+ messageID: messageInfo.id,
+ action: modalInfo.action,
+ });
+ return {
+ newMessageInfos: result.newMessageInfos,
+ threadID: result.threadID,
+ };
+ };
+
+ dispatchActionPromise(
+ toggleMessagePinActionTypes,
+ createToggleMessagePinPromise(),
+ );
+ popModal();
+ }, [
+ modalInfo,
+ callToggleMessagePin,
+ dispatchActionPromise,
+ messageInfo.id,
+ popModal,
+ ]);
+
+ return (
+
+ {modalInfo.confirmationText}
+
+
+
+
+ Cancel
+
+
+
+ );
}
export default TogglePinModal;
diff --git a/web/theme.css b/web/theme.css
index 98bd4c51c..c48d61a79 100644
--- a/web/theme.css
+++ b/web/theme.css
@@ -1,215 +1,217 @@
:root {
/* Never use color values defined here directly in CSS. Add color variables to "Color Theme" below
The reason we never use color values defined here directly in CSS is
1. It makes changing themes from light / dark / user generated impossible.
2. Gives the programmer context into the color being used.
3. If our color system changes it's much easier to change color values in one place.
Add a color value to the theme below, and then use it in your CSS.
naming convention:
- bg: background.
- fg: foreground.
- color: text-color
*/
--shades-white-100: #ffffff;
--shades-white-90: #f5f5f5;
--shades-white-80: #ebebeb;
--shades-white-70: #e0e0e0;
--shades-white-60: #cccccc;
--shades-black-100: #0a0a0a;
--shades-black-90: #1f1f1f;
--shades-black-80: #404040;
--shades-black-70: #666666;
--shades-black-60: #808080;
--violet-dark-100: #7e57c2;
--violet-dark-80: #6d49ab;
--violet-dark-60: #563894;
--violet-dark-40: #44297a;
--violet-dark-20: #331f5c;
--violet-light-100: #ae94db;
--violet-light-80: #b9a4df;
--violet-light-60: #d3c6ec;
--violet-light-40: #e8e0f5;
--violet-light-20: #f3f0fa;
--success-light-10: #d5f6e3;
--success-light-50: #6cdf9c;
--success-primary: #00c853;
--success-dark-50: #029841;
--success-dark-90: #034920;
--error-light-10: #feebe6;
--error-light-50: #f9947b;
--error-primary: #f53100;
--error-dark-50: #b62602;
--error-dark-90: #4f1203;
--logo-bg: #111827;
--spoiler-color: #33332c;
--loading-foreground: #1b0e38;
--bg: var(--shades-black-100);
--fg: var(--shades-white-100);
--color-disabled: var(--shades-black-60);
--text-input-bg: var(--shades-black-80);
--text-input-color: var(--shades-white-60);
--text-input-placeholder: var(--shades-white-60);
--border: var(--shades-black-80);
--error: var(--error-primary);
--success: var(--success-dark-50);
/* Color Theme */
--btn-bg-filled: var(--violet-dark-100);
--btn-bg-outline: var(--shades-black-90);
--btn-bg-success: var(--success-dark-50);
--btn-bg-danger: var(--error-primary);
--btn-bg-disabled: var(--shades-black-80);
--btn-disabled-color: var(--shades-black-60);
--chat-bg: var(--violet-dark-80);
--chat-confirmation-icon: var(--violet-dark-100);
--keyserver-selection: var(--violet-dark-60);
--thread-selection: var(--violet-light-80);
--thread-hover-bg: var(--shades-black-80);
--thread-active-bg: var(--shades-black-80);
--chat-timestamp-color: var(--shades-black-60);
--tool-tip-bg: var(--shades-black-80);
--tool-tip-color: var(--shades-white-60);
--border-color: var(--shades-black-80);
--calendar-chevron: var(--shades-black-60);
--calendar-day-bg: var(--shades-black-60);
--calendar-day-selected-color: var(--violet-dark-80);
--community-bg: var(--shades-black-90);
--community-settings-selected: var(--violet-dark-60);
--unread-bg: var(--error-primary);
--settings-btn-bg: var(--violet-dark-100);
--modal-bg: var(--shades-black-90);
--modal-fg: var(--shades-white-60);
--join-bg: var(--shades-black-90);
--help-color: var(--shades-black-60);
--breadcrumb-color: var(--shades-white-60);
--breadcrumb-color-unread: var(--shades-white-60);
--btn-outline-border: var(--shades-black-60);
--thread-color-read: var(--shades-black-60);
--thread-preview-secondary: var(--shades-black-70);
--relationship-button-green: var(--success-dark-50);
--relationship-button-red: var(--error-primary);
--relationship-button-text: var(--fg);
--disconnected-bar-alert-bg: var(--error-dark-50);
--disconnected-bar-alert-color: var(--shades-white-100);
--disconnected-bar-connecting-bg: var(--shades-white-70);
--disconnected-bar-connecting-color: var(--shades-black-100);
--permission-color: var(--shades-white-60);
--thread-top-bar-color: var(--shades-white-100);
--thread-top-bar-menu-color: var(--shades-white-70);
--thread-ancestor-keyserver-border: var(--shades-black-70);
--thread-ancestor-color: var(--shades-white-100);
--thread-ancestor-separator-color: var(--shades-white-60);
--text-message-default-background: var(--shades-black-80);
--message-action-tooltip-bg: var(--shades-black-90);
--message-action-tooltip-bg-light: var(--shades-black-80);
--menu-bg: var(--shades-black-90);
--menu-bg-light: var(--shades-black-80);
--menu-separator-color: var(--shades-black-80);
--menu-color: var(--shades-black-60);
--menu-color-light: var(--shades-white-60);
--menu-color-hover: var(--shades-white-100);
--menu-color-dangerous: var(--error-primary);
--menu-color-dangerous-hover: var(--error-light-50);
--app-list-icon-read-only-color: var(--shades-black-60);
--app-list-icon-enabled-color: var(--success-primary);
--app-list-icon-disabled-color: var(--shades-white-80);
--account-settings-label: var(--shades-black-60);
--account-button-color: var(--violet-dark-100);
--chat-thread-list-color-active: var(--shades-white-60);
--chat-thread-list-menu-color: var(--shades-white-60);
--chat-thread-list-menu-bg: var(--shades-black-80);
--chat-thread-list-menu-active-color: var(--shades-white-60);
--chat-thread-list-menu-active-bg: var(--shades-black-90);
--search-clear-color: var(--shades-white-100);
--search-clear-bg: var(--shades-black-70);
--search-input-color: var(--shades-white-100);
--search-input-placeholder: var(--shades-black-60);
--search-icon-color: var(--shades-black-60);
--tabs-header-active-color: var(--shades-white-100);
--tabs-header-active-border: var(--violet-light-100);
--tabs-header-active-background: var(--violet-dark-100);
--tabs-header-background-color: var(--shades-black-60);
--tabs-header-background-color-pill: var(--shades-white-60);
--tabs-header-background-border: var(--shades-black-80);
--tabs-header-background-color-hover: var(--shades-white-80);
--tabs-header-background-border-hover: var(--shades-black-70);
--members-modal-member-text: var(--shades-black-60);
--members-modal-member-text-hover: var(--shades-white-100);
--label-default-bg: var(--violet-dark-80);
--label-default-color: var(--shades-white-80);
--subchannels-modal-color: var(--shades-black-60);
--subchannels-modal-color-hover: var(--shades-white-100);
--color-selector-active-bg: var(--shades-black-80);
--relationship-modal-color: var(--shades-black-60);
--arrow-extension-color: var(--shades-black-60);
--modal-close-color: var(--shades-black-60);
--modal-close-color-hover: var(--shades-white-100);
--add-members-group-header-color: var(--shades-black-60);
--add-members-item-color: var(--shades-black-60);
--add-members-item-color-hover: var(--shades-white-100);
--add-members-item-disabled-color: var(--shades-black-80);
--add-members-item-disabled-color-hover: var(--shades-black-60);
--add-members-remove-pending-color: var(--error-primary);
--add-members-remove-pending-color-hover: var(--error-light-50);
--radio-border: var(--shades-black-70);
--radio-color: var(--shades-white-60);
--notification-settings-option-selected-bg: var(--shades-black-80);
--notification-settings-option-title-color: var(--shades-white-90);
--notification-settings-option-color: var(--shades-white-60);
--notification-settings-option-invalid-color: var(--shades-black-80);
--notification-settings-option-invalid-selected-color: var(--shades-black-60);
--danger-zone-subheading-color: var(--shades-white-60);
--danger-zone-explanation-color: var(--shades-black-60);
--thread-creation-search-container-bg: var(--shades-black-90);
--thread-creation-close-search-color: var(--shades-black-60);
--thread-creation-search-item-bg-hover: var(--shades-black-80);
--thread-creation-search-item-info-color: var(--shades-black-60);
--chat-message-list-active-border: #5989d6;
--sidebars-modal-color: var(--shades-black-60);
--sidebars-modal-color-hover: var(--shades-white-100);
--inline-engagement-bg: var(--shades-black-70);
--inline-engagement-bg-hover: var(--shades-black-80);
--inline-engagement-color: var(--fg);
--compose-subchannel-header-fg: var(--shades-black-60);
--compose-subchannel-header-bg: var(--shades-black-80);
--compose-subchannel-label-color: var(--shades-black-60);
--compose-subchannel-mark-color: var(--violet-light-100);
--enum-option-icon-color: var(--violet-dark-100);
--show-password-bg-hover: var(--shades-black-70);
--typeahead-overlay-light: var(--shades-black-80);
--typeahead-overlay-dark: var(--shades-black-90);
--typeahead-overlay-text: var(--shades-white-100);
--typeahead-overlay-shadow-primary: rgba(0, 0, 0, 0.25);
--typeahead-overlay-shadow-secondary: rgba(0, 0, 0, 0.4);
--spoiler-text-color: var(--spoiler-color);
--spoiler-background-color: var(--spoiler-color);
--purple-link: var(--violet-light-100);
--drawer-expand-button: var(--shades-black-60);
--drawer-expand-button-disabled: var(--shades-black-80);
--drawer-item-color: var(--shades-white-60);
--drawer-active-item-color: var(--shades-white-100);
--drawer-open-community-bg: #191919;
--active-drawer-item-bg: rgba(0, 0, 0, 0.5);
--community-drawer-lines: rgba(255, 255, 255, 0.08);
--topbar-button-bg: var(--shades-black-90);
--filters-button-bg: var(--shades-black-100);
--filters-button-border: var(--shades-black-80);
--filters-button-hover-bg: var(--shades-black-90);
--filter-panel-fg: var(--shades-black-60);
--filter-panel-bg: #0d0d0d;
--topbar-button-bg-hover: var(--shades-black-80);
--topbar-button-fg: var(--shades-white-60);
--message-label-color: var(--shades-black-60);
--topbar-lines: rgba(255, 255, 255, 0.08);
+ --pin-message-information-text-color: var(--shades-white-60);
+ --pin-message-modal-border-color: var(--shades-black-80);
}
diff --git a/web/utils/tooltip-action-utils.js b/web/utils/tooltip-action-utils.js
index e77fb0750..27094d9df 100644
--- a/web/utils/tooltip-action-utils.js
+++ b/web/utils/tooltip-action-utils.js
@@ -1,429 +1,435 @@
// @flow
import invariant from 'invariant';
import _debounce from 'lodash/debounce.js';
import * as React from 'react';
import { useModalContext } from 'lib/components/modal-provider.react.js';
import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js';
import { createMessageReply } from 'lib/shared/message-utils.js';
import { useCanCreateReactionFromMessage } from 'lib/shared/reaction-utils.js';
import {
threadHasPermission,
useSidebarExistsOrCanBeCreated,
} from 'lib/shared/thread-utils.js';
import {
isComposableMessageType,
messageTypes,
} from 'lib/types/message-types.js';
import type { ThreadInfo } from 'lib/types/thread-types.js';
import { threadPermissions } from 'lib/types/thread-types.js';
import { longAbsoluteDate } from 'lib/utils/date-utils.js';
import {
type MessageTooltipAction,
findTooltipPosition,
getMessageActionTooltipStyle,
calculateTooltipSize,
type TooltipSize,
type TooltipPosition,
} from './tooltip-utils.js';
import MessageTooltip from '../chat/message-tooltip.react.js';
import type { PositionInfo } from '../chat/position-types.js';
import { useTooltipContext } from '../chat/tooltip-provider.js';
import CommIcon from '../CommIcon.react.js';
import { InputStateContext } from '../input/input-state.js';
import TogglePinModal from '../modals/chat/toggle-pin-modal.react.js';
import {
useOnClickPendingSidebar,
useOnClickThread,
} from '../selectors/thread-selectors.js';
function useMessageTooltipSidebarAction(
item: ChatMessageInfoItem,
threadInfo: ThreadInfo,
): ?MessageTooltipAction {
const { threadCreatedFromMessage, messageInfo } = item;
const sidebarExists = !!threadCreatedFromMessage;
const sidebarExistsOrCanBeCreated = useSidebarExistsOrCanBeCreated(
threadInfo,
item,
);
const openThread = useOnClickThread(threadCreatedFromMessage);
const openPendingSidebar = useOnClickPendingSidebar(messageInfo, threadInfo);
return React.useMemo(() => {
if (!sidebarExistsOrCanBeCreated) {
return null;
}
const buttonContent = ;
const onClick = (event: SyntheticEvent) => {
if (threadCreatedFromMessage) {
openThread(event);
} else {
openPendingSidebar(event);
}
};
return {
actionButtonContent: buttonContent,
onClick,
label: sidebarExists ? 'Go to thread' : 'Create thread',
};
}, [
openPendingSidebar,
openThread,
sidebarExists,
sidebarExistsOrCanBeCreated,
threadCreatedFromMessage,
]);
}
function useMessageTooltipReplyAction(
item: ChatMessageInfoItem,
threadInfo: ThreadInfo,
): ?MessageTooltipAction {
const { messageInfo } = item;
const inputState = React.useContext(InputStateContext);
invariant(inputState, 'inputState is required');
const { addReply } = inputState;
return React.useMemo(() => {
if (
!isComposableMessageType(item.messageInfo.type) ||
!threadHasPermission(threadInfo, threadPermissions.VOICED)
) {
return null;
}
const buttonContent = ;
const onClick = () => {
if (!messageInfo.text) {
return;
}
addReply(createMessageReply(messageInfo.text));
};
return {
actionButtonContent: buttonContent,
onClick,
label: 'Reply',
};
}, [addReply, item.messageInfo.type, messageInfo, threadInfo]);
}
const copiedMessageDurationMs = 2000;
function useMessageCopyAction(
item: ChatMessageInfoItem,
): ?MessageTooltipAction {
const { messageInfo } = item;
const [successful, setSuccessful] = React.useState(false);
const resetStatusAfterTimeout = React.useRef(
_debounce(() => setSuccessful(false), copiedMessageDurationMs),
);
const onSuccess = React.useCallback(() => {
setSuccessful(true);
resetStatusAfterTimeout.current();
}, []);
React.useEffect(() => resetStatusAfterTimeout.current.cancel, []);
return React.useMemo(() => {
if (messageInfo.type !== messageTypes.TEXT) {
return null;
}
const buttonContent = ;
const onClick = async () => {
try {
await navigator.clipboard.writeText(messageInfo.text);
onSuccess();
} catch (e) {
setSuccessful(false);
}
};
return {
actionButtonContent: buttonContent,
onClick,
label: successful ? 'Copied!' : 'Copy',
};
}, [messageInfo.text, messageInfo.type, onSuccess, successful]);
}
function useMessageReactAction(
item: ChatMessageInfoItem,
threadInfo: ThreadInfo,
): ?MessageTooltipAction {
const { messageInfo } = item;
const { setShouldRenderEmojiKeyboard } = useTooltipContext();
const canCreateReactionFromMessage = useCanCreateReactionFromMessage(
threadInfo,
messageInfo,
);
return React.useMemo(() => {
if (!canCreateReactionFromMessage) {
return null;
}
const buttonContent = ;
const onClickReact = () => {
if (!setShouldRenderEmojiKeyboard) {
return;
}
setShouldRenderEmojiKeyboard(true);
};
return {
actionButtonContent: buttonContent,
onClick: onClickReact,
label: 'React',
};
}, [canCreateReactionFromMessage, setShouldRenderEmojiKeyboard]);
}
function useMessageTogglePinAction(
item: ChatMessageInfoItem,
threadInfo: ThreadInfo,
): ?MessageTooltipAction {
const { pushModal } = useModalContext();
const { messageInfo, isPinned } = item;
const canTogglePin =
isComposableMessageType(messageInfo.type) &&
threadHasPermission(threadInfo, threadPermissions.MANAGE_PINS);
+ const inputState = React.useContext(InputStateContext);
+
return React.useMemo(() => {
if (!canTogglePin) {
return null;
}
const iconName = isPinned ? 'unpin' : 'pin';
const buttonContent = ;
const onClickTogglePin = () => {
- pushModal();
+ pushModal(
+
+
+ ,
+ );
};
return {
actionButtonContent: buttonContent,
onClick: onClickTogglePin,
label: isPinned ? 'Unpin' : 'Pin',
};
- }, [canTogglePin, isPinned, pushModal, item, threadInfo]);
+ }, [canTogglePin, inputState, isPinned, pushModal, item, threadInfo]);
}
function useMessageTooltipActions(
item: ChatMessageInfoItem,
threadInfo: ThreadInfo,
): $ReadOnlyArray {
const sidebarAction = useMessageTooltipSidebarAction(item, threadInfo);
const replyAction = useMessageTooltipReplyAction(item, threadInfo);
const copyAction = useMessageCopyAction(item);
const reactAction = useMessageReactAction(item, threadInfo);
const togglePinAction = useMessageTogglePinAction(item, threadInfo);
return React.useMemo(
() =>
[
replyAction,
sidebarAction,
copyAction,
reactAction,
togglePinAction,
].filter(Boolean),
[replyAction, sidebarAction, copyAction, reactAction, togglePinAction],
);
}
type UseMessageTooltipArgs = {
+availablePositions: $ReadOnlyArray,
+item: ChatMessageInfoItem,
+threadInfo: ThreadInfo,
};
type UseMessageTooltipResult = {
onMouseEnter: (event: SyntheticEvent) => void,
onMouseLeave: ?() => mixed,
};
type CreateTooltipParams = {
+tooltipMessagePosition: ?PositionInfo,
+tooltipSize: TooltipSize,
+availablePositions: $ReadOnlyArray,
+containsInlineEngagement: boolean,
+tooltipActions: $ReadOnlyArray,
+messageTimestamp: string,
+item: ChatMessageInfoItem,
+threadInfo: ThreadInfo,
};
function createTooltip(params: CreateTooltipParams) {
const {
tooltipMessagePosition,
tooltipSize,
availablePositions,
containsInlineEngagement,
tooltipActions,
messageTimestamp,
item,
threadInfo,
} = params;
if (!tooltipMessagePosition) {
return;
}
const tooltipPosition = findTooltipPosition({
sourcePositionInfo: tooltipMessagePosition,
tooltipSize,
availablePositions,
defaultPosition: availablePositions[0],
preventDisplayingBelowSource: containsInlineEngagement,
});
if (!tooltipPosition) {
return;
}
const tooltipPositionStyle = getMessageActionTooltipStyle({
tooltipPosition,
sourcePositionInfo: tooltipMessagePosition,
tooltipSize,
});
const tooltip = (
);
return { tooltip, tooltipPositionStyle };
}
function useMessageTooltip({
availablePositions,
item,
threadInfo,
}: UseMessageTooltipArgs): UseMessageTooltipResult {
const [onMouseLeave, setOnMouseLeave] = React.useState() => mixed>(null);
const { renderTooltip } = useTooltipContext();
const tooltipActions = useMessageTooltipActions(item, threadInfo);
const containsInlineEngagement = !!item.threadCreatedFromMessage;
const messageTimestamp = React.useMemo(() => {
const time = item.messageInfo.time;
return longAbsoluteDate(time);
}, [item.messageInfo.time]);
const tooltipSize = React.useMemo(() => {
if (typeof document === 'undefined') {
return {
width: 0,
height: 0,
};
}
const tooltipLabels = tooltipActions.map(action => action.label);
return calculateTooltipSize({
tooltipLabels,
timestamp: messageTimestamp,
});
}, [messageTimestamp, tooltipActions]);
const updateTooltip = React.useRef();
const [tooltipMessagePosition, setTooltipMessagePosition] = React.useState();
const onMouseEnter = React.useCallback(
(event: SyntheticEvent) => {
if (!renderTooltip) {
return;
}
const rect = event.currentTarget.getBoundingClientRect();
const { top, bottom, left, right, height, width } = rect;
const messagePosition = { top, bottom, left, right, height, width };
setTooltipMessagePosition(messagePosition);
const tooltipResult = createTooltip({
tooltipMessagePosition,
tooltipSize,
availablePositions,
containsInlineEngagement,
tooltipActions,
messageTimestamp,
item,
threadInfo,
});
if (!tooltipResult) {
return;
}
const { tooltip, tooltipPositionStyle } = tooltipResult;
const renderTooltipResult = renderTooltip({
newNode: tooltip,
tooltipPositionStyle,
});
if (renderTooltipResult) {
const { onMouseLeaveCallback: callback } = renderTooltipResult;
setOnMouseLeave((() => callback: () => () => mixed));
updateTooltip.current = renderTooltipResult.updateTooltip;
}
},
[
availablePositions,
containsInlineEngagement,
item,
messageTimestamp,
renderTooltip,
threadInfo,
tooltipActions,
tooltipMessagePosition,
tooltipSize,
],
);
React.useEffect(() => {
if (!updateTooltip.current) {
return;
}
const tooltipResult = createTooltip({
tooltipMessagePosition,
tooltipSize,
availablePositions,
containsInlineEngagement,
tooltipActions,
messageTimestamp,
item,
threadInfo,
});
if (!tooltipResult) {
return;
}
updateTooltip.current?.(tooltipResult.tooltip);
}, [
availablePositions,
containsInlineEngagement,
item,
messageTimestamp,
threadInfo,
tooltipActions,
tooltipMessagePosition,
tooltipSize,
]);
return {
onMouseEnter,
onMouseLeave,
};
}
export {
useMessageTooltipSidebarAction,
useMessageTooltipReplyAction,
useMessageReactAction,
useMessageTooltipActions,
useMessageTooltip,
};