import type { FC } from '../../../lib/teact/teact'; import React, { memo, useCallback, useEffect, useMemo, useRef, useState, } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; import type { ActiveEmojiInteraction, ActiveReaction, MessageListType } from '../../../global/types'; import type { ApiMessage, ApiMessageOutgoingStatus, ApiUser, ApiChat, ApiThreadInfo, ApiAvailableReaction, ApiChatMember, ApiUsername, ApiTopic, ApiReaction, ApiStickerSet, } from '../../../api/types'; import type { AnimationLevel, FocusDirection, IAlbum, ISettings, } from '../../../types'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import { AudioOrigin } from '../../../types'; import { MAIN_THREAD_ID } from '../../../api/types'; import { IS_ANDROID, IS_TOUCH_ENV } from '../../../util/environment'; import { EMOJI_STATUS_LOOP_LIMIT, GENERAL_TOPIC_ID } from '../../../config'; import { selectChat, selectChatMessage, selectUploadProgress, selectIsChatWithSelf, selectOutgoingStatus, selectUser, selectIsMessageFocused, selectCurrentTextSearch, selectIsInSelectMode, selectIsMessageSelected, selectIsDocumentGroupSelected, selectSender, selectForwardedSender, selectThreadTopMessageId, selectCanAutoLoadMedia, selectCanAutoPlayMedia, selectShouldLoopStickers, selectTheme, selectAllowedMessageActions, selectIsDownloading, selectThreadInfo, selectMessageIdsByGroupId, selectIsMessageProtected, selectDefaultReaction, selectReplySender, selectAnimatedEmoji, selectIsCurrentUserPremium, selectIsChatProtected, selectTopicFromMessage, selectTabState, } from '../../../global/selectors'; import { getMessageContent, isOwnMessage, isReplyMessage, isAnonymousOwnMessage, isMessageLocal, isUserId, isChatWithRepliesBot, getMessageCustomShape, isChatChannel, getMessageSingleRegularEmoji, getSenderTitle, getUserColorKey, areReactionsEmpty, getMessageHtmlId, isGeoLiveExpired, getMessageSingleCustomEmoji, hasMessageText, isChatGroup, getMessageLocation, } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; import { calculateDimensionsForMessageMedia, REM, ROUND_VIDEO_DIMENSIONS_PX, } from '../../common/helpers/mediaDimensions'; import { buildContentClassName } from './helpers/buildContentClassName'; import { getMinMediaWidth, calculateMediaDimensions } from './helpers/mediaDimensions'; import { calculateAlbumLayout } from './helpers/calculateAlbumLayout'; import renderText from '../../common/helpers/renderText'; import calculateAuthorWidth from './helpers/calculateAuthorWidth'; import { getServerTime } from '../../../util/serverTime'; import { isElementInViewport } from '../../../util/isElementInViewport'; import { getCustomEmojiSize } from '../composer/helpers/customEmoji'; import { isAnimatingScroll } from '../../../util/fastSmoothScroll'; import useEnsureMessage from '../../../hooks/useEnsureMessage'; import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; import { useOnIntersect } from '../../../hooks/useIntersectionObserver'; import useLang from '../../../hooks/useLang'; import useShowTransition from '../../../hooks/useShowTransition'; import useFlag from '../../../hooks/useFlag'; import useFocusMessage from './hooks/useFocusMessage'; import useOuterHandlers from './hooks/useOuterHandlers'; import useInnerHandlers from './hooks/useInnerHandlers'; import useAppLayout from '../../../hooks/useAppLayout'; import useResizeObserver from '../../../hooks/useResizeObserver'; import useThrottledCallback from '../../../hooks/useThrottledCallback'; import Button from '../../ui/Button'; import Avatar from '../../common/Avatar'; import EmbeddedMessage from '../../common/EmbeddedMessage'; import Document from '../../common/Document'; import Audio from '../../common/Audio'; import MessageMeta from './MessageMeta'; import ContextMenuContainer from './ContextMenuContainer.async'; import Sticker from './Sticker'; import AnimatedEmoji from './AnimatedEmoji'; import AnimatedCustomEmoji from './AnimatedCustomEmoji'; import Photo from './Photo'; import Video from './Video'; import Contact from './Contact'; import Poll from './Poll'; import WebPage from './WebPage'; import Invoice from './Invoice'; import InvoiceMediaPreview from './InvoiceMediaPreview'; import Location from './Location'; import Game from './Game'; import Album from './Album'; import RoundVideo from './RoundVideo'; import InlineButtons from './InlineButtons'; import CommentButton from './CommentButton'; import Reactions from './Reactions'; import ReactionStaticEmoji from '../../common/ReactionStaticEmoji'; import MessagePhoneCall from './MessagePhoneCall'; import DotAnimation from '../../common/DotAnimation'; import CustomEmoji from '../../common/CustomEmoji'; import PremiumIcon from '../../common/PremiumIcon'; import FakeIcon from '../../common/FakeIcon'; import MessageText from '../../common/MessageText'; import TopicChip from '../../common/TopicChip'; import './Message.scss'; type MessagePositionProperties = { isFirstInGroup: boolean; isLastInGroup: boolean; isFirstInDocumentGroup: boolean; isLastInDocumentGroup: boolean; isLastInList: boolean; }; type OwnProps = { message: ApiMessage; observeIntersectionForBottom: ObserveFn; observeIntersectionForLoading: ObserveFn; observeIntersectionForPlaying: ObserveFn; album?: IAlbum; noAvatars?: boolean; withAvatar?: boolean; withSenderName?: boolean; threadId: number; messageListType: MessageListType; noComments: boolean; appearanceOrder: number; memoFirstUnreadIdRef: { current: number | undefined }; } & MessagePositionProperties; type StateProps = { theme: ISettings['theme']; forceSenderName?: boolean; chatUsernames?: ApiUsername[]; sender?: ApiUser | ApiChat; canShowSender: boolean; originSender?: ApiUser | ApiChat; botSender?: ApiUser; isThreadTop?: boolean; shouldHideReply?: boolean; replyMessage?: ApiMessage; replyMessageSender?: ApiUser | ApiChat; outgoingStatus?: ApiMessageOutgoingStatus; uploadProgress?: number; isInDocumentGroup: boolean; isProtected?: boolean; isChatProtected?: boolean; isFocused?: boolean; focusDirection?: FocusDirection; noFocusHighlight?: boolean; isResizingContainer?: boolean; isForwarding?: boolean; isChatWithSelf?: boolean; isRepliesChat?: boolean; isChannel?: boolean; isGroup?: boolean; canReply?: boolean; lastSyncTime?: number; highlight?: string; animatedEmoji?: string; animatedCustomEmoji?: string; genericEffects?: ApiStickerSet; isInSelectMode?: boolean; isSelected?: boolean; isGroupSelected?: boolean; isDownloading: boolean; threadId?: number; isPinnedList?: boolean; canAutoLoadMedia?: boolean; canAutoPlayMedia?: boolean; shouldLoopStickers?: boolean; autoLoadFileMaxSizeMb: number; repliesThreadInfo?: ApiThreadInfo; reactionMessage?: ApiMessage; availableReactions?: ApiAvailableReaction[]; defaultReaction?: ApiReaction; activeReactions?: ActiveReaction[]; activeEmojiInteractions?: ActiveEmojiInteraction[]; hasUnreadReaction?: boolean; isTranscribing?: boolean; transcribedText?: string; isTranscriptionError?: boolean; isPremium: boolean; animationLevel: AnimationLevel; senderAdminMember?: ApiChatMember; messageTopic?: ApiTopic; hasTopicChip?: boolean; }; type MetaPosition = 'in-text' | 'standalone' | 'none'; type ReactionsPosition = 'inside' | 'outside' | 'none'; const NBSP = '\u00A0'; // eslint-disable-next-line max-len const APPENDIX_OWN = { __html: '' }; // eslint-disable-next-line max-len const APPENDIX_NOT_OWN = { __html: '' }; const APPEARANCE_DELAY = 10; const NO_MEDIA_CORNERS_THRESHOLD = 18; const QUICK_REACTION_SIZE = 1.75 * REM; const BOTTOM_FOCUS_SCROLL_THRESHOLD = 5; const THROTTLE_MS = 300; const Message: FC = ({ message, chatUsernames, observeIntersectionForBottom, observeIntersectionForLoading, observeIntersectionForPlaying, album, noAvatars, withAvatar, withSenderName, noComments, appearanceOrder, isFirstInGroup, isPremium, isLastInGroup, isFirstInDocumentGroup, isLastInDocumentGroup, isTranscribing, transcribedText, isLastInList, theme, forceSenderName, sender, canShowSender, originSender, botSender, isThreadTop, shouldHideReply, replyMessage, replyMessageSender, outgoingStatus, uploadProgress, isInDocumentGroup, isProtected, isChatProtected, isFocused, focusDirection, noFocusHighlight, isResizingContainer, isForwarding, isChatWithSelf, isRepliesChat, isChannel, isGroup, canReply, lastSyncTime, highlight, animatedEmoji, animatedCustomEmoji, genericEffects, isInSelectMode, isSelected, isGroupSelected, threadId, reactionMessage, availableReactions, defaultReaction, activeReactions, activeEmojiInteractions, messageListType, isPinnedList, isDownloading, canAutoLoadMedia, canAutoPlayMedia, shouldLoopStickers, autoLoadFileMaxSizeMb, repliesThreadInfo, hasUnreadReaction, memoFirstUnreadIdRef, animationLevel, senderAdminMember, messageTopic, hasTopicChip, }) => { const { toggleMessageSelection, clickBotInlineButton, disableContextMenuHint, animateUnreadReaction, focusLastMessage, } = getActions(); // eslint-disable-next-line no-null/no-null const ref = useRef(null); // eslint-disable-next-line no-null/no-null const bottomMarkerRef = useRef(null); // eslint-disable-next-line no-null/no-null const contentRef = useRef(null); const messageHeightRef = useRef(0); const lang = useLang(); const [isTranscriptionHidden, setTranscriptionHidden] = useState(false); const [hasActiveStickerEffect, startStickerEffect, stopStickerEffect] = useFlag(); const { isMobile } = useAppLayout(); useOnIntersect(bottomMarkerRef, observeIntersectionForBottom); const { isContextMenuOpen, contextMenuPosition, handleBeforeContextMenu, handleContextMenu: onContextMenu, handleContextMenuClose, handleContextMenuHide, } = useContextMenuHandlers(ref, IS_TOUCH_ENV && isInSelectMode, true, IS_ANDROID); useEffect(() => { if (isContextMenuOpen) { disableContextMenuHint(); } }, [isContextMenuOpen, disableContextMenuHint]); const noAppearanceAnimation = appearanceOrder <= 0; const [isShown, markShown] = useFlag(noAppearanceAnimation); useEffect(() => { if (noAppearanceAnimation) { return; } setTimeout(markShown, appearanceOrder * APPEARANCE_DELAY); }, [appearanceOrder, markShown, noAppearanceAnimation]); const { transitionClassNames } = useShowTransition(isShown, undefined, noAppearanceAnimation, false); const { id: messageId, chatId, forwardInfo, viaBotId, isTranscriptionError, } = message; const isLocal = isMessageLocal(message); const isOwn = isOwnMessage(message); const isScheduled = messageListType === 'scheduled' || message.isScheduled; const hasReply = isReplyMessage(message) && !shouldHideReply; const hasThread = Boolean(repliesThreadInfo) && messageListType === 'thread'; const isCustomShape = getMessageCustomShape(message); const hasAnimatedEmoji = isCustomShape && (animatedEmoji || animatedCustomEmoji); const hasReactions = reactionMessage?.reactions && !areReactionsEmpty(reactionMessage.reactions); const asForwarded = ( forwardInfo && (!isChatWithSelf || isScheduled) && !isRepliesChat && !forwardInfo.isLinkedChannelPost && !isCustomShape ); const isAlbum = Boolean(album) && album!.messages.length > 1 && !album?.messages.some((msg) => Object.keys(msg.content).length === 0); const isInDocumentGroupNotFirst = isInDocumentGroup && !isFirstInDocumentGroup; const isInDocumentGroupNotLast = isInDocumentGroup && !isLastInDocumentGroup; const isContextMenuShown = contextMenuPosition !== undefined; const canShowActionButton = ( !(isContextMenuShown || isInSelectMode || isForwarding) && !isInDocumentGroupNotLast ); const canForward = isChannel && !isScheduled && message.isForwardingAllowed && !isChatProtected; const canFocus = Boolean(isPinnedList || (forwardInfo && (forwardInfo.isChannelPost || (isChatWithSelf && !isOwn) || isRepliesChat) && forwardInfo.fromMessageId )); const hasSubheader = hasTopicChip || hasReply; const selectMessage = useCallback((e?: React.MouseEvent, groupedId?: string) => { toggleMessageSelection({ messageId, groupedId, ...(e?.shiftKey && { withShift: true }), ...(isAlbum && { childMessageIds: album!.messages.map(({ id }) => id) }), }); }, [toggleMessageSelection, messageId, isAlbum, album]); const messageSender = canShowSender ? sender : undefined; const withVoiceTranscription = Boolean(!isTranscriptionHidden && (isTranscriptionError || transcribedText)); const avatarPeer = forwardInfo && (isChatWithSelf || isRepliesChat || !messageSender) ? originSender : messageSender; const senderPeer = forwardInfo ? originSender : messageSender; const { handleMouseDown, handleClick, handleContextMenu, handleDoubleClick, handleContentDoubleClick, handleMouseMove, handleSendQuickReaction, handleMouseLeave, isSwiped, isQuickReactionVisible, handleDocumentGroupMouseEnter, } = useOuterHandlers( selectMessage, ref, messageId, isAlbum, Boolean(isInSelectMode), Boolean(canReply), Boolean(isProtected), onContextMenu, handleBeforeContextMenu, chatId, isContextMenuShown, contentRef, isOwn, isInDocumentGroupNotLast, ); const { handleAvatarClick, handleSenderClick, handleViaBotClick, handleReplyClick, handleMediaClick, handleAudioPlay, handleAlbumMediaClick, handleMetaClick, handleReadMedia, handleCancelUpload, handleVoteSend, handleGroupForward, handleForward, handleFocus, handleFocusForwarded, handleDocumentGroupSelectAll, handleTopicChipClick, } = useInnerHandlers( lang, selectMessage, message, chatId, threadId, isInDocumentGroup, asForwarded, isScheduled, isRepliesChat, album, avatarPeer, senderPeer, botSender, messageTopic, ); useEffect(() => { if (!isLastInList) { return; } if (withVoiceTranscription && transcribedText) { focusLastMessage(); } }, [focusLastMessage, isLastInList, transcribedText, withVoiceTranscription]); const containerClassName = buildClassName( 'Message message-list-item', isFirstInGroup && 'first-in-group', isProtected && 'is-protected', isLastInGroup && 'last-in-group', isFirstInDocumentGroup && 'first-in-document-group', isLastInDocumentGroup && 'last-in-document-group', isLastInList && 'last-in-list', isOwn && 'own', Boolean(message.views) && 'has-views', message.isEdited && 'was-edited', hasReply && 'has-reply', isContextMenuShown && 'has-menu-open', isFocused && !noFocusHighlight && 'focused', isForwarding && 'is-forwarding', message.isDeleting && 'is-deleting', isInDocumentGroup && 'is-in-document-group', isAlbum && 'is-album', message.hasUnreadMention && 'has-unread-mention', isSelected && 'is-selected', isInSelectMode && 'is-in-selection-mode', isThreadTop && 'is-thread-top', Boolean(message.inlineButtons) && 'has-inline-buttons', isSwiped && 'is-swiped', transitionClassNames, (Boolean(activeReactions) || hasActiveStickerEffect) && 'has-active-reaction', ); const { text, photo, video, audio, voice, document, sticker, contact, poll, webPage, invoice, location, action, game, } = getMessageContent(message); const { phoneCall } = action || {}; const withCommentButton = repliesThreadInfo && !isInDocumentGroupNotLast && messageListType === 'thread' && !noComments; const withQuickReactionButton = !IS_TOUCH_ENV && !phoneCall && !isInSelectMode && defaultReaction && !isInDocumentGroupNotLast; const contentClassName = buildContentClassName(message, { hasSubheader, isCustomShape, isLastInGroup, asForwarded, hasThread, forceSenderName, hasComments: repliesThreadInfo && repliesThreadInfo.messagesCount > 0, hasActionButton: canForward || canFocus, hasReactions, isGeoLiveActive: location?.type === 'geoLive' && !isGeoLiveExpired(message, getServerTime()), withVoiceTranscription, }); const withAppendix = contentClassName.includes('has-appendix'); const hasText = hasMessageText(message); const emojiSize = getCustomEmojiSize(message.emojiOnlyCount); let metaPosition!: MetaPosition; if (phoneCall) { metaPosition = 'none'; } else if (isInDocumentGroupNotLast) { metaPosition = 'none'; } else if (hasText && !webPage && !hasAnimatedEmoji) { metaPosition = 'in-text'; } else { metaPosition = 'standalone'; } let reactionsPosition!: ReactionsPosition; if (hasReactions) { if (isCustomShape || ((photo || video) && !hasText)) { reactionsPosition = 'outside'; } else if (asForwarded) { metaPosition = 'standalone'; reactionsPosition = 'inside'; } else { reactionsPosition = 'inside'; } } else { reactionsPosition = 'none'; } useEnsureMessage( isRepliesChat && message.replyToChatId ? message.replyToChatId : chatId, hasReply ? message.replyToMessageId : undefined, replyMessage, message.id, ); useFocusMessage(ref, chatId, isFocused, focusDirection, noFocusHighlight, isResizingContainer); const shouldFocusOnResize = isLastInGroup; const handleResize = useCallback((entry: ResizeObserverEntry) => { const lastHeight = messageHeightRef.current; const newHeight = entry.target.clientHeight; messageHeightRef.current = newHeight; if (isAnimatingScroll() || !lastHeight || newHeight <= lastHeight) return; const container = entry.target.closest('.MessageList'); if (!container) return; const resizeDiff = newHeight - lastHeight; const { offsetHeight, scrollHeight, scrollTop } = container; const currentScrollBottom = Math.round(scrollHeight - scrollTop - offsetHeight); const previousScrollBottom = currentScrollBottom - resizeDiff; if (previousScrollBottom <= BOTTOM_FOCUS_SCROLL_THRESHOLD) { focusLastMessage(); } }, [focusLastMessage]); const throttledResize = useThrottledCallback(handleResize, [handleResize], THROTTLE_MS, false); useResizeObserver(shouldFocusOnResize ? ref : undefined, throttledResize); useEffect(() => { const bottomMarker = bottomMarkerRef.current; if (hasUnreadReaction && bottomMarker && isElementInViewport(bottomMarker)) { animateUnreadReaction({ messageIds: [messageId] }); } }, [hasUnreadReaction, messageId, animateUnreadReaction]); let style = ''; let calculatedWidth; let noMediaCorners = false; const albumLayout = useMemo(() => { return isAlbum ? calculateAlbumLayout(isOwn, Boolean(asForwarded), Boolean(noAvatars), album!, isMobile) : undefined; }, [isAlbum, isOwn, asForwarded, noAvatars, album, isMobile]); const extraPadding = asForwarded ? 28 : 0; if (!isAlbum && (photo || video || invoice?.extendedMedia)) { let width: number | undefined; if (photo) { width = calculateMediaDimensions(message, asForwarded, noAvatars, isMobile).width; } else if (video) { if (video.isRound) { width = ROUND_VIDEO_DIMENSIONS_PX; } else { width = calculateMediaDimensions(message, 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) { calculatedWidth = Math.max(getMinMediaWidth(Boolean(text), withCommentButton), width); if (invoice?.extendedMedia && calculatedWidth - width > NO_MEDIA_CORNERS_THRESHOLD) { noMediaCorners = true; } } } else if (albumLayout) { calculatedWidth = Math.max(getMinMediaWidth(Boolean(text), withCommentButton), albumLayout.containerStyle.width); if (calculatedWidth - albumLayout.containerStyle.width > NO_MEDIA_CORNERS_THRESHOLD) { noMediaCorners = true; } } if (calculatedWidth) { style = `width: ${calculatedWidth + extraPadding}px`; } const signature = (isChannel && message.postAuthorTitle) || (!asForwarded && forwardInfo?.postAuthorTitle) || undefined; const metaSafeAuthorWidth = useMemo(() => { return signature ? calculateAuthorWidth(signature) : undefined; }, [signature]); function renderAvatar() { const isAvatarPeerUser = avatarPeer && isUserId(avatarPeer.id); const avatarUser = (avatarPeer && isAvatarPeerUser) ? avatarPeer as ApiUser : undefined; const avatarChat = (avatarPeer && !isAvatarPeerUser) ? avatarPeer as ApiChat : undefined; const hiddenName = (!avatarPeer && forwardInfo) ? forwardInfo.hiddenUserName : undefined; return ( ); } function renderReactionsAndMeta() { const meta = ( ); if (reactionsPosition !== 'inside') { return meta; } return ( ); } function renderContent() { const className = buildClassName( 'content-inner', asForwarded && 'forwarded-message', hasSubheader && 'with-subheader', noMediaCorners && 'no-media-corners', ); const hasCustomAppendix = isLastInGroup && !hasText && !asForwarded && !hasThread; const textContentClass = buildClassName( 'text-content', metaPosition === 'in-text' && 'with-meta', outgoingStatus && 'with-outgoing-icon', ); return ( {renderSenderName()} {hasSubheader && ( {hasTopicChip && ( )} {hasReply && ( )} )} {sticker && ( = memoFirstUnreadIdRef.current ) || isLocal) ) || undefined} onPlayEffect={startStickerEffect} onStopEffect={stopStickerEffect} /> )} {hasAnimatedEmoji && animatedCustomEmoji && ( )} {hasAnimatedEmoji && animatedEmoji && ( )} {isAlbum && ( )} {phoneCall && ( )} {!isAlbum && photo && ( )} {!isAlbum && video && video.isRound && ( )} {!isAlbum && video && !video.isRound && ( )} {(audio || voice) && ( )} {document && ( )} {contact && ( )} {poll && ( )} {game && ( )} {invoice?.extendedMedia && ( )} {withVoiceTranscription && ( {(isTranscriptionError ? lang('NoWordsRecognized') : ( isTranscribing && transcribedText ? : transcribedText ))} )} {!hasAnimatedEmoji && hasText && ( {metaPosition === 'in-text' && renderReactionsAndMeta()} )} {webPage && ( )} {invoice && !invoice.extendedMedia && ( )} {location && ( )} ); } function renderSenderName() { const media = photo || video || location; const shouldRender = !(isCustomShape && !viaBotId) && ( (withSenderName && (!media || hasTopicChip)) || asForwarded || viaBotId || forceSenderName ) && !isInDocumentGroupNotFirst && !(hasReply && isCustomShape); if (!shouldRender) { return undefined; } let senderTitle; let senderColor; if (senderPeer && !(isCustomShape && viaBotId)) { senderTitle = getSenderTitle(lang, senderPeer); if (!asForwarded) { senderColor = `color-${getUserColorKey(senderPeer)}`; } } else if (forwardInfo?.hiddenUserName) { senderTitle = forwardInfo.hiddenUserName; } const senderEmojiStatus = senderPeer && 'emojiStatus' in senderPeer && senderPeer.emojiStatus; const senderIsPremium = senderPeer && 'isPremium' in senderPeer && senderPeer.isPremium; return ( {senderTitle ? ( {renderText(senderTitle)} {!asForwarded && senderEmojiStatus && ( )} {!asForwarded && !senderEmojiStatus && senderIsPremium && } {senderPeer?.fakeType && } ) : !botSender ? ( NBSP ) : undefined} {botSender && ( <> {lang('ViaBot')} {renderText(`@${botSender.usernames![0].username}`)} > )} {forwardInfo?.isLinkedChannelPost ? ( {lang('DiscussChannel')} ) : message.forwardInfo?.postAuthorTitle && isGroup && asForwarded ? ( {message.forwardInfo?.postAuthorTitle} ) : message.postAuthorTitle && isGroup && !asForwarded ? ( {message.postAuthorTitle} ) : senderAdminMember && !asForwarded ? ( {senderAdminMember.customTitle || lang( senderAdminMember.isOwner ? 'GroupInfo.LabelOwner' : 'GroupInfo.LabelAdmin', )} ) : undefined} ); } const forwardAuthor = isGroup && asForwarded ? message.postAuthorTitle : undefined; const chatUsername = useMemo(() => chatUsernames?.find((c) => c.isActive), [chatUsernames]); return ( {!isInDocumentGroup && ( {isSelected && } )} {isLastInDocumentGroup && ( {isGroupSelected && ( )} )} {withAvatar && renderAvatar()} {asForwarded && !isInDocumentGroupNotFirst && ( {lang('ForwardedMessage')} {forwardAuthor && {forwardAuthor}} )} {renderContent()} {!isInDocumentGroupNotLast && metaPosition === 'standalone' && renderReactionsAndMeta()} {canShowActionButton && canForward ? ( ) : canShowActionButton && canFocus ? ( ) : undefined} {withCommentButton && } {withAppendix && ( )} {withQuickReactionButton && ( )} {message.inlineButtons && ( )} {reactionsPosition === 'outside' && ( )} {contextMenuPosition && ( )} ); }; export default memo(withGlobal( (global, ownProps): StateProps => { const { focusedMessage, forwardMessages, activeReactions, activeEmojiInteractions, } = selectTabState(global); const { lastSyncTime } = global; const { message, album, withSenderName, withAvatar, threadId, messageListType, isLastInDocumentGroup, isFirstInGroup, } = ownProps; const { id, chatId, viaBotId, replyToChatId, replyToMessageId, isOutgoing, repliesThreadInfo, forwardInfo, transcriptionId, } = message; const chat = selectChat(global, chatId); const isChatWithSelf = selectIsChatWithSelf(global, chatId); const isRepliesChat = isChatWithRepliesBot(chatId); const isChannel = chat && isChatChannel(chat); const isGroup = chat && isChatGroup(chat); const chatUsernames = chat?.usernames; const isForwarding = forwardMessages.messageIds && forwardMessages.messageIds.includes(id); const forceSenderName = !isChatWithSelf && isAnonymousOwnMessage(message); const canShowSender = withSenderName || withAvatar || forceSenderName; const sender = selectSender(global, message); const originSender = selectForwardedSender(global, message); const botSender = viaBotId ? selectUser(global, viaBotId) : undefined; const senderAdminMember = sender?.id && isGroup ? chat.fullInfo?.adminMembersById?.[sender?.id] : undefined; const threadTopMessageId = threadId ? selectThreadTopMessageId(global, chatId, threadId) : undefined; const isThreadTop = message.id === threadTopMessageId; const shouldHideReply = replyToMessageId === threadTopMessageId; const replyMessage = replyToMessageId && !shouldHideReply ? selectChatMessage(global, isRepliesChat && replyToChatId ? replyToChatId : chatId, replyToMessageId) : undefined; const replyMessageSender = replyMessage && selectReplySender(global, replyMessage, Boolean(forwardInfo)); const isReplyToTopicStart = replyMessage?.content.action?.type === 'topicCreate'; const uploadProgress = selectUploadProgress(global, message); const isFocused = messageListType === 'thread' && ( album ? album.messages.some((m) => selectIsMessageFocused(global, m)) : selectIsMessageFocused(global, message) ); const { direction: focusDirection, noHighlight: noFocusHighlight, isResizingContainer, } = (isFocused && focusedMessage) || {}; const { query: highlight } = selectCurrentTextSearch(global) || {}; const singleEmoji = getMessageSingleRegularEmoji(message); const animatedEmoji = singleEmoji && selectAnimatedEmoji(global, singleEmoji) ? singleEmoji : undefined; const animatedCustomEmoji = getMessageSingleCustomEmoji(message); let isSelected: boolean; if (album?.messages) { isSelected = album.messages.every(({ id: messageId }) => selectIsMessageSelected(global, messageId)); } else { isSelected = selectIsMessageSelected(global, id); } const { canReply } = (messageListType === 'thread' && selectAllowedMessageActions(global, message, threadId)) || {}; const isDownloading = selectIsDownloading(global, message); const actualRepliesThreadInfo = repliesThreadInfo ? selectThreadInfo(global, repliesThreadInfo.chatId, repliesThreadInfo.threadId) || repliesThreadInfo : undefined; const isInDocumentGroup = Boolean(message.groupedId) && !message.isInAlbum; const documentGroupFirstMessageId = isInDocumentGroup ? selectMessageIdsByGroupId(global, chatId, message.groupedId!)![0] : undefined; const reactionMessage = isInDocumentGroup ? ( isLastInDocumentGroup ? selectChatMessage(global, chatId, documentGroupFirstMessageId!) : undefined ) : message; const hasUnreadReaction = chat?.unreadReactions?.includes(message.id); const hasTopicChip = threadId === MAIN_THREAD_ID && chat?.isForum && isFirstInGroup; const messageTopic = hasTopicChip ? (selectTopicFromMessage(global, message) || chat?.topics?.[GENERAL_TOPIC_ID]) : undefined; const isLocation = Boolean(getMessageLocation(message)); return { theme: selectTheme(global), chatUsernames, forceSenderName, canShowSender, originSender, botSender, shouldHideReply: shouldHideReply || isReplyToTopicStart, isThreadTop, replyMessage, replyMessageSender, isInDocumentGroup, isProtected: selectIsMessageProtected(global, message), isChatProtected: selectIsChatProtected(global, chatId), isFocused, isForwarding, reactionMessage, isChatWithSelf, isRepliesChat, isChannel, isGroup, canReply, lastSyncTime, highlight, animatedEmoji, animatedCustomEmoji, isInSelectMode: selectIsInSelectMode(global), isSelected, isGroupSelected: ( Boolean(message.groupedId) && !message.isInAlbum && selectIsDocumentGroupSelected(global, chatId, message.groupedId) ), threadId, isDownloading, isPinnedList: messageListType === 'pinned', canAutoLoadMedia: selectCanAutoLoadMedia(global, message), canAutoPlayMedia: selectCanAutoPlayMedia(global, message), autoLoadFileMaxSizeMb: global.settings.byKey.autoLoadFileMaxSizeMb, shouldLoopStickers: selectShouldLoopStickers(global), repliesThreadInfo: actualRepliesThreadInfo, availableReactions: global.availableReactions, defaultReaction: isMessageLocal(message) ? undefined : selectDefaultReaction(global, chatId), activeReactions: reactionMessage && activeReactions[reactionMessage.id], activeEmojiInteractions, hasUnreadReaction, isTranscribing: transcriptionId !== undefined && global.transcriptions[transcriptionId]?.isPending, transcribedText: transcriptionId !== undefined ? global.transcriptions[transcriptionId]?.text : undefined, isPremium: selectIsCurrentUserPremium(global), animationLevel: global.settings.byKey.animationLevel, senderAdminMember, messageTopic, genericEffects: global.genericEmojiEffects, hasTopicChip, ...((canShowSender || isLocation) && { sender }), ...(isOutgoing && { outgoingStatus: selectOutgoingStatus(global, message, messageListType === 'scheduled') }), ...(typeof uploadProgress === 'number' && { uploadProgress }), ...(isFocused && { focusDirection, noFocusHighlight, isResizingContainer }), }; }, )(Message));
{(isTranscriptionError ? lang('NoWordsRecognized') : ( isTranscribing && transcribedText ? : transcribedText ))}