import { memo, useCallback, useEffect, useMemo, useRef, useState, useUnmountCleanup, } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; import type { ApiAvailableEffect, ApiAvailableReaction, ApiChat, ApiChatMember, ApiKeyboardButton, ApiMessage, ApiMessageOutgoingStatus, ApiPeer, ApiPoll, ApiReaction, ApiReactionKey, ApiSavedReactionTag, ApiThreadInfo, ApiTopic, ApiTypeStory, ApiUser, ApiWebPage, } from '../../../api/types'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ActiveEmojiInteraction, ChatTranslatedMessages, FocusDirection, IAlbum, IDocumentGroup, MessageListType, ScrollTargetPosition, TextSummary, ThemeKey, ThreadId, } from '../../../types'; import type { Signal } from '../../../util/signals'; import { MAIN_THREAD_ID } from '../../../api/types'; import { AudioOrigin } from '../../../types'; import { EMOJI_STATUS_LOOP_LIMIT, MESSAGE_APPEARANCE_DELAY } from '../../../config'; import { areReactionsEmpty, getAllowedAttachmentOptions, getIsDownloading, getMainUsername, getMessageContent, getMessageCustomShape, getMessageHtmlId, getMessageSingleCustomEmoji, getMessageSingleRegularEmoji, getMessageWebPage, hasMessageText, hasMessageTtl, isAnonymousForwardsChat, isAnonymousOwnMessage, isChatChannel, isChatGroup, isChatPublic, isGeoLiveExpired, isMessageLocal, isMessageTranslatable, isOwnMessage, isReplyToMessage, isSystemBot, } from '../../../global/helpers'; import { getPeerFullTitle } from '../../../global/helpers/peers'; import { getMessageReplyInfo, getStoryReplyInfo } from '../../../global/helpers/replies'; import { selectActiveDownloads, selectAnimatedEmoji, selectCanAutoLoadMedia, selectCanAutoPlayMedia, selectCanReplyToMessage, selectChat, selectChatFullInfo, selectChatMessage, selectChatTranslations, selectCurrentMiddleSearch, selectDefaultReaction, selectForwardedSender, selectFullWebPageFromMessage, selectIsChatProtected, selectIsChatRestricted, selectIsChatWithBot, selectIsChatWithSelf, selectIsCurrentUserFrozen, selectIsCurrentUserPremium, selectIsDocumentGroupSelected, selectIsInSelectMode, selectIsMessageFocused, selectIsMessageProtected, selectIsMessageSelected, selectMessageSummary, selectOutgoingStatus, selectPeer, selectPeerStory, selectPerformanceSettingsValue, selectPollFromMessage, selectReplyMessage, selectRequestedChatTranslationLanguage, selectRequestedMessageTranslationLanguage, selectSender, selectSenderFromHeader, selectShouldDetectChatLanguage, selectShouldLoopStickers, selectTabState, selectTheme, selectTopicFromMessage, selectUploadProgress, selectUser, } from '../../../global/selectors'; import { selectIsMediaNsfw, selectMessageDownloadableMedia, selectMessageLastPlaybackTimestamp, selectMessageTimestampableDuration, } from '../../../global/selectors/media'; import { selectSharedSettings } from '../../../global/selectors/sharedState'; import { selectThreadInfo, selectThreadReadState } from '../../../global/selectors/threads'; import { IS_TAURI } from '../../../util/browser/globalEnvironment'; import { IS_ANDROID, IS_TRANSLATION_SUPPORTED } from '../../../util/browser/windowEnvironment'; import buildClassName from '../../../util/buildClassName'; import buildStyle from '../../../util/buildStyle'; import { isUserId } from '../../../util/entities/ids'; import { getMessageKey } from '../../../util/keys/messageKey'; import { getServerTime } from '../../../util/serverTime'; import stopEvent from '../../../util/stopEvent'; import { isElementInViewport } from '../../../util/visibility/isElementInViewport'; import { calculateDimensionsForMessageMedia, getStickerDimensions, REM } from '../../common/helpers/mediaDimensions'; import renderText from '../../common/helpers/renderText'; import { getCustomEmojiSize } from '../composer/helpers/customEmoji'; import { buildContentClassName } from './helpers/buildContentClassName'; import { calculateAlbumLayout } from './helpers/calculateAlbumLayout'; import getSingularPaidMedia from './helpers/getSingularPaidMedia'; import { calculateMediaDimensions, getMinMediaWidth, getMinMediaWidthWithText } from './helpers/mediaDimensions'; import useAppLayout from '../../../hooks/useAppLayout'; import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps'; import useEnsureMessage from '../../../hooks/useEnsureMessage'; import useEnsureStory from '../../../hooks/useEnsureStory'; import useFlag from '../../../hooks/useFlag'; import { useOnIntersect } from '../../../hooks/useIntersectionObserver'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import useOldLang from '../../../hooks/useOldLang'; import usePeerColor from '../../../hooks/usePeerColor'; import usePreviousDeprecated from '../../../hooks/usePreviousDeprecated'; import useShowTransition from '../../../hooks/useShowTransition'; import useTextLanguage from '../../../hooks/useTextLanguage'; import useDetectChatLanguage from './hooks/useDetectChatLanguage'; import useFocusMessageListElement from './hooks/useFocusMessageListElement'; import useInnerHandlers from './hooks/useInnerHandlers'; import useMessageTranslation from './hooks/useMessageTranslation'; import useOuterHandlers from './hooks/useOuterHandlers'; import Audio from '../../common/Audio'; import Avatar from '../../common/Avatar'; import CustomEmoji from '../../common/CustomEmoji'; import Document from '../../common/Document'; import DotAnimation from '../../common/DotAnimation'; import EmbeddedMessage from '../../common/embedded/EmbeddedMessage'; import EmbeddedStory from '../../common/embedded/EmbeddedStory'; import FakeIcon from '../../common/FakeIcon'; import Icon from '../../common/icons/Icon'; import StarIcon from '../../common/icons/StarIcon'; import MessageText from '../../common/MessageText'; import PeerColorWrapper from '../../common/PeerColorWrapper'; import RankBadge from '../../common/RankBadge'; import ReactionStaticEmoji from '../../common/reactions/ReactionStaticEmoji'; import Sparkles from '../../common/Sparkles'; import TopicChip from '../../common/TopicChip'; import { animateSnap } from '../../main/visualEffects/SnapEffectContainer'; import Button from '../../ui/Button'; import ConfirmDialog from '../../ui/ConfirmDialog'; import InputText from '../../ui/InputText'; import Album from './Album'; import AnimatedCustomEmoji from './AnimatedCustomEmoji'; import AnimatedEmoji from './AnimatedEmoji'; import CommentButton from './CommentButton'; import Contact from './Contact'; import ContextMenuContainer from './ContextMenuContainer.async'; import DiceWrapper from './dice/DiceWrapper'; import FactCheck from './FactCheck'; import Game from './Game'; import Giveaway from './Giveaway'; import InlineButtons from './InlineButtons'; import Invoice from './Invoice'; import InvoiceMediaPreview from './InvoiceMediaPreview'; import Location from './Location'; import MessageAppendix from './MessageAppendix'; import MessageEffect from './MessageEffect'; import MessageMeta from './MessageMeta'; import MessagePhoneCall from './MessagePhoneCall'; import PaidMediaOverlay from './PaidMediaOverlay'; import Photo from './Photo'; import Poll from './Poll'; import Reactions from './reactions/Reactions'; import RoundVideo from './RoundVideo'; import Sticker from './Sticker'; import Story from './Story'; import StoryMention from './StoryMention'; import TodoList from './TodoList'; import Video from './Video'; import WebPage from './WebPage'; import './Message.scss'; type MessagePositionProperties = { isFirstInGroup: boolean; isLastInGroup: boolean; isFirstInDocumentGroup: boolean; isLastInDocumentGroup: boolean; isLastInList: boolean; }; type OwnProps = { message: ApiMessage; album?: IAlbum; documentGroup?: IDocumentGroup; noAvatars?: boolean; withAvatar?: boolean; withSenderName?: boolean; threadId: ThreadId; messageListType: MessageListType; noComments: boolean; noReplies: boolean; appearanceOrder: number; isJustAdded: boolean; isThreadTop?: boolean; memoFirstUnreadIdRef?: { current: number | undefined }; getIsMessageListReady?: Signal; observeIntersectionForBottom?: ObserveFn; observeIntersectionForLoading?: ObserveFn; observeIntersectionForPlaying?: ObserveFn; onMessageUnmount?: (messageId: number) => void; } & MessagePositionProperties; type StateProps = { theme: ThemeKey; forceSenderName?: boolean; sender?: ApiPeer; canShowSender: boolean; originSender?: ApiPeer; botSender?: ApiUser; shouldHideReply?: boolean; replyMessage?: ApiMessage; replyMessageSender?: ApiPeer; replyMessageForwardSender?: ApiPeer; replyMessageChat?: ApiChat; isReplyPrivate?: boolean; replyStory?: ApiTypeStory; storySender?: ApiPeer; outgoingStatus?: ApiMessageOutgoingStatus; uploadProgress?: number; isInDocumentGroup: boolean; isProtected?: boolean; isChatProtected?: boolean; isFocused?: boolean; focusDirection?: FocusDirection; focusedQuote?: string; focusedQuoteOffset?: number; noFocusHighlight?: boolean; scrollTargetPosition?: ScrollTargetPosition; isResizingContainer?: boolean; isForwarding?: boolean; isChatWithSelf?: boolean; isBotForum?: boolean; isRepliesChat?: boolean; isAnonymousForwards?: boolean; isChannel?: boolean; isGroup?: boolean; canReply?: boolean; highlight?: string; animatedEmoji?: string; animatedCustomEmoji?: string; hasActiveReactions?: boolean; isInSelectMode?: boolean; isSelected?: boolean; isGroupSelected?: boolean; isDownloading?: boolean; threadId?: ThreadId; isPinnedList?: boolean; isPinned?: boolean; canAutoLoadMedia?: boolean; canAutoPlayMedia?: boolean; hasLinkedChat?: boolean; shouldLoopStickers?: boolean; autoLoadFileMaxSizeMb: number; repliesThreadInfo?: ApiThreadInfo; reactionMessage?: ApiMessage; availableReactions?: ApiAvailableReaction[]; defaultReaction?: ApiReaction; activeEmojiInteractions?: ActiveEmojiInteraction[]; hasUnreadReaction?: boolean; isTranscribing?: boolean; transcribedText?: string; isPremium: boolean; senderChatMember?: ApiChatMember; messageTopic?: ApiTopic; hasTopicChip?: boolean; chatTranslations?: ChatTranslatedMessages; areTranslationsEnabled?: boolean; shouldDetectChatLanguage?: boolean; requestedTranslationLanguage?: string; requestedChatTranslationLanguage?: string; withAnimatedEffects?: boolean; canAnimateTextStreaming?: boolean; webPageStory?: ApiTypeStory; isConnected: boolean; isLoadingComments?: boolean; shouldWarnAboutFiles?: boolean; senderBoosts?: number; tags?: Record; canTranscribeVoice?: boolean; viaBusinessBot?: ApiUser; effect?: ApiAvailableEffect; poll?: ApiPoll; webPage?: ApiWebPage; maxTimestamp?: number; lastPlaybackTimestamp?: number; paidMessageStars?: number; isChatWithUser?: boolean; isAccountFrozen?: boolean; minFutureTime?: number; isMediaNsfw?: boolean; isReplyMediaNsfw?: boolean; summary?: TextSummary; canSendStickers?: boolean; }; type MetaPosition = 'in-text' | 'standalone' | 'none'; type ReactionsPosition = 'inside' | 'outside' | 'none'; type QuickReactionPosition = 'in-content' | 'in-meta'; const NBSP = '\u00A0'; const QUICK_REACTION_SIZE = 1.75 * REM; const EXTRA_SPACE_FOR_REACTIONS = 2.25 * REM; const MAX_REASON_LENGTH = 200; const Message = ({ message, album, noAvatars, withAvatar, withSenderName, noComments, noReplies, appearanceOrder, isJustAdded, isFirstInGroup, isPremium, isLastInGroup, isFirstInDocumentGroup, isLastInDocumentGroup, isTranscribing, transcribedText, isLastInList, theme, forceSenderName, sender, canShowSender, originSender, botSender, isThreadTop, shouldHideReply, replyMessage, replyMessageSender, replyMessageForwardSender, replyMessageChat, replyStory, isReplyPrivate, storySender, outgoingStatus, uploadProgress, isInDocumentGroup, isLoadingComments, isProtected, isChatProtected, isFocused, focusDirection, focusedQuote, focusedQuoteOffset, noFocusHighlight, scrollTargetPosition, isResizingContainer, isForwarding, isChatWithSelf, isBotForum, isRepliesChat, isAnonymousForwards, isChannel, isGroup, canReply, highlight, animatedEmoji, animatedCustomEmoji, hasActiveReactions, hasLinkedChat, isInSelectMode, isSelected, isGroupSelected, threadId, reactionMessage, availableReactions, defaultReaction, activeEmojiInteractions, messageListType, isPinnedList, isPinned, isDownloading, canAutoLoadMedia, canAutoPlayMedia, shouldLoopStickers, autoLoadFileMaxSizeMb, repliesThreadInfo, hasUnreadReaction, memoFirstUnreadIdRef, senderChatMember, messageTopic, hasTopicChip, chatTranslations, areTranslationsEnabled, shouldDetectChatLanguage, requestedTranslationLanguage, requestedChatTranslationLanguage, withAnimatedEffects, canAnimateTextStreaming, webPageStory, isConnected, getIsMessageListReady, shouldWarnAboutFiles, senderBoosts, tags, canTranscribeVoice, viaBusinessBot, effect, poll, maxTimestamp, lastPlaybackTimestamp, isMediaNsfw, isReplyMediaNsfw, paidMessageStars, isChatWithUser, isAccountFrozen, minFutureTime, webPage, summary, canSendStickers, observeIntersectionForBottom, observeIntersectionForLoading, observeIntersectionForPlaying, onMessageUnmount, }: OwnProps & StateProps) => { const { toggleMessageSelection, clickBotInlineButton, clickSuggestedMessageButton, rejectSuggestedPost, openSuggestedPostApprovalModal, disableContextMenuHint, animateUnreadReaction, focusMessage, markMentionsRead, openThread, summarizeMessage, } = getActions(); const ref = useRef(); const bottomMarkerRef = useRef(); const quickReactionRef = useRef(); const oldLang = useOldLang(); const lang = useLang(); const [isTranscriptionHidden, setIsTranscriptionHidden] = useState(false); const [isPlayingSnapAnimation, setIsPlayingSnapAnimation] = useState(false); const [isPlayingDeleteAnimation, setIsPlayingDeleteAnimation] = useState(false); const [shouldPlayEffect, requestEffect, hideEffect] = useFlag(); const [shouldPlayDiceEffect, requestDiceEffect, hideDiceEffect] = useFlag(); const [isDeclineDialogOpen, openDeclineDialog, closeDeclineDialog] = useFlag(); const [isShowingSummary, showSummary, hideSummary] = useFlag(); const [declineReason, setDeclineReason] = useState(''); const { isMobile, isTouchScreen } = useAppLayout(); useOnIntersect(bottomMarkerRef, observeIntersectionForBottom); const { isContextMenuOpen, contextMenuAnchor, contextMenuTarget, handleBeforeContextMenu, handleContextMenu: onContextMenu, handleContextMenuClose, handleContextMenuHide, } = useContextMenuHandlers( ref, (isTouchScreen && isInSelectMode) || isAccountFrozen, !IS_TAURI, IS_ANDROID, getIsMessageListReady, ); useEffect(() => { if (isContextMenuOpen) { disableContextMenuHint(); } }, [isContextMenuOpen, disableContextMenuHint]); const noAppearanceAnimation = appearanceOrder <= 0; const [isShown, markShown] = useFlag(noAppearanceAnimation); useEffect(() => { if (noAppearanceAnimation) { return; } // Keep as is for now to avoid breaking appearance order setTimeout(markShown, appearanceOrder * MESSAGE_APPEARANCE_DELAY); }, [appearanceOrder, noAppearanceAnimation]); useShowTransition({ ref, isOpen: isShown || isJustAdded, noMountTransition: noAppearanceAnimation && !isJustAdded, className: false, }); useUnmountCleanup(() => { onMessageUnmount?.(messageId); }); const { id: messageId, chatId, forwardInfo, viaBotId, isTranscriptionError, factCheck, isTypingDraft, fromRank, } = message; const hasSummary = Boolean(message.summaryLanguageCode); const isLocal = isMessageLocal(message); const isOwn = isOwnMessage(message); const isScheduled = messageListType === 'scheduled' || message.isScheduled; const hasMessageReply = isReplyToMessage(message) && !shouldHideReply; const { paidMedia } = getMessageContent(message); const { photo: paidMediaPhoto, video: paidMediaVideo } = getSingularPaidMedia(paidMedia); const { photo = paidMediaPhoto, video = paidMediaVideo, audio, voice, document, sticker, contact, invoice, location, action, game, storyData, giveaway, giveawayResults, todo, dice, } = getMessageContent(message); const messageReplyInfo = getMessageReplyInfo(message); const storyReplyInfo = getStoryReplyInfo(message); const withVoiceTranscription = Boolean(!isTranscriptionHidden && (isTranscriptionError || transcribedText)); const hasStoryReply = Boolean(storyReplyInfo); const hasThread = Boolean(repliesThreadInfo) && messageListType === 'thread'; const isCustomShape = !withVoiceTranscription && getMessageCustomShape(message); const hasAnimatedEmoji = isCustomShape && (animatedEmoji || animatedCustomEmoji); const hasReactions = reactionMessage?.reactions && !areReactionsEmpty(reactionMessage.reactions); const asForwarded = ( forwardInfo && (!isChatWithSelf || isScheduled) && !isRepliesChat && !forwardInfo.isLinkedChannelPost && !isAnonymousForwards && !botSender ) || Boolean(storyData && !storyData.isMention); const canShowSenderBoosts = Boolean(senderBoosts) && !asForwarded && isFirstInGroup; const isStoryMention = storyData?.isMention; const isRoundVideo = video?.mediaType === 'video' && video.isRound; const isAlbum = Boolean(album) && ( (album.isPaidMedia && paidMedia!.extendedMedia.length > 1) || album.messages.length > 1 ) && !album.messages.some((msg) => Object.keys(msg.content).length === 0); const isInDocumentGroupNotFirst = isInDocumentGroup && !isFirstInDocumentGroup; const isInDocumentGroupNotLast = isInDocumentGroup && !isLastInDocumentGroup; const isContextMenuShown = contextMenuAnchor !== undefined; const canShowActionButton = ( !(isContextMenuShown || isInSelectMode || isForwarding) && !isInDocumentGroupNotLast && !isStoryMention ); const canForward = isChannel && !isScheduled && message.isForwardingAllowed && !isChatProtected; const canFocus = Boolean(isPinnedList || (forwardInfo && (forwardInfo.isChannelPost || isChatWithSelf || isRepliesChat || isAnonymousForwards) && forwardInfo.fromMessageId )); const hasFactCheck = Boolean(factCheck?.text); const hasForwardedCustomShape = asForwarded && isCustomShape; const hasSubheader = hasTopicChip || hasMessageReply || hasStoryReply || hasForwardedCustomShape || Boolean(isShowingSummary && summary?.text); const selectMessage = useLastCallback((e?: React.MouseEvent, groupedId?: string) => { if (isAccountFrozen) return; toggleMessageSelection({ messageId, groupedId, ...(e?.shiftKey && { withShift: true }), ...(isAlbum && { childMessageIds: album.messages.map(({ id }) => id) }), }); }); const messageSender = canShowSender ? sender : undefined; const shouldPreferOriginSender = forwardInfo && (isChatWithSelf || isRepliesChat || isAnonymousForwards || !messageSender); const avatarPeer = shouldPreferOriginSender ? originSender : messageSender; const messageColorPeer = asForwarded ? originSender : sender; const noUserColors = isOwn && !isCustomShape; const senderPeer = (forwardInfo || storyData) ? originSender : messageSender; const hasTtl = hasMessageTtl(message); const { handleMouseDown, handleClick, handleContextMenu, handleDoubleClick, handleContentDoubleClick, handleMouseMove, handleSendQuickReaction, handleMouseLeave, isSwiped, isQuickReactionVisible, handleDocumentGroupMouseEnter, } = useOuterHandlers( selectMessage, ref, messageId, Boolean(isInSelectMode), Boolean(canReply), Boolean(isProtected), onContextMenu, handleBeforeContextMenu, chatId, isContextMenuShown, quickReactionRef, isInDocumentGroupNotLast, getIsMessageListReady, ); const { handleSenderClick, handleViaBotClick, handleReplyClick, handleMediaClick, handleDocumentClick, handleAudioPlay, handleAlbumMediaClick, handlePhotoMediaClick, handleVideoMediaClick, handleMetaClick, handleTranslationClick, handleOpenThread, handleReadMedia, handleCancelUpload, handleVoteSend, handleGroupForward, handleForward, handleFocus, handleFocusForwarded, handleDocumentGroupSelectAll, handleTopicChipClick, handleStoryClick, } = useInnerHandlers({ lang: oldLang, selectMessage, message, webPage, chatId, threadId, isInDocumentGroup, asForwarded, isScheduled, album, avatarPeer, senderPeer, botSender, messageTopic, isTranslatingChat: Boolean(requestedChatTranslationLanguage), story: replyStory && 'content' in replyStory ? replyStory : undefined, isReplyPrivate, isRepliesChat, isSavedMessages: isChatWithSelf, lastPlaybackTimestamp, }); useEffect(() => { if (hasSummary && isShowingSummary && !summary) { summarizeMessage({ chatId, id: message.id, toLanguageCode: requestedTranslationLanguage, }); } }, [hasSummary, chatId, message.id, requestedTranslationLanguage, isShowingSummary, summary]); const handleEffectClick = useLastCallback((e: React.MouseEvent) => { e.stopPropagation(); requestEffect(); }); const handleFocusSelf = useLastCallback(() => { focusMessage({ chatId, threadId, messageId, scrollTargetPosition: 'start', noHighlight: true, }); }); useEffect(() => { if (!isLastInList) { return; } if (withVoiceTranscription && transcribedText) { handleFocusSelf(); } }, [isLastInList, transcribedText, withVoiceTranscription]); useEffect(() => { const element = ref.current; const isPartialAlbumDelete = message.isInAlbum && album?.messages.some((msg) => !msg.isDeleting); if (message.isDeleting && element && !isPartialAlbumDelete) { if (animateSnap(element)) { setIsPlayingSnapAnimation(true); } else { setIsPlayingDeleteAnimation(true); } } // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps -- Only start animation on `isDeleting` change }, [message.isDeleting]); const textMessage = album?.hasMultipleCaptions ? undefined : (album?.captionMessage || message); const hasTextContent = textMessage && hasMessageText(textMessage); const hasText = hasTextContent || hasFactCheck; const containerClassName = buildClassName( 'Message message-list-item', isFirstInGroup && 'first-in-group', isProtected && 'hide-on-print', isProtected && !hasTextContent ? 'is-protected' : 'allow-selection', isLastInGroup && 'last-in-group', isFirstInDocumentGroup && 'first-in-document-group', isLastInDocumentGroup && 'last-in-document-group', isLastInList && 'last-in-list', isOwn && 'own', Boolean(message.viewsCount) && 'has-views', message.isEdited && 'was-edited', hasMessageReply && 'has-reply', isContextMenuOpen && 'has-menu-open', isFocused && !noFocusHighlight && 'focused', isForwarding && 'is-forwarding', isPlayingDeleteAnimation && 'is-deleting', isPlayingSnapAnimation && 'is-dissolving', isInDocumentGroup && 'is-in-document-group', isAlbum && 'is-album', message.hasUnreadMention && 'has-unread-mention', isSelected && 'is-selected', isInSelectMode && 'is-in-selection-mode', isThreadTop && !withAvatar && 'is-thread-top', Boolean(message.inlineButtons) && 'has-inline-buttons', isSwiped && 'is-swiped', isJustAdded && 'is-just-added', (hasActiveReactions || shouldPlayEffect) && 'has-active-effect', isStoryMention && 'is-story-mention', ); const text = textMessage && getMessageContent(textMessage).text; const isInvertedMedia = Boolean(message.isInvertedMedia); const { replyToMsgId, replyToPeerId } = messageReplyInfo || {}; const { peerId: storyReplyPeerId, storyId: storyReplyId } = storyReplyInfo || {}; useEffect(() => { if ((sticker?.hasEffect || effect) && (( memoFirstUnreadIdRef?.current && messageId >= memoFirstUnreadIdRef.current ) || isLocal)) { requestEffect(); } }, [effect, isLocal, memoFirstUnreadIdRef, messageId, sticker?.hasEffect]); useEffect(() => { if (dice && (( memoFirstUnreadIdRef?.current && messageId >= memoFirstUnreadIdRef.current ) || isLocal)) { requestDiceEffect(); } }, [dice, memoFirstUnreadIdRef, messageId, isLocal]); const detectedLanguage = useTextLanguage( text?.text, !(areTranslationsEnabled && shouldDetectChatLanguage) || isTypingDraft, getIsMessageListReady, ); useDetectChatLanguage(message, detectedLanguage, !shouldDetectChatLanguage, getIsMessageListReady); const shouldTranslate = isMessageTranslatable(message, !requestedChatTranslationLanguage); const { isPending: isTranslationPending, translatedText } = useMessageTranslation( chatTranslations, chatId, shouldTranslate ? messageId : undefined, requestedTranslationLanguage, ); const isSummaryPending = Boolean(summary?.isPending); const isNewTextPending = isTranslationPending || isSummaryPending; // Used to display previous result while new one is loading const previousTranslatedText = usePreviousDeprecated(translatedText, Boolean(shouldTranslate)); useEffectWithPrevDeps(([prevIsShowingSummary]) => { if (summary?.text || (prevIsShowingSummary && !isShowingSummary)) { handleFocusSelf(); } }, [isShowingSummary, summary?.text]); const currentTranslatedText = translatedText || previousTranslatedText; const phoneCall = action?.type === 'phoneCall' ? action : undefined; const commentsThreadInfo = repliesThreadInfo?.isCommentsInfo ? repliesThreadInfo : undefined; const isLocalWithCommentButton = hasLinkedChat && isChannel && isLocal; const isMediaWithCommentButton = (commentsThreadInfo || isLocalWithCommentButton) && !isInDocumentGroupNotLast && messageListType === 'thread' && !noComments; const withCommentButton = (commentsThreadInfo || isLocalWithCommentButton) && !isInDocumentGroupNotLast && messageListType === 'thread' && !noComments; const withQuickReactionButton = !isTouchScreen && !phoneCall && !isInSelectMode && defaultReaction && !isInDocumentGroupNotLast && !isStoryMention && !hasTtl && !isAccountFrozen; const hasOutsideReactions = !withVoiceTranscription && hasReactions && (isCustomShape || ( (photo || video || storyData || (location?.mediaType === 'geo')) && (!hasText || isInvertedMedia)) ); const { className: peerColorClass, style: peerColorStyle } = usePeerColor({ peer: messageColorPeer, noUserColors, shouldReset: true, theme, }); const contentClassName = buildContentClassName(message, album, { poll, webPage, hasSubheader, isCustomShape, isLastInGroup, asForwarded, hasThread: hasThread && !noComments, forceSenderName, hasCommentCounter: hasThread && repliesThreadInfo.messagesCount !== undefined && repliesThreadInfo.messagesCount > 0, hasBottomCommentButton: withCommentButton && !isCustomShape, hasActionButton: canForward || canFocus || (withCommentButton && isCustomShape), hasReactions, isGeoLiveActive: location?.mediaType === 'geoLive' && !isGeoLiveExpired(message), withVoiceTranscription, peerColorClass, hasOutsideReactions, }); const withAppendix = contentClassName.includes('has-appendix'); const emojiSize = getCustomEmojiSize(text?.emojiOnlyCount); const paidMessageStarsInMeta = !isChatWithUser ? (isAlbum && paidMessageStars ? album.messages.length * paidMessageStars : paidMessageStars) : undefined; let metaPosition!: MetaPosition; if (phoneCall) { metaPosition = 'none'; } else if (isInDocumentGroupNotLast) { metaPosition = 'none'; } else if (hasText && !webPage && !emojiSize && !isInvertedMedia) { metaPosition = 'in-text'; } else if (isInvertedMedia && !emojiSize && (hasFactCheck || webPage)) { metaPosition = 'in-text'; } else { metaPosition = 'standalone'; } let reactionsPosition!: ReactionsPosition; if (hasReactions) { if (hasOutsideReactions) { reactionsPosition = 'outside'; } else if (asForwarded) { metaPosition = 'standalone'; reactionsPosition = 'inside'; } else { reactionsPosition = 'inside'; } } else { reactionsPosition = 'none'; } const quickReactionPosition: QuickReactionPosition = isCustomShape ? 'in-meta' : 'in-content'; useEnsureMessage( replyToPeerId || chatId, replyToMsgId, replyMessage, message.id, shouldHideReply || isReplyPrivate, ); useEnsureStory( storyReplyPeerId || chatId, storyReplyId, replyStory, ); useFocusMessageListElement({ elementRef: ref, isFocused, focusDirection, noFocusHighlight, isResizingContainer, isJustAdded, isQuote: Boolean(focusedQuote), scrollTargetPosition, }); const viaBusinessBotTitle = viaBusinessBot ? getPeerFullTitle(oldLang, viaBusinessBot) : undefined; const canShowPostAuthor = !message.senderId; const signature = viaBusinessBotTitle || (canShowPostAuthor && message.postAuthorTitle) || ((asForwarded || isChatWithSelf) && forwardInfo?.postAuthorTitle) || undefined; useEffect(() => { const bottomMarker = bottomMarkerRef.current; if (!bottomMarker || !isElementInViewport(bottomMarker)) return; if (hasUnreadReaction) { animateUnreadReaction({ chatId, messageIds: [messageId] }); } let unreadMentionIds: number[] = []; if (message.hasUnreadMention) { unreadMentionIds = [messageId]; } if (album) { unreadMentionIds = album.messages.filter((msg) => msg.hasUnreadMention).map((msg) => msg.id); } if (unreadMentionIds.length) { markMentionsRead({ chatId, messageIds: unreadMentionIds }); } }, [hasUnreadReaction, album, chatId, messageId, animateUnreadReaction, message.hasUnreadMention]); const albumLayout = useMemo(() => { return isAlbum ? calculateAlbumLayout(isOwn, Boolean(noAvatars), album, isMobile) : undefined; }, [isAlbum, isOwn, noAvatars, album, isMobile]); const extraPadding = asForwarded && !isCustomShape ? 28 : 0; const sizeCalculations = useMemo(() => { let calculatedWidth; let contentWidth: number | undefined; let style = ''; let reactionsMaxWidth; if (!isAlbum && (photo || video || invoice?.extendedMedia)) { let width: number | undefined; if (photo || video) { const media = (photo || video); if (media && !isRoundVideo) { width = calculateMediaDimensions({ media, isOwn, asForwarded, noAvatars, isMobile, }).width; } } else if (invoice?.extendedMedia && ( invoice.extendedMedia.width && invoice.extendedMedia.height )) { const { width: previewWidth, height: previewHeight } = invoice.extendedMedia; width = calculateDimensionsForMessageMedia({ width: previewWidth, height: previewHeight, fromOwnMessage: isOwn, asForwarded, noAvatars, isMobile, }).width; } if (width) { if (width < getMinMediaWidthWithText(isMobile)) { contentWidth = width; } calculatedWidth = Math.max(getMinMediaWidth(text?.text, isMobile, isMediaWithCommentButton), width); } } else if (albumLayout) { const minWidth = getMinMediaWidth(text?.text, isMobile, isMediaWithCommentButton); calculatedWidth = Math.max(minWidth, albumLayout.containerStyle.width); } if (calculatedWidth) { style = `width: ${calculatedWidth}px`; reactionsMaxWidth = calculatedWidth + EXTRA_SPACE_FOR_REACTIONS; } else if (sticker && !hasSubheader) { const { width } = getStickerDimensions(sticker, isMobile); style = `width: ${width + extraPadding}px`; reactionsMaxWidth = width + EXTRA_SPACE_FOR_REACTIONS; } return { contentWidth, style, reactionsMaxWidth, }; }, [ albumLayout, asForwarded, extraPadding, hasSubheader, invoice?.extendedMedia, isAlbum, isMediaWithCommentButton, isMobile, isOwn, noAvatars, photo, sticker, text?.text, video, isRoundVideo, ]); const { contentWidth, style: sizeStyles, reactionsMaxWidth, } = sizeCalculations; const contentStyle = buildStyle(peerColorStyle, sizeStyles); function renderMessageText(isForAnimation?: boolean) { if (!textMessage) return undefined; const forcedText = (isShowingSummary && summary?.text) || (requestedTranslationLanguage ? currentTranslatedText : undefined); return ( ); } function renderMessageTextAnimation() { return (
{renderMessageText(true)}
); } const renderQuickReactionButton = useCallback(() => { if (!defaultReaction) return undefined; return (
); }, [ hasActiveReactions, availableReactions, defaultReaction, handleSendQuickReaction, isQuickReactionVisible, observeIntersectionForPlaying, ]); function renderReactionsAndMeta() { const meta = ( ); if (reactionsPosition !== 'inside') { return meta; } return ( ); } function renderContent() { const className = buildClassName( 'content-inner', asForwarded && 'forwarded-message', hasForwardedCustomShape && 'forwarded-custom-shape', hasSubheader && 'with-subheader', ); const hasCustomAppendix = isLastInGroup && (!hasText || (isInvertedMedia && !hasFactCheck && reactionsPosition !== 'inside')) && !withCommentButton; const textContentClass = buildClassName( 'text-content', 'clearfix', metaPosition === 'in-text' && 'with-meta', outgoingStatus && 'with-outgoing-icon', ); const shouldReadMedia = !hasTtl || !isOwn || isChatWithSelf; return (
{!asForwarded && shouldRenderSenderName() && renderSenderName()} {hasSubheader && (
{hasTopicChip && ( )} {hasForwardedCustomShape && (
{renderForwardTitle()}
{renderSenderName(true, true)}
)} {hasMessageReply && ( )} {hasStoryReply && ( )} {hasSummary && isShowingSummary && !summary?.isPending && ( {lang('MessageSummaryTitle')} {lang('MessageSummaryDescription')} )}
)} {sticker && observeIntersectionForLoading && observeIntersectionForPlaying && ( )} {hasAnimatedEmoji && animatedCustomEmoji && ( )} {hasAnimatedEmoji && animatedEmoji && ( )} {withAnimatedEffects && effect && !isLocal && ( )} {phoneCall && ( )} {!isAlbum && isRoundVideo && !withVoiceTranscription && ( )} {(audio || voice || withVoiceTranscription) && (
); } function renderInvertedMediaContent(hasCustomAppendix: boolean) { const textContentClass = buildClassName( 'text-content', 'clearfix', ); const footerClass = buildClassName( 'text-content', 'clearfix', metaPosition === 'in-text' && 'with-meta', outgoingStatus && 'with-outgoing-icon', ); const hasMediaAfterText = isAlbum || (!isAlbum && photo) || (!isAlbum && video && !isRoundVideo); const hasContentAfterText = hasMediaAfterText || (!hasAnimatedEmoji && hasFactCheck); const isMetaInText = metaPosition === 'in-text'; return ( <> {renderWebPage()} {hasText && !hasAnimatedEmoji && (
{renderMessageText()} {isNewTextPending && renderMessageTextAnimation()} {!hasContentAfterText && isMetaInText && renderReactionsAndMeta()}
)} {hasContentAfterText && ( <> {renderInvertibleMediaContent(hasCustomAppendix)} {!hasAnimatedEmoji && (
{hasFactCheck && ( )} {isMetaInText && renderReactionsAndMeta()}
)} )} ); } function renderWebPage() { const messageWebPage = getMessageWebPage(message); if (!messageWebPage || !webPage) return undefined; return ( ); } function renderInvertibleMediaContent(hasCustomAppendix: boolean) { const content = ( <> {isAlbum && observeIntersectionForLoading && ( )} {!isAlbum && photo && ( )} {!isAlbum && video && !isRoundVideo && (