TelegramPWA/src/components/middle/composer/ComposerEmbeddedMessage.tsx
2022-08-31 15:00:45 +02:00

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));