343 lines
11 KiB
TypeScript
343 lines
11 KiB
TypeScript
import React, {
|
|
memo, useCallback, useEffect, useMemo, useRef,
|
|
} from '../../../lib/teact/teact';
|
|
import { getActions, withGlobal } from '../../../global';
|
|
|
|
import type { FC } from '../../../lib/teact/teact';
|
|
import type { ApiChat, ApiMessage, ApiUser } from '../../../api/types';
|
|
import { ApiMessageEntityTypes } from '../../../api/types';
|
|
|
|
import {
|
|
selectChat,
|
|
selectChatMessage,
|
|
selectSender,
|
|
selectForwardedSender,
|
|
selectUser,
|
|
selectCurrentMessageList,
|
|
selectReplyingToId,
|
|
selectEditingId,
|
|
selectEditingScheduledId,
|
|
selectEditingMessage,
|
|
selectIsChatWithSelf,
|
|
selectIsCurrentUserPremium,
|
|
} from '../../../global/selectors';
|
|
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
|
import buildClassName from '../../../util/buildClassName';
|
|
import { isUserId } from '../../../global/helpers';
|
|
|
|
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
|
import useShowTransition from '../../../hooks/useShowTransition';
|
|
import useLang from '../../../hooks/useLang';
|
|
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
|
import useContextMenuPosition from '../../../hooks/useContextMenuPosition';
|
|
|
|
import Button from '../../ui/Button';
|
|
import EmbeddedMessage from '../../common/EmbeddedMessage';
|
|
import MenuItem from '../../ui/MenuItem';
|
|
import Menu from '../../ui/Menu';
|
|
import MenuSeparator from '../../ui/MenuSeparator';
|
|
|
|
import './ComposerEmbeddedMessage.scss';
|
|
|
|
type StateProps = {
|
|
replyingToId?: number;
|
|
editingId?: number;
|
|
message?: ApiMessage;
|
|
sender?: ApiUser | ApiChat;
|
|
shouldAnimate?: boolean;
|
|
forwardedMessagesCount?: number;
|
|
noAuthors?: boolean;
|
|
noCaptions?: boolean;
|
|
forwardsHaveCaptions?: boolean;
|
|
isCurrentUserPremium?: boolean;
|
|
};
|
|
|
|
type OwnProps = {
|
|
onClear?: () => void;
|
|
};
|
|
|
|
const FORWARD_RENDERING_DELAY = 300;
|
|
|
|
const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
|
|
replyingToId,
|
|
editingId,
|
|
message,
|
|
sender,
|
|
shouldAnimate,
|
|
forwardedMessagesCount,
|
|
noAuthors,
|
|
noCaptions,
|
|
forwardsHaveCaptions,
|
|
isCurrentUserPremium,
|
|
onClear,
|
|
}) => {
|
|
const {
|
|
setReplyingToId,
|
|
setEditingId,
|
|
focusMessage,
|
|
changeForwardRecipient,
|
|
setForwardNoAuthors,
|
|
setForwardNoCaptions,
|
|
exitForwardMode,
|
|
} = getActions();
|
|
// eslint-disable-next-line no-null/no-null
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const lang = useLang();
|
|
|
|
const isForwarding = Boolean(forwardedMessagesCount);
|
|
const isShown = Boolean(
|
|
((replyingToId || editingId) && message)
|
|
|| (sender && forwardedMessagesCount),
|
|
);
|
|
const canAnimate = useAsyncRendering(
|
|
[forwardedMessagesCount],
|
|
forwardedMessagesCount ? FORWARD_RENDERING_DELAY : undefined,
|
|
);
|
|
|
|
const {
|
|
shouldRender, transitionClassNames,
|
|
} = useShowTransition(canAnimate && isShown, undefined, !shouldAnimate, undefined, !shouldAnimate);
|
|
|
|
const clearEmbedded = useCallback(() => {
|
|
if (replyingToId) {
|
|
setReplyingToId({ messageId: undefined });
|
|
} else if (editingId) {
|
|
setEditingId({ messageId: undefined });
|
|
} else if (forwardedMessagesCount) {
|
|
exitForwardMode();
|
|
}
|
|
onClear?.();
|
|
}, [replyingToId, editingId, forwardedMessagesCount, onClear, setReplyingToId, setEditingId, exitForwardMode]);
|
|
|
|
useEffect(() => (isShown ? captureEscKeyListener(clearEmbedded) : undefined), [isShown, clearEmbedded]);
|
|
|
|
const handleMessageClick = useCallback((): void => {
|
|
if (isForwarding) return;
|
|
focusMessage({ chatId: message!.chatId, messageId: message!.id });
|
|
}, [focusMessage, isForwarding, message]);
|
|
|
|
const handleClearClick = useCallback((e: React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
|
|
e.stopPropagation();
|
|
clearEmbedded();
|
|
}, [clearEmbedded]);
|
|
|
|
const handleChangeRecipientClick = useCallback(() => {
|
|
changeForwardRecipient();
|
|
}, [changeForwardRecipient]);
|
|
|
|
const {
|
|
isContextMenuOpen, contextMenuPosition, handleContextMenu,
|
|
handleContextMenuClose, handleContextMenuHide,
|
|
} = useContextMenuHandlers(ref);
|
|
|
|
const getTriggerElement = useCallback(() => ref.current, []);
|
|
const getRootElement = useCallback(() => ref.current!, []);
|
|
const getMenuElement = useCallback(() => ref.current!.querySelector('.forward-context-menu .bubble'), []);
|
|
|
|
const {
|
|
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
|
} = useContextMenuPosition(
|
|
contextMenuPosition,
|
|
getTriggerElement,
|
|
getRootElement,
|
|
getMenuElement,
|
|
);
|
|
|
|
const className = buildClassName('ComposerEmbeddedMessage', transitionClassNames);
|
|
|
|
const leftIcon = useMemo(() => {
|
|
if (replyingToId) {
|
|
return 'icon-reply';
|
|
}
|
|
if (editingId) {
|
|
return 'icon-edit';
|
|
}
|
|
if (isForwarding) {
|
|
return 'icon-forward';
|
|
}
|
|
|
|
return undefined;
|
|
}, [editingId, isForwarding, replyingToId]);
|
|
|
|
const customText = forwardedMessagesCount && forwardedMessagesCount > 1
|
|
? lang('ForwardedMessageCount', forwardedMessagesCount)
|
|
: undefined;
|
|
|
|
const strippedMessage = useMemo(() => {
|
|
const textEntities = message?.content.text?.entities;
|
|
if (!message || !isForwarding || !textEntities?.length || !noAuthors || isCurrentUserPremium) return message;
|
|
|
|
const filteredEntities = textEntities.filter((entity) => entity.type !== ApiMessageEntityTypes.CustomEmoji);
|
|
return {
|
|
...message,
|
|
content: {
|
|
...message.content,
|
|
text: {
|
|
text: message.content.text!.text,
|
|
entities: filteredEntities,
|
|
},
|
|
},
|
|
};
|
|
}, [isCurrentUserPremium, isForwarding, message, noAuthors]);
|
|
|
|
if (!shouldRender) {
|
|
return undefined;
|
|
}
|
|
|
|
return (
|
|
<div className={className} ref={ref} onContextMenu={handleContextMenu} onClick={handleContextMenu}>
|
|
<div>
|
|
<div className="embedded-left-icon">
|
|
<i className={leftIcon} />
|
|
</div>
|
|
<EmbeddedMessage
|
|
className="inside-input"
|
|
message={strippedMessage}
|
|
sender={!noAuthors ? sender : undefined}
|
|
customText={customText}
|
|
title={editingId ? lang('EditMessage') : noAuthors ? lang('HiddenSendersNameDescription') : undefined}
|
|
onClick={handleMessageClick}
|
|
hasContextMenu={isForwarding}
|
|
/>
|
|
<Button
|
|
className="embedded-cancel"
|
|
round
|
|
faded
|
|
color="translucent"
|
|
ariaLabel={lang('Cancel')}
|
|
onClick={handleClearClick}
|
|
>
|
|
<i className="icon-close" />
|
|
</Button>
|
|
{isForwarding && (
|
|
<Menu
|
|
isOpen={isContextMenuOpen}
|
|
transformOriginX={transformOriginX}
|
|
transformOriginY={transformOriginY}
|
|
positionX={positionX}
|
|
positionY={positionY}
|
|
style={menuStyle}
|
|
className="forward-context-menu"
|
|
onClose={handleContextMenuClose}
|
|
onCloseAnimationEnd={handleContextMenuHide}
|
|
>
|
|
<MenuItem
|
|
icon={!noAuthors ? 'message-succeeded' : undefined}
|
|
customIcon={noAuthors ? <i className="icon-placeholder" /> : undefined}
|
|
// eslint-disable-next-line react/jsx-no-bind
|
|
onClick={() => setForwardNoAuthors(false)}
|
|
>
|
|
{lang(forwardedMessagesCount > 1 ? 'ShowSenderNames' : 'ShowSendersName')}
|
|
</MenuItem>
|
|
<MenuItem
|
|
icon={noAuthors ? 'message-succeeded' : undefined}
|
|
customIcon={!noAuthors ? <i className="icon-placeholder" /> : undefined}
|
|
// eslint-disable-next-line react/jsx-no-bind
|
|
onClick={() => setForwardNoAuthors(true)}
|
|
>
|
|
{lang(forwardedMessagesCount > 1 ? 'HideSenderNames' : 'HideSendersName')}
|
|
</MenuItem>
|
|
{forwardsHaveCaptions && (
|
|
<>
|
|
<MenuSeparator />
|
|
<MenuItem
|
|
icon={!noCaptions ? 'message-succeeded' : undefined}
|
|
customIcon={noCaptions ? <i className="icon-placeholder" /> : undefined}
|
|
// eslint-disable-next-line react/jsx-no-bind
|
|
onClick={() => setForwardNoCaptions(false)}
|
|
>
|
|
{lang(forwardedMessagesCount > 1 ? 'Conversation.ForwardOptions.ShowCaption' : 'ShowCaption')}
|
|
</MenuItem>
|
|
<MenuItem
|
|
icon={noCaptions ? 'message-succeeded' : undefined}
|
|
customIcon={!noCaptions ? <i className="icon-placeholder" /> : undefined}
|
|
// eslint-disable-next-line react/jsx-no-bind
|
|
onClick={() => setForwardNoCaptions(true)}
|
|
>
|
|
{lang(forwardedMessagesCount > 1 ? 'Conversation.ForwardOptions.HideCaption' : 'HideCaption')}
|
|
</MenuItem>
|
|
</>
|
|
)}
|
|
<MenuSeparator />
|
|
<MenuItem icon="replace" onClick={handleChangeRecipientClick}>
|
|
{lang('ChangeRecipient')}
|
|
</MenuItem>
|
|
</Menu>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default memo(withGlobal<OwnProps>(
|
|
(global): StateProps => {
|
|
const { chatId, threadId, type: messageListType } = selectCurrentMessageList(global) || {};
|
|
if (!chatId || !threadId || !messageListType) {
|
|
return {};
|
|
}
|
|
|
|
const {
|
|
forwardMessages: {
|
|
fromChatId, toChatId, messageIds: forwardMessageIds, noAuthors, noCaptions,
|
|
},
|
|
} = global;
|
|
|
|
const replyingToId = selectReplyingToId(global, chatId, threadId);
|
|
const editingId = messageListType === 'scheduled'
|
|
? selectEditingScheduledId(global, chatId)
|
|
: selectEditingId(global, chatId, threadId);
|
|
const shouldAnimate = global.settings.byKey.animationLevel >= 1;
|
|
const isForwarding = toChatId === chatId;
|
|
const forwardedMessages = forwardMessageIds?.map((id) => selectChatMessage(global, fromChatId!, id)!);
|
|
|
|
let message: ApiMessage | undefined;
|
|
if (replyingToId) {
|
|
message = selectChatMessage(global, chatId, replyingToId);
|
|
} else if (editingId) {
|
|
message = selectEditingMessage(global, chatId, threadId, messageListType);
|
|
} else if (isForwarding && forwardMessageIds!.length === 1) {
|
|
message = forwardedMessages?.[0];
|
|
}
|
|
|
|
let sender: ApiChat | ApiUser | undefined;
|
|
if (replyingToId && message) {
|
|
const { forwardInfo } = message;
|
|
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
|
|
if (forwardInfo && (forwardInfo.isChannelPost || isChatWithSelf)) {
|
|
sender = selectForwardedSender(global, message);
|
|
}
|
|
|
|
if (!sender && !forwardInfo?.hiddenUserName) {
|
|
sender = selectSender(global, message);
|
|
}
|
|
} else if (isForwarding) {
|
|
if (message) {
|
|
sender = selectForwardedSender(global, message);
|
|
if (!sender) {
|
|
sender = selectSender(global, message);
|
|
}
|
|
}
|
|
if (!sender) {
|
|
sender = isUserId(fromChatId!) ? selectUser(global, fromChatId!) : selectChat(global, fromChatId!);
|
|
}
|
|
}
|
|
|
|
const forwardsHaveCaptions = forwardedMessages?.some((forward) => (
|
|
forward?.content.text && Object.keys(forward.content).length > 1
|
|
));
|
|
|
|
return {
|
|
replyingToId,
|
|
editingId,
|
|
message,
|
|
sender,
|
|
shouldAnimate,
|
|
forwardedMessagesCount: isForwarding ? forwardMessageIds!.length : undefined,
|
|
noAuthors,
|
|
noCaptions,
|
|
forwardsHaveCaptions,
|
|
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
|
};
|
|
},
|
|
)(ComposerEmbeddedMessage));
|