import type { FC } from '../../../lib/teact/teact'; import React, { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from '../../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../../global'; import type { GlobalState, MessageListType } from '../../../global/types'; import type { ApiAttachment, ApiBotInlineResult, ApiBotInlineMediaResult, ApiSticker, ApiVideo, ApiNewPoll, ApiMessage, ApiFormattedText, ApiChat, ApiChatMember, ApiUser, ApiBotCommand, ApiBotMenuButton, ApiAttachMenuPeerType, } from '../../../api/types'; import { MAIN_THREAD_ID, } from '../../../api/types'; import type { InlineBotSettings, ISettings } from '../../../types'; import { BASE_EMOJI_KEYWORD_LANG, EDITABLE_INPUT_ID, REPLIES_USER_ID, SEND_MESSAGE_ACTION_INTERVAL, EDITABLE_INPUT_CSS_SELECTOR, MAX_UPLOAD_FILEPART_SIZE, } from '../../../config'; import { IS_VOICE_RECORDING_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT, IS_IOS } from '../../../util/environment'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; import { selectChat, selectIsRightColumnShown, selectIsInSelectMode, selectNewestMessageWithBotKeyboardButtons, selectDraft, selectScheduledIds, selectEditingMessage, selectIsChatWithSelf, selectChatBot, selectChatMessage, selectUser, selectCanScheduleUntilOnline, selectEditingScheduledDraft, selectEditingDraft, selectRequestedDraftText, selectTheme, selectCurrentMessageList, selectIsCurrentUserPremium, selectChatType, selectRequestedDraftFiles, } from '../../../global/selectors'; import { getAllowedAttachmentOptions, getChatSlowModeOptions, isChatAdmin, isChatSuperGroup, isChatChannel, } from '../../../global/helpers'; import { formatMediaDuration, formatVoiceRecordDuration } from '../../../util/dateFormat'; import focusEditableElement from '../../../util/focusEditableElement'; import parseMessageInput from '../../../util/parseMessageInput'; import buildAttachment from './helpers/buildAttachment'; import renderText from '../../common/helpers/renderText'; import { insertHtmlInSelection } from '../../../util/selection'; import deleteLastCharacterOutsideSelection from '../../../util/deleteLastCharacterOutsideSelection'; import buildClassName from '../../../util/buildClassName'; import windowSize from '../../../util/windowSize'; import { isSelectionInsideInput } from './helpers/selection'; import applyIosAutoCapitalizationFix from './helpers/applyIosAutoCapitalizationFix'; import { getServerTime } from '../../../util/serverTime'; import { hasPreview } from '../../../util/files'; import { selectCurrentLimit } from '../../../global/selectors/limits'; import { buildCustomEmojiHtml } from './helpers/customEmoji'; import { processMessageInputForCustomEmoji } from '../../../util/customEmojiManager'; import { getTextWithEntitiesAsHtml } from '../../common/helpers/renderTextWithEntities'; import useFlag from '../../../hooks/useFlag'; import usePrevious from '../../../hooks/usePrevious'; import useStickerTooltip from './hooks/useStickerTooltip'; import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; import useLang from '../../../hooks/useLang'; import useSendMessageAction from '../../../hooks/useSendMessageAction'; import useInterval from '../../../hooks/useInterval'; import useOnChange from '../../../hooks/useOnChange'; import { useStateRef } from '../../../hooks/useStateRef'; import useVoiceRecording from './hooks/useVoiceRecording'; import useClipboardPaste from './hooks/useClipboardPaste'; import useDraft from './hooks/useDraft'; import useEditing from './hooks/useEditing'; import useEmojiTooltip from './hooks/useEmojiTooltip'; import useMentionTooltip from './hooks/useMentionTooltip'; import useInlineBotTooltip from './hooks/useInlineBotTooltip'; import useBotCommandTooltip from './hooks/useBotCommandTooltip'; import useSchedule from '../../../hooks/useSchedule'; import useCustomEmojiTooltip from './hooks/useCustomEmojiTooltip'; import DeleteMessageModal from '../../common/DeleteMessageModal.async'; import Button from '../../ui/Button'; import ResponsiveHoverButton from '../../ui/ResponsiveHoverButton'; import Spinner from '../../ui/Spinner'; import AttachMenu from './AttachMenu'; import Avatar from '../../common/Avatar'; import SymbolMenu from './SymbolMenu.async'; import InlineBotTooltip from './InlineBotTooltip.async'; import MentionTooltip from './MentionTooltip.async'; import CustomSendMenu from './CustomSendMenu.async'; import StickerTooltip from './StickerTooltip.async'; import CustomEmojiTooltip from './CustomEmojiTooltip.async'; import EmojiTooltip from './EmojiTooltip.async'; import BotCommandTooltip from './BotCommandTooltip.async'; import BotKeyboardMenu from './BotKeyboardMenu'; import MessageInput from './MessageInput'; import ComposerEmbeddedMessage from './ComposerEmbeddedMessage'; import AttachmentModal from './AttachmentModal.async'; import BotCommandMenu from './BotCommandMenu.async'; import PollModal from './PollModal.async'; import DropArea, { DropAreaState } from './DropArea.async'; import WebPagePreview from './WebPagePreview'; import SendAsMenu from './SendAsMenu.async'; import BotMenuButton from './BotMenuButton'; import './Composer.scss'; type OwnProps = { chatId: string; threadId: number; messageListType: MessageListType; dropAreaState: string; isReady: boolean; onDropHide: NoneToVoidFunction; }; type StateProps = { editingMessage?: ApiMessage; chat?: ApiChat; draft?: ApiFormattedText; isChatWithBot?: boolean; isChatWithSelf?: boolean; isChannel?: boolean; isForCurrentMessageList: boolean; isRightColumnShown?: boolean; isSelectModeActive?: boolean; isForwarding?: boolean; pollModal: GlobalState['pollModal']; botKeyboardMessageId?: number; botKeyboardPlaceholder?: string; withScheduledButton?: boolean; shouldSchedule?: boolean; canScheduleUntilOnline?: boolean; stickersForEmoji?: ApiSticker[]; customEmojiForEmoji?: ApiSticker[]; groupChatMembers?: ApiChatMember[]; currentUserId?: string; recentEmojis: string[]; lastSyncTime?: number; contentToBeScheduled?: GlobalState['messages']['contentToBeScheduled']; shouldSuggestStickers?: boolean; shouldSuggestCustomEmoji?: boolean; baseEmojiKeywords?: Record; emojiKeywords?: Record; topInlineBotIds?: string[]; isInlineBotLoading: boolean; inlineBots?: Record; botCommands?: ApiBotCommand[] | false; botMenuButton?: ApiBotMenuButton; chatBotCommands?: ApiBotCommand[]; sendAsUser?: ApiUser; sendAsChat?: ApiChat; sendAsId?: string; editingDraft?: ApiFormattedText; requestedDraftText?: string; requestedDraftFiles?: File[]; attachBots: GlobalState['attachMenu']['bots']; attachMenuPeerType?: ApiAttachMenuPeerType; theme: ISettings['theme']; fileSizeLimit: number; captionLimit: number; isCurrentUserPremium?: boolean; canSendVoiceByPrivacy?: boolean; } & Pick; enum MainButtonState { Send = 'send', Record = 'record', Edit = 'edit', Schedule = 'schedule', } type ScheduledMessageArgs = GlobalState['messages']['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 MESSAGE_MAX_LENGTH = 4096; const SENDING_ANIMATION_DURATION = 350; // eslint-disable-next-line max-len const APPENDIX = ''; const Composer: FC = ({ dropAreaState, shouldSchedule, canScheduleUntilOnline, isReady, onDropHide, editingMessage, chatId, threadId, messageListType, draft, chat, isForCurrentMessageList, isCurrentUserPremium, canSendVoiceByPrivacy, connectionState, isChatWithBot, isChatWithSelf, isChannel, fileSizeLimit, isRightColumnShown, isSelectModeActive, isForwarding, pollModal, botKeyboardMessageId, botKeyboardPlaceholder, withScheduledButton, stickersForEmoji, customEmojiForEmoji, groupChatMembers, topInlineBotIds, currentUserId, captionLimit, lastSyncTime, contentToBeScheduled, shouldSuggestStickers, shouldSuggestCustomEmoji, baseEmojiKeywords, emojiKeywords, recentEmojis, inlineBots, isInlineBotLoading, botCommands, chatBotCommands, sendAsUser, sendAsChat, sendAsId, editingDraft, requestedDraftText, requestedDraftFiles, botMenuButton, attachBots, attachMenuPeerType, theme, }) => { const { sendMessage, clearDraft, showDialog, setStickerSearchQuery, setGifSearchQuery, forwardMessages, openPollModal, closePollModal, loadScheduledHistory, openChat, addRecentEmoji, sendInlineBotResult, loadSendAs, resetOpenChatWithDraft, callAttachBot, openLimitReachedModal, openPremiumModal, addRecentCustomEmoji, showNotification, } = getActions(); const lang = useLang(); // eslint-disable-next-line no-null/no-null const appendixRef = useRef(null); const [html, setInnerHtml] = useState(''); const htmlRef = useStateRef(html); const lastMessageSendTimeSeconds = useRef(); const prevDropAreaState = usePrevious(dropAreaState); const { width: windowWidth } = windowSize.get(); const sendAsPeerIds = chat?.sendAsPeerIds; 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 setHtml = useCallback((newHtml: string) => { setInnerHtml(newHtml); requestAnimationFrame(() => { processMessageInputForCustomEmoji(); }); }, []); const customEmojiNotificationNumber = useRef(0); const handleScheduleCancel = useCallback(() => { cancelForceShowSymbolMenu(); }, [cancelForceShowSymbolMenu]); const [requestCalendar, calendar] = useSchedule(canScheduleUntilOnline, handleScheduleCancel); useEffect(() => { lastMessageSendTimeSeconds.current = undefined; }, [chatId]); useEffect(() => { if (chatId && lastSyncTime && threadId === MAIN_THREAD_ID && isReady) { loadScheduledHistory({ chatId }); } }, [isReady, chatId, loadScheduledHistory, lastSyncTime, threadId]); useEffect(() => { if (chatId && chat && lastSyncTime && !sendAsPeerIds && isReady && isChatSuperGroup(chat)) { loadSendAs({ chatId }); } }, [chat, chatId, isReady, lastSyncTime, loadSendAs, sendAsPeerIds]); const shouldAnimateSendAsButtonRef = useRef(false); useOnChange(([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]); useLayoutEffect(() => { if (!appendixRef.current) return; appendixRef.current.innerHTML = APPENDIX; }, []); const [attachments, setAttachments] = useState([]); const [isBotKeyboardOpen, openBotKeyboard, closeBotKeyboard] = useFlag(); const [isBotCommandMenuOpen, openBotCommandMenu, closeBotCommandMenu] = useFlag(); const [isSymbolMenuOpen, openSymbolMenu, closeSymbolMenu] = useFlag(); const [isSendAsMenuOpen, openSendAsMenu, closeSendAsMenu] = useFlag(); const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(); const [isSymbolMenuLoaded, onSymbolMenuLoadingComplete] = useFlag(); const [isHoverDisabled, disableHover, enableHover] = useFlag(); const handleSetAttachments = useCallback( (newValue: ApiAttachment[] | ((current: ApiAttachment[]) => ApiAttachment[])) => { const newAttachments = typeof newValue === 'function' ? newValue(attachments) : newValue; if (newAttachments.some(({ size }) => size > fileSizeLimit)) { openLimitReachedModal({ limit: 'uploadMaxFileparts', }); } else { setAttachments(newAttachments); } }, [attachments, fileSizeLimit, openLimitReachedModal], ); const { startRecordingVoice, stopRecordingVoice, pauseRecordingVoice, activeVoiceRecording, currentRecordTime, recordButtonRef: mainButtonRef, startRecordTimeRef, } = useVoiceRecording(); useInterval(() => { sendMessageAction({ type: 'recordAudio' }); }, activeVoiceRecording && SEND_MESSAGE_ACTION_INTERVAL); useEffect(() => { if (!activeVoiceRecording) { sendMessageAction({ type: 'cancel' }); } }, [activeVoiceRecording, sendMessageAction]); useEffect(() => { if (!html || editingMessage) return; sendMessageAction({ type: 'typing' }); }, [editingMessage, html, sendMessageAction]); const mainButtonState = editingMessage ? MainButtonState.Edit : (!IS_VOICE_RECORDING_SUPPORTED || activeVoiceRecording || (html && !attachments.length) || isForwarding) ? (shouldSchedule ? MainButtonState.Schedule : MainButtonState.Send) : MainButtonState.Record; const canShowCustomSendMenu = !shouldSchedule; const { isMentionTooltipOpen, closeMentionTooltip, insertMention, mentionFilteredUsers, } = useMentionTooltip( !attachments.length, EDITABLE_INPUT_CSS_SELECTOR, setHtml, groupChatMembers, topInlineBotIds, currentUserId, ); const { isOpen: isInlineBotTooltipOpen, id: inlineBotId, isGallery: isInlineBotTooltipGallery, switchPm: inlineBotSwitchPm, results: inlineBotResults, closeTooltip: closeInlineBotTooltip, help: inlineBotHelp, loadMore: loadMoreForInlineBot, } = useInlineBotTooltip( Boolean(!attachments.length && lastSyncTime), chatId, html, inlineBots, ); const { isOpen: isBotCommandTooltipOpen, close: closeBotCommandTooltip, filteredBotCommands: botTooltipCommands, } = useBotCommandTooltip( Boolean((botCommands && botCommands.length) || (chatBotCommands && chatBotCommands.length)), html, botCommands, chatBotCommands, ); const { isContextMenuOpen: isCustomSendMenuOpen, handleContextMenu, handleContextMenuClose, handleContextMenuHide, } = useContextMenuHandlers(mainButtonRef, !(mainButtonState === MainButtonState.Send && canShowCustomSendMenu)); const { canSendStickers, canSendGifs, canAttachMedia, canAttachPolls, canAttachEmbedLinks, } = useMemo(() => getAllowedAttachmentOptions(chat, isChatWithBot), [chat, isChatWithBot]); const isAdmin = chat && isChatAdmin(chat); const slowMode = getChatSlowModeOptions(chat); const { isStickerTooltipOpen, closeStickerTooltip } = useStickerTooltip( Boolean(shouldSuggestStickers && canSendStickers && !attachments.length), html, stickersForEmoji, !isReady, ); const { isCustomEmojiTooltipOpen, closeCustomEmojiTooltip, insertCustomEmoji } = useCustomEmojiTooltip( Boolean(shouldSuggestCustomEmoji && !attachments.length), EDITABLE_INPUT_CSS_SELECTOR, html, setHtml, customEmojiForEmoji, !isReady, ); const { isEmojiTooltipOpen, closeEmojiTooltip, filteredEmojis, filteredCustomEmojis, insertEmoji, insertCustomEmoji: insertCustomEmojiFromEmojiTooltip, } = useEmojiTooltip( Boolean(shouldSuggestStickers && canSendStickers && !attachments.length), htmlRef, recentEmojis, undefined, setHtml, baseEmojiKeywords, emojiKeywords, !isReady, ); const insertHtmlAndUpdateCursor = useCallback((newHtml: string, inputId: string = EDITABLE_INPUT_ID) => { const selection = window.getSelection()!; let messageInput: HTMLDivElement; if (inputId === EDITABLE_INPUT_ID) { messageInput = document.querySelector(EDITABLE_INPUT_CSS_SELECTOR)!; } else { messageInput = document.getElementById(inputId) as HTMLDivElement; } if (selection.rangeCount) { const selectionRange = selection.getRangeAt(0); if (isSelectionInsideInput(selectionRange, inputId)) { insertHtmlInSelection(newHtml); messageInput.dispatchEvent(new Event('input', { bubbles: true })); return; } } setHtml(`${htmlRef.current!}${newHtml}`); // If selection is outside of input, set cursor at the end of input requestAnimationFrame(() => { focusEditableElement(messageInput); }); }, [htmlRef, setHtml]); const insertFormattedTextAndUpdateCursor = useCallback(( text: ApiFormattedText, inputId: string = EDITABLE_INPUT_ID, ) => { const newHtml = getTextWithEntitiesAsHtml(text); insertHtmlAndUpdateCursor(newHtml, inputId); }, [insertHtmlAndUpdateCursor]); const insertTextAndUpdateCursor = useCallback((text: string, inputId: string = EDITABLE_INPUT_ID) => { const newHtml = renderText(text, ['escape_html', 'emoji_html', 'br_html']) .join('') .replace(/\u200b+/g, '\u200b'); insertHtmlAndUpdateCursor(newHtml, inputId); }, [insertHtmlAndUpdateCursor]); const insertCustomEmojiAndUpdateCursor = useCallback((emoji: ApiSticker, inputId: string = EDITABLE_INPUT_ID) => { insertHtmlAndUpdateCursor(buildCustomEmojiHtml(emoji), inputId); }, [insertHtmlAndUpdateCursor]); const removeSymbol = useCallback(() => { const selection = window.getSelection()!; if (selection.rangeCount) { const selectionRange = selection.getRangeAt(0); if (isSelectionInsideInput(selectionRange, EDITABLE_INPUT_ID)) { document.execCommand('delete', false); return; } } setHtml(deleteLastCharacterOutsideSelection(htmlRef.current!)); }, [htmlRef, setHtml]); const resetComposer = useCallback((shouldPreserveInput = false) => { if (!shouldPreserveInput) { setHtml(''); } setAttachments(MEMO_EMPTY_ARRAY); closeStickerTooltip(); closeCustomEmojiTooltip(); closeMentionTooltip(); closeEmojiTooltip(); if (IS_SINGLE_COLUMN_LAYOUT) { // @optimization setTimeout(() => closeSymbolMenu(), SENDING_ANIMATION_DURATION); } else { closeSymbolMenu(); } }, [closeStickerTooltip, closeCustomEmojiTooltip, closeMentionTooltip, closeEmojiTooltip, closeSymbolMenu, setHtml]); // Handle chat change (ref is used to avoid redundant effect calls) const stopRecordingVoiceRef = useRef(); stopRecordingVoiceRef.current = stopRecordingVoice; useEffect(() => { return () => { stopRecordingVoiceRef.current!(); resetComposer(); }; }, [chatId, resetComposer, stopRecordingVoiceRef]); const showCustomEmojiPremiumNotification = useCallback(() => { const notificationNumber = customEmojiNotificationNumber.current; if (!notificationNumber) { showNotification({ message: lang('UnlockPremiumEmojiHint'), action: () => openPremiumModal({ initialSection: 'animated_emoji' }), actionText: lang('PremiumMore'), }); } else { showNotification({ message: lang('UnlockPremiumEmojiHint2'), action: () => openChat({ id: currentUserId, shouldReplaceHistory: true }), actionText: lang('Open'), }); } customEmojiNotificationNumber.current = Number(!notificationNumber); }, [currentUserId, lang, openChat, openPremiumModal, showNotification]); const [handleEditComplete, handleEditCancel] = useEditing( htmlRef, setHtml, editingMessage, resetComposer, openDeleteModal, chatId, threadId, messageListType, draft, editingDraft, ); useDraft(draft, chatId, threadId, htmlRef, setHtml, editingMessage, lastSyncTime); useClipboardPaste( isForCurrentMessageList, insertFormattedTextAndUpdateCursor, handleSetAttachments, editingMessage, !isCurrentUserPremium && !isChatWithSelf, showCustomEmojiPremiumNotification, ); const handleEmbeddedClear = useCallback(() => { if (editingMessage) { handleEditCancel(); } }, [editingMessage, handleEditCancel]); const handleFileSelect = useCallback(async (files: File[], isQuick: boolean) => { handleSetAttachments(await Promise.all(files.map((file) => buildAttachment(file.name, file, isQuick)))); }, [handleSetAttachments]); const handleAppendFiles = useCallback(async (files: File[], isQuick: boolean) => { handleSetAttachments([ ...attachments, ...await Promise.all(files.map((file) => buildAttachment(file.name, file, isQuick))), ]); }, [attachments, handleSetAttachments]); const handleClearAttachment = useCallback(() => { setAttachments(MEMO_EMPTY_ARRAY); }, []); const handleSend = useCallback(async (isSilent = false, scheduledAt?: number) => { if (connectionState !== 'connectionStateReady') { return; } let currentAttachments = attachments; if (activeVoiceRecording) { const record = await stopRecordingVoice(); if (record) { const { blob, duration, waveform } = record; currentAttachments = [await buildAttachment( VOICE_RECORDING_FILENAME, blob, false, { voice: { duration, waveform } }, )]; } } const { text, entities } = parseMessageInput(htmlRef.current!); if (!currentAttachments.length && !text && !isForwarding) { return; } // No need to subscribe on updates in `mapStateToProps` const { serverTimeOffset } = getGlobal(); const maxLength = currentAttachments.length ? captionLimit : MESSAGE_MAX_LENGTH; if (text?.length > maxLength) { const extraLength = text.length - maxLength; showDialog({ data: { message: 'MESSAGE_TOO_LONG_PLEASE_REMOVE_CHARACTERS', textParams: { '{EXTRA_CHARS_COUNT}': extraLength, '{PLURAL_S}': extraLength > 1 ? 's' : '', }, hasErrorKey: true, }, }); return; } const messageInput = document.querySelector(EDITABLE_INPUT_CSS_SELECTOR); if (currentAttachments.length || text) { if (slowMode && !isAdmin) { const nowSeconds = getServerTime(serverTimeOffset); 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; } } sendMessage({ text, entities, attachments: currentAttachments, scheduledAt, isSilent, shouldUpdateStickerSetsOrder: true, }); } if (isForwarding) { forwardMessages({ scheduledAt, isSilent, }); } lastMessageSendTimeSeconds.current = getServerTime(serverTimeOffset); clearDraft({ chatId, localOnly: true }); if (IS_IOS && messageInput && messageInput === document.activeElement) { applyIosAutoCapitalizationFix(messageInput); } // Wait until message animation starts requestAnimationFrame(() => { resetComposer(); }); }, [ connectionState, attachments, activeVoiceRecording, isForwarding, clearDraft, chatId, captionLimit, resetComposer, stopRecordingVoice, showDialog, slowMode, isAdmin, sendMessage, forwardMessages, lang, htmlRef, ]); const handleClickBotMenu = useCallback(() => { if (botMenuButton?.type !== 'webApp') return; callAttachBot({ botId: chatId, chatId, isFromBotMenu: true, url: botMenuButton.url, }); }, [botMenuButton, callAttachBot, chatId]); const handleActivateBotCommandMenu = useCallback(() => { closeSymbolMenu(); openBotCommandMenu(); }, [closeSymbolMenu, openBotCommandMenu]); const handleActivateSymbolMenu = useCallback(() => { closeBotCommandMenu(); closeSendAsMenu(); openSymbolMenu(); }, [closeBotCommandMenu, closeSendAsMenu, openSymbolMenu]); const handleMessageSchedule = useCallback(( args: ScheduledMessageArgs, scheduledAt: number, ) => { if (args && 'queryId' in args) { const { id, queryId, isSilent } = args; sendInlineBotResult({ id, queryId, scheduledAt, isSilent, }); return; } const { isSilent, ...restArgs } = args || {}; if (!args || Object.keys(restArgs).length === 0) { void handleSend(Boolean(isSilent), scheduledAt); } else { sendMessage({ ...args, scheduledAt, }); } }, [handleSend, sendInlineBotResult, sendMessage]); useEffect(() => { if (contentToBeScheduled) { requestCalendar((scheduledAt) => { handleMessageSchedule(contentToBeScheduled, scheduledAt); }); } }, [contentToBeScheduled, handleMessageSchedule, requestCalendar]); useEffect(() => { if (requestedDraftText) { setHtml(requestedDraftText); resetOpenChatWithDraft(); requestAnimationFrame(() => { const messageInput = document.getElementById(EDITABLE_INPUT_ID)!; focusEditableElement(messageInput, true); }); } }, [requestedDraftText, resetOpenChatWithDraft, setHtml]); useEffect(() => { if (requestedDraftFiles?.length) { const isQuick = requestedDraftFiles.every((file) => hasPreview(file)); handleFileSelect(requestedDraftFiles, isQuick); resetOpenChatWithDraft(); } }, [handleFileSelect, requestedDraftFiles, resetOpenChatWithDraft]); const handleCustomEmojiSelect = useCallback((emoji: ApiSticker) => { if (!emoji.isFree && !isCurrentUserPremium && !isChatWithSelf) { showCustomEmojiPremiumNotification(); return; } insertCustomEmojiAndUpdateCursor(emoji); }, [insertCustomEmojiAndUpdateCursor, isChatWithSelf, isCurrentUserPremium, showCustomEmojiPremiumNotification]); const handleStickerSelect = useCallback(( sticker: ApiSticker, isSilent?: boolean, isScheduleRequested?: boolean, shouldPreserveInput = false, shouldUpdateStickerSetsOrder?: boolean, ) => { sticker = { ...sticker, isPreloadedGlobally: true, }; if (shouldSchedule || isScheduleRequested) { forceShowSymbolMenu(); requestCalendar((scheduledAt) => { cancelForceShowSymbolMenu(); handleMessageSchedule({ sticker, isSilent }, scheduledAt); requestAnimationFrame(() => { resetComposer(shouldPreserveInput); }); }); } else { sendMessage({ sticker, isSilent, shouldUpdateStickerSetsOrder }); requestAnimationFrame(() => { resetComposer(shouldPreserveInput); }); } }, [ shouldSchedule, forceShowSymbolMenu, requestCalendar, cancelForceShowSymbolMenu, handleMessageSchedule, resetComposer, sendMessage, ]); const handleGifSelect = useCallback((gif: ApiVideo, isSilent?: boolean, isScheduleRequested?: boolean) => { if (shouldSchedule || isScheduleRequested) { forceShowSymbolMenu(); requestCalendar((scheduledAt) => { cancelForceShowSymbolMenu(); handleMessageSchedule({ gif, isSilent }, scheduledAt); requestAnimationFrame(() => { resetComposer(true); }); }); } else { sendMessage({ gif, isSilent }); requestAnimationFrame(() => { resetComposer(true); }); } }, [ shouldSchedule, forceShowSymbolMenu, requestCalendar, cancelForceShowSymbolMenu, handleMessageSchedule, resetComposer, sendMessage, ]); const handleInlineBotSelect = useCallback(( inlineResult: ApiBotInlineResult | ApiBotInlineMediaResult, isSilent?: boolean, isScheduleRequested?: boolean, ) => { if (connectionState !== 'connectionStateReady') { return; } if (shouldSchedule || isScheduleRequested) { requestCalendar((scheduledAt) => { handleMessageSchedule({ id: inlineResult.id, queryId: inlineResult.queryId, isSilent, }, scheduledAt); }); } else { sendInlineBotResult({ id: inlineResult.id, queryId: inlineResult.queryId, isSilent, }); } const messageInput = document.querySelector(EDITABLE_INPUT_CSS_SELECTOR); if (IS_IOS && messageInput && messageInput === document.activeElement) { applyIosAutoCapitalizationFix(messageInput); } clearDraft({ chatId, localOnly: true }); requestAnimationFrame(() => { resetComposer(); }); }, [ chatId, clearDraft, connectionState, handleMessageSchedule, requestCalendar, resetComposer, sendInlineBotResult, shouldSchedule, ]); const handleBotCommandSelect = useCallback(() => { clearDraft({ chatId, localOnly: true }); requestAnimationFrame(() => { resetComposer(); }); }, [chatId, clearDraft, resetComposer]); const handlePollSend = useCallback((poll: ApiNewPoll) => { if (shouldSchedule) { requestCalendar((scheduledAt) => { handleMessageSchedule({ poll }, scheduledAt); }); closePollModal(); } else { sendMessage({ poll }); closePollModal(); } }, [closePollModal, handleMessageSchedule, requestCalendar, sendMessage, shouldSchedule]); const handleSendSilent = useCallback(() => { if (shouldSchedule) { requestCalendar((scheduledAt) => { handleMessageSchedule({ isSilent: true }, scheduledAt); }); } else { void handleSend(true); } }, [handleMessageSchedule, handleSend, requestCalendar, shouldSchedule]); const handleSearchOpen = useCallback((type: 'stickers' | 'gifs') => { if (type === 'stickers') { setStickerSearchQuery({ query: '' }); setGifSearchQuery({ query: undefined }); } else { setGifSearchQuery({ query: '' }); setStickerSearchQuery({ query: undefined }); } }, [setStickerSearchQuery, setGifSearchQuery]); const handleSymbolMenuOpen = useCallback(() => { const messageInput = document.querySelector(EDITABLE_INPUT_CSS_SELECTOR); if (!IS_SINGLE_COLUMN_LAYOUT || messageInput !== document.activeElement) { openSymbolMenu(); return; } messageInput?.blur(); setTimeout(() => { closeBotCommandMenu(); openSymbolMenu(); }, MOBILE_KEYBOARD_HIDE_DELAY_MS); }, [openSymbolMenu, closeBotCommandMenu]); const handleSendAsMenuOpen = useCallback(() => { const messageInput = document.querySelector(EDITABLE_INPUT_CSS_SELECTOR); if (!IS_SINGLE_COLUMN_LAYOUT || messageInput !== document.activeElement) { closeBotCommandMenu(); closeSymbolMenu(); openSendAsMenu(); return; } messageInput?.blur(); setTimeout(() => { closeBotCommandMenu(); closeSymbolMenu(); openSendAsMenu(); }, MOBILE_KEYBOARD_HIDE_DELAY_MS); }, [closeBotCommandMenu, closeSymbolMenu, openSendAsMenu]); const handleAllScheduledClick = useCallback(() => { openChat({ id: chatId, threadId, type: 'scheduled' }); }, [openChat, chatId, threadId]); useEffect(() => { if (isRightColumnShown && IS_SINGLE_COLUMN_LAYOUT) { closeSymbolMenu(); } }, [isRightColumnShown, closeSymbolMenu]); useEffect(() => { if (!isReady) return; if (isSelectModeActive) { disableHover(); } else { setTimeout(() => { enableHover(); }, SELECT_MODE_TRANSITION_MS); } }, [isSelectModeActive, enableHover, disableHover, isReady]); const areVoiceMessagesNotAllowed = mainButtonState === MainButtonState.Record && (!canAttachMedia || !canSendVoiceByPrivacy); const mainButtonHandler = useCallback(() => { switch (mainButtonState) { case MainButtonState.Send: handleSend(); break; case MainButtonState.Record: { if (areVoiceMessagesNotAllowed) { if (!canSendVoiceByPrivacy) { showNotification({ message: lang('VoiceMessagesRestrictedByPrivacy', chat?.title), }); } } else { startRecordingVoice(); } break; } case MainButtonState.Edit: handleEditComplete(); break; case MainButtonState.Schedule: if (activeVoiceRecording) { pauseRecordingVoice(); } requestCalendar((scheduledAt) => { handleMessageSchedule({}, scheduledAt); }); break; default: break; } }, [ mainButtonState, handleSend, handleEditComplete, activeVoiceRecording, requestCalendar, areVoiceMessagesNotAllowed, canSendVoiceByPrivacy, showNotification, lang, chat?.title, startRecordingVoice, pauseRecordingVoice, handleMessageSchedule, ]); const prevEditedMessage = usePrevious(editingMessage, true); const renderedEditedMessage = editingMessage || prevEditedMessage; 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.Edit: sendButtonAriaLabel = 'Save edited message'; break; case MainButtonState.Record: sendButtonAriaLabel = !canAttachMedia ? 'Conversation.DefaultRestrictedMedia' : 'AccDescrVoiceMessage'; } const className = buildClassName( 'Composer', !isSelectModeActive && 'shown', isHoverDisabled && 'hover-disabled', ); const symbolMenuButtonClassName = buildClassName( 'mobile-symbol-menu-button', !isReady && 'not-ready', isSymbolMenuLoaded ? (isSymbolMenuOpen && 'menu-opened') : (isSymbolMenuOpen && 'is-loading'), ); const handleSendScheduled = useCallback(() => { requestCalendar((scheduledAt) => { handleMessageSchedule({}, scheduledAt); }); }, [handleMessageSchedule, requestCalendar]); const onSend = mainButtonState === MainButtonState.Edit ? handleEditComplete : mainButtonState === MainButtonState.Schedule ? handleSendScheduled : handleSend; const isBotMenuButtonCommands = botMenuButton && botMenuButton?.type === 'commands'; return (
{canAttachMedia && isReady && ( )} {renderedEditedMessage && ( )}
{isChatWithBot && botMenuButton && botMenuButton.type === 'webApp' && !editingMessage && ( )} {(isChatWithBot && isBotMenuButtonCommands && botCommands !== false && !activeVoiceRecording && !editingMessage) && ( )} {canShowSendAs && (sendAsUser || sendAsChat) && ( )} {IS_SINGLE_COLUMN_LAYOUT ? ( ) : ( )} 0} shouldSuppressFocus={IS_SINGLE_COLUMN_LAYOUT && isSymbolMenuOpen} shouldSuppressTextFormatter={isEmojiTooltipOpen || isMentionTooltipOpen || isInlineBotTooltipOpen} onUpdate={setHtml} onSend={onSend} onSuppressedFocus={closeSymbolMenu} /> {isInlineBotLoading && Boolean(inlineBotId) && ( )} {withScheduledButton && ( )} {botKeyboardMessageId && !activeVoiceRecording && !editingMessage && ( )} {activeVoiceRecording && currentRecordTime && ( {formatVoiceRecordDuration(currentRecordTime - startRecordTimeRef.current!)} )} {botKeyboardMessageId && ( )} {botCommands && ( )}
{activeVoiceRecording && ( )} {canShowCustomSendMenu && ( )} {calendar}
); }; export default memo(withGlobal( (global, { chatId, threadId, messageListType }): StateProps => { const chat = selectChat(global, chatId); const chatBot = chatId !== REPLIES_USER_ID ? selectChatBot(global, chatId) : undefined; const isChatWithBot = Boolean(chatBot); const isChatWithSelf = selectIsChatWithSelf(global, chatId); const messageWithActualBotKeyboard = isChatWithBot && selectNewestMessageWithBotKeyboardButtons(global, chatId); const scheduledIds = selectScheduledIds(global, chatId); const { language, shouldSuggestStickers, shouldSuggestCustomEmoji } = 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 defaultSendAsId = chat?.fullInfo ? chat?.fullInfo?.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 requestedDraftText = selectRequestedDraftText(global, chatId); const requestedDraftFiles = selectRequestedDraftFiles(global, chatId); const currentMessageList = selectCurrentMessageList(global); const isForCurrentMessageList = chatId === currentMessageList?.chatId && threadId === currentMessageList?.threadId && messageListType === currentMessageList?.type; const user = selectUser(global, chatId); const canSendVoiceByPrivacy = (user && !user.fullInfo?.noVoiceMessages) ?? true; const editingDraft = messageListType === 'scheduled' ? selectEditingScheduledDraft(global, chatId) : selectEditingDraft(global, chatId, threadId); return { editingMessage: selectEditingMessage(global, chatId, threadId, messageListType), connectionState: global.connectionState, draft: selectDraft(global, chatId, threadId), chat, isChatWithBot, isChatWithSelf, isForCurrentMessageList, canScheduleUntilOnline: selectCanScheduleUntilOnline(global, chatId), isChannel: chat ? isChatChannel(chat) : undefined, isRightColumnShown: selectIsRightColumnShown(global), isSelectModeActive: selectIsInSelectMode(global), withScheduledButton: ( threadId === MAIN_THREAD_ID && messageListType === 'thread' && Boolean(scheduledIds?.length) ), shouldSchedule: messageListType === 'scheduled', botKeyboardMessageId, botKeyboardPlaceholder: keyboardMessage?.keyboardPlaceholder, isForwarding: chatId === global.forwardMessages.toChatId, pollModal: global.pollModal, stickersForEmoji: global.stickers.forEmoji.stickers, customEmojiForEmoji: global.customEmojis.forEmoji.stickers, groupChatMembers: chat?.fullInfo?.members, topInlineBotIds: global.topInlineBots?.userIds, currentUserId, lastSyncTime: global.lastSyncTime, contentToBeScheduled: global.messages.contentToBeScheduled, shouldSuggestStickers, shouldSuggestCustomEmoji, recentEmojis: global.recentEmojis, baseEmojiKeywords: baseEmojiKeywords?.keywords, emojiKeywords: emojiKeywords?.keywords, inlineBots: global.inlineBots.byUsername, isInlineBotLoading: global.inlineBots.isLoading, chatBotCommands: chat?.fullInfo && chat.fullInfo.botCommands, botCommands: chatBot?.fullInfo ? (chatBot.fullInfo.botInfo?.commands || false) : undefined, botMenuButton: chatBot?.fullInfo?.botInfo?.menuButton, sendAsUser, sendAsChat, sendAsId, editingDraft, requestedDraftText, 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: selectIsCurrentUserPremium(global), canSendVoiceByPrivacy, }; }, )(Composer));