import type { FC } from '../../lib/teact/teact'; import React, { memo, useCallback, useEffect, useMemo, useRef, } from '../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../global'; import type { ApiChat, ApiMessage, ApiSticker, ApiTopic, ApiUser, } from '../../api/types'; import type { MessageListType } from '../../global/types'; import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import type { FocusDirection } from '../../types'; import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage'; import { getChatTitle, getMessageHtmlId, isChatChannel } from '../../global/helpers'; import { getMessageReplyInfo } from '../../global/helpers/replies'; import { selectCanPlayAnimatedEmojis, selectChat, selectChatMessage, selectGiftStickerForDuration, selectIsMessageFocused, selectTabState, selectTopicFromMessage, selectUser, } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import { renderActionMessageText } from '../common/helpers/renderActionMessageText'; import renderText from '../common/helpers/renderText'; import { preventMessageInputBlur } from './helpers/preventMessageInputBlur'; import useContextMenuHandlers from '../../hooks/useContextMenuHandlers'; import useEnsureMessage from '../../hooks/useEnsureMessage'; import useFlag from '../../hooks/useFlag'; import { useIsIntersecting, useOnIntersect } from '../../hooks/useIntersectionObserver'; import useLang from '../../hooks/useLang'; import useShowTransition from '../../hooks/useShowTransition'; import useFocusMessage from './message/hooks/useFocusMessage'; import AnimatedIconFromSticker from '../common/AnimatedIconFromSticker'; import ActionMessageSuggestedAvatar from './ActionMessageSuggestedAvatar'; import ContextMenuContainer from './message/ContextMenuContainer.async'; type OwnProps = { message: ApiMessage; threadId?: number; messageListType?: MessageListType; observeIntersectionForReading?: ObserveFn; observeIntersectionForLoading?: ObserveFn; observeIntersectionForPlaying?: ObserveFn; isEmbedded?: boolean; appearanceOrder?: number; isJustAdded?: boolean; isLastInList?: boolean; isInsideTopic?: boolean; memoFirstUnreadIdRef?: { current: number | undefined }; onPinnedIntersectionChange?: PinnedIntersectionChangedCallback; }; type StateProps = { senderUser?: ApiUser; senderChat?: ApiChat; targetUserIds?: string[]; targetMessage?: ApiMessage; targetChatId?: string; targetChat?: ApiChat; isFocused: boolean; topic?: ApiTopic; focusDirection?: FocusDirection; noFocusHighlight?: boolean; premiumGiftSticker?: ApiSticker; canPlayAnimatedEmojis?: boolean; }; const APPEARANCE_DELAY = 10; const ActionMessage: FC = ({ message, isEmbedded, appearanceOrder = 0, isJustAdded, isLastInList, senderUser, senderChat, targetUserIds, targetMessage, targetChatId, targetChat, isFocused, focusDirection, noFocusHighlight, premiumGiftSticker, isInsideTopic, topic, memoFirstUnreadIdRef, canPlayAnimatedEmojis, observeIntersectionForReading, observeIntersectionForLoading, observeIntersectionForPlaying, onPinnedIntersectionChange, }) => { const { openPremiumModal, requestConfetti, checkGiftCode } = getActions(); const lang = useLang(); // eslint-disable-next-line no-null/no-null const ref = useRef(null); useOnIntersect(ref, observeIntersectionForReading); useEnsureMessage( message.chatId, message.replyInfo?.type === 'message' ? message.replyInfo.replyToMsgId : undefined, targetMessage, ); useFocusMessage(ref, message.chatId, isFocused, focusDirection, noFocusHighlight, isJustAdded); useEffect(() => { if (!message.isPinned) return undefined; return () => { onPinnedIntersectionChange?.({ viewportPinnedIdsToRemove: [message.id], isUnmount: true }); }; }, [onPinnedIntersectionChange, message.isPinned, message.id]); const noAppearanceAnimation = appearanceOrder <= 0; const [isShown, markShown] = useFlag(noAppearanceAnimation); const isGift = Boolean(message.content.action?.text.startsWith('ActionGift')); const isGiftCode = Boolean(message.content.action?.text.startsWith('BoostingReceivedGift')); const isSuggestedAvatar = message.content.action?.type === 'suggestProfilePhoto' && message.content.action!.photo; useEffect(() => { if (noAppearanceAnimation) { return; } setTimeout(markShown, appearanceOrder * APPEARANCE_DELAY); }, [appearanceOrder, markShown, noAppearanceAnimation]); const isVisible = useIsIntersecting(ref, observeIntersectionForPlaying); const shouldShowConfettiRef = useRef((() => { const isUnread = memoFirstUnreadIdRef?.current && message.id >= memoFirstUnreadIdRef.current; return isGift && !message.isOutgoing && isUnread; })()); useEffect(() => { if (isVisible && shouldShowConfettiRef.current) { shouldShowConfettiRef.current = false; requestConfetti({}); } }, [isVisible, requestConfetti]); const { transitionClassNames } = useShowTransition(isShown, undefined, noAppearanceAnimation, false); // No need for expensive global updates on users and chats, so we avoid them const usersById = getGlobal().users.byId; const targetUsers = useMemo(() => { return targetUserIds ? targetUserIds.map((userId) => usersById?.[userId]).filter(Boolean) : undefined; }, [targetUserIds, usersById]); const renderContent = useCallback(() => { return renderActionMessageText( lang, message, senderUser, senderChat, targetUsers, targetMessage, targetChatId, topic, { isEmbedded }, observeIntersectionForLoading, observeIntersectionForPlaying, ); }, [ isEmbedded, lang, message, observeIntersectionForLoading, observeIntersectionForPlaying, senderChat, senderUser, targetChatId, targetMessage, targetUsers, topic, ]); const { isContextMenuOpen, contextMenuPosition, handleBeforeContextMenu, handleContextMenu, handleContextMenuClose, handleContextMenuHide, } = useContextMenuHandlers(ref); const isContextMenuShown = contextMenuPosition !== undefined; const handleMouseDown = (e: React.MouseEvent) => { preventMessageInputBlur(e); handleBeforeContextMenu(e); }; const handlePremiumGiftClick = () => { openPremiumModal({ isGift: true, fromUserId: senderUser?.id, toUserId: targetUserIds?.[0], monthsAmount: message.content.action?.months || 0, }); }; const handleGiftCodeClick = () => { const slug = message.content.action?.slug; if (!slug) return; checkGiftCode({ slug }); }; // TODO Refactoring for action rendering const shouldSkipRender = isInsideTopic && message.content.action?.text === 'TopicWasCreatedAction'; if (shouldSkipRender) { return ; } if (isEmbedded) { return {renderContent()}; } function renderGift() { return ( {lang('ActionGiftPremiumTitle')} {lang('ActionGiftPremiumSubtitle', lang('Months', message.content.action?.months, 'i'))} {lang('ActionGiftPremiumView')} ); } function renderGiftCode() { const isFromGiveaway = message.content.action?.isGiveaway; const isUnclaimed = message.content.action?.isUnclaimed; return ( {lang(isUnclaimed ? 'BoostingUnclaimedPrize' : 'BoostingCongratulations')} {renderText(lang(isFromGiveaway ? 'BoostingReceivedGiftFrom' : isUnclaimed ? 'BoostingReceivedPrizeFrom' : 'BoostingYouHaveUnclaimedPrize', getChatTitle(lang, targetChat!)), ['simple_markdown'])} {renderText(lang( 'BoostingUnclaimedPrizeDuration', lang('Months', message.content.action?.months, 'i'), ), ['simple_markdown'])} {lang('BoostingReceivedGiftOpenBtn')} ); } const className = buildClassName( 'ActionMessage message-list-item', isFocused && !noFocusHighlight && 'focused', (isGift || isSuggestedAvatar) && 'centered-action', isContextMenuShown && 'has-menu-open', isLastInList && 'last-in-list', transitionClassNames, ); return (
{!isSuggestedAvatar && !isGiftCode && {renderContent()}} {isGift && renderGift()} {isGiftCode && renderGiftCode()} {isSuggestedAvatar && ( )} {contextMenuPosition && ( )}
); }; export default memo(withGlobal( (global, { message, threadId }): StateProps => { const { chatId, senderId, content, } = message; const userId = senderId; const { targetUserIds, targetChatId } = content.action || {}; const targetMessageId = getMessageReplyInfo(message)?.replyToMsgId; const targetMessage = targetMessageId ? selectChatMessage(global, chatId, targetMessageId) : undefined; const isFocused = threadId ? selectIsMessageFocused(global, message, threadId) : false; const { direction: focusDirection, noHighlight: noFocusHighlight, } = (isFocused && selectTabState(global).focusedMessage) || {}; const chat = selectChat(global, chatId); const isChat = chat && (isChatChannel(chat) || userId === chatId); const senderUser = !isChat && userId ? selectUser(global, userId) : undefined; const senderChat = isChat ? chat : undefined; const targetChat = targetChatId ? selectChat(global, targetChatId) : undefined; const giftDuration = content.action?.months; const premiumGiftSticker = selectGiftStickerForDuration(global, giftDuration); const topic = selectTopicFromMessage(global, message); return { senderUser, senderChat, targetChat, targetChatId, targetUserIds, targetMessage, isFocused, premiumGiftSticker, topic, canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global), ...(isFocused && { focusDirection, noFocusHighlight, }), }; }, )(ActionMessage));