import type { TeactNode } from '../../lib/teact/teact'; import { 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, ApiDisallowedGifts, ApiDraft, ApiFormattedText, ApiMessage, ApiMessageEntity, ApiNewMediaTodo, ApiNewPoll, ApiPeer, ApiQuickReply, ApiReaction, ApiStealthMode, ApiSticker, ApiTopic, ApiUser, ApiVideo, ApiWebPage, } from '../../api/types'; import type { GlobalState, TabState } from '../../global/types'; import type { IAnchorPosition, InlineBotSettings, MessageList, MessageListType, ThemeKey, ThreadId, } from '../../types'; import { ApiMediaFormat, MAIN_THREAD_ID } from '../../api/types'; import { BASE_EMOJI_KEYWORD_LANG, 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, STARS_CURRENCY_CODE, } from '../../config'; import { requestMeasure, requestNextMutation } from '../../lib/fasterdom/fasterdom'; import { canEditMedia, getAllowedAttachmentOptions, getMediaFilename, getMediaHash, getMessageDocumentPhoto, getMessagePhoto, getReactionKey, getStoryKey, isChatAdmin, isChatChannel, isChatPublic, isChatSuperGroup, isSameReaction, isSystemBot, } from '../../global/helpers'; import { getChatNotifySettings } from '../../global/helpers/notifications'; import { getPeerTitle } from '../../global/helpers/peers'; import { selectBot, selectCanPlayAnimatedEmojis, selectCanScheduleUntilOnline, selectChat, selectChatFullInfo, selectChatMessage, selectChatType, selectCurrentMessageList, selectCustomEmoji, selectEditingMessage, selectIsChatWithSelf, selectIsCurrentUserFrozen, selectIsCurrentUserPremium, selectIsInSelectMode, selectIsPremiumPurchaseBlocked, selectIsReactionPickerOpen, selectIsRightColumnShown, selectNewestMessageWithBotKeyboardButtons, selectNotifyDefaults, selectNotifyException, selectPeer, selectPeerPaidMessagesStars, selectPeerStory, selectPerformanceSettingsValue, selectRequestedDraft, selectRequestedDraftFiles, selectTabState, selectTheme, selectTopicFromMessage, selectUser, selectUserFullInfo, selectWebPage, } from '../../global/selectors'; import { selectCurrentLimit } from '../../global/selectors/limits'; import { selectSharedSettings } from '../../global/selectors/sharedState'; import { selectDraft, selectEditingDraft, selectEditingScheduledDraft, selectNoWebPage, } from '../../global/selectors/threads'; import { IS_IOS, IS_VOICE_RECORDING_SUPPORTED } from '../../util/browser/windowEnvironment'; import buildClassName from '../../util/buildClassName'; import { formatMediaDuration, formatVoiceRecordDuration } from '../../util/dates/oldDateFormat'; import { processDeepLink } from '../../util/deeplink'; import { tryParseDeepLink } from '../../util/deepLinkParser'; import deleteLastCharacterOutsideSelection from '../../util/deleteLastCharacterOutsideSelection'; import calcTextLineHeightAndCount from '../../util/element/calcTextLineHeightAndCount'; import { processMessageInputForCustomEmoji } from '../../util/emoji/customEmojiManager'; import { isUserId } from '../../util/entities/ids'; import { fetchBlob } from '../../util/files'; import focusEditableElement from '../../util/focusEditableElement'; import { formatStarsAsIcon } from '../../util/localization/format'; import { fetch } from '../../util/mediaLoader'; import { MEMO_EMPTY_ARRAY } from '../../util/memo'; import parseHtmlAsFormattedText from '../../util/parseHtmlAsFormattedText'; import { insertHtmlInSelection } from '../../util/selection'; import { getServerTime } from '../../util/serverTime'; import windowSize from '../../util/windowSize'; import { DEFAULT_MAX_MESSAGE_LENGTH } from '../../limits'; import applyIosAutoCapitalizationFix from '../middle/composer/helpers/applyIosAutoCapitalizationFix'; import buildAttachment, { buildGifAttachment, prepareAttachmentsToSend, } from '../middle/composer/helpers/buildAttachment'; import { buildCustomEmojiHtml } from '../middle/composer/helpers/customEmoji'; import { isSelectionInsideInput } from '../middle/composer/helpers/selection'; 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 useForceUpdate from '../../hooks/useForceUpdate'; import useGetSelectionRange from '../../hooks/useGetSelectionRange'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; import useOldLang from '../../hooks/useOldLang'; import usePeerColor from '../../hooks/usePeerColor'; import usePrevious from '../../hooks/usePrevious'; 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 useLoadLinkPreview from '../middle/composer/hooks/useLoadLinkPreview'; import useMentionTooltip from '../middle/composer/hooks/useMentionTooltip'; import usePaidMessageConfirmation from '../middle/composer/hooks/usePaidMessageConfirmation'; 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 ToDoListModal from '../middle/composer/ToDoListModal.async'; 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 TextTimer from '../ui/TextTimer'; import Transition from '../ui/Transition'; import AnimatedCounter from './AnimatedCounter'; import Avatar from './Avatar'; import Icon from './icons/Icon'; import PaymentMessageConfirmDialog from './PaymentMessageConfirmDialog'; 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?: TeactNode | string; onDropHide?: NoneToVoidFunction; onForward?: NoneToVoidFunction; onFocus?: NoneToVoidFunction; onBlur?: NoneToVoidFunction; }; type StateProps = { isOnActiveTab: boolean; editingMessage?: ApiMessage; chat?: ApiChat; user?: ApiUser; chatFullInfo?: ApiChatFullInfo; draft?: ApiDraft; replyToTopic?: ApiTopic; currentMessageList?: MessageList; isChatWithBot?: boolean; isChatWithSelf?: boolean; isChannel?: boolean; isForCurrentMessageList: boolean; isRightColumnShown?: boolean; isSelectModeActive?: boolean; isReactionPickerOpen?: boolean; shouldDisplayGiftsButton?: boolean; isForwarding?: boolean; isReplying?: boolean; hasSuggestedPost?: boolean; forwardedMessagesCount?: number; pollModal: TabState['pollModal']; todoListModal: TabState['todoListModal']; aiMessageEditorPendingResult: TabState['aiMessageEditorPendingResult']; 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; sendAsPeer?: ApiPeer; sendAsId?: string; editingDraft?: ApiFormattedText; requestedDraft?: ApiFormattedText; requestedDraftFiles?: File[]; attachBots: GlobalState['attachMenu']['bots']; attachMenuPeerType?: ApiAttachMenuPeerType; theme: ThemeKey; 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; paidMessagesStars?: number; effect?: ApiAvailableEffect; effectReactions?: ApiReaction[]; areEffectsSupported?: boolean; canPlayEffect?: boolean; shouldPlayEffect?: boolean; maxMessageLength: number; shouldPaidMessageAutoApprove?: boolean; isSilentPosting?: boolean; isPaymentMessageConfirmDialogOpen: boolean; starsBalance: number; isStarsBalanceModalOpen: boolean; disallowedGifts?: ApiDisallowedGifts; isAccountFrozen?: boolean; isAppConfigLoaded?: boolean; insertingPeerIdMention?: string; pollMaxAnswers?: number; replyToMessage?: ApiMessage; shouldOpenMessageMediaEditor?: TabState['shouldOpenMessageMediaEditor']; }; 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 = ({ type, isOnActiveTab, dropAreaState, isInScheduledList, canScheduleUntilOnline, isReady, isMobile, editingMessage, chatId, threadId, storyId, currentMessageList, messageListType, draft, chat, chatFullInfo, user, replyToTopic, isForCurrentMessageList, isCurrentUserPremium, canSendVoiceByPrivacy, isChatWithBot, isChatWithSelf, isChannel, fileSizeLimit, isRightColumnShown, isSelectModeActive, isReactionPickerOpen, shouldDisplayGiftsButton, isForwarding, isReplying, hasSuggestedPost, forwardedMessagesCount, pollModal, todoListModal, aiMessageEditorPendingResult, botKeyboardMessageId, botKeyboardPlaceholder, inputPlaceholder, withScheduledButton, stickersForEmoji, customEmojiForEmoji, topInlineBotIds, currentUserId, currentUser, captionLimit, contentToBeScheduled, shouldSuggestStickers, shouldSuggestCustomEmoji, baseEmojiKeywords, emojiKeywords, recentEmojis, inlineBots, isInlineBotLoading, botCommands, sendAsPeer, 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, webPagePreview, noWebPage, isContactRequirePremium, paidMessagesStars, effect, effectReactions, areEffectsSupported, canPlayEffect, shouldPlayEffect, maxMessageLength, isSilentPosting, isPaymentMessageConfirmDialogOpen, starsBalance, isStarsBalanceModalOpen, disallowedGifts, isAccountFrozen, isAppConfigLoaded, insertingPeerIdMention, pollMaxAnswers, replyToMessage, shouldOpenMessageMediaEditor, onDropHide, onFocus, onBlur, onForward, }: OwnProps & StateProps) => { const { sendMessage, clearDraft, saveDraft, showDialog, openPollModal, closePollModal, openTodoListModal, closeTodoListModal, openAiMessageEditorModal, clearAiMessageEditorPendingResult, loadScheduledHistory, openThread, addRecentEmoji, sendInlineBotResult, loadSendAs, resetOpenChatWithDraft, callAttachBot, addRecentCustomEmoji, showNotification, showAllowedMessageTypesNotification, openStoryReactionPicker, openGiftModal, closeReactionPicker, sendStoryReaction, editMessage, updateAttachmentSettings, saveEffectInDraft, setReactionEffect, hideEffectInComposer, updateChatSilentPosting, updateInsertingPeerIdMention, updateDraftSuggestedPostInfo, updateShouldSaveAttachmentsCompression, applyDefaultAttachmentsCompression, } = getActions(); const oldLang = useOldLang(); const lang = useLang(); const inputRef = useRef(); const counterRef = useRef(); const storyReactionRef = useRef(); const [getHtml, setHtml] = useSignal(''); const [isMounted, setIsMounted] = useState(false); const getSelectionRange = useGetSelectionRange(editableInputCssSelector); const lastMessageSendTimeSecondsRef = useRef(); const prevDropAreaState = usePreviousDeprecated(dropAreaState); const { width: windowWidth } = windowSize.get(); const forceUpdate = useForceUpdate(); const isInMessageList = type === 'messageList'; const isInStoryViewer = type === 'story'; const sendAsPeerIds = isInMessageList ? chat?.sendAsPeerIds : undefined; const canShowSendAs = Boolean(sendAsPeerIds?.length); // 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 isMonoforum = chat?.isMonoforum; const { emojiSet, members: groupChatMembers, botCommands: chatBotCommands } = chatFullInfo || {}; const chatEmojiSetId = emojiSet?.id; const canSchedule = !paidMessagesStars && !isMonoforum; const isSentStoryReactionHeart = sentStoryReaction && isSameReaction(sentStoryReaction, HEART_REACTION); useEffect(processMessageInputForCustomEmoji, [getHtml]); const customEmojiNotificationNumberRef = useRef(0); const [requestCalendar, calendar] = useSchedule( isInMessageList && canScheduleUntilOnline, cancelForceShowSymbolMenu, ); useTimeout(() => { setIsMounted(true); }, MOUNT_ANIMATION_DURATION); useEffect(() => { if (isInMessageList) return; closeReactionPicker(); }, [isInMessageList, storyId]); useEffect(() => { lastMessageSendTimeSecondsRef.current = undefined; }, [chatId]); useEffect(() => { if (isAppConfigLoaded && chatId && isReady && !isInStoryViewer && !isMonoforum) { loadScheduledHistory({ chatId }); } }, [isReady, chatId, threadId, isInStoryViewer, isAppConfigLoaded, isMonoforum]); useEffect(() => { const isChannelWithProfiles = isChannel && chat?.areProfilesShown; const isChatWithSendAs = chat && isChatSuperGroup(chat) && Boolean(isChatPublic(chat) || chat.isLinkedInDiscussion || chat.hasGeo); if (!sendAsPeerIds && isReady && (isChatWithSendAs || 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); useEffect(() => { if (!attachments.length || !attachments) { updateShouldSaveAttachmentsCompression({ shouldSave: false }); } }, [attachments]); const { canSendStickers, canSendGifs, canAttachMedia, canAttachPolls, canAttachEmbedLinks, canAttachToDoLists, canSendVoices, canSendPlainText, canSendAudios, canSendVideos, canSendPhotos, canSendDocuments, } = useMemo( () => getAllowedAttachmentOptions( chat, chatFullInfo, isChatWithBot, isChatWithSelf, isInStoryViewer, paidMessagesStars, isInScheduledList, ), [chat, chatFullInfo, isChatWithBot, isChatWithSelf, isInStoryViewer, paidMessagesStars, isInScheduledList], ); const isNeedPremium = isContactRequirePremium && isInStoryViewer; const isSendTextBlocked = isNeedPremium || !canSendPlainText; const messagesCount = useDerivedState(() => { if (hasAttachments) return attachments.length; const messagesInInput = (getHtml() || hasAttachments) ? 1 : 0; if (!isForwarding || !forwardedMessagesCount) return messagesInInput || 1; return forwardedMessagesCount + messagesInInput; }, [getHtml, hasAttachments, attachments, isForwarding, forwardedMessagesCount]); const starsForAllMessages = paidMessagesStars ? messagesCount * paidMessagesStars : 0; const { closeConfirmDialog: closeConfirmModalPayForMessage, dialogHandler: paymentMessageConfirmDialogHandler, shouldAutoApprove: shouldPaidMessageAutoApprove, setAutoApprove: setShouldPaidMessageAutoApprove, handleWithConfirmation: handleActionWithPaymentConfirmation, } = usePaidMessageConfirmation(starsForAllMessages, isStarsBalanceModalOpen, starsBalance); const hasWebPagePreview = !hasAttachments && canAttachEmbedLinks && !noWebPage && webPagePreview?.webpageType === 'full'; const isComposerBlocked = isSendTextBlocked && !editingMessage; useEffect(() => { if (!hasWebPagePreview) { updateAttachmentSettings({ isInvertedMedia: undefined }); } }, [hasWebPagePreview]); const insertHtmlAndUpdateCursor = useLastCallback(( newHtml: string, inInputId: string = editableInputId, shouldPrepend = false, ) => { if (inInputId === editableInputId && isComposerBlocked) return; const selection = window.getSelection()!; const savedSelectionRange = getSelectionRange(); let messageInput: HTMLDivElement; if (inInputId === editableInputId) { messageInput = document.querySelector(editableInputCssSelector)!; } else { messageInput = document.getElementById(inInputId) as HTMLDivElement; } if (!shouldPrepend) { let selectionRange: Range | undefined; if (selection.rangeCount) { const currentSelectionRange = selection.getRangeAt(0); if (isSelectionInsideInput(currentSelectionRange, inInputId)) { selectionRange = currentSelectionRange; } } if (!selectionRange && savedSelectionRange && isSelectionInsideInput(savedSelectionRange, inInputId)) { selectionRange = savedSelectionRange.cloneRange(); } if (selectionRange) { try { if (!selection.rangeCount || selection.getRangeAt(0) !== selectionRange) { selection.removeAllRanges(); selection.addRange(selectionRange); } insertHtmlInSelection(newHtml); messageInput.dispatchEvent(new Event('input', { bubbles: true })); return; } catch { // Fall back to appending below if restoring the previous range fails. } } } if (shouldPrepend) { const newFirstWord = newHtml.split(' ')[0]; const shouldReplace = getHtml().startsWith(newFirstWord); setHtml(shouldReplace ? newHtml : `${newHtml}${getHtml()}`); } else { 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']) as string[]) .join('') .replace(/\u200b+/g, '\u200b'); insertHtmlAndUpdateCursor(newHtml, inInputId); }); const insertFormattedTextAndUpdateCursor = useLastCallback(( text: ApiFormattedText, inInputId: string = editableInputId, shouldPrepend = false, ) => { const newHtml = getTextWithEntitiesAsHtml(text); insertHtmlAndUpdateCursor(newHtml, inInputId, shouldPrepend); }); const insertCustomEmojiAndUpdateCursor = useLastCallback((emoji: ApiSticker, inInputId: string = editableInputId) => { insertHtmlAndUpdateCursor(buildCustomEmojiHtml(emoji), inInputId); }); const insertNextText = useLastCallback(() => { if (!nextText) return; insertFormattedTextAndUpdateCursor(nextText, editableInputId); setNextText(undefined); }); const { shouldForceCompression, shouldForceAsFile, handleAppendFiles, handleFileSelect, onCaptionUpdate, handleClearAttachments, handleSetAttachments, } = useAttachmentModal({ attachments, setHtml, setAttachments, fileSizeLimit, chatId, canSendAudios, canSendVideos, canSendPhotos, canSendDocuments, insertNextText, editedMessage: editingMessage, shouldSendInHighQuality: attachmentSettings.shouldSendInHighQuality, }); const mediaEditRequestRef = useRef(); useEffect(() => { if (!shouldOpenMessageMediaEditor) return; const targetMessage = editingMessage || replyToMessage; const media = targetMessage && (getMessagePhoto(targetMessage) || getMessageDocumentPhoto(targetMessage)); if (!media) return; const mediaHash = getMediaHash(media, 'full'); if (!mediaHash) return; const now = Date.now(); mediaEditRequestRef.current = now; fetch(mediaHash, ApiMediaFormat.BlobUrl).then(async (blobUrl) => { if (mediaEditRequestRef.current !== now) return; const blob = await fetchBlob(blobUrl); const attachment = await buildAttachment(getMediaFilename(media), blob); handleSetAttachments([attachment]); }); }, [editingMessage, replyToMessage, shouldOpenMessageMediaEditor, handleSetAttachments]); 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, ); useEffect(() => { if (!insertingPeerIdMention) return; const peer = selectPeer(getGlobal(), insertingPeerIdMention); if (peer) { insertMention(peer, true, true); } updateInsertingPeerIdMention({ peerId: undefined }); }, [insertingPeerIdMention, insertMention]); useEffect(() => { if (!aiMessageEditorPendingResult) return; const { text, shouldClear, shouldSendWithAttachments } = aiMessageEditorPendingResult; if (shouldSendWithAttachments) return; if (shouldClear) { setHtml(''); clearDraft({ chatId, threadId, isLocalOnly: true }); } else if (text) { setHtml(getTextWithEntitiesAsHtml(text)); saveDraft({ chatId, threadId, text }); } clearAiMessageEditorPendingResult(); }, [aiMessageEditorPendingResult, chatId, clearDraft, clearAiMessageEditorPendingResult, saveDraft, setHtml, threadId]); 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) || (!hasSuggestedPost && isMonoforum), }); useLoadLinkPreview({ chatId, threadId, getHtml, }); 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 validateTextLength = useLastCallback((text: string, isAttachmentModal?: boolean) => { const maxLength = isAttachmentModal ? captionLimit : maxMessageLength; if (text?.length > maxLength) { const extraLength = text.length - maxLength; showDialog({ data: { type: 'localized', text: { key: 'ErrorMessageTooLong', variables: { count: extraLength, }, options: { pluralValue: extraLength, }, }, }, }); return false; } return true; }); const [handleEditComplete, handleEditCancel, shouldForceShowEditing] = useEditing( getHtml, setHtml, editingMessage, resetComposer, validateTextLength, 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 areAllGiftsDisallowed = useMemo(() => { if (!disallowedGifts) { return undefined; } return Object.values(disallowedGifts).every(Boolean); }, [disallowedGifts]); const shouldShowGiftButton = Boolean(!isChatWithSelf && shouldDisplayGiftsButton && !areAllGiftsDisallowed); const shouldShowSuggestedPostButton = isMonoforum && !editingMessage && !isForwarding && !isReplying && !draft?.suggestedPostInfo; const showCustomEmojiPremiumNotification = useLastCallback(() => { const notificationNumber = customEmojiNotificationNumberRef.current; if (!notificationNumber) { showNotification({ message: oldLang('UnlockPremiumEmojiHint'), action: { action: 'openPremiumModal', payload: { initialSection: 'animated_emoji' }, }, actionText: oldLang('PremiumMore'), }); } else { showNotification({ message: oldLang('UnlockPremiumEmojiHint2'), action: { action: 'openChat', payload: { id: currentUserId, shouldReplaceHistory: true }, }, actionText: oldLang('Open'), }); } customEmojiNotificationNumberRef.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]); const { className: peerColorClass, style: peerColorStyle } = usePeerColor({ peer: sendAsPeer || currentUser, theme, }); const hasGifFromPicker = attachments.some((a) => a.gif); useClipboardPaste( isForCurrentMessageList || isInStoryViewer, insertFormattedTextAndUpdateCursor, handleSetAttachments, setNextText, editingMessage, !isCurrentUserPremium && !isChatWithSelf, showCustomEmojiPremiumNotification, !attachments.length, hasGifFromPicker, ); const handleEmbeddedClear = useLastCallback(() => { if (editingMessage) { handleEditCancel(); } }); const checkSlowMode = useLastCallback(() => { if (slowMode && !isAdmin) { const messageInput = document.querySelector(editableInputCssSelector); const nowSeconds = getServerTime(); const secondsSinceLastMessage = lastMessageSendTimeSecondsRef.current && Math.floor(nowSeconds - lastMessageSendTimeSecondsRef.current); const nextSendDateNotReached = slowMode.nextSendDate && slowMode.nextSendDate > nowSeconds; if ( (secondsSinceLastMessage !== undefined && secondsSinceLastMessage < slowMode.seconds) || nextSendDateNotReached ) { const secondsRemaining = nextSendDateNotReached ? slowMode.nextSendDate! - nowSeconds : slowMode.seconds - secondsSinceLastMessage!; showDialog({ data: { type: 'localized', text: { key: 'SlowModeHint', variables: { time: formatMediaDuration(secondsRemaining), }, }, }, }); messageInput?.blur(); return false; } } return true; }); const canSendAttachments = (attachmentsToSend: ApiAttachment[]): boolean => { if (!currentMessageList && !storyId) { return false; } const { text } = parseHtmlAsFormattedText(getHtml()); if (!text && !attachmentsToSend.length) { return false; } if (!validateTextLength(text, true)) return false; if (!checkSlowMode()) return false; return true; }; const sendAttachments = useLastCallback(({ attachments: attachmentsToSend, sendCompressed = attachmentSettings.shouldCompress, sendGrouped = attachmentSettings.shouldSendGrouped, isSilent, scheduledAt, scheduleRepeatPeriod, isInvertedMedia, }: { attachments: ApiAttachment[]; sendCompressed?: boolean; sendGrouped?: boolean; isSilent?: boolean; scheduledAt?: number; scheduleRepeatPeriod?: number; isInvertedMedia?: true; }) => { if (!currentMessageList && !storyId) { return; } isSilent = isSilent || isSilentPosting; const { text, entities } = parseHtmlAsFormattedText(getHtml()); isInvertedMedia = text && sendCompressed && sendGrouped ? isInvertedMedia : undefined; if (editingMessage) { editMessage({ messageList: currentMessageList, text, entities, attachments: prepareAttachmentsToSend(attachmentsToSend, sendCompressed), }); } else { sendMessage({ messageList: currentMessageList, text, entities, scheduledAt, scheduleRepeatPeriod, isSilent, shouldUpdateStickerSetOrder, attachments: prepareAttachmentsToSend(attachmentsToSend, sendCompressed), shouldGroupMessages: sendGrouped, isInvertedMedia, }); } lastMessageSendTimeSecondsRef.current = getServerTime(); clearDraft({ chatId, threadId, isLocalOnly: true }); // Wait until message animation starts requestMeasure(() => { resetComposer(); }); }); const handleSendAttachmentsFromModal = useLastCallback(( sendCompressed: boolean, sendGrouped: boolean, isInvertedMedia?: true, ) => { if (canSendAttachments(attachments)) { if (editingMessage) { sendAttachments({ attachments, sendCompressed, sendGrouped, isInvertedMedia, }); return; } handleActionWithPaymentConfirmation(sendAttachments, { attachments, sendCompressed, sendGrouped, isInvertedMedia, }); } }); const handleSendAttachments = useLastCallback(( sendCompressed: boolean, sendGrouped: boolean, isSilent?: boolean, scheduledAt?: number, isInvertedMedia?: true, scheduleRepeatPeriod?: number, ) => { if (canSendAttachments(attachments)) { sendAttachments({ attachments, sendCompressed, sendGrouped, isSilent, scheduledAt, scheduleRepeatPeriod, isInvertedMedia, }); } }); const handleSendCore = useLastCallback( ( currentAttachments: ApiAttachment[], isSilent = false, scheduledAt?: number, scheduleRepeatPeriod?: number, ) => { const { text, entities } = parseHtmlAsFormattedText(getHtml()); if (currentAttachments.length) { if (canSendAttachments(currentAttachments)) { sendAttachments({ attachments: currentAttachments, scheduledAt, scheduleRepeatPeriod, isSilent, }); } return; } if (!text && !isForwarding) { return; } if (!validateTextLength(text)) return; const messageInput = document.querySelector(editableInputCssSelector); const effectId = effect?.id; if (text || isForwarding) { if (!checkSlowMode()) return; const isInvertedMedia = hasWebPagePreview ? attachmentSettings.isInvertedMedia : undefined; if (areEffectsSupported) saveEffectInDraft({ chatId, threadId, effectId: undefined }); sendMessage({ messageList: currentMessageList, text, entities, scheduledAt, scheduleRepeatPeriod, isSilent, shouldUpdateStickerSetOrder, isInvertedMedia, effectId, webPageMediaSize: attachmentSettings.webPageMediaSize, webPageUrl: hasWebPagePreview ? webPagePreview.url : undefined, }); } lastMessageSendTimeSecondsRef.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 handleSend = useLastCallback(async ( isSilent = false, scheduledAt?: number, scheduleRepeatPeriod?: number, ) => { if (!currentMessageList && !storyId) { return; } isSilent = isSilent || isSilentPosting; 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 }, )]; } } handleSendCore(currentAttachments, isSilent, scheduledAt, scheduleRepeatPeriod); }); const handleSendWithConfirmation = useLastCallback(( isSilent = false, scheduledAt?: number, scheduleRepeatPeriod?: number, ) => { handleActionWithPaymentConfirmation(handleSend, isSilent, scheduledAt, scheduleRepeatPeriod); }); const handleTodoListCreate = useLastCallback(() => { if (!isCurrentUserPremium) { showNotification({ message: lang('SubscribeToTelegramPremiumForCreateToDo'), action: { action: 'openPremiumModal', payload: { initialSection: 'todo' }, }, actionText: lang('PremiumMore'), }); return; } openTodoListModal({ chatId }); }); const handleOpenAiEditor = useLastCallback(() => { const { text, entities } = parseHtmlAsFormattedText(getHtml()); openAiMessageEditorModal({ chatId, text: { text, entities }, }); }); 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, scheduleRepeatPeriod: number | undefined, messageList: MessageList, effectId?: string, ) => { if (args && 'queryId' in args) { const { id, queryId, isSilent } = args; sendInlineBotResult({ id, chatId, threadId, queryId, scheduledAt, isSilent: isSilent || isSilentPosting, }); return; } const { isSilent, ...restArgs } = args || {}; if (!args || Object.keys(restArgs).length === 0) { void handleSend(Boolean(isSilent), scheduledAt, scheduleRepeatPeriod); } else if (args.sendCompressed !== undefined || args.sendGrouped !== undefined) { const { sendCompressed = false, sendGrouped = false, isInvertedMedia } = args; void handleSendAttachments(sendCompressed, sendGrouped, isSilent, scheduledAt, isInvertedMedia, scheduleRepeatPeriod); } else { sendMessage({ ...args, messageList, scheduledAt, scheduleRepeatPeriod, effectId, }); } }); useEffectWithPrevDeps(([prevContentToBeScheduled]) => { if (currentMessageList && contentToBeScheduled && contentToBeScheduled !== prevContentToBeScheduled) { requestCalendar((scheduledAt, scheduleRepeatPeriod) => { handleMessageSchedule(contentToBeScheduled, scheduledAt, scheduleRepeatPeriod, currentMessageList, undefined); }); } }, [contentToBeScheduled, currentMessageList, handleMessageSchedule, requestCalendar]); useEffect(() => { if (requestedDraft) { insertFormattedTextAndUpdateCursor(requestedDraft, undefined, true); 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]); useEffect(() => { if (requestedDraftFiles?.length) { updateShouldSaveAttachmentsCompression({ shouldSave: true }); applyDefaultAttachmentsCompression(); } else { updateShouldSaveAttachmentsCompression({ shouldSave: false }); } }, [requestedDraftFiles, updateShouldSaveAttachmentsCompression, applyDefaultAttachmentsCompression]); 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; } isSilent = isSilent || isSilentPosting; if (isInScheduledList || isScheduleRequested) { forceShowSymbolMenu(); requestCalendar((scheduledAt, scheduleRepeatPeriod) => { cancelForceShowSymbolMenu(); handleActionWithPaymentConfirmation( handleMessageSchedule, { gif, isSilent }, scheduledAt, scheduleRepeatPeriod, currentMessageList!, ); requestMeasure(() => { resetComposer(true); }); }); } else { handleActionWithPaymentConfirmation(sendMessage, { messageList: currentMessageList, gif, isSilent }); requestMeasure(() => { resetComposer(true); }); } clearDraft({ chatId, threadId, isLocalOnly: true }); }); const handleGifAddCaption = useLastCallback((gif: ApiVideo) => { handleSetAttachments([buildGifAttachment(gif)]); closeSymbolMenu(); }); const handleStickerSelect = useLastCallback(( sticker: ApiSticker, isSilent?: boolean, isScheduleRequested?: boolean, shouldPreserveInput = false, canUpdateStickerSetsOrder?: boolean, ) => { if (!currentMessageList && !storyId) { return; } isSilent = isSilent || isSilentPosting; sticker = { ...sticker, isPreloadedGlobally: true, }; if (isInScheduledList || isScheduleRequested) { forceShowSymbolMenu(); requestCalendar((scheduledAt, scheduleRepeatPeriod) => { cancelForceShowSymbolMenu(); handleActionWithPaymentConfirmation( handleMessageSchedule, { sticker, isSilent }, scheduledAt, scheduleRepeatPeriod, currentMessageList!, ); requestMeasure(() => { resetComposer(shouldPreserveInput); }); }); } else { handleActionWithPaymentConfirmation( 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; } isSilent = isSilent || isSilentPosting; if (isInScheduledList || isScheduleRequested) { requestCalendar((scheduledAt, scheduleRepeatPeriod) => { handleActionWithPaymentConfirmation( handleMessageSchedule, { id: inlineResult.id, queryId: inlineResult.queryId, isSilent, }, scheduledAt, scheduleRepeatPeriod, currentMessageList!, ); }); } else { handleActionWithPaymentConfirmation( sendInlineBotResult, { id: inlineResult.id, queryId: inlineResult.queryId, threadId, chatId, isSilent, }, ); } const messageInput = document.querySelector(editableInputCssSelector); if (IS_IOS && messageInput && messageInput === document.activeElement) { applyIosAutoCapitalizationFix(messageInput); } clearDraft({ chatId, threadId, isLocalOnly: true }); requestMeasure(() => { resetComposer(); }); }); const handleBotCommandSelect = useLastCallback(() => { clearDraft({ chatId, threadId, isLocalOnly: true }); requestMeasure(() => { resetComposer(); }); }); const handlePollSend = useLastCallback((poll: ApiNewPoll) => { if (!currentMessageList) { return; } if (isInScheduledList) { requestCalendar((scheduledAt, scheduleRepeatPeriod) => { handleActionWithPaymentConfirmation( handleMessageSchedule, { poll }, scheduledAt, scheduleRepeatPeriod, currentMessageList, ); }); closePollModal(); } else { handleActionWithPaymentConfirmation( sendMessage, { messageList: currentMessageList, poll, isSilent: isSilentPosting }, ); closePollModal(); } }); const handleToDoListSend = useLastCallback((todo: ApiNewMediaTodo) => { if (!currentMessageList) { return; } if (isInScheduledList) { requestCalendar((scheduledAt, scheduleRepeatPeriod) => { handleActionWithPaymentConfirmation( handleMessageSchedule, { todo }, scheduledAt, scheduleRepeatPeriod, currentMessageList, ); }); } else { handleActionWithPaymentConfirmation( sendMessage, { messageList: currentMessageList, todo, isSilent: isSilentPosting }, ); } }); const sendSilent = useLastCallback((additionalArgs?: ScheduledMessageArgs) => { if (isInScheduledList) { requestCalendar((scheduledAt, scheduleRepeatPeriod) => { handleMessageSchedule( { ...additionalArgs, isSilent: true }, scheduledAt, scheduleRepeatPeriod, 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 handleFormattedDateInsert = useLastCallback((text: ApiFormattedText) => { const targetInputId = attachments.length ? EDITABLE_INPUT_MODAL_ID : editableInputId; insertFormattedTextAndUpdateCursor(text, targetInputId); }); 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, }); }); const handleGiftClick = useLastCallback(() => { openGiftModal({ forUserId: chatId }); }); const handleSuggestPostClick = useLastCallback(() => { updateDraftSuggestedPostInfo({ price: { currency: STARS_CURRENCY_CODE, amount: 0, nanos: 0 }, }); }); const handleToggleSilentPosting = useLastCallback(() => { const newValue = !isSilentPosting; updateChatSilentPosting({ chatId, isEnabled: newValue }); showNotification({ localId: 'silentPosting', icon: newValue ? 'mute' : 'unmute', message: lang(`ComposerSilentPosting${newValue ? 'Enabled' : 'Disabled'}Tootlip`), }); }); useEffect(() => { if (isRightColumnShown && isMobile) { closeSymbolMenu(); } }, [isRightColumnShown, closeSymbolMenu, isMobile]); useEffect(() => { if (!isReady) return undefined; let timeout: number | undefined; if (isSelectModeActive) { disableHover(); } else { timeout = window.setTimeout(() => { enableHover(); }, SELECT_MODE_TRANSITION_MS); } return () => { if (timeout) { clearTimeout(timeout); } }; }, [isSelectModeActive, enableHover, disableHover, isReady]); const html = useDerivedState(() => getHtml(), [getHtml]); const hasText = Boolean(html); const [shouldShowAiButton, setShouldShowAiButton] = useState(false); useEffect(() => { if (hasAttachments) { return; } requestMeasure(() => { const input = inputRef.current; if (!html || !input) { setShouldShowAiButton(false); return; } const { totalLines } = calcTextLineHeightAndCount(input, true); setShouldShowAiButton(totalLines >= 3); }); }, [html, hasAttachments]); const withBotMenuButton = isChatWithBot && botMenuButton?.type === 'webApp' && !editingMessage && messageListType === 'thread'; const isBotMenuButtonOpen = withBotMenuButton && !hasText && !activeVoiceRecording; 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 slowModePlaceholder = (() => { if (!slowMode?.nextSendDate || slowMode.nextSendDate < getServerTime()) return undefined; return lang('SlowModePlaceholder', { timer: , }, { withNodes: true }); })(); const placeholder = useMemo(() => { if (activeVoiceRecording && windowWidth <= SCREEN_WIDTH_TO_HIDE_PLACEHOLDER) { return ''; } if (!isComposerBlocked) { if (slowModePlaceholder) return slowModePlaceholder; if (botKeyboardPlaceholder) return botKeyboardPlaceholder; if (inputPlaceholder) return inputPlaceholder; if (paidMessagesStars) { return lang('ComposerPlaceholderPaidMessage', { amount: formatStarsAsIcon(lang, paidMessagesStars, { asFont: true, className: 'placeholder-star-icon' }), }, { withNodes: true, }); } if (isReplying && hasSuggestedPost) { return lang('ComposerPlaceholderCaption'); } if (stealthMode?.activeUntil && isInStoryViewer && stealthMode.activeUntil > getServerTime()) { return lang('StealthModeComposerPlaceholder', { timer: , }, { withNodes: true }); } if (chat?.adminRights?.anonymous) { return lang('ComposerPlaceholderAnonymous'); } if (chat?.isBotForum && !user?.canManageBotForumTopics && threadId === MAIN_THREAD_ID) { return lang('ComposerPlaceholderBotTopicGeneral'); } if (chat?.isForum && !chat.isBotForum && chat.isForumAsMessages && threadId === MAIN_THREAD_ID) { return replyToTopic ? lang('ComposerPlaceholderTopic', { topic: replyToTopic.title }) : lang('ComposerPlaceholderTopicGeneral'); } if (isChannel) { return lang(isSilentPosting ? 'ComposerPlaceholderBroadcastSilent' : 'ComposerPlaceholderBroadcast'); } return lang('ComposerPlaceholder'); } if (isInStoryViewer) return lang('ComposerStoryPlaceholderLocked'); return lang('ComposerPlaceholderNoText'); }, [ activeVoiceRecording, botKeyboardPlaceholder, chat, inputPlaceholder, isChannel, isComposerBlocked, isInStoryViewer, isSilentPosting, lang, replyToTopic, isReplying, threadId, windowWidth, paidMessagesStars, hasSuggestedPost, slowModePlaceholder, stealthMode?.activeUntil, user?.canManageBotForumTopics, ]); 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: handleSendWithConfirmation(); break; case MainButtonState.Record: { if (areVoiceMessagesNotAllowed) { if (!canSendVoiceByPrivacy) { showNotification({ message: oldLang('VoiceMessagesRestrictedByPrivacy', chat?.title), }); } else if (!canSendVoices) { showAllowedMessageTypesNotification({ chatId, messageListType }); } } else { setIsViewOnceEnabled(false); void startRecordingVoice(); } break; } case MainButtonState.Edit: handleEditComplete(); break; case MainButtonState.Schedule: if (activeVoiceRecording) { pauseRecordingVoice(); } if (!currentMessageList) { return; } requestCalendar((scheduledAt, scheduleRepeatPeriod) => { handleMessageSchedule({}, scheduledAt, scheduleRepeatPeriod, currentMessageList, effect?.id); }); break; default: break; } }); 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 = selectCustomEmoji(getGlobal(), reaction.documentId); if (!sticker) { return; } if (!sticker.isFree && !isCurrentUserPremium && !isChatWithSelf) { showCustomEmojiPremiumNotification(); return; } const customEmojiMessage = parseHtmlAsFormattedText(buildCustomEmojiHtml(sticker)); text = customEmojiMessage.text; entities = customEmojiMessage.entities; } handleActionWithPaymentConfirmation(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, scheduleRepeatPeriod) => { handleMessageSchedule({}, scheduledAt, scheduleRepeatPeriod, currentMessageList!, undefined); }); }); const handleSendSilent = useLastCallback(() => { handleActionWithPaymentConfirmation(sendSilent); }); const handleSendWhenOnline = useLastCallback(() => { handleActionWithPaymentConfirmation( handleMessageSchedule, {}, SCHEDULED_WHEN_ONLINE, undefined, currentMessageList!, effect?.id, ); }); const handleSendScheduledAttachments = useLastCallback( ( sendCompressed: boolean, sendGrouped: boolean, isInvertedMedia?: true, scheduledAt?: number, scheduleRepeatPeriod?: number, ) => { if (scheduledAt) { handleActionWithPaymentConfirmation( handleMessageSchedule, { sendCompressed, sendGrouped, isInvertedMedia }, scheduledAt, scheduleRepeatPeriod, currentMessageList!, undefined, ); } else { requestCalendar((calendarScheduledAt, calendarRepeatPeriod) => { handleActionWithPaymentConfirmation( handleMessageSchedule, { sendCompressed, sendGrouped, isInvertedMedia }, calendarScheduledAt, calendarRepeatPeriod, currentMessageList!, undefined, ); }); } }, ); const handleSendSilentAttachments = useLastCallback( (sendCompressed: boolean, sendGrouped: boolean, isInvertedMedia?: true) => { handleActionWithPaymentConfirmation(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 handleSendWithConfirmation; } }, [mainButtonState, handleEditComplete, handleSendWithConfirmation]); const withBotCommands = isChatWithBot && botMenuButton?.type === 'commands' && !editingMessage && botCommands !== false && !activeVoiceRecording; const effectEmoji = areEffectsSupported && effect?.emoticon; const shouldRenderPaidBadge = Boolean(paidMessagesStars && mainButtonState === MainButtonState.Send); const prevShouldRenderPaidBadge = usePrevious(shouldRenderPaidBadge); return (
{isInMessageList && canAttachMedia && isReady && ( )} {shouldRenderReactionSelector && !isNeedPremium && ( )}
{!isNeedPremium && ( )} )} )} {((!isComposerBlocked || canSendGifs || canSendStickers) && !isNeedPremium && !isAccountFrozen) && ( )} {isInMessageList && ( <> {isInlineBotLoading && Boolean(inlineBotId) && ( )} {!hasText && ( <> {isChannel && (
{canSendOneTimeMedia && activeVoiceRecording && ( )} {activeVoiceRecording && ( )} {effectEmoji && ( {renderText(effectEmoji)} )} {effect && canPlayEffect && ( )} {canShowCustomSendMenu && ( )} {calendar} ); }; export default memo(withGlobal( (global, { chatId, threadId, storyId, messageListType, isMobile, type, }): Complete => { const appConfig = global.appConfig; 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 paidMessagesStars = selectPeerPaidMessagesStars(global, chatId); const chatFullInfo = !isChatWithUser ? selectChatFullInfo(global, chatId) : undefined; const messageWithActualBotKeyboard = (isChatWithBot || !isChatWithUser) && selectNewestMessageWithBotKeyboardButtons(global, chatId, threadId); const { shouldSuggestStickers, shouldSuggestCustomEmoji, shouldUpdateStickerSetOrder, shouldPaidMessageAutoApprove, } = global.settings.byKey; const { language, shouldCollectDebugLogs } = selectSharedSettings(global); const { forwardMessages: { messageIds: forwardMessageIds }, shouldOpenMessageMediaEditor, } = selectTabState(global); 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 = defaultSendAsId; const sendAsPeer = sendAsId ? selectPeer(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 isSilentPosting = chat && getChatNotifySettings( chat, selectNotifyDefaults(global), selectNotifyException(global, chatId), )?.isSilentPosting; 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; const isForwarding = chatId === tabState.forwardMessages.toChatId; const isReplying = Boolean(draft?.replyInfo); const hasSuggestedPost = Boolean(draft?.suggestedPostInfo); const starsBalance = global.stars?.balance.amount || 0; const isStarsBalanceModalOpen = Boolean(tabState.starsBalanceModal); const isAccountFrozen = selectIsCurrentUserFrozen(global); const isAppConfigLoaded = global.isAppConfigLoaded; const insertingPeerIdMention = tabState.insertingPeerIdMention; const webPagePreview = tabState.webPagePreviewId ? selectWebPage(global, tabState.webPagePreviewId) : undefined; return { availableReactions: global.reactions.availableReactions, topReactions: type === 'story' ? global.reactions.topReactions : undefined, isOnActiveTab: !tabState.isBlurred, editingMessage: selectEditingMessage(global, chatId, threadId, messageListType), draft, chat, user, 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, isReplying, hasSuggestedPost, forwardedMessagesCount: isForwarding ? forwardMessageIds!.length : undefined, pollModal: tabState.pollModal, todoListModal: tabState.todoListModal, aiMessageEditorPendingResult: tabState.aiMessageEditorPendingResult, 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, sendAsPeer, 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, sentStoryReaction, stealthMode: global.stories.stealthMode, replyToTopic, quickReplyMessages: global.quickReplies.messagesById, quickReplies: global.quickReplies.byId, canSendQuickReplies, noWebPage, webPagePreview, isContactRequirePremium: userFullInfo?.isContactRequirePremium, effect, effectReactions, areEffectsSupported, canPlayEffect, shouldPlayEffect, maxMessageLength, paidMessagesStars, shouldPaidMessageAutoApprove, isSilentPosting, isPaymentMessageConfirmDialogOpen: tabState.isPaymentMessageConfirmDialogOpen && !tabState.aiMessageEditorModal, starsBalance, isStarsBalanceModalOpen, shouldDisplayGiftsButton: userFullInfo?.shouldDisplayGiftsButton, disallowedGifts: userFullInfo?.disallowedGifts, isAccountFrozen, isAppConfigLoaded, insertingPeerIdMention, pollMaxAnswers: appConfig.pollMaxAnswers, shouldOpenMessageMediaEditor, replyToMessage, }; }, )(Composer));