TelegramPWA/src/components/middle/message/SenderGroupContainer.tsx
2025-08-21 12:07:28 +02:00

245 lines
6.4 KiB
TypeScript

import type { FC } from '../../../lib/teact/teact';
import type React from '../../../lib/teact/teact';
import {
memo,
useEffect,
useRef,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type {
ApiMessage,
ApiPeer,
} from '../../../api/types';
import {
EDITABLE_INPUT_CSS_SELECTOR,
MESSAGE_APPEARANCE_DELAY,
} from '../../../config';
import {
getMainUsername,
isAnonymousForwardsChat,
isAnonymousOwnMessage,
isSystemBot,
} from '../../../global/helpers';
import { isApiPeerUser } from '../../../global/helpers/peers';
import {
selectForwardedSender,
selectIsChatWithSelf,
selectSender,
} from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import useFlag from '../../../hooks/useFlag';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useShowTransition from '../../../hooks/useShowTransition';
import Avatar from '../../common/Avatar';
import Menu from '../../ui/Menu';
import MenuItem from '../../ui/MenuItem';
import styles from './SenderGroupContainer.module.scss';
type OwnProps =
{
message: ApiMessage;
withAvatar?: boolean;
children: React.ReactNode;
id: string;
appearanceOrder: number;
canPost?: boolean;
};
type StateProps = {
sender?: ApiPeer;
canShowSender: boolean;
originSender?: ApiPeer;
isChatWithSelf?: boolean;
isRepliesChat?: boolean;
isAnonymousForwards?: boolean;
};
const SenderGroupContainer: FC<OwnProps & StateProps> = ({
message,
withAvatar,
children,
id,
appearanceOrder,
sender,
canShowSender,
originSender,
isChatWithSelf,
isRepliesChat,
isAnonymousForwards,
canPost,
}) => {
const { openChat, updateInsertingPeerIdMention } = getActions();
const ref = useRef<HTMLDivElement>();
const { forwardInfo } = message;
const messageSender = canShowSender ? sender : undefined;
const lang = useLang();
const noAppearanceAnimation = appearanceOrder <= 0;
const [isShown, markShown] = useFlag(noAppearanceAnimation);
useEffect(() => {
if (noAppearanceAnimation) {
return;
}
setTimeout(markShown, appearanceOrder * MESSAGE_APPEARANCE_DELAY);
}, [appearanceOrder, markShown, noAppearanceAnimation]);
const shouldPreferOriginSender = forwardInfo
&& (isChatWithSelf || isRepliesChat || isAnonymousForwards || !messageSender);
const avatarPeer = shouldPreferOriginSender ? originSender : messageSender;
const isAvatarPeerUser = avatarPeer && isApiPeerUser(avatarPeer);
const handleOpenChat = useLastCallback(() => {
if (!avatarPeer) {
return;
}
openChat({ id: avatarPeer.id });
});
const handleMention = useLastCallback(() => {
if (!avatarPeer) {
return;
}
const messageInput = document.querySelector<HTMLDivElement>(EDITABLE_INPUT_CSS_SELECTOR);
if (messageInput) {
updateInsertingPeerIdMention({ peerId: avatarPeer.id });
}
});
const handleAvatarClick = useLastCallback(() => {
handleOpenChat();
});
const {
ref: avatarRef,
shouldRender,
} = useShowTransition({
isOpen: withAvatar && isShown,
noMountTransition: isShown,
withShouldRender: true,
});
const {
isContextMenuOpen, contextMenuAnchor,
handleContextMenu, handleContextMenuClose,
handleContextMenuHide,
} = useContextMenuHandlers(ref);
const getTriggerElement = useLastCallback(() => avatarRef.current);
const getRootElement = useLastCallback(() => document.querySelector('.Transition_slide-active > .MessageList'));
const getMenuElement = useLastCallback(
() => ref?.current?.querySelector(`.${styles.contextMenu} .bubble`),
);
const getLayout = useLastCallback(() => ({ withPortal: true }));
const canMention = canPost && avatarPeer && (isAvatarPeerUser || Boolean(getMainUsername(avatarPeer)));
const shouldRenderContextMenu = Boolean(contextMenuAnchor) && (isAvatarPeerUser || canMention);
function renderContextMenu() {
return (
<Menu
isOpen={isContextMenuOpen}
anchor={contextMenuAnchor}
getTriggerElement={getTriggerElement}
getRootElement={getRootElement}
getLayout={getLayout}
getMenuElement={getMenuElement}
className={styles.contextMenu}
onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide}
withPortal
autoClose
>
<>
{isAvatarPeerUser && (
<MenuItem
icon="comments"
onClick={handleOpenChat}
>
{lang('SendMessage')}
</MenuItem>
)}
{canMention && (
<MenuItem
icon="mention"
onClick={handleMention}
>
{lang('ContextMenuItemMention')}
</MenuItem>
)}
</>
</Menu>
);
}
function renderAvatar() {
const hiddenName = (!avatarPeer && forwardInfo) ? forwardInfo.hiddenUserName : undefined;
return (
<Avatar
size="small"
className={styles.senderAvatar}
peer={avatarPeer}
text={hiddenName}
onClick={avatarPeer ? handleAvatarClick : undefined}
onContextMenu={handleContextMenu}
/>
);
}
const className = buildClassName(
'sender-group-container',
styles.root,
);
return (
<div id={id} className={className} ref={ref}>
{shouldRender && (
<div ref={avatarRef} className={styles.avatarContainer}>
{renderAvatar()}
</div>
)}
{children}
{shouldRenderContextMenu && renderContextMenu()}
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, ownProps): StateProps => {
const {
message, withAvatar,
} = ownProps;
const { chatId } = message;
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
const isSystemBotChat = isSystemBot(chatId);
const isAnonymousForwards = isAnonymousForwardsChat(chatId);
const forceSenderName = !isChatWithSelf && isAnonymousOwnMessage(message);
const canShowSender = withAvatar || forceSenderName;
const sender = selectSender(global, message);
const originSender = selectForwardedSender(global, message);
return {
sender,
canShowSender,
originSender,
isChatWithSelf,
isRepliesChat: isSystemBotChat,
isAnonymousForwards,
};
},
)(SenderGroupContainer));