import type { FC } from '../../lib/teact/teact'; import React, { memo, useCallback, useEffect, useMemo, useRef, useUnmountCleanup, } from '../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../global'; import type { ApiChat, ApiMessage, ApiSticker, ApiTopic, ApiUser, } from '../../api/types'; import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import type { FocusDirection, MessageListType, ThreadId } from '../../types'; import type { OnIntersectPinnedMessage } from './hooks/usePinnedMessage'; import { getChatTitle, getMessageHtmlId, isJoinedChannelMessage } from '../../global/helpers'; import { getMessageReplyInfo } from '../../global/helpers/replies'; import { selectCanPlayAnimatedEmojis, selectChat, selectChatMessage, selectGiftStickerForDuration, selectGiftStickerForStars, selectIsCurrentUserPremium, selectIsMessageFocused, selectStarGiftSticker, selectTabState, selectTheme, selectTopicFromMessage, selectUser, } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import { formatInteger, formatIntegerCompact } from '../../util/textFormat'; import { renderActionMessageText } from '../common/helpers/renderActionMessageText'; import renderText from '../common/helpers/renderText'; import { renderTextWithEntities } from '../common/helpers/renderTextWithEntities'; 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 useOldLang from '../../hooks/useOldLang'; import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated'; import useFocusMessage from './message/hooks/useFocusMessage'; import AnimatedIconFromSticker from '../common/AnimatedIconFromSticker'; import Avatar from '../common/Avatar'; import GiftRibbon from '../common/gift/GiftRibbon'; import Sparkles from '../common/Sparkles'; import ActionMessageSuggestedAvatar from './ActionMessageSuggestedAvatar'; import ActionMessageUpdatedAvatar from './ActionMessageUpdatedAvatar'; import ContextMenuContainer from './message/ContextMenuContainer.async'; import Reactions from './message/reactions/Reactions'; import SimilarChannels from './message/SimilarChannels'; type OwnProps = { message: ApiMessage; threadId?: ThreadId; messageListType?: MessageListType; observeIntersectionForReading?: ObserveFn; observeIntersectionForLoading?: ObserveFn; observeIntersectionForPlaying?: ObserveFn; isEmbedded?: boolean; appearanceOrder?: number; isJustAdded?: boolean; isLastInList?: boolean; isInsideTopic?: boolean; memoFirstUnreadIdRef?: { current: number | undefined }; onIntersectPinnedMessage?: OnIntersectPinnedMessage; }; type StateProps = { senderUser?: ApiUser; senderChat?: ApiChat; targetUserIds?: string[]; targetMessage?: ApiMessage; targetChatId?: string; targetChat?: ApiChat; isFocused: boolean; topic?: ApiTopic; focusDirection?: FocusDirection; noFocusHighlight?: boolean; premiumGiftSticker?: ApiSticker; starGiftSticker?: ApiSticker; starsGiftSticker?: ApiSticker; canPlayAnimatedEmojis?: boolean; patternColor?: string; isCurrentUserPremium?: boolean; }; const APPEARANCE_DELAY = 10; const STAR_GIFT_STICKER_SIZE = 120; const ActionMessage: FC = ({ message, threadId, isEmbedded, appearanceOrder = 0, isJustAdded, isLastInList, senderUser, senderChat, targetUserIds, targetMessage, targetChatId, targetChat, isFocused, focusDirection, noFocusHighlight, premiumGiftSticker, starGiftSticker, starsGiftSticker, isInsideTopic, topic, memoFirstUnreadIdRef, canPlayAnimatedEmojis, patternColor, observeIntersectionForReading, observeIntersectionForLoading, observeIntersectionForPlaying, onIntersectPinnedMessage, isCurrentUserPremium, }) => { const { openPremiumModal, requestConfetti, checkGiftCode, getReceipt, openGiftInfoModalFromMessage, openPrizeStarsTransactionFromGiveaway, } = getActions(); const oldLang = useOldLang(); 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({ elementRef: ref, chatId: message.chatId, isFocused, focusDirection, noFocusHighlight, isJustAdded, }); useUnmountCleanup(() => { if (message.isPinned) { onIntersectPinnedMessage?.({ viewportPinnedIdsToRemove: [message.id] }); } }); const noAppearanceAnimation = appearanceOrder <= 0; const [isShown, markShown] = useFlag(noAppearanceAnimation); const isPremiumGift = message.content.action?.type === 'giftPremium'; const isGiftCode = message.content.action?.type === 'giftCode'; const isSuggestedAvatar = message.content.action?.type === 'suggestProfilePhoto' && message.content.action!.photo; const isUpdatedAvatar = message.content.action?.type === 'updateProfilePhoto' && message.content.action!.photo; const isJoinedMessage = isJoinedChannelMessage(message); const isStarsGift = message.content.action?.type === 'giftStars'; const isStarGift = message.content.action?.type === 'starGift'; const isPrizeStars = message.content.action?.type === 'prizeStars'; const withServiceReactions = Boolean(message.areReactionsPossible && message?.reactions); 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 isPremiumGift && !message.isOutgoing && isUnread; })()); useEffect(() => { if (isVisible && shouldShowConfettiRef.current) { shouldShowConfettiRef.current = false; requestConfetti({ withStars: true }); } }, [isVisible, requestConfetti]); const { transitionClassNames } = useShowTransitionDeprecated(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( oldLang, message, senderUser, senderChat, targetUsers, targetMessage, targetChatId, topic, { isEmbedded }, observeIntersectionForLoading, observeIntersectionForPlaying, ); }, [ isEmbedded, message, observeIntersectionForLoading, observeIntersectionForPlaying, oldLang, senderChat, senderUser, targetChatId, targetMessage, targetUsers, topic, ]); const { isContextMenuOpen, contextMenuAnchor, handleBeforeContextMenu, handleContextMenu, handleContextMenuClose, handleContextMenuHide, } = useContextMenuHandlers(ref); const isContextMenuShown = contextMenuAnchor !== undefined; const handleMouseDown = (e: React.MouseEvent) => { preventMessageInputBlur(e); handleBeforeContextMenu(e); }; const handleStarGiftClick = () => { openGiftInfoModalFromMessage({ chatId: message.chatId, messageId: message.id, }); }; const handlePremiumGiftClick = () => { openPremiumModal({ isGift: true, fromUserId: senderUser?.id, toUserId: targetUserIds?.[0], monthsAmount: message.content.action?.months || 0, }); }; const handlePrizeStarsClick = () => { openPrizeStarsTransactionFromGiveaway({ chatId: message.chatId, messageId: message.id, }); }; const handleGiftCodeClick = () => { const slug = message.content.action?.slug; if (!slug) return; checkGiftCode({ slug, message: { chatId: message.chatId, messageId: message.id } }); }; const handleClick = () => { if (message.content.action?.type === 'receipt') { getReceipt({ chatId: message.chatId, messageId: message.id, }); } }; // TODO Refactoring for action rendering const shouldSkipRender = isInsideTopic && message.content.action?.text === 'TopicWasCreatedAction'; if (shouldSkipRender) { return ; } if (isEmbedded) { return {renderContent()}; } function renderGift() { const giftMessage = message.content.action?.message; return ( {oldLang('ActionGiftPremiumTitle')} {oldLang('ActionGiftPremiumSubtitle', oldLang('Months', message.content.action?.months, 'i'))} {giftMessage && (
{renderTextWithEntities({ text: giftMessage.text, entities: giftMessage.entities })}
)} {oldLang('ActionGiftPremiumView')}
); } function renderGiftCode() { const isFromGiveaway = message.content.action?.isGiveaway; const isUnclaimed = message.content.action?.isUnclaimed; const giftMessage = message.content.action?.message; return ( {oldLang(isUnclaimed ? 'BoostingUnclaimedPrize' : 'BoostingCongratulations')} {targetChat && renderText( oldLang( isFromGiveaway ? 'BoostingReceivedGiftFrom' : isUnclaimed ? 'BoostingReceivedPrizeFrom' : 'BoostingYouHaveUnclaimedPrize', getChatTitle(oldLang, targetChat), ), ['simple_markdown'], )} {renderText(oldLang( 'BoostingUnclaimedPrizeDuration', oldLang('Months', message.content.action?.months, 'i'), ), ['simple_markdown'])} {giftMessage && (
{renderTextWithEntities({ text: giftMessage.text, entities: giftMessage.entities })}
)} {oldLang('BoostingReceivedGiftOpenBtn')}
); } function renderStarsGift() { return (
{formatInteger(message.content.action!.stars!)} {oldLang('Stars')}
{renderText( oldLang(!message.isOutgoing ? 'ActionGiftStarsSubtitleYou' : 'ActionGiftStarsSubtitle', getChatTitle(oldLang, targetChat!)), ['simple_markdown'], )} {oldLang('ActionGiftPremiumView')}
); } function renderStarGiftUserCaption() { const targetUser = targetUsers && targetUsers[0]; if (!targetUser || !senderUser) return undefined; if (message.isOutgoing) { return (
{lang('GiftTo')} {targetUser.firstName}
); } return (
{lang('GiftFrom')} {senderUser.firstName}
); } function renderStarGiftUserDescription() { const starGift = message.content.action?.starGift; const targetUser = targetUsers && targetUsers[0]?.firstName; const starGiftMessage = message.content.action?.starGift?.message; if (!starGift) return undefined; if (starGiftMessage) { return renderTextWithEntities({ text: starGiftMessage.text, entities: starGiftMessage.entities }); } const amount = starGift?.starsToConvert; if (message.isOutgoing) { return lang('ActionStarGiftOutDescription', { user: targetUser || 'User', count: amount, }, { withNodes: true }); } if (starGift.isSaved) { return lang('ActionStarGiftDisplaying'); } if (starGift.isConverted) { return message.isOutgoing ? lang('GiftInfoDescriptionOutConverted', { amount: formatInteger(amount!), user: targetUser || 'User', }, { pluralValue: amount!, withNodes: true, withMarkdown: true, }) : lang('GiftInfoDescriptionConverted', { amount: formatInteger(amount!), }, { pluralValue: amount!, withNodes: true, withMarkdown: true, }); } return lang('ActionStarGiftDescription', { count: amount, }, { withNodes: true }); } function renderStarGift() { const starGift = message.content.action?.starGift; if (!starGift) return undefined; return ( {renderStarGiftUserCaption()}
{renderStarGiftUserDescription()}
{!message.isOutgoing && (
{oldLang('ActionGiftPremiumView')}
)} {starGift.gift.availabilityTotal && ( )}
); } function renderPrizeStars() { const isUnclaimed = message.content.action?.isUnclaimed; return ( {oldLang(isUnclaimed ? 'BoostingUnclaimedPrize' : 'BoostingCongratulations')} {targetChat && renderText(oldLang(isUnclaimed ? 'BoostingReceivedPrizeFrom' : 'BoostingYouHaveUnclaimedPrize', getChatTitle(oldLang, targetChat)), ['simple_markdown'])} {renderText(lang( 'PrizeCredits', { count: ( {formatInteger(message.content.action?.stars!)} ), }, { withNodes: true, }, ), ['simple_markdown'])} { oldLang('ActionGiftPremiumView') } ); } const className = buildClassName( 'ActionMessage message-list-item', isFocused && !noFocusHighlight && 'focused', (isPremiumGift || isSuggestedAvatar || isUpdatedAvatar) && 'centered-action', isContextMenuShown && 'has-menu-open', isLastInList && 'last-in-list', transitionClassNames, ); return (
{!isSuggestedAvatar && !isGiftCode && !isJoinedMessage && !isUpdatedAvatar && ( {renderContent()} )} {isPremiumGift && renderGift()} {isGiftCode && renderGiftCode()} {isStarsGift && renderStarsGift()} {isStarGift && renderStarGift()} {isPrizeStars && renderPrizeStars()} {isSuggestedAvatar && ( )} {isUpdatedAvatar && ( )} {isJoinedMessage && } {contextMenuAnchor && ( )} {withServiceReactions && ( )}
); }; export default memo(withGlobal( (global, { message, threadId }): StateProps => { const { chatId, senderId, content, } = message; const { targetUserIds, targetChatId } = content.action || {}; const targetMessageId = getMessageReplyInfo(message)?.replyToMsgId; const targetMessage = targetMessageId ? selectChatMessage(global, chatId, targetMessageId) : undefined; const theme = selectTheme(global); const { patternColor, } = global.settings.themes[theme] || {}; const isFocused = threadId ? selectIsMessageFocused(global, message, threadId) : false; const { direction: focusDirection, noHighlight: noFocusHighlight, } = (isFocused && selectTabState(global).focusedMessage) || {}; const senderUser = selectUser(global, senderId || chatId); const senderChat = selectChat(global, chatId); const targetChat = targetChatId ? selectChat(global, targetChatId) : undefined; const giftDuration = content.action?.months; const premiumGiftSticker = selectGiftStickerForDuration(global, giftDuration); const starGift = content.action?.type === 'starGift' ? content.action.starGift?.gift : undefined; const starCount = content.action?.stars; const starGiftSticker = starGift?.stickerId ? selectStarGiftSticker(global, starGift.stickerId) : undefined; const starsGiftSticker = selectGiftStickerForStars(global, starCount); const topic = selectTopicFromMessage(global, message); return { senderUser, senderChat, targetChat, targetChatId, targetUserIds, targetMessage, isFocused, premiumGiftSticker, starGiftSticker, starsGiftSticker, topic, patternColor, canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global), ...(isFocused && { focusDirection, noFocusHighlight, }), isCurrentUserPremium: selectIsCurrentUserPremium(global), }; }, )(ActionMessage));