import type { FC } from '../../lib/teact/teact'; import React, { memo, useEffect, useMemo, useRef, useSignal, useState, } from '../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../global'; import type { ApiAttachment, ApiAttachMenuPeerType, ApiAvailableEffect, ApiAvailableReaction, ApiBotCommand, ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotMenuButton, ApiChat, ApiChatFullInfo, ApiDraft, ApiFormattedText, ApiMessage, ApiMessageEntity, ApiNewPoll, ApiQuickReply, ApiReaction, ApiStealthMode, ApiSticker, ApiTopic, ApiUser, ApiVideo, ApiWebPage, } from '../../api/types'; import type { GlobalState, TabState, } from '../../global/types'; import type { IAnchorPosition, InlineBotSettings, ISettings, MessageList, MessageListType, ThreadId, } from '../../types'; import { MAIN_THREAD_ID } from '../../api/types'; import { BASE_EMOJI_KEYWORD_LANG, DEFAULT_MAX_MESSAGE_LENGTH, EDITABLE_INPUT_MODAL_ID, HEART_REACTION, MAX_UPLOAD_FILEPART_SIZE, ONE_TIME_MEDIA_TTL_SECONDS, SCHEDULED_WHEN_ONLINE, SEND_MESSAGE_ACTION_INTERVAL, SERVICE_NOTIFICATIONS_USER_ID, } from '../../config'; import { requestMeasure, requestNextMutation } from '../../lib/fasterdom/fasterdom'; import { canEditMedia, getAllowedAttachmentOptions, getReactionKey, getStoryKey, isChatAdmin, isChatChannel, isChatSuperGroup, isSameReaction, isSystemBot, isUserId, } from '../../global/helpers'; import { selectBot, selectCanPlayAnimatedEmojis, selectCanScheduleUntilOnline, selectChat, selectChatFullInfo, selectChatMessage, selectChatType, selectCurrentMessageList, selectDraft, selectEditingDraft, selectEditingMessage, selectEditingScheduledDraft, selectIsChatWithSelf, selectIsCurrentUserPremium, selectIsInSelectMode, selectIsPremiumPurchaseBlocked, selectIsReactionPickerOpen, selectIsRightColumnShown, selectNewestMessageWithBotKeyboardButtons, selectNoWebPage, selectPeerStory, selectPerformanceSettingsValue, selectRequestedDraft, selectRequestedDraftFiles, selectTabState, selectTheme, selectTopicFromMessage, selectUser, selectUserFullInfo, } from '../../global/selectors'; import { selectCurrentLimit } from '../../global/selectors/limits'; import buildClassName from '../../util/buildClassName'; import { formatMediaDuration, formatVoiceRecordDuration } from '../../util/dates/dateFormat'; import { processDeepLink } from '../../util/deeplink'; import { tryParseDeepLink } from '../../util/deepLinkParser'; import deleteLastCharacterOutsideSelection from '../../util/deleteLastCharacterOutsideSelection'; import { processMessageInputForCustomEmoji } from '../../util/emoji/customEmojiManager'; import focusEditableElement from '../../util/focusEditableElement'; import { MEMO_EMPTY_ARRAY } from '../../util/memo'; import parseHtmlAsFormattedText from '../../util/parseHtmlAsFormattedText'; import { insertHtmlInSelection } from '../../util/selection'; import { getServerTime } from '../../util/serverTime'; import { IS_IOS, IS_VOICE_RECORDING_SUPPORTED } from '../../util/windowEnvironment'; import windowSize from '../../util/windowSize'; import applyIosAutoCapitalizationFix from '../middle/composer/helpers/applyIosAutoCapitalizationFix'; import buildAttachment, { prepareAttachmentsToSend } from '../middle/composer/helpers/buildAttachment'; import { buildCustomEmojiHtml } from '../middle/composer/helpers/customEmoji'; import { isSelectionInsideInput } from '../middle/composer/helpers/selection'; import { getPeerColorClass } from './helpers/peerColor'; import renderText from './helpers/renderText'; import { getTextWithEntitiesAsHtml } from './helpers/renderTextWithEntities'; import useInterval from '../../hooks/schedulers/useInterval'; import useTimeout from '../../hooks/schedulers/useTimeout'; import useContextMenuHandlers from '../../hooks/useContextMenuHandlers'; import useDerivedState from '../../hooks/useDerivedState'; import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps'; import useFlag from '../../hooks/useFlag'; import useGetSelectionRange from '../../hooks/useGetSelectionRange'; import useLastCallback from '../../hooks/useLastCallback'; import useOldLang from '../../hooks/useOldLang'; import usePreviousDeprecated from '../../hooks/usePreviousDeprecated'; import useSchedule from '../../hooks/useSchedule'; import useSendMessageAction from '../../hooks/useSendMessageAction'; import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated'; import { useStateRef } from '../../hooks/useStateRef'; import useSyncEffect from '../../hooks/useSyncEffect'; import useAttachmentModal from '../middle/composer/hooks/useAttachmentModal'; import useChatCommandTooltip from '../middle/composer/hooks/useChatCommandTooltip'; import useClipboardPaste from '../middle/composer/hooks/useClipboardPaste'; import useCustomEmojiTooltip from '../middle/composer/hooks/useCustomEmojiTooltip'; import useDraft from '../middle/composer/hooks/useDraft'; import useEditing from '../middle/composer/hooks/useEditing'; import useEmojiTooltip from '../middle/composer/hooks/useEmojiTooltip'; import useInlineBotTooltip from '../middle/composer/hooks/useInlineBotTooltip'; import useMentionTooltip from '../middle/composer/hooks/useMentionTooltip'; import useStickerTooltip from '../middle/composer/hooks/useStickerTooltip'; import useVoiceRecording from '../middle/composer/hooks/useVoiceRecording'; import AttachmentModal from '../middle/composer/AttachmentModal.async'; import AttachMenu from '../middle/composer/AttachMenu'; import BotCommandMenu from '../middle/composer/BotCommandMenu.async'; import BotKeyboardMenu from '../middle/composer/BotKeyboardMenu'; import BotMenuButton from '../middle/composer/BotMenuButton'; import ChatCommandTooltip from '../middle/composer/ChatCommandTooltip.async'; import ComposerEmbeddedMessage from '../middle/composer/ComposerEmbeddedMessage'; import CustomEmojiTooltip from '../middle/composer/CustomEmojiTooltip.async'; import CustomSendMenu from '../middle/composer/CustomSendMenu.async'; import DropArea, { DropAreaState } from '../middle/composer/DropArea.async'; import EmojiTooltip from '../middle/composer/EmojiTooltip.async'; import InlineBotTooltip from '../middle/composer/InlineBotTooltip.async'; import MentionTooltip from '../middle/composer/MentionTooltip.async'; import MessageInput from '../middle/composer/MessageInput'; import PollModal from '../middle/composer/PollModal.async'; import SendAsMenu from '../middle/composer/SendAsMenu.async'; import StickerTooltip from '../middle/composer/StickerTooltip.async'; import SymbolMenuButton from '../middle/composer/SymbolMenuButton'; import WebPagePreview from '../middle/composer/WebPagePreview'; import MessageEffect from '../middle/message/MessageEffect'; import ReactionSelector from '../middle/message/reactions/ReactionSelector'; import Button from '../ui/Button'; import ResponsiveHoverButton from '../ui/ResponsiveHoverButton'; import Spinner from '../ui/Spinner'; import Avatar from './Avatar'; import Icon from './icons/Icon'; import ReactionAnimatedEmoji from './reactions/ReactionAnimatedEmoji'; import './Composer.scss'; type ComposerType = 'messageList' | 'story'; type OwnProps = { type: ComposerType; chatId: string; threadId: ThreadId; storyId?: number; messageListType: MessageListType; dropAreaState?: string; isReady: boolean; isMobile?: boolean; inputId: string; editableInputCssSelector: string; editableInputId: string; className?: string; inputPlaceholder?: string; onDropHide?: NoneToVoidFunction; onForward?: NoneToVoidFunction; onFocus?: NoneToVoidFunction; onBlur?: NoneToVoidFunction; }; type StateProps = { isOnActiveTab: boolean; editingMessage?: ApiMessage; chat?: ApiChat; chatFullInfo?: ApiChatFullInfo; draft?: ApiDraft; replyToTopic?: ApiTopic; currentMessageList?: MessageList; isChatWithBot?: boolean; isChatWithSelf?: boolean; isChannel?: boolean; isForCurrentMessageList: boolean; isRightColumnShown?: boolean; isSelectModeActive?: boolean; isReactionPickerOpen?: boolean; isForwarding?: boolean; pollModal: TabState['pollModal']; botKeyboardMessageId?: number; botKeyboardPlaceholder?: string; withScheduledButton?: boolean; isInScheduledList?: boolean; canScheduleUntilOnline?: boolean; stickersForEmoji?: ApiSticker[]; customEmojiForEmoji?: ApiSticker[]; currentUserId?: string; currentUser?: ApiUser; recentEmojis: string[]; contentToBeScheduled?: TabState['contentToBeScheduled']; shouldSuggestStickers?: boolean; shouldSuggestCustomEmoji?: boolean; baseEmojiKeywords?: Record; emojiKeywords?: Record; topInlineBotIds?: string[]; isInlineBotLoading: boolean; inlineBots?: Record; botCommands?: ApiBotCommand[] | false; botMenuButton?: ApiBotMenuButton; sendAsUser?: ApiUser; sendAsChat?: ApiChat; sendAsId?: string; editingDraft?: ApiFormattedText; requestedDraft?: ApiFormattedText; requestedDraftFiles?: File[]; attachBots: GlobalState['attachMenu']['bots']; attachMenuPeerType?: ApiAttachMenuPeerType; theme: ISettings['theme']; fileSizeLimit: number; captionLimit: number; isCurrentUserPremium?: boolean; canSendVoiceByPrivacy?: boolean; attachmentSettings: GlobalState['attachmentSettings']; slowMode?: ApiChatFullInfo['slowMode']; shouldUpdateStickerSetOrder?: boolean; availableReactions?: ApiAvailableReaction[]; topReactions?: ApiReaction[]; canPlayAnimatedEmojis?: boolean; canBuyPremium?: boolean; shouldCollectDebugLogs?: boolean; sentStoryReaction?: ApiReaction; stealthMode?: ApiStealthMode; canSendOneTimeMedia?: boolean; quickReplyMessages?: Record; quickReplies?: Record; canSendQuickReplies?: boolean; webPagePreview?: ApiWebPage; noWebPage?: boolean; isContactRequirePremium?: boolean; effect?: ApiAvailableEffect; effectReactions?: ApiReaction[]; areEffectsSupported?: boolean; canPlayEffect?: boolean; shouldPlayEffect?: boolean; maxMessageLength: number; }; enum MainButtonState { Send = 'send', Record = 'record', Edit = 'edit', Schedule = 'schedule', Forward = 'forward', SendOneTime = 'sendOneTime', } type ScheduledMessageArgs = TabState['contentToBeScheduled'] | { id: string; queryId: string; isSilent?: boolean; }; const VOICE_RECORDING_FILENAME = 'wonderful-voice-message.ogg'; // When voice recording is active, composer placeholder will hide to prevent overlapping const SCREEN_WIDTH_TO_HIDE_PLACEHOLDER = 600; // px const MOBILE_KEYBOARD_HIDE_DELAY_MS = 100; const SELECT_MODE_TRANSITION_MS = 200; const SENDING_ANIMATION_DURATION = 350; const MOUNT_ANIMATION_DURATION = 430; const Composer: FC = ({ type, isOnActiveTab, dropAreaState, isInScheduledList, canScheduleUntilOnline, isReady, isMobile, onDropHide, onFocus, onBlur, editingMessage, chatId, threadId, storyId, currentMessageList, messageListType, draft, chat, chatFullInfo, replyToTopic, isForCurrentMessageList, isCurrentUserPremium, canSendVoiceByPrivacy, isChatWithBot, isChatWithSelf, isChannel, fileSizeLimit, isRightColumnShown, isSelectModeActive, isReactionPickerOpen, isForwarding, pollModal, botKeyboardMessageId, botKeyboardPlaceholder, inputPlaceholder, withScheduledButton, stickersForEmoji, customEmojiForEmoji, topInlineBotIds, currentUserId, currentUser, captionLimit, contentToBeScheduled, shouldSuggestStickers, shouldSuggestCustomEmoji, baseEmojiKeywords, emojiKeywords, recentEmojis, inlineBots, isInlineBotLoading, botCommands, sendAsUser, sendAsChat, sendAsId, editingDraft, requestedDraft, requestedDraftFiles, botMenuButton, attachBots, attachMenuPeerType, attachmentSettings, theme, slowMode, shouldUpdateStickerSetOrder, editableInputCssSelector, editableInputId, inputId, className, availableReactions, topReactions, canBuyPremium, canPlayAnimatedEmojis, shouldCollectDebugLogs, sentStoryReaction, stealthMode, canSendOneTimeMedia, quickReplyMessages, quickReplies, canSendQuickReplies, onForward, webPagePreview, noWebPage, isContactRequirePremium, effect, effectReactions, areEffectsSupported, canPlayEffect, shouldPlayEffect, maxMessageLength, }) => { const { sendMessage, clearDraft, showDialog, forwardMessages, openPollModal, closePollModal, loadScheduledHistory, openThread, addRecentEmoji, sendInlineBotResult, loadSendAs, resetOpenChatWithDraft, callAttachBot, addRecentCustomEmoji, showNotification, showAllowedMessageTypesNotification, openStoryReactionPicker, closeReactionPicker, sendStoryReaction, editMessage, updateAttachmentSettings, saveEffectInDraft, setReactionEffect, hideEffectInComposer, } = getActions(); const lang = useOldLang(); // eslint-disable-next-line no-null/no-null const inputRef = useRef(null); // eslint-disable-next-line no-null/no-null const storyReactionRef = useRef(null); const [getHtml, setHtml] = useSignal(''); const [isMounted, setIsMounted] = useState(false); const getSelectionRange = useGetSelectionRange(editableInputCssSelector); const lastMessageSendTimeSeconds = useRef(); const prevDropAreaState = usePreviousDeprecated(dropAreaState); const { width: windowWidth } = windowSize.get(); const isInMessageList = type === 'messageList'; const isInStoryViewer = type === 'story'; const sendAsPeerIds = isInMessageList ? chat?.sendAsPeerIds : undefined; const canShowSendAs = sendAsPeerIds && (sendAsPeerIds.length > 1 || !sendAsPeerIds.some((peer) => peer.id === currentUserId!)); // Prevent Symbol Menu from closing when calendar is open const [isSymbolMenuForced, forceShowSymbolMenu, cancelForceShowSymbolMenu] = useFlag(); const sendMessageAction = useSendMessageAction(chatId, threadId); const [isInputHasFocus, markInputHasFocus, unmarkInputHasFocus] = useFlag(); const [isAttachMenuOpen, onAttachMenuOpen, onAttachMenuClose] = useFlag(); const canMediaBeReplaced = editingMessage && canEditMedia(editingMessage); const { emojiSet, members: groupChatMembers, botCommands: chatBotCommands } = chatFullInfo || {}; const chatEmojiSetId = emojiSet?.id; const isSentStoryReactionHeart = sentStoryReaction && isSameReaction(sentStoryReaction, HEART_REACTION); useEffect(processMessageInputForCustomEmoji, [getHtml]); const customEmojiNotificationNumber = useRef(0); const [requestCalendar, calendar] = useSchedule( isInMessageList && canScheduleUntilOnline, cancelForceShowSymbolMenu, ); useTimeout(() => { setIsMounted(true); }, MOUNT_ANIMATION_DURATION); useEffect(() => { if (isInMessageList) return; closeReactionPicker(); }, [isInMessageList, storyId]); useEffect(() => { lastMessageSendTimeSeconds.current = undefined; }, [chatId]); useEffect(() => { if (chatId && isReady && !isInStoryViewer) { loadScheduledHistory({ chatId }); } }, [isReady, chatId, threadId, isInStoryViewer]); useEffect(() => { const isChannelWithProfiles = isChannel && chat?.areProfilesShown; if (chatId && chat && !sendAsPeerIds && isReady && (isChatSuperGroup(chat) || isChannelWithProfiles)) { loadSendAs({ chatId }); } }, [chat, chatId, isChannel, isReady, loadSendAs, sendAsPeerIds]); const shouldAnimateSendAsButtonRef = useRef(false); useSyncEffect(([prevChatId, prevSendAsPeerIds]) => { // We only animate send-as button if `sendAsPeerIds` was missing when opening the chat shouldAnimateSendAsButtonRef.current = Boolean(chatId === prevChatId && sendAsPeerIds && !prevSendAsPeerIds); }, [chatId, sendAsPeerIds]); const [attachments, setAttachments] = useState([]); const hasAttachments = Boolean(attachments.length); const [nextText, setNextText] = useState(undefined); const { canSendStickers, canSendGifs, canAttachMedia, canAttachPolls, canAttachEmbedLinks, canSendVoices, canSendPlainText, canSendAudios, canSendVideos, canSendPhotos, canSendDocuments, } = useMemo( () => getAllowedAttachmentOptions(chat, chatFullInfo, isChatWithBot, isInStoryViewer), [chat, chatFullInfo, isChatWithBot, isInStoryViewer], ); const isNeedPremium = isContactRequirePremium && isInStoryViewer; const isSendTextBlocked = isNeedPremium || !canSendPlainText; const hasWebPagePreview = !hasAttachments && canAttachEmbedLinks && !noWebPage && Boolean(webPagePreview); const isComposerBlocked = isSendTextBlocked && !editingMessage; useEffect(() => { if (!hasWebPagePreview) { updateAttachmentSettings({ isInvertedMedia: undefined }); } }, [hasWebPagePreview]); const insertHtmlAndUpdateCursor = useLastCallback((newHtml: string, inInputId: string = editableInputId) => { if (inInputId === editableInputId && isComposerBlocked) return; const selection = window.getSelection()!; let messageInput: HTMLDivElement; if (inInputId === editableInputId) { messageInput = document.querySelector(editableInputCssSelector)!; } else { messageInput = document.getElementById(inInputId) as HTMLDivElement; } if (selection.rangeCount) { const selectionRange = selection.getRangeAt(0); if (isSelectionInsideInput(selectionRange, inInputId)) { insertHtmlInSelection(newHtml); messageInput.dispatchEvent(new Event('input', { bubbles: true })); return; } } setHtml(`${getHtml()}${newHtml}`); // If selection is outside of input, set cursor at the end of input requestNextMutation(() => { focusEditableElement(messageInput); }); }); const insertTextAndUpdateCursor = useLastCallback(( text: string, inInputId: string = editableInputId, ) => { const newHtml = renderText(text, ['escape_html', 'emoji_html', 'br_html']) .join('') .replace(/\u200b+/g, '\u200b'); insertHtmlAndUpdateCursor(newHtml, inInputId); }); const insertFormattedTextAndUpdateCursor = useLastCallback(( text: ApiFormattedText, inInputId: string = editableInputId, ) => { const newHtml = getTextWithEntitiesAsHtml(text); insertHtmlAndUpdateCursor(newHtml, inInputId); }); const insertCustomEmojiAndUpdateCursor = useLastCallback((emoji: ApiSticker, inInputId: string = editableInputId) => { insertHtmlAndUpdateCursor(buildCustomEmojiHtml(emoji), inInputId); }); const insertNextText = useLastCallback(() => { if (!nextText) return; insertFormattedTextAndUpdateCursor(nextText, editableInputId); setNextText(undefined); }); const { shouldSuggestCompression, shouldForceCompression, shouldForceAsFile, handleAppendFiles, handleFileSelect, onCaptionUpdate, handleClearAttachments, handleSetAttachments, } = useAttachmentModal({ attachments, setHtml, setAttachments, fileSizeLimit, chatId, canSendAudios, canSendVideos, canSendPhotos, canSendDocuments, insertNextText, editedMessage: editingMessage, }); const [isBotKeyboardOpen, openBotKeyboard, closeBotKeyboard] = useFlag(); const [isBotCommandMenuOpen, openBotCommandMenu, closeBotCommandMenu] = useFlag(); const [isSymbolMenuOpen, openSymbolMenu, closeSymbolMenu] = useFlag(); const [isSendAsMenuOpen, openSendAsMenu, closeSendAsMenu] = useFlag(); const [isHoverDisabled, disableHover, enableHover] = useFlag(); const { startRecordingVoice, stopRecordingVoice, pauseRecordingVoice, activeVoiceRecording, currentRecordTime, recordButtonRef: mainButtonRef, startRecordTimeRef, isViewOnceEnabled, setIsViewOnceEnabled, toogleViewOnceEnabled, } = useVoiceRecording(); const shouldSendRecordingStatus = isForCurrentMessageList && !isInStoryViewer; useInterval(() => { sendMessageAction({ type: 'recordAudio' }); }, shouldSendRecordingStatus ? activeVoiceRecording && SEND_MESSAGE_ACTION_INTERVAL : undefined); useEffect(() => { if (!isForCurrentMessageList || isInStoryViewer) return; if (!activeVoiceRecording) { sendMessageAction({ type: 'cancel' }); } }, [activeVoiceRecording, isForCurrentMessageList, isInStoryViewer, sendMessageAction]); const isEditingRef = useStateRef(Boolean(editingMessage)); useEffect(() => { if (!isForCurrentMessageList || isInStoryViewer) return; if (getHtml() && !isEditingRef.current) { sendMessageAction({ type: 'typing' }); } }, [getHtml, isEditingRef, isForCurrentMessageList, isInStoryViewer, sendMessageAction]); const isAdmin = chat && isChatAdmin(chat); const { isEmojiTooltipOpen, closeEmojiTooltip, filteredEmojis, filteredCustomEmojis, insertEmoji, } = useEmojiTooltip( Boolean(isReady && isOnActiveTab && (isInStoryViewer || isForCurrentMessageList) && shouldSuggestStickers && !hasAttachments), getHtml, setHtml, undefined, recentEmojis, baseEmojiKeywords, emojiKeywords, ); const { isCustomEmojiTooltipOpen, closeCustomEmojiTooltip, insertCustomEmoji, } = useCustomEmojiTooltip( Boolean(isReady && isOnActiveTab && (isInStoryViewer || isForCurrentMessageList) && shouldSuggestCustomEmoji && !hasAttachments), getHtml, setHtml, getSelectionRange, inputRef, customEmojiForEmoji, ); const { isStickerTooltipOpen, closeStickerTooltip, } = useStickerTooltip( Boolean(isReady && isOnActiveTab && (isInStoryViewer || isForCurrentMessageList) && shouldSuggestStickers && canSendStickers && !hasAttachments), getHtml, stickersForEmoji, ); const { isMentionTooltipOpen, closeMentionTooltip, insertMention, mentionFilteredUsers, } = useMentionTooltip( Boolean(isInMessageList && isReady && isForCurrentMessageList && !hasAttachments), getHtml, setHtml, getSelectionRange, inputRef, groupChatMembers, topInlineBotIds, currentUserId, ); const { isOpen: isInlineBotTooltipOpen, botId: inlineBotId, isGallery: isInlineBotTooltipGallery, switchPm: inlineBotSwitchPm, switchWebview: inlineBotSwitchWebview, results: inlineBotResults, closeTooltip: closeInlineBotTooltip, help: inlineBotHelp, loadMore: loadMoreForInlineBot, } = useInlineBotTooltip( Boolean(isInMessageList && isReady && isForCurrentMessageList && !hasAttachments), chatId, getHtml, inlineBots, ); const hasQuickReplies = Boolean(quickReplies && Object.keys(quickReplies).length); const { isOpen: isChatCommandTooltipOpen, close: closeChatCommandTooltip, filteredBotCommands: botTooltipCommands, filteredQuickReplies: quickReplyCommands, } = useChatCommandTooltip( Boolean(isInMessageList && isReady && isForCurrentMessageList && ((botCommands && botCommands?.length) || chatBotCommands?.length || (hasQuickReplies && canSendQuickReplies))), getHtml, botCommands, chatBotCommands, canSendQuickReplies ? quickReplies : undefined, ); useDraft({ draft, chatId, threadId, getHtml, setHtml, editedMessage: editingMessage, isDisabled: isInStoryViewer || Boolean(requestedDraft), }); const resetComposer = useLastCallback((shouldPreserveInput = false) => { if (!shouldPreserveInput) { setHtml(''); } setAttachments(MEMO_EMPTY_ARRAY); setNextText(undefined); closeEmojiTooltip(); closeCustomEmojiTooltip(); closeStickerTooltip(); closeMentionTooltip(); if (isMobile) { // @optimization setTimeout(() => closeSymbolMenu(), SENDING_ANIMATION_DURATION); } else { closeSymbolMenu(); } }); const [handleEditComplete, handleEditCancel, shouldForceShowEditing] = useEditing( getHtml, setHtml, editingMessage, resetComposer, chatId, threadId, messageListType, draft, editingDraft, ); // Handle chat change (should be placed after `useDraft` and `useEditing`) const resetComposerRef = useStateRef(resetComposer); const stopRecordingVoiceRef = useStateRef(stopRecordingVoice); useEffect(() => { return () => { // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps stopRecordingVoiceRef.current(); // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps resetComposerRef.current(); }; }, [chatId, threadId, resetComposerRef, stopRecordingVoiceRef]); const showCustomEmojiPremiumNotification = useLastCallback(() => { const notificationNumber = customEmojiNotificationNumber.current; if (!notificationNumber) { showNotification({ message: lang('UnlockPremiumEmojiHint'), action: { action: 'openPremiumModal', payload: { initialSection: 'animated_emoji' }, }, actionText: lang('PremiumMore'), }); } else { showNotification({ message: lang('UnlockPremiumEmojiHint2'), action: { action: 'openChat', payload: { id: currentUserId, shouldReplaceHistory: true }, }, actionText: lang('Open'), }); } customEmojiNotificationNumber.current = Number(!notificationNumber); }); const mainButtonState = useDerivedState(() => { if (!isInputHasFocus && onForward && !(getHtml() && !hasAttachments)) { return MainButtonState.Forward; } if (editingMessage && shouldForceShowEditing) { return MainButtonState.Edit; } if (IS_VOICE_RECORDING_SUPPORTED && !activeVoiceRecording && !isForwarding && !(getHtml() && !hasAttachments)) { return MainButtonState.Record; } if (isInScheduledList) { return MainButtonState.Schedule; } return MainButtonState.Send; }, [ activeVoiceRecording, editingMessage, getHtml, hasAttachments, isForwarding, isInputHasFocus, onForward, shouldForceShowEditing, isInScheduledList, ]); const canShowCustomSendMenu = !isInScheduledList; const { isContextMenuOpen: isCustomSendMenuOpen, handleContextMenu, handleContextMenuClose, handleContextMenuHide, } = useContextMenuHandlers(mainButtonRef, !(mainButtonState === MainButtonState.Send && canShowCustomSendMenu)); const { contextMenuAnchor: storyReactionPickerAnchor, handleContextMenu: handleStoryPickerContextMenu, handleBeforeContextMenu: handleBeforeStoryPickerContextMenu, handleContextMenuHide: handleStoryPickerContextMenuHide, } = useContextMenuHandlers(storyReactionRef, !isInStoryViewer); useEffect(() => { if (isReactionPickerOpen) return; if (storyReactionPickerAnchor) { openStoryReactionPicker({ peerId: chatId, storyId: storyId!, position: storyReactionPickerAnchor, }); handleStoryPickerContextMenuHide(); } }, [chatId, handleStoryPickerContextMenuHide, isReactionPickerOpen, storyId, storyReactionPickerAnchor]); useClipboardPaste( isForCurrentMessageList || isInStoryViewer, insertFormattedTextAndUpdateCursor, handleSetAttachments, setNextText, editingMessage, !isCurrentUserPremium && !isChatWithSelf, showCustomEmojiPremiumNotification, ); const handleEmbeddedClear = useLastCallback(() => { if (editingMessage) { handleEditCancel(); } }); const validateTextLength = useLastCallback((text: string, isAttachmentModal?: boolean) => { const maxLength = isAttachmentModal ? captionLimit : maxMessageLength; if (text?.length > maxLength) { const extraLength = text.length - maxLength; showDialog({ data: { message: 'MESSAGE_TOO_LONG_PLEASE_REMOVE_CHARACTERS', textParams: { '{EXTRA_CHARS_COUNT}': extraLength.toString(), '{PLURAL_S}': extraLength > 1 ? 's' : '', }, hasErrorKey: true, }, }); return false; } return true; }); const checkSlowMode = useLastCallback(() => { if (slowMode && !isAdmin) { const messageInput = document.querySelector(editableInputCssSelector); const nowSeconds = getServerTime(); const secondsSinceLastMessage = lastMessageSendTimeSeconds.current && Math.floor(nowSeconds - lastMessageSendTimeSeconds.current); const nextSendDateNotReached = slowMode.nextSendDate && slowMode.nextSendDate > nowSeconds; if ( (secondsSinceLastMessage && secondsSinceLastMessage < slowMode.seconds) || nextSendDateNotReached ) { const secondsRemaining = nextSendDateNotReached ? slowMode.nextSendDate! - nowSeconds : slowMode.seconds - secondsSinceLastMessage!; showDialog({ data: { message: lang('SlowModeHint', formatMediaDuration(secondsRemaining)), isSlowMode: true, hasErrorKey: false, }, }); messageInput?.blur(); return false; } } return true; }); const sendAttachments = useLastCallback(({ attachments: attachmentsToSend, sendCompressed = attachmentSettings.shouldCompress, sendGrouped = attachmentSettings.shouldSendGrouped, isSilent, scheduledAt, isInvertedMedia, }: { attachments: ApiAttachment[]; sendCompressed?: boolean; sendGrouped?: boolean; isSilent?: boolean; scheduledAt?: number; isInvertedMedia?: true; }) => { if (!currentMessageList && !storyId) { return; } const { text, entities } = parseHtmlAsFormattedText(getHtml()); if (!text && !attachmentsToSend.length) { return; } if (!validateTextLength(text, true)) return; if (!checkSlowMode()) return; isInvertedMedia = text && sendCompressed && sendGrouped ? isInvertedMedia : undefined; if (editingMessage) { editMessage({ messageList: currentMessageList, text, entities, attachments: prepareAttachmentsToSend(attachmentsToSend, sendCompressed), }); } else { sendMessage({ messageList: currentMessageList, text, entities, scheduledAt, isSilent, shouldUpdateStickerSetOrder, attachments: prepareAttachmentsToSend(attachmentsToSend, sendCompressed), shouldGroupMessages: sendGrouped, isInvertedMedia, }); } lastMessageSendTimeSeconds.current = getServerTime(); clearDraft({ chatId, isLocalOnly: true }); // Wait until message animation starts requestMeasure(() => { resetComposer(); }); }); const handleSendAttachmentsFromModal = useLastCallback(( sendCompressed: boolean, sendGrouped: boolean, isInvertedMedia?: true, ) => { sendAttachments({ attachments, sendCompressed, sendGrouped, isInvertedMedia, }); }); const handleSendAttachments = useLastCallback(( sendCompressed: boolean, sendGrouped: boolean, isSilent?: boolean, scheduledAt?: number, isInvertedMedia?: true, ) => { sendAttachments({ attachments, sendCompressed, sendGrouped, isSilent, scheduledAt, isInvertedMedia, }); }); const handleSend = useLastCallback(async (isSilent = false, scheduledAt?: number) => { if (!currentMessageList && !storyId) { return; } let currentAttachments = attachments; if (activeVoiceRecording) { const record = await stopRecordingVoice(); const ttlSeconds = isViewOnceEnabled ? ONE_TIME_MEDIA_TTL_SECONDS : undefined; if (record) { const { blob, duration, waveform } = record; currentAttachments = [await buildAttachment( VOICE_RECORDING_FILENAME, blob, { voice: { duration, waveform }, ttlSeconds }, )]; } } const { text, entities } = parseHtmlAsFormattedText(getHtml()); if (currentAttachments.length) { sendAttachments({ attachments: currentAttachments, scheduledAt, isSilent, }); return; } if (!text && !isForwarding) { return; } if (!validateTextLength(text)) return; const messageInput = document.querySelector(editableInputCssSelector); const effectId = effect?.id; if (text) { if (!checkSlowMode()) return; const isInvertedMedia = hasWebPagePreview ? attachmentSettings.isInvertedMedia : undefined; if (areEffectsSupported) saveEffectInDraft({ chatId, threadId, effectId: undefined }); sendMessage({ messageList: currentMessageList, text, entities, scheduledAt, isSilent, shouldUpdateStickerSetOrder, isInvertedMedia, effectId, webPageMediaSize: attachmentSettings.webPageMediaSize, webPageUrl: hasWebPagePreview ? webPagePreview!.url : undefined, }); } if (isForwarding) { forwardMessages({ scheduledAt, isSilent, }); } lastMessageSendTimeSeconds.current = getServerTime(); clearDraft({ chatId, threadId, isLocalOnly: true, shouldKeepReply: isForwarding, }); if (IS_IOS && messageInput && messageInput === document.activeElement) { applyIosAutoCapitalizationFix(messageInput); } // Wait until message animation starts requestMeasure(() => { resetComposer(); }); }); const handleClickBotMenu = useLastCallback(() => { if (botMenuButton?.type !== 'webApp') { return; } const parsedLink = tryParseDeepLink(botMenuButton.url); if (parsedLink?.type === 'publicUsernameOrBotLink' && parsedLink.appName) { processDeepLink(botMenuButton.url); } else { callAttachBot({ chatId, url: botMenuButton.url, threadId, }); } }); const handleActivateBotCommandMenu = useLastCallback(() => { closeSymbolMenu(); openBotCommandMenu(); }); const handleMessageSchedule = useLastCallback(( args: ScheduledMessageArgs, scheduledAt: number, messageList: MessageList, effectId?: string, ) => { if (args && 'queryId' in args) { const { id, queryId, isSilent } = args; sendInlineBotResult({ id, queryId, scheduledAt, isSilent, messageList, }); return; } const { isSilent, ...restArgs } = args || {}; if (!args || Object.keys(restArgs).length === 0) { void handleSend(Boolean(isSilent), scheduledAt); } else if (args.sendCompressed !== undefined || args.sendGrouped !== undefined) { const { sendCompressed = false, sendGrouped = false, isInvertedMedia } = args; void handleSendAttachments(sendCompressed, sendGrouped, isSilent, scheduledAt, isInvertedMedia); } else { sendMessage({ ...args, messageList, scheduledAt, effectId, }); } }); useEffectWithPrevDeps(([prevContentToBeScheduled]) => { if (currentMessageList && contentToBeScheduled && contentToBeScheduled !== prevContentToBeScheduled) { requestCalendar((scheduledAt) => { handleMessageSchedule(contentToBeScheduled, scheduledAt, currentMessageList); }); } }, [contentToBeScheduled, currentMessageList, handleMessageSchedule, requestCalendar]); useEffect(() => { if (requestedDraft) { insertFormattedTextAndUpdateCursor(requestedDraft); resetOpenChatWithDraft(); requestNextMutation(() => { const messageInput = document.getElementById(editableInputId)!; focusEditableElement(messageInput, true); }); } }, [editableInputId, requestedDraft, resetOpenChatWithDraft, setHtml]); useEffect(() => { if (requestedDraftFiles?.length) { void handleFileSelect(requestedDraftFiles); resetOpenChatWithDraft(); } }, [handleFileSelect, requestedDraftFiles, resetOpenChatWithDraft]); const handleCustomEmojiSelect = useLastCallback((emoji: ApiSticker, inInputId?: string) => { const emojiSetId = 'id' in emoji.stickerSetInfo && emoji.stickerSetInfo.id; if (!emoji.isFree && !isCurrentUserPremium && !isChatWithSelf && emojiSetId !== chatEmojiSetId) { showCustomEmojiPremiumNotification(); return; } insertCustomEmojiAndUpdateCursor(emoji, inInputId); }); const handleCustomEmojiSelectAttachmentModal = useLastCallback((emoji: ApiSticker) => { handleCustomEmojiSelect(emoji, EDITABLE_INPUT_MODAL_ID); }); const handleGifSelect = useLastCallback((gif: ApiVideo, isSilent?: boolean, isScheduleRequested?: boolean) => { if (!currentMessageList && !storyId) { return; } if (isInScheduledList || isScheduleRequested) { forceShowSymbolMenu(); requestCalendar((scheduledAt) => { cancelForceShowSymbolMenu(); handleMessageSchedule({ gif, isSilent }, scheduledAt, currentMessageList!); requestMeasure(() => { resetComposer(true); }); }); } else { sendMessage({ messageList: currentMessageList, gif, isSilent }); requestMeasure(() => { resetComposer(true); }); } }); const handleStickerSelect = useLastCallback(( sticker: ApiSticker, isSilent?: boolean, isScheduleRequested?: boolean, shouldPreserveInput = false, canUpdateStickerSetsOrder?: boolean, ) => { if (!currentMessageList && !storyId) { return; } sticker = { ...sticker, isPreloadedGlobally: true, }; if (isInScheduledList || isScheduleRequested) { forceShowSymbolMenu(); requestCalendar((scheduledAt) => { cancelForceShowSymbolMenu(); handleMessageSchedule({ sticker, isSilent }, scheduledAt, currentMessageList!); requestMeasure(() => { resetComposer(shouldPreserveInput); }); }); } else { sendMessage({ messageList: currentMessageList, sticker, isSilent, shouldUpdateStickerSetOrder: shouldUpdateStickerSetOrder && canUpdateStickerSetsOrder, }); clearDraft({ chatId, threadId, isLocalOnly: true }); requestMeasure(() => { resetComposer(shouldPreserveInput); }); } }); const handleInlineBotSelect = useLastCallback(( inlineResult: ApiBotInlineResult | ApiBotInlineMediaResult, isSilent?: boolean, isScheduleRequested?: boolean, ) => { if (!currentMessageList && !storyId) { return; } if (isInScheduledList || isScheduleRequested) { requestCalendar((scheduledAt) => { handleMessageSchedule({ id: inlineResult.id, queryId: inlineResult.queryId, isSilent, }, scheduledAt, currentMessageList!); }); } else { sendInlineBotResult({ id: inlineResult.id, queryId: inlineResult.queryId, isSilent, messageList: currentMessageList!, }); } const messageInput = document.querySelector(editableInputCssSelector); if (IS_IOS && messageInput && messageInput === document.activeElement) { applyIosAutoCapitalizationFix(messageInput); } clearDraft({ chatId, isLocalOnly: true }); requestMeasure(() => { resetComposer(); }); }); const handleBotCommandSelect = useLastCallback(() => { clearDraft({ chatId, isLocalOnly: true }); requestMeasure(() => { resetComposer(); }); }); const handlePollSend = useLastCallback((poll: ApiNewPoll) => { if (!currentMessageList) { return; } if (isInScheduledList) { requestCalendar((scheduledAt) => { handleMessageSchedule({ poll }, scheduledAt, currentMessageList); }); closePollModal(); } else { sendMessage({ messageList: currentMessageList, poll }); closePollModal(); } }); const sendSilent = useLastCallback((additionalArgs?: ScheduledMessageArgs) => { if (isInScheduledList) { requestCalendar((scheduledAt) => { handleMessageSchedule({ ...additionalArgs, isSilent: true }, scheduledAt, currentMessageList!); }); } else if (additionalArgs && ('sendCompressed' in additionalArgs || 'sendGrouped' in additionalArgs)) { const { sendCompressed = false, sendGrouped = false, isInvertedMedia } = additionalArgs; void handleSendAttachments(sendCompressed, sendGrouped, true, undefined, isInvertedMedia); } else { void handleSend(true); } }); const handleSendAsMenuOpen = useLastCallback(() => { const messageInput = document.querySelector(editableInputCssSelector); if (!isMobile || messageInput !== document.activeElement) { closeBotCommandMenu(); closeSymbolMenu(); openSendAsMenu(); return; } messageInput?.blur(); setTimeout(() => { closeBotCommandMenu(); closeSymbolMenu(); openSendAsMenu(); }, MOBILE_KEYBOARD_HIDE_DELAY_MS); }); useEffect(() => { if (!isComposerBlocked) return; setHtml(''); }, [isComposerBlocked, setHtml, attachments]); const insertTextAndUpdateCursorAttachmentModal = useLastCallback((text: string) => { insertTextAndUpdateCursor(text, EDITABLE_INPUT_MODAL_ID); }); const removeSymbol = useLastCallback((inInputId = editableInputId) => { const selection = window.getSelection()!; if (selection.rangeCount) { const selectionRange = selection.getRangeAt(0); if (isSelectionInsideInput(selectionRange, inInputId)) { document.execCommand('delete', false); return; } } setHtml(deleteLastCharacterOutsideSelection(getHtml())); }); const removeSymbolAttachmentModal = useLastCallback(() => { removeSymbol(EDITABLE_INPUT_MODAL_ID); }); const handleAllScheduledClick = useLastCallback(() => { openThread({ chatId, threadId, type: 'scheduled', noForumTopicPanel: true, }); }); useEffect(() => { if (isRightColumnShown && isMobile) { closeSymbolMenu(); } }, [isRightColumnShown, closeSymbolMenu, isMobile]); useEffect(() => { if (!isReady) return; if (isSelectModeActive) { disableHover(); } else { setTimeout(() => { enableHover(); }, SELECT_MODE_TRANSITION_MS); } }, [isSelectModeActive, enableHover, disableHover, isReady]); const withBotMenuButton = isChatWithBot && botMenuButton?.type === 'webApp' && !editingMessage; const isBotMenuButtonOpen = useDerivedState(() => { return withBotMenuButton && !getHtml() && !activeVoiceRecording; }, [withBotMenuButton, getHtml, activeVoiceRecording]); const [timedPlaceholderLangKey, timedPlaceholderDate] = useMemo(() => { if (slowMode?.nextSendDate) { return ['SlowModeWait', slowMode.nextSendDate]; } if (stealthMode?.activeUntil && isInStoryViewer) { return ['StealthModeActiveHint', stealthMode.activeUntil]; } return []; }, [isInStoryViewer, slowMode?.nextSendDate, stealthMode?.activeUntil]); const isComposerHasFocus = isBotKeyboardOpen || isSymbolMenuOpen || isEmojiTooltipOpen || isSendAsMenuOpen || isMentionTooltipOpen || isInlineBotTooltipOpen || isBotCommandMenuOpen || isAttachMenuOpen || isStickerTooltipOpen || isChatCommandTooltipOpen || isCustomEmojiTooltipOpen || isBotMenuButtonOpen || isCustomSendMenuOpen || Boolean(activeVoiceRecording) || attachments.length > 0 || isInputHasFocus; const isReactionSelectorOpen = isComposerHasFocus && !isReactionPickerOpen && isInStoryViewer && !isAttachMenuOpen && !isSymbolMenuOpen; const placeholderForForumAsMessages = chat?.isForum && chat?.isForumAsMessages && threadId === MAIN_THREAD_ID ? (replyToTopic ? lang('Chat.InputPlaceholderReplyInTopic', replyToTopic.title) : lang('Message.Placeholder.MessageInGeneral')) : undefined; useEffect(() => { if (isComposerHasFocus) { onFocus?.(); } else { onBlur?.(); } }, [isComposerHasFocus, onBlur, onFocus]); const { shouldRender: shouldRenderReactionSelector, transitionClassNames: reactionSelectorTransitonClassNames, } = useShowTransitionDeprecated(isReactionSelectorOpen); const areVoiceMessagesNotAllowed = mainButtonState === MainButtonState.Record && (!canAttachMedia || !canSendVoiceByPrivacy || !canSendVoices); const mainButtonHandler = useLastCallback(() => { switch (mainButtonState) { case MainButtonState.Forward: onForward?.(); break; case MainButtonState.Send: void handleSend(); break; case MainButtonState.Record: { if (areVoiceMessagesNotAllowed) { if (!canSendVoiceByPrivacy) { showNotification({ message: lang('VoiceMessagesRestrictedByPrivacy', chat?.title), }); } else if (!canSendVoices) { showAllowedMessageTypesNotification({ chatId }); } } else { setIsViewOnceEnabled(false); void startRecordingVoice(); } break; } case MainButtonState.Edit: handleEditComplete(); break; case MainButtonState.Schedule: if (activeVoiceRecording) { pauseRecordingVoice(); } if (!currentMessageList) { return; } requestCalendar((scheduledAt) => { handleMessageSchedule({}, scheduledAt, currentMessageList, effect?.id); }); break; default: break; } }); const scheduledDefaultDate = new Date(); scheduledDefaultDate.setSeconds(0); scheduledDefaultDate.setMilliseconds(0); const scheduledMaxDate = new Date(); scheduledMaxDate.setFullYear(scheduledMaxDate.getFullYear() + 1); let sendButtonAriaLabel = 'SendMessage'; switch (mainButtonState) { case MainButtonState.Forward: sendButtonAriaLabel = 'Forward'; break; case MainButtonState.Edit: sendButtonAriaLabel = 'Save edited message'; break; case MainButtonState.Record: sendButtonAriaLabel = !canAttachMedia ? 'Conversation.DefaultRestrictedMedia' : 'AccDescrVoiceMessage'; } const fullClassName = buildClassName( 'Composer', !isSelectModeActive && 'shown', isHoverDisabled && 'hover-disabled', isMounted && 'mounted', className, ); const handleToggleReaction = useLastCallback((reaction: ApiReaction) => { let text: string | undefined; let entities: ApiMessageEntity[] | undefined; if (reaction.type === 'emoji') { text = reaction.emoticon; } if (reaction.type === 'custom') { const sticker = getGlobal().customEmojis.byId[reaction.documentId]; if (!sticker) { return; } if (!sticker.isFree && !isCurrentUserPremium && !isChatWithSelf) { showCustomEmojiPremiumNotification(); return; } const customEmojiMessage = parseHtmlAsFormattedText(buildCustomEmojiHtml(sticker)); text = customEmojiMessage.text; entities = customEmojiMessage.entities; } sendMessage({ text, entities, isReaction: true }); closeReactionPicker(); }); const handleToggleEffectReaction = useLastCallback((reaction: ApiReaction) => { setReactionEffect({ chatId, threadId, reaction }); closeReactionPicker(); }); const handleReactionPickerOpen = useLastCallback((position: IAnchorPosition) => { openStoryReactionPicker({ peerId: chatId, storyId: storyId!, position, sendAsMessage: true, }); }); const handleLikeStory = useLastCallback(() => { const reaction = sentStoryReaction ? undefined : HEART_REACTION; sendStoryReaction({ peerId: chatId, storyId: storyId!, containerId: getStoryKey(chatId, storyId!), reaction, }); }); const handleSendScheduled = useLastCallback(() => { requestCalendar((scheduledAt) => { handleMessageSchedule({}, scheduledAt, currentMessageList!); }); }); const handleSendSilent = useLastCallback(() => { sendSilent(); }); const handleSendWhenOnline = useLastCallback(() => { handleMessageSchedule({}, SCHEDULED_WHEN_ONLINE, currentMessageList!, effect?.id); }); const handleSendScheduledAttachments = useLastCallback( (sendCompressed: boolean, sendGrouped: boolean, isInvertedMedia?: true) => { requestCalendar((scheduledAt) => { handleMessageSchedule({ sendCompressed, sendGrouped, isInvertedMedia }, scheduledAt, currentMessageList!); }); }, ); const handleSendSilentAttachments = useLastCallback( (sendCompressed: boolean, sendGrouped: boolean, isInvertedMedia?: true) => { sendSilent({ sendCompressed, sendGrouped, isInvertedMedia }); }, ); const handleRemoveEffect = useLastCallback(() => { saveEffectInDraft({ chatId, threadId, effectId: undefined }); }); const handleStopEffect = useLastCallback(() => { hideEffectInComposer({ }); }); const onSend = useMemo(() => { switch (mainButtonState) { case MainButtonState.Edit: return handleEditComplete; case MainButtonState.Schedule: return handleSendScheduled; default: return handleSend; } }, [mainButtonState, handleEditComplete]); const withBotCommands = isChatWithBot && botMenuButton?.type === 'commands' && !editingMessage && botCommands !== false && !activeVoiceRecording; const effectEmoji = areEffectsSupported && effect?.emoticon; return (
{isInMessageList && canAttachMedia && isReady && ( )} {shouldRenderReactionSelector && !isNeedPremium && ( )}
{!isNeedPremium && ( )} {isInMessageList && ( <> )}
{isInMessageList && ( <> {withBotMenuButton && ( )} {withBotCommands && ( )} {canShowSendAs && (sendAsUser || sendAsChat) && ( )} )} {((!isComposerBlocked || canSendGifs || canSendStickers) && !isNeedPremium) && ( )} {isInMessageList && ( <> {isInlineBotLoading && Boolean(inlineBotId) && ( )} {withScheduledButton && ( )} {Boolean(botKeyboardMessageId) && !activeVoiceRecording && !editingMessage && ( )} )} {activeVoiceRecording && Boolean(currentRecordTime) && ( {formatVoiceRecordDuration(currentRecordTime - startRecordTimeRef.current!)} )} {!isNeedPremium && ( )} {isInMessageList && Boolean(botKeyboardMessageId) && ( )} {isInMessageList && botCommands && ( )}
{canSendOneTimeMedia && activeVoiceRecording && ( )} {activeVoiceRecording && ( )} {isInStoryViewer && !activeVoiceRecording && ( )} {effectEmoji && ( {renderText(effectEmoji)} )} {effect && canPlayEffect && ( )} {canShowCustomSendMenu && ( )} {calendar}
); }; export default memo(withGlobal( (global, { chatId, threadId, storyId, messageListType, isMobile, type, }): StateProps => { const chat = selectChat(global, chatId); const chatBot = !isSystemBot(chatId) ? selectBot(global, chatId) : undefined; const isChatWithBot = Boolean(chatBot); const isChatWithSelf = selectIsChatWithSelf(global, chatId); const isChatWithUser = isUserId(chatId); const userFullInfo = isChatWithUser ? selectUserFullInfo(global, chatId) : undefined; const chatFullInfo = !isChatWithUser ? selectChatFullInfo(global, chatId) : undefined; const messageWithActualBotKeyboard = (isChatWithBot || !isChatWithUser) && selectNewestMessageWithBotKeyboardButtons(global, chatId, threadId); const { language, shouldSuggestStickers, shouldSuggestCustomEmoji, shouldUpdateStickerSetOrder, } = global.settings.byKey; const baseEmojiKeywords = global.emojiKeywords[BASE_EMOJI_KEYWORD_LANG]; const emojiKeywords = language !== BASE_EMOJI_KEYWORD_LANG ? global.emojiKeywords[language] : undefined; const botKeyboardMessageId = messageWithActualBotKeyboard ? messageWithActualBotKeyboard.id : undefined; const keyboardMessage = botKeyboardMessageId ? selectChatMessage(global, chatId, botKeyboardMessageId) : undefined; const { currentUserId } = global; const currentUser = selectUser(global, currentUserId!)!; const defaultSendAsId = chatFullInfo ? chatFullInfo?.sendAsId || currentUserId : undefined; const sendAsId = chat?.sendAsPeerIds && defaultSendAsId && ( chat.sendAsPeerIds.some((peer) => peer.id === defaultSendAsId) ? defaultSendAsId : (chat?.adminRights?.anonymous ? chat?.id : undefined) ); const sendAsUser = sendAsId ? selectUser(global, sendAsId) : undefined; const sendAsChat = !sendAsUser && sendAsId ? selectChat(global, sendAsId) : undefined; const requestedDraft = selectRequestedDraft(global, chatId); const requestedDraftFiles = selectRequestedDraftFiles(global, chatId); const tabState = selectTabState(global); const isStoryViewerOpen = Boolean(tabState.storyViewer.storyId); const currentMessageList = selectCurrentMessageList(global); const isForCurrentMessageList = chatId === currentMessageList?.chatId && threadId === currentMessageList?.threadId && messageListType === currentMessageList?.type && !isStoryViewerOpen; const user = selectUser(global, chatId); const canSendVoiceByPrivacy = (user && !userFullInfo?.noVoiceMessages) ?? true; const slowMode = chatFullInfo?.slowMode; const isCurrentUserPremium = selectIsCurrentUserPremium(global); const editingDraft = messageListType === 'scheduled' ? selectEditingScheduledDraft(global, chatId) : selectEditingDraft(global, chatId, threadId); const story = storyId && selectPeerStory(global, chatId, storyId); const sentStoryReaction = story && 'sentReaction' in story ? story.sentReaction : undefined; const draft = selectDraft(global, chatId, threadId); const replyToMessage = draft?.replyInfo ? selectChatMessage(global, chatId, draft.replyInfo.replyToMsgId) : undefined; const replyToTopic = chat?.isForum && chat.isForumAsMessages && threadId === MAIN_THREAD_ID && replyToMessage ? selectTopicFromMessage(global, replyToMessage) : undefined; const isInScheduledList = messageListType === 'scheduled'; const canSendQuickReplies = isChatWithUser && !isChatWithBot && !isInScheduledList && !isChatWithSelf; const noWebPage = selectNoWebPage(global, chatId, threadId); const areEffectsSupported = isChatWithUser && !isChatWithBot && !isInScheduledList && !isChatWithSelf && type !== 'story' && chatId !== SERVICE_NOTIFICATIONS_USER_ID; const canPlayEffect = selectPerformanceSettingsValue(global, 'stickerEffects'); const shouldPlayEffect = tabState.shouldPlayEffectInComposer; const effectId = areEffectsSupported && draft?.effectId; const effect = effectId ? global.availableEffectById[effectId] : undefined; const effectReactions = global.reactions.effectReactions; const maxMessageLength = global.config?.maxMessageLength || DEFAULT_MAX_MESSAGE_LENGTH; return { availableReactions: global.reactions.availableReactions, topReactions: type === 'story' ? global.reactions.topReactions : undefined, isOnActiveTab: !tabState.isBlurred, editingMessage: selectEditingMessage(global, chatId, threadId, messageListType), draft, chat, isChatWithBot, isChatWithSelf, isForCurrentMessageList, canScheduleUntilOnline: selectCanScheduleUntilOnline(global, chatId), isChannel: chat ? isChatChannel(chat) : undefined, isRightColumnShown: selectIsRightColumnShown(global, isMobile), isSelectModeActive: selectIsInSelectMode(global), withScheduledButton: ( messageListType === 'thread' && (userFullInfo || chatFullInfo)?.hasScheduledMessages ), isInScheduledList, botKeyboardMessageId, botKeyboardPlaceholder: keyboardMessage?.keyboardPlaceholder, isForwarding: chatId === tabState.forwardMessages.toChatId, pollModal: tabState.pollModal, stickersForEmoji: global.stickers.forEmoji.stickers, customEmojiForEmoji: global.customEmojis.forEmoji.stickers, chatFullInfo, topInlineBotIds: global.topInlineBots?.userIds, currentUserId, currentUser, contentToBeScheduled: tabState.contentToBeScheduled, shouldSuggestStickers, shouldSuggestCustomEmoji, shouldUpdateStickerSetOrder, recentEmojis: global.recentEmojis, baseEmojiKeywords: baseEmojiKeywords?.keywords, emojiKeywords: emojiKeywords?.keywords, inlineBots: tabState.inlineBots.byUsername, isInlineBotLoading: tabState.inlineBots.isLoading, botCommands: userFullInfo ? (userFullInfo.botInfo?.commands || false) : undefined, botMenuButton: userFullInfo?.botInfo?.menuButton, sendAsUser, sendAsChat, sendAsId, editingDraft, requestedDraft, requestedDraftFiles, attachBots: global.attachMenu.bots, attachMenuPeerType: selectChatType(global, chatId), theme: selectTheme(global), fileSizeLimit: selectCurrentLimit(global, 'uploadMaxFileparts') * MAX_UPLOAD_FILEPART_SIZE, captionLimit: selectCurrentLimit(global, 'captionLength'), isCurrentUserPremium, canSendVoiceByPrivacy, attachmentSettings: global.attachmentSettings, slowMode, currentMessageList, isReactionPickerOpen: selectIsReactionPickerOpen(global), canBuyPremium: !isCurrentUserPremium && !selectIsPremiumPurchaseBlocked(global), canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global), canSendOneTimeMedia: !isChatWithSelf && isChatWithUser && !isChatWithBot && !isInScheduledList, shouldCollectDebugLogs: global.settings.byKey.shouldCollectDebugLogs, sentStoryReaction, stealthMode: global.stories.stealthMode, replyToTopic, quickReplyMessages: global.quickReplies.messagesById, quickReplies: global.quickReplies.byId, canSendQuickReplies, noWebPage, webPagePreview: selectTabState(global).webPagePreview, isContactRequirePremium: userFullInfo?.isContactRequirePremium, effect, effectReactions, areEffectsSupported, canPlayEffect, shouldPlayEffect, maxMessageLength, }; }, )(Composer));