import type { FC } from '../../../lib/teact/teact'; import { memo, useEffect, useMemo, useRef, } from '../../../lib/teact/teact'; import { getActions } from '../../../global'; import type { ApiAvailableReaction, ApiChat, ApiChatReactions, ApiMessage, ApiPeer, ApiPoll, ApiReaction, ApiStickerSet, ApiThreadInfo, ApiTypeStory, ApiUser, ApiWebPage, } from '../../../api/types'; import type { IAnchorPosition } from '../../../types'; import { getUserFullName, groupStatefulContent, } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; import { isUserId } from '../../../util/entities/ids'; import { disableScrolling } from '../../../util/scrollLock'; import { REM } from '../../common/helpers/mediaDimensions'; import renderText from '../../common/helpers/renderText'; import { getMessageCopyOptions } from './helpers/copyOptions'; import useAppLayout from '../../../hooks/useAppLayout'; import useFlag from '../../../hooks/useFlag'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import useOldLang from '../../../hooks/useOldLang'; import AvatarList from '../../common/AvatarList'; import Menu from '../../ui/Menu'; import MenuItem from '../../ui/MenuItem'; import MenuSeparator from '../../ui/MenuSeparator'; import Skeleton from '../../ui/placeholder/Skeleton'; import LastEditTimeMenuItem from './LastEditTimeMenuItem'; import ReactionSelector from './reactions/ReactionSelector'; import ReadTimeMenuItem from './ReadTimeMenuItem'; import './MessageContextMenu.scss'; type OwnProps = { isReactionPickerOpen?: boolean; availableReactions?: ApiAvailableReaction[]; topReactions?: ApiReaction[]; defaultTagReactions?: ApiReaction[]; isOpen: boolean; anchor: IAnchorPosition; targetHref?: string; message: ApiMessage; poll?: ApiPoll; webPage?: ApiWebPage; story?: ApiTypeStory; canSendNow?: boolean; enabledReactions?: ApiChatReactions; isWithPaidReaction?: boolean; reactionsLimit?: number; canReschedule?: boolean; canReply?: boolean; canQuote?: boolean; repliesThreadInfo?: ApiThreadInfo; canPin?: boolean; canUnpin?: boolean; canDelete?: boolean; canReport?: boolean; canShowReactionsCount?: boolean; canShowReactionList?: boolean; canBuyPremium?: boolean; canEdit?: boolean; canAppendTodoList?: boolean; canForward?: boolean; canFaveSticker?: boolean; canUnfaveSticker?: boolean; canCopy?: boolean; canCopyLink?: boolean; canSelect?: boolean; canTranslate?: boolean; canShowOriginal?: boolean; canSelectLanguage?: boolean; isPrivate?: boolean; isCurrentUserPremium?: boolean; canDownload?: boolean; canSaveGif?: boolean; canRevote?: boolean; canClosePoll?: boolean; isDownloading?: boolean; canShowSeenBy?: boolean; seenByRecentPeers?: ApiPeer[]; noReplies?: boolean; hasCustomEmoji?: boolean; customEmojiSets?: ApiStickerSet[]; canPlayAnimatedEmojis?: boolean; isInSavedMessages?: boolean; shouldRenderShowWhen?: boolean; canLoadReadDate?: boolean; onReply?: NoneToVoidFunction; onOpenThread?: VoidFunction; onEdit?: NoneToVoidFunction; onAppendTodoList?: NoneToVoidFunction; onPin?: NoneToVoidFunction; onUnpin?: NoneToVoidFunction; onForward?: NoneToVoidFunction; onDelete?: NoneToVoidFunction; onFaveSticker?: NoneToVoidFunction; onReport?: NoneToVoidFunction; onUnfaveSticker?: NoneToVoidFunction; onSelect?: NoneToVoidFunction; onSend?: NoneToVoidFunction; onReschedule?: NoneToVoidFunction; onClose: NoneToVoidFunction; onCloseAnimationEnd?: NoneToVoidFunction; onCopyLink?: NoneToVoidFunction; onCopyMessages?: (messageIds: number[]) => void; onCopyNumber?: NoneToVoidFunction; onDownload?: NoneToVoidFunction; onSaveGif?: NoneToVoidFunction; onCancelVote?: NoneToVoidFunction; onClosePoll?: NoneToVoidFunction; onShowSeenBy?: NoneToVoidFunction; onShowReactors?: NoneToVoidFunction; onTranslate?: NoneToVoidFunction; onShowOriginal?: NoneToVoidFunction; onSelectLanguage?: NoneToVoidFunction; onToggleReaction?: (reaction: ApiReaction) => void; onSendPaidReaction?: NoneToVoidFunction; onShowPaidReactionModal?: NoneToVoidFunction; onReactionPickerOpen?: (position: IAnchorPosition) => void; userFullName?: string; canGift?: boolean; }; const SCROLLBAR_WIDTH = 10; const REACTION_SELECTOR_WIDTH_REM = 19.25; const ANIMATION_DURATION = 200; const MessageContextMenu: FC = ({ isReactionPickerOpen, availableReactions, topReactions, defaultTagReactions, isOpen, message, poll, webPage, story, isPrivate, isCurrentUserPremium, enabledReactions, isWithPaidReaction, reactionsLimit, anchor, targetHref, canSendNow, canReschedule, canBuyPremium, canReply, canQuote, canEdit, canAppendTodoList, noReplies, canPin, canUnpin, canDelete, canForward, canReport, canFaveSticker, canUnfaveSticker, canCopy, canCopyLink, canSelect, canDownload, canSaveGif, canRevote, canClosePoll, canTranslate, canShowOriginal, canSelectLanguage, isDownloading, repliesThreadInfo, canShowSeenBy, canShowReactionsCount, canShowReactionList, seenByRecentPeers, hasCustomEmoji, customEmojiSets, canPlayAnimatedEmojis, isInSavedMessages, shouldRenderShowWhen, canLoadReadDate, onReply, onOpenThread, onEdit, onAppendTodoList, onPin, onUnpin, onForward, onDelete, onFaveSticker, onReport, onUnfaveSticker, onSelect, onSend, onReschedule, onClose, onCloseAnimationEnd, onCopyLink, onCopyNumber, onDownload, onSaveGif, onCancelVote, onClosePoll, onShowSeenBy, onShowReactors, onToggleReaction, onSendPaidReaction, onShowPaidReactionModal, onCopyMessages, onReactionPickerOpen, onTranslate, onShowOriginal, onSelectLanguage, userFullName, canGift, }) => { const { showNotification, openStickerSet, openCustomEmojiSets, loadStickers, openGiftModal, } = getActions(); const menuRef = useRef(); const scrollableRef = useRef(); const oldLang = useOldLang(); const lang = useLang(); const noReactions = !isPrivate && !enabledReactions; const areReactionsPossible = message.areReactionsPossible; const withReactions = (canShowReactionList && !noReactions) || areReactionsPossible; const isEdited = ('isEdited' in message) && message.isEdited; const seenByDates = message.seenByDates; const isPremiumGift = message.content.action?.type === 'giftPremium'; const isGiftCode = message.content.action?.type === 'giftCode'; const isStarGift = message.content.action?.type === 'starGift'; const isStarGiftUnique = message.content.action?.type === 'starGiftUnique'; const shouldShowGiftButton = isUserId(message.chatId) && canGift && (isPremiumGift || isGiftCode || isStarGift || isStarGiftUnique); const [areItemsHidden, hideItems] = useFlag(); const [isReady, markIsReady, unmarkIsReady] = useFlag(); const { isMobile, isDesktop } = useAppLayout(); const seenByDatesCount = useMemo(() => (seenByDates ? Object.keys(seenByDates).length : 0), [seenByDates]); const handleAfterCopy = useLastCallback(() => { showNotification({ message: oldLang('Share.Link.Copied'), }); onClose(); }); const handleGiftClick = useLastCallback(() => { openGiftModal({ forUserId: message.chatId }); onClose(); }); useEffect(() => { if (isOpen && areItemsHidden && !isReactionPickerOpen) { onClose(); } }, [onClose, isOpen, isReactionPickerOpen, areItemsHidden]); useEffect(() => { if (customEmojiSets?.length) { customEmojiSets.map((customEmojiSet) => { return loadStickers({ stickerSetInfo: { id: customEmojiSet.id, accessHash: customEmojiSet.accessHash, }, }); }); } }, [customEmojiSets, openCustomEmojiSets]); const handleOpenCustomEmojiSets = useLastCallback(() => { if (!customEmojiSets) return; if (customEmojiSets.length === 1) { openStickerSet({ stickerSetInfo: { shortName: customEmojiSets[0].shortName, }, }); } else { openCustomEmojiSets({ setIds: customEmojiSets.map((set) => set.id), }); } onClose(); }); const copyOptions = getMessageCopyOptions( message, groupStatefulContent({ poll, webPage, story }), targetHref, canCopy, handleAfterCopy, canCopyLink ? onCopyLink : undefined, onCopyMessages, onCopyNumber, ); const getTriggerElement = useLastCallback(() => { return document.querySelector(`.Transition_slide-active > .MessageList div[data-message-id="${message.id}"]`); }); const getRootElement = useLastCallback(() => document.querySelector('.Transition_slide-active > .MessageList')); const getMenuElement = useLastCallback(() => document.querySelector('.MessageContextMenu .bubble')); const getLayout = useLastCallback(() => { const extraHeightAudioPlayer = (isMobile && (document.querySelector('.AudioPlayer-content'))?.offsetHeight) || 0; const middleColumn = document.getElementById('MiddleColumn')!; const middleColumnComputedStyle = getComputedStyle(middleColumn); const headerToolsHeight = parseFloat(middleColumnComputedStyle.getPropertyValue('--middle-header-panes-height')); const extraHeightPinned = headerToolsHeight || 0; return { extraPaddingX: SCROLLBAR_WIDTH, extraTopPadding: (document.querySelector('.MiddleHeader')!).offsetHeight, extraMarginTop: extraHeightPinned + extraHeightAudioPlayer, shouldAvoidNegativePosition: !isDesktop, menuElMinWidth: withReactions && isMobile ? REACTION_SELECTOR_WIDTH_REM * REM : undefined, }; }); useEffect(() => { if (!isOpen) { unmarkIsReady(); return; } setTimeout(() => { markIsReady(); }, ANIMATION_DURATION); }, [isOpen, markIsReady, unmarkIsReady]); useEffect(() => { return disableScrolling(scrollableRef.current, '.ReactionPicker'); }, [isOpen]); const handleOpenMessageReactionPicker = useLastCallback((position: IAnchorPosition) => { onReactionPickerOpen!(position); hideItems(); }); return ( {withReactions && ( )}
{shouldShowGiftButton && ( {message?.isOutgoing ? oldLang('SendAnotherGift') : oldLang('Conversation.ContextMenuSendGiftTo', userFullName)} )} {canSendNow && {oldLang('MessageScheduleSend')}} {canReschedule && ( {oldLang('MessageScheduleEditTime')} )} {canReply && ( {oldLang(canQuote ? 'lng_context_quote_and_reply' : 'Reply')} )} {!noReplies && Boolean(repliesThreadInfo?.messagesCount) && ( {oldLang('Conversation.ContextViewReplies', repliesThreadInfo.messagesCount, 'i')} )} {canEdit && {oldLang('Edit')}} {canAppendTodoList && ( {lang('MenuButtonAppendTodoList')} )} {canFaveSticker && ( {oldLang('AddToFavorites')} )} {canUnfaveSticker && ( {oldLang('Stickers.RemoveFromFavorites')} )} {canTranslate && {oldLang('TranslateMessage')}} {canShowOriginal && ( {oldLang('ShowOriginalButton')} )} {canSelectLanguage && ( {oldLang('lng_settings_change_lang')} )} {copyOptions.map((option) => ( {oldLang(option.label)} ))} {canPin && {oldLang('DialogPin')}} {canUnpin && {oldLang('DialogUnpin')}} {canSaveGif && {oldLang('lng_context_save_gif')}} {canRevote && {oldLang('lng_polls_retract')}} {canClosePoll && {oldLang('lng_polls_stop')}} {canDownload && ( {isDownloading ? oldLang('lng_context_cancel_download') : oldLang('lng_media_download')} )} {canForward && {oldLang('Forward')}} {canSelect && {oldLang('Common.Select')}} {canReport && {oldLang('lng_context_report_msg')}} {canDelete && {oldLang('Delete')}} {hasCustomEmoji && ( <> {!customEmojiSets && ( <> )} {customEmojiSets && customEmojiSets.length === 1 && ( {renderText( oldLang('MessageContainsEmojiPack', customEmojiSets[0].title), ['simple_markdown', 'emoji'], )} )} {customEmojiSets && customEmojiSets.length > 1 && ( {renderText(oldLang('MessageContainsEmojiPacks', customEmojiSets.length), ['simple_markdown'])} )} )} {(canShowSeenBy || canShowReactionsCount) && ( <> {canShowReactionsCount && message.reactors?.count ? ( canShowSeenBy && seenByDatesCount ? oldLang( 'Chat.OutgoingContextMixedReactionCount', [message.reactors.count, seenByDatesCount], ) : oldLang('Chat.ContextReactionCount', message.reactors.count, 'i') ) : ( seenByDatesCount === 1 && seenByRecentPeers ? renderText( isUserId(seenByRecentPeers[0].id) ? getUserFullName(seenByRecentPeers[0] as ApiUser)! : (seenByRecentPeers[0] as ApiChat).title, ) : ( seenByDatesCount ? oldLang('Conversation.ContextMenuSeen', seenByDatesCount, 'i') : oldLang('Conversation.ContextMenuNoViews') ) )} )} {(canLoadReadDate || shouldRenderShowWhen || isEdited) && ( )} {(canLoadReadDate || shouldRenderShowWhen) && ( )} {isEdited && ( )}
); }; export default memo(MessageContextMenu);