import React, { FC, memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from '../../../lib/teact/teact'; import { getDispatch, withGlobal } from '../../../lib/teact/teactn'; import { GlobalState, MessageListType } from '../../../global/types'; import { ApiAttachment, ApiBotInlineResult, ApiBotInlineMediaResult, ApiSticker, ApiVideo, ApiNewPoll, ApiMessage, ApiFormattedText, ApiChat, ApiChatMember, ApiUser, MAIN_THREAD_ID, ApiBotCommand, } from '../../../api/types'; import { InlineBotSettings } from '../../../types'; import { BASE_EMOJI_KEYWORD_LANG, EDITABLE_INPUT_ID, REPLIES_USER_ID, SCHEDULED_WHEN_ONLINE, SEND_MESSAGE_ACTION_INTERVAL, } from '../../../config'; import { IS_VOICE_RECORDING_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT, IS_IOS } from '../../../util/environment'; import { selectChat, selectIsRightColumnShown, selectIsInSelectMode, selectNewestMessageWithBotKeyboardButtons, selectDraft, selectScheduledIds, selectEditingMessage, selectIsChatWithSelf, selectChatBot, selectChatUser, selectChatMessage, selectUserStatus, } from '../../../modules/selectors'; import { getAllowedAttachmentOptions, getChatSlowModeOptions, isUserId, isChatAdmin, isChatSuperGroup, } from '../../../modules/helpers'; import { formatMediaDuration, formatVoiceRecordDuration, getDayStartAt } 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/insertHtmlInSelection'; 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 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 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 DeleteMessageModal from '../../common/DeleteMessageModal.async'; import Button from '../../ui/Button'; import ResponsiveHoverButton from '../../ui/ResponsiveHoverButton'; import Spinner from '../../ui/Spinner'; import AttachMenu from './AttachMenu.async'; 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 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 Portal from '../../ui/Portal'; import CalendarModal from '../../common/CalendarModal.async'; import SendAsMenu from './SendAsMenu.async'; import Avatar from '../../common/Avatar'; 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; isRightColumnShown?: boolean; isSelectModeActive?: boolean; isForwarding?: boolean; isPollModalOpen?: boolean; botKeyboardMessageId?: number; botKeyboardPlaceholder?: string; withScheduledButton?: boolean; shouldSchedule?: boolean; canScheduleUntilOnline?: boolean; stickersForEmoji?: ApiSticker[]; groupChatMembers?: ApiChatMember[]; currentUserId?: string; usersById?: Record; recentEmojis: string[]; lastSyncTime?: number; contentToBeScheduled?: GlobalState['messages']['contentToBeScheduled']; shouldSuggestStickers?: boolean; baseEmojiKeywords?: Record; emojiKeywords?: Record; serverTimeOffset: number; topInlineBotIds?: string[]; isInlineBotLoading: boolean; inlineBots?: Record; botCommands?: ApiBotCommand[] | false; chatBotCommands?: ApiBotCommand[]; sendAsUser?: ApiUser; sendAsChat?: ApiChat; sendAsId?: string; } & Pick; enum MainButtonState { Send = 'send', Record = 'record', Edit = 'edit', Schedule = 'schedule', } 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 CAPTION_MAX_LENGTH = 1024; 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, connectionState, isChatWithBot, isChatWithSelf, isRightColumnShown, isSelectModeActive, isForwarding, isPollModalOpen, botKeyboardMessageId, botKeyboardPlaceholder, withScheduledButton, stickersForEmoji, groupChatMembers, topInlineBotIds, currentUserId, usersById, lastSyncTime, contentToBeScheduled, shouldSuggestStickers, baseEmojiKeywords, emojiKeywords, serverTimeOffset, recentEmojis, inlineBots, isInlineBotLoading, botCommands, chatBotCommands, sendAsUser, sendAsChat, sendAsId, }) => { const { sendMessage, clearDraft, showDialog, setStickerSearchQuery, setGifSearchQuery, forwardMessages, openPollModal, closePollModal, loadScheduledHistory, openChat, addRecentEmoji, sendInlineBotResult, loadSendAs, } = getDispatch(); const lang = useLang(); // eslint-disable-next-line no-null/no-null const appendixRef = useRef(null); const [html, setHtml] = useState(''); const lastMessageSendTimeSeconds = useRef(); const prevDropAreaState = usePrevious(dropAreaState); const [isCalendarOpen, openCalendar, closeCalendar] = useFlag(); const [ scheduledMessageArgs, setScheduledMessageArgs, ] = useState(); const { width: windowWidth } = windowSize.get(); const sendAsIds = chat?.sendAsIds; const sendMessageAction = useSendMessageAction(chatId, threadId); // Cache for frequently updated state const htmlRef = useRef(html); useEffect(() => { htmlRef.current = html; }, [html]); 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 && !sendAsIds && isReady && isChatSuperGroup(chat)) { loadSendAs({ chatId }); } }, [chat, chatId, isReady, lastSyncTime, loadSendAs, sendAsIds]); const shouldAnimateSendAsButtonRef = useRef(false); useOnChange(([prevChatId, prevSendAsIds]) => { // We only animate send-as button if `sendAsIds` was missing when opening the chat shouldAnimateSendAsButtonRef.current = Boolean(chatId === prevChatId && sendAsIds && !prevSendAsIds); }, [chatId, sendAsIds]); useLayoutEffect(() => { if (!appendixRef.current) return; appendixRef.current.innerHTML = APPENDIX; }, []); useEffect(() => { if (contentToBeScheduled) { setScheduledMessageArgs(contentToBeScheduled); openCalendar(); } }, [contentToBeScheduled, openCalendar]); const [attachments, setAttachments] = useState([]); const [isBotKeyboardOpen, openBotKeyboard, closeBotKeyboard] = useFlag(); const [isBotCommandMenuOpen, openBotCommandMenu, closeBotCommandMenu] = useFlag(); const [isAttachMenuOpen, openAttachMenu, closeAttachMenu] = 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 { 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]); 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, html, setHtml, undefined, 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 allowedAttachmentOptions = useMemo(() => { return getAllowedAttachmentOptions(chat, isChatWithBot); }, [chat, isChatWithBot]); const isAdmin = chat && isChatAdmin(chat); const slowMode = getChatSlowModeOptions(chat); const { isStickerTooltipOpen, closeStickerTooltip } = useStickerTooltip( Boolean(shouldSuggestStickers && allowedAttachmentOptions.canSendStickers && !attachments.length), html, stickersForEmoji, !isReady, ); const { isEmojiTooltipOpen, closeEmojiTooltip, filteredEmojis, insertEmoji, } = useEmojiTooltip( Boolean(shouldSuggestStickers && allowedAttachmentOptions.canSendStickers && !attachments.length), html, recentEmojis, undefined, setHtml, baseEmojiKeywords, emojiKeywords, !isReady, ); const insertTextAndUpdateCursor = useCallback((text: string, inputId: string = EDITABLE_INPUT_ID) => { const selection = window.getSelection()!; const messageInput = document.getElementById(inputId)!; const newHtml = renderText(text, ['escape_html', 'emoji_html', 'br_html']) .join('') .replace(/\u200b+/g, '\u200b'); 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); }); }, []); 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!)); }, []); const resetComposer = useCallback((shouldPreserveInput = false) => { if (!shouldPreserveInput) { setHtml(''); } setAttachments([]); closeStickerTooltip(); closeCalendar(); setScheduledMessageArgs(undefined); closeMentionTooltip(); closeEmojiTooltip(); if (IS_SINGLE_COLUMN_LAYOUT) { // @optimization setTimeout(() => closeSymbolMenu(), SENDING_ANIMATION_DURATION); } else { closeSymbolMenu(); } }, [closeStickerTooltip, closeCalendar, closeMentionTooltip, closeEmojiTooltip, closeSymbolMenu]); // 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 handleEditComplete = useEditing(htmlRef, setHtml, editingMessage, resetComposer, openDeleteModal); useDraft(draft, chatId, threadId, html, htmlRef, setHtml, editingMessage); useClipboardPaste(insertTextAndUpdateCursor, setAttachments, editingMessage); const handleFileSelect = useCallback(async (files: File[], isQuick: boolean) => { setAttachments(await Promise.all(files.map((file) => buildAttachment(file.name, file, isQuick)))); }, []); const handleAppendFiles = useCallback(async (files: File[], isQuick: boolean) => { setAttachments([ ...attachments, ...await Promise.all(files.map((file) => buildAttachment(file.name, file, isQuick))), ]); }, [attachments]); const handleClearAttachment = useCallback(() => { setAttachments([]); }, []); 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; } const maxLength = currentAttachments.length ? CAPTION_MAX_LENGTH : 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.getElementById(EDITABLE_INPUT_ID)!; 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, }); } if (isForwarding) { forwardMessages({ scheduledAt, isSilent, }); } lastMessageSendTimeSeconds.current = getServerTime(serverTimeOffset); clearDraft({ chatId, localOnly: true }); if (IS_IOS && messageInput === document.activeElement) { applyIosAutoCapitalizationFix(messageInput); } // Wait until message animation starts requestAnimationFrame(() => { resetComposer(); }); }, [ connectionState, attachments, activeVoiceRecording, isForwarding, clearDraft, chatId, serverTimeOffset, resetComposer, stopRecordingVoice, showDialog, slowMode, isAdmin, sendMessage, forwardMessages, lang, ]); const handleActivateBotCommandMenu = useCallback(() => { closeSymbolMenu(); openBotCommandMenu(); }, [closeSymbolMenu, openBotCommandMenu]); const handleActivateSymbolMenu = useCallback(() => { closeBotCommandMenu(); closeSendAsMenu(); openSymbolMenu(); }, [closeBotCommandMenu, closeSendAsMenu, openSymbolMenu]); const handleStickerSelect = useCallback((sticker: ApiSticker, shouldPreserveInput = false) => { sticker = { ...sticker, isPreloadedGlobally: true, }; if (shouldSchedule) { setScheduledMessageArgs({ sticker }); openCalendar(); } else { sendMessage({ sticker }); requestAnimationFrame(() => { resetComposer(shouldPreserveInput); }); } }, [shouldSchedule, openCalendar, sendMessage, resetComposer]); const handleGifSelect = useCallback((gif: ApiVideo) => { if (shouldSchedule) { setScheduledMessageArgs({ gif }); openCalendar(); } else { sendMessage({ gif }); requestAnimationFrame(() => { resetComposer(true); }); } }, [shouldSchedule, openCalendar, sendMessage, resetComposer]); const handleInlineBotSelect = useCallback((inlineResult: ApiBotInlineResult | ApiBotInlineMediaResult) => { if (connectionState !== 'connectionStateReady') { return; } sendInlineBotResult({ id: inlineResult.id, queryId: inlineResult.queryId, }); const messageInput = document.getElementById(EDITABLE_INPUT_ID)!; if (IS_IOS && messageInput === document.activeElement) { applyIosAutoCapitalizationFix(messageInput); } clearDraft({ chatId, localOnly: true }); requestAnimationFrame(() => { resetComposer(); }); }, [chatId, clearDraft, connectionState, resetComposer, sendInlineBotResult]); const handleBotCommandSelect = useCallback(() => { clearDraft({ chatId, localOnly: true }); requestAnimationFrame(() => { resetComposer(); }); }, [chatId, clearDraft, resetComposer]); const handlePollSend = useCallback((poll: ApiNewPoll) => { if (shouldSchedule) { setScheduledMessageArgs({ poll }); closePollModal(); openCalendar(); } else { sendMessage({ poll }); closePollModal(); } }, [closePollModal, openCalendar, sendMessage, shouldSchedule]); const handleSilentSend = useCallback(() => { if (shouldSchedule) { setScheduledMessageArgs({ isSilent: true }); openCalendar(); } else { void handleSend(true); } }, [handleSend, openCalendar, shouldSchedule]); const handleMessageSchedule = useCallback((date: Date, isWhenOnline = false) => { const { isSilent, ...restArgs } = scheduledMessageArgs || {}; // Scheduled time can not be less than 10 seconds in future const scheduledAt = Math.round(Math.max(date.getTime(), Date.now() + 60 * 1000) / 1000) + (isWhenOnline ? 0 : serverTimeOffset); if (!scheduledMessageArgs || Object.keys(restArgs).length === 0) { void handleSend(!!isSilent, scheduledAt); } else { sendMessage({ ...scheduledMessageArgs, scheduledAt, }); requestAnimationFrame(() => { resetComposer(); }); } closeCalendar(); }, [closeCalendar, handleSend, resetComposer, scheduledMessageArgs, sendMessage, serverTimeOffset]); const handleMessageScheduleUntilOnline = useCallback(() => { handleMessageSchedule(new Date(SCHEDULED_WHEN_ONLINE * 1000), true); }, [handleMessageSchedule]); const handleCloseCalendar = useCallback(() => { closeCalendar(); setScheduledMessageArgs(undefined); }, [closeCalendar]); 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.getElementById(EDITABLE_INPUT_ID)!; 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.getElementById(EDITABLE_INPUT_ID)!; 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 mainButtonHandler = useCallback(() => { switch (mainButtonState) { case MainButtonState.Send: handleSend(); break; case MainButtonState.Record: void startRecordingVoice(); break; case MainButtonState.Edit: handleEditComplete(); break; case MainButtonState.Schedule: if (activeVoiceRecording) { pauseRecordingVoice(); } openCalendar(); break; default: break; } }, [ mainButtonState, handleSend, startRecordingVoice, handleEditComplete, activeVoiceRecording, openCalendar, pauseRecordingVoice, ]); const areVoiceMessagesNotAllowed = mainButtonState === MainButtonState.Record && !allowedAttachmentOptions.canAttachMedia; 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 = areVoiceMessagesNotAllowed ? '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 onSend = mainButtonState === MainButtonState.Edit ? handleEditComplete : mainButtonState === MainButtonState.Schedule ? openCalendar : handleSend; return (
{allowedAttachmentOptions.canAttachMedia && isReady && ( )} {renderedEditedMessage && ( )}
{isChatWithBot && botCommands !== false && !activeVoiceRecording && !editingMessage && ( )} {!!sendAsIds?.length && (sendAsUser || sendAsChat) && ( )} {IS_SINGLE_COLUMN_LAYOUT ? ( ) : ( )} {isInlineBotLoading && Boolean(inlineBotId) && ( )} {withScheduledButton && ( )} {botKeyboardMessageId && !activeVoiceRecording && !editingMessage && ( )} {!activeVoiceRecording && !editingMessage && ( )} {activeVoiceRecording && currentRecordTime && ( {formatVoiceRecordDuration(currentRecordTime - startRecordTimeRef.current!)} )} {botKeyboardMessageId && ( )} {botCommands && ( )}
{activeVoiceRecording && ( )} {canShowCustomSendMenu && ( )}
); }; export default memo(withGlobal( (global, { chatId, threadId, messageListType }): StateProps => { const chat = selectChat(global, chatId); const chatUser = chat && selectChatUser(global, chat); 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 } = 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 usersById = global.users.byId; const chatsById = global.chats.byId; const { currentUserId } = global; const sendAsId = chat?.fullInfo ? chat?.fullInfo?.sendAsId || currentUserId : undefined; const sendAsUser = sendAsId ? usersById?.[sendAsId] : undefined; const sendAsChat = !sendAsUser && sendAsId ? chatsById?.[sendAsId] : undefined; return { editingMessage: selectEditingMessage(global, chatId, threadId, messageListType), connectionState: global.connectionState, draft: selectDraft(global, chatId, threadId), chat, isChatWithBot, isChatWithSelf, canScheduleUntilOnline: Boolean( !isChatWithSelf && !isChatWithBot && chat && chatUser && isUserId(chatId) && selectUserStatus(global, chatId)?.wasOnline, ), 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, isPollModalOpen: global.isPollModalOpen, stickersForEmoji: global.stickers.forEmoji.stickers, groupChatMembers: chat?.fullInfo?.members, topInlineBotIds: global.topInlineBots?.userIds, currentUserId, usersById, lastSyncTime: global.lastSyncTime, contentToBeScheduled: global.messages.contentToBeScheduled, shouldSuggestStickers, recentEmojis: global.recentEmojis, baseEmojiKeywords: baseEmojiKeywords?.keywords, emojiKeywords: emojiKeywords?.keywords, serverTimeOffset: global.serverTimeOffset, inlineBots: global.inlineBots.byUsername, isInlineBotLoading: global.inlineBots.isLoading, chatBotCommands: chat && chat.fullInfo && chat.fullInfo.botCommands, botCommands: chatBot && chatBot.fullInfo ? (chatBot.fullInfo.botCommands || false) : undefined, sendAsUser, sendAsChat, sendAsId, }; }, )(Composer));