diff --git a/web/chat/composed-message.react.js b/web/chat/composed-message.react.js
--- a/web/chat/composed-message.react.js
+++ b/web/chat/composed-message.react.js
@@ -20,7 +20,8 @@
import UserAvatar from '../components/user-avatar.react.js';
import { type InputState, InputStateContext } from '../input/input-state.js';
import { shouldRenderAvatars } from '../utils/avatar-utils.js';
-import { tooltipPositions, useMessageTooltip } from '../utils/tooltip-utils.js';
+import { useMessageTooltip } from '../utils/tooltip-action-utils.js';
+import { tooltipPositions } from '../utils/tooltip-utils.js';
const availableTooltipPositionsForViewerMessage = [
tooltipPositions.LEFT,
diff --git a/web/chat/robotext-message.react.js b/web/chat/robotext-message.react.js
--- a/web/chat/robotext-message.react.js
+++ b/web/chat/robotext-message.react.js
@@ -19,7 +19,8 @@
import { linkRules } from '../markdown/rules.react.js';
import { updateNavInfoActionType } from '../redux/action-types.js';
import { useSelector } from '../redux/redux-utils.js';
-import { tooltipPositions, useMessageTooltip } from '../utils/tooltip-utils.js';
+import { useMessageTooltip } from '../utils/tooltip-action-utils.js';
+import { tooltipPositions } from '../utils/tooltip-utils.js';
const availableTooltipPositionsForRobotext = [
tooltipPositions.LEFT,
diff --git a/web/components/pinned-message.css b/web/components/pinned-message.css
new file mode 100644
--- /dev/null
+++ b/web/components/pinned-message.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/pinned-message.react.js b/web/components/pinned-message.react.js
new file mode 100644
--- /dev/null
+++ b/web/components/pinned-message.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 './pinned-message.css';
+import { MessageListContext } from '../chat/message-list-types.js';
+import Message from '../chat/message.react.js';
+import { useTextMessageRulesFunc } from '../markdown/rules.react.js';
+
+type PinnedMessageProps = {
+ +item: ChatMessageInfoItem,
+ +threadInfo: ThreadInfo,
+};
+
+function PinnedMessage(props: PinnedMessageProps): 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 PinnedMessage;
diff --git a/web/modals/chat/toggle-pin-modal.css b/web/modals/chat/toggle-pin-modal.css
--- 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
--- 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 PinnedMessage from '../../components/pinned-message.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
--- a/web/theme.css
+++ b/web/theme.css
@@ -212,4 +212,6 @@
--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-utils.js b/web/utils/tooltip-action-utils.js
copy from web/utils/tooltip-utils.js
copy to web/utils/tooltip-action-utils.js
--- a/web/utils/tooltip-utils.js
+++ b/web/utils/tooltip-action-utils.js
@@ -20,12 +20,14 @@
import { threadPermissions } from 'lib/types/thread-types.js';
import { longAbsoluteDate } from 'lib/utils/date-utils.js';
-import { getAppContainerPositionInfo } from './window-utils.js';
import {
- tooltipButtonStyle,
- tooltipLabelStyle,
- tooltipStyle,
-} from '../chat/chat-constants.js';
+ 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';
@@ -36,310 +38,6 @@
useOnClickPendingSidebar,
useOnClickThread,
} from '../selectors/thread-selectors.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,
-};
-
-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,
- +defaultPosition: TooltipPosition,
- +preventDisplayingBelowSource?: boolean,
-};
-
-function findTooltipPosition({
- sourcePositionInfo,
- tooltipSize,
- availablePositions,
- defaultPosition,
- preventDisplayingBelowSource,
-}: 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 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 {
- 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) {
- return {
- anchorPoint: {
- x:
- sourcePositionInfo.left +
- sourcePositionInfo.width / 2 -
- tooltipSize.width / 2,
- y: sourcePositionInfo.top,
- },
- horizontalPosition: 'right',
- verticalPosition: 'top',
- alignment: 'center',
- };
- } else if (tooltipPosition === tooltipPositions.BOTTOM) {
- return {
- anchorPoint: {
- x:
- sourcePositionInfo.left +
- sourcePositionInfo.width / 2 -
- tooltipSize.width / 2,
- y: sourcePositionInfo.bottom,
- },
- horizontalPosition: 'right',
- verticalPosition: 'bottom',
- alignment: 'center',
- };
- }
- invariant(false, `Unexpected tooltip position value: ${tooltipPosition}`);
-}
-
-type CalculateTooltipSizeArgs = {
- +tooltipLabels: $ReadOnlyArray,
- +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,
@@ -494,6 +192,8 @@
isComposableMessageType(messageInfo.type) &&
threadHasPermission(threadInfo, threadPermissions.MANAGE_PINS);
+ const inputState = React.useContext(InputStateContext);
+
return React.useMemo(() => {
if (!canTogglePin) {
return null;
@@ -504,7 +204,11 @@
const buttonContent = ;
const onClickTogglePin = () => {
- pushModal();
+ pushModal(
+
+
+ ,
+ );
};
return {
@@ -512,7 +216,7 @@
onClick: onClickTogglePin,
label: isPinned ? 'Unpin' : 'Pin',
};
- }, [canTogglePin, isPinned, pushModal, item, threadInfo]);
+ }, [canTogglePin, inputState, isPinned, pushModal, item, threadInfo]);
}
function useMessageTooltipActions(
@@ -723,9 +427,6 @@
}
export {
- findTooltipPosition,
- calculateTooltipSize,
- getMessageActionTooltipStyle,
useMessageTooltipSidebarAction,
useMessageTooltipReplyAction,
useMessageReactAction,
diff --git a/web/utils/tooltip-utils.js b/web/utils/tooltip-utils.js
--- a/web/utils/tooltip-utils.js
+++ b/web/utils/tooltip-utils.js
@@ -1,41 +1,15 @@
// @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 { getAppContainerPositionInfo } from './window-utils.js';
import {
tooltipButtonStyle,
tooltipLabelStyle,
tooltipStyle,
} from '../chat/chat-constants.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';
import { calculateMaxTextWidth } from '../utils/text-utils.js';
export const tooltipPositions = Object.freeze({
@@ -341,394 +315,8 @@
};
}
-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);
-
- 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, 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 {
findTooltipPosition,
calculateTooltipSize,
getMessageActionTooltipStyle,
- useMessageTooltipSidebarAction,
- useMessageTooltipReplyAction,
- useMessageReactAction,
- useMessageTooltipActions,
- useMessageTooltip,
};