2023-01-22 18:13:25 +01:00

1500 lines
51 KiB
TypeScript

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 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<string, string[]>;
emojiKeywords?: Record<string, string[]>;
topInlineBotIds?: string[];
isInlineBotLoading: boolean;
inlineBots?: Record<string, false | InlineBotSettings>;
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<GlobalState, 'connectionState'>;
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 = '<svg width="9" height="20" xmlns="http://www.w3.org/2000/svg"><defs><filter x="-50%" y="-14.7%" width="200%" height="141.2%" filterUnits="objectBoundingBox" id="a"><feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/><feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"/><feColorMatrix values="0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0" in="shadowBlurOuter1"/></filter></defs><g fill="none" fill-rule="evenodd"><path d="M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z" fill="#000" filter="url(#a)"/><path d="M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z" fill="#FFF" class="corner"/></g></svg>';
const Composer: FC<OwnProps & StateProps> = ({
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<HTMLDivElement>(null);
const [html, setInnerHtml] = useState<string>('');
const htmlRef = useStateRef(html);
const lastMessageSendTimeSeconds = useRef<number>();
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 && 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<ApiAttachment[]>([]);
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<HTMLDivElement>(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<typeof stopRecordingVoice>();
stopRecordingVoiceRef.current = stopRecordingVoice;
useEffect(() => {
return () => {
stopRecordingVoiceRef.current!();
resetComposer();
};
}, [chatId, threadId, 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<HTMLDivElement>(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;
}
const attachBot = attachBots[chatId];
callAttachBot({
bot: attachBot, chatId, isFromBotMenu: true, url: botMenuButton.url, threadId,
});
}, [attachBots, botMenuButton, callAttachBot, chatId, threadId]);
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<HTMLDivElement>(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<HTMLDivElement>(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<HTMLDivElement>(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 (
<div className={className}>
{canAttachMedia && isReady && (
<DropArea
isOpen={dropAreaState !== DropAreaState.None}
withQuick={dropAreaState === DropAreaState.QuickFile || prevDropAreaState === DropAreaState.QuickFile}
onHide={onDropHide}
onFileSelect={handleFileSelect}
/>
)}
<AttachmentModal
chatId={chatId}
threadId={threadId}
canShowCustomSendMenu={canShowCustomSendMenu}
attachments={attachments}
captionLimit={captionLimit}
caption={attachments.length ? html : ''}
groupChatMembers={groupChatMembers}
currentUserId={currentUserId}
recentEmojis={recentEmojis}
isReady={isReady}
isChatWithSelf={isChatWithSelf}
onCaptionUpdate={setHtml}
baseEmojiKeywords={baseEmojiKeywords}
emojiKeywords={emojiKeywords}
shouldSchedule={shouldSchedule}
onSendSilent={handleSendSilent}
onSend={handleSend}
onSendScheduled={handleSendScheduled}
onFileAppend={handleAppendFiles}
onClear={handleClearAttachment}
shouldSuggestCustomEmoji={shouldSuggestCustomEmoji}
customEmojiForEmoji={customEmojiForEmoji}
/>
<PollModal
isOpen={pollModal.isOpen}
isQuiz={pollModal.isQuiz}
shouldBeAnonymous={isChannel}
onClear={closePollModal}
onSend={handlePollSend}
/>
{renderedEditedMessage && (
<DeleteMessageModal
isOpen={isDeleteModalOpen}
isSchedule={messageListType === 'scheduled'}
onClose={closeDeleteModal}
message={renderedEditedMessage}
/>
)}
<SendAsMenu
isOpen={isSendAsMenuOpen}
onClose={closeSendAsMenu}
chatId={chatId}
selectedSendAsId={sendAsId}
sendAsPeerIds={sendAsPeerIds}
isCurrentUserPremium={isCurrentUserPremium}
/>
<MentionTooltip
isOpen={isMentionTooltipOpen}
onClose={closeMentionTooltip}
onInsertUserName={insertMention}
filteredUsers={mentionFilteredUsers}
/>
<InlineBotTooltip
isOpen={isInlineBotTooltipOpen}
botId={inlineBotId}
isGallery={isInlineBotTooltipGallery}
inlineBotResults={inlineBotResults}
switchPm={inlineBotSwitchPm}
onSelectResult={handleInlineBotSelect}
loadMore={loadMoreForInlineBot}
onClose={closeInlineBotTooltip}
isSavedMessages={isChatWithSelf}
canSendGifs={canSendGifs}
isCurrentUserPremium={isCurrentUserPremium}
/>
<BotCommandTooltip
isOpen={isBotCommandTooltipOpen}
withUsername={Boolean(chatBotCommands)}
botCommands={botTooltipCommands}
onClick={handleBotCommandSelect}
onClose={closeBotCommandTooltip}
/>
<div id="message-compose">
<div className="svg-appendix" ref={appendixRef} />
<ComposerEmbeddedMessage onClear={handleEmbeddedClear} />
<WebPagePreview
chatId={chatId}
threadId={threadId}
messageText={!attachments.length ? html : ''}
disabled={!canAttachEmbedLinks}
/>
<div className="message-input-wrapper">
{isChatWithBot && botMenuButton && botMenuButton.type === 'webApp' && !editingMessage
&& (
<BotMenuButton
isOpen={!html && !activeVoiceRecording}
onClick={handleClickBotMenu}
text={botMenuButton.text}
isDisabled={Boolean(activeVoiceRecording)}
/>
)}
{(isChatWithBot && isBotMenuButtonCommands
&& botCommands !== false && !activeVoiceRecording && !editingMessage) && (
<ResponsiveHoverButton
className={buildClassName('bot-commands', isBotCommandMenuOpen && 'activated')}
round
disabled={botCommands === undefined}
color="translucent"
onActivate={handleActivateBotCommandMenu}
ariaLabel="Open bot command keyboard"
>
<i className="icon-bot-commands-filled" />
</ResponsiveHoverButton>
)}
{canShowSendAs && (sendAsUser || sendAsChat) && (
<Button
round
color="translucent"
onClick={isSendAsMenuOpen ? closeSendAsMenu : handleSendAsMenuOpen}
ariaLabel={lang('SendMessageAsTitle')}
className={buildClassName('send-as-button', shouldAnimateSendAsButtonRef.current && 'appear-animation')}
>
<Avatar
user={sendAsUser}
chat={sendAsChat}
size="tiny"
/>
</Button>
)}
{IS_SINGLE_COLUMN_LAYOUT ? (
<Button
className={symbolMenuButtonClassName}
round
color="translucent"
onClick={isSymbolMenuOpen ? closeSymbolMenu : handleSymbolMenuOpen}
ariaLabel="Choose emoji, sticker or GIF"
>
<i className="icon-smile" />
<i className="icon-keyboard" />
{isSymbolMenuOpen && !isSymbolMenuLoaded && <Spinner color="gray" />}
</Button>
) : (
<ResponsiveHoverButton
className={buildClassName('symbol-menu-button', isSymbolMenuOpen && 'activated')}
round
color="translucent"
onActivate={handleActivateSymbolMenu}
ariaLabel="Choose emoji, sticker or GIF"
>
<i className="icon-smile" />
</ResponsiveHoverButton>
)}
<MessageInput
id="message-input-text"
editableInputId={EDITABLE_INPUT_ID}
chatId={chatId}
threadId={threadId}
html={!attachments.length ? html : ''}
placeholder={
activeVoiceRecording && windowWidth <= SCREEN_WIDTH_TO_HIDE_PLACEHOLDER
? ''
: botKeyboardPlaceholder || lang('Message')
}
forcedPlaceholder={inlineBotHelp}
canAutoFocus={isReady && !attachments.length}
noFocusInterception={attachments.length > 0}
shouldSuppressFocus={IS_SINGLE_COLUMN_LAYOUT && isSymbolMenuOpen}
shouldSuppressTextFormatter={isEmojiTooltipOpen || isMentionTooltipOpen || isInlineBotTooltipOpen}
onUpdate={setHtml}
onSend={onSend}
onSuppressedFocus={closeSymbolMenu}
/>
{isInlineBotLoading && Boolean(inlineBotId) && (
<Spinner color="gray" />
)}
{withScheduledButton && (
<Button
round
faded
className="scheduled-button"
color="translucent"
onClick={handleAllScheduledClick}
ariaLabel="Open scheduled messages"
>
<i className="icon-schedule" />
</Button>
)}
{Boolean(botKeyboardMessageId) && !activeVoiceRecording && !editingMessage && (
<ResponsiveHoverButton
className={isBotKeyboardOpen ? 'activated' : ''}
round
color="translucent"
onActivate={openBotKeyboard}
ariaLabel="Open bot command keyboard"
>
<i className="icon-bot-command" />
</ResponsiveHoverButton>
)}
{activeVoiceRecording && Boolean(currentRecordTime) && (
<span className="recording-state">
{formatVoiceRecordDuration(currentRecordTime - startRecordTimeRef.current!)}
</span>
)}
<AttachMenu
chatId={chatId}
threadId={threadId}
isButtonVisible={!activeVoiceRecording && !editingMessage}
canAttachMedia={canAttachMedia}
canAttachPolls={canAttachPolls}
onFileSelect={handleFileSelect}
onPollCreate={openPollModal}
isScheduled={shouldSchedule}
attachBots={attachBots}
peerType={attachMenuPeerType}
theme={theme}
/>
{Boolean(botKeyboardMessageId) && (
<BotKeyboardMenu
messageId={botKeyboardMessageId}
isOpen={isBotKeyboardOpen}
onClose={closeBotKeyboard}
/>
)}
{botCommands && (
<BotCommandMenu
isOpen={isBotCommandMenuOpen}
botCommands={botCommands}
onClose={closeBotCommandMenu}
/>
)}
<CustomEmojiTooltip
chatId={chatId}
isOpen={isCustomEmojiTooltipOpen}
onCustomEmojiSelect={insertCustomEmoji}
addRecentCustomEmoji={addRecentCustomEmoji}
/>
<StickerTooltip
chatId={chatId}
threadId={threadId}
isOpen={isStickerTooltipOpen}
onStickerSelect={handleStickerSelect}
/>
<EmojiTooltip
isOpen={isEmojiTooltipOpen}
emojis={filteredEmojis}
customEmojis={filteredCustomEmojis}
onClose={closeEmojiTooltip}
onEmojiSelect={insertEmoji}
addRecentEmoji={addRecentEmoji}
onCustomEmojiSelect={insertCustomEmojiFromEmojiTooltip}
addRecentCustomEmoji={addRecentCustomEmoji}
/>
<SymbolMenu
chatId={chatId}
threadId={threadId}
isOpen={isSymbolMenuOpen || isSymbolMenuForced}
canSendGifs={canSendGifs}
canSendStickers={canSendStickers}
onLoad={onSymbolMenuLoadingComplete}
onClose={closeSymbolMenu}
onEmojiSelect={insertTextAndUpdateCursor}
onStickerSelect={handleStickerSelect}
onCustomEmojiSelect={handleCustomEmojiSelect}
onGifSelect={handleGifSelect}
onRemoveSymbol={removeSymbol}
onSearchOpen={handleSearchOpen}
addRecentEmoji={addRecentEmoji}
addRecentCustomEmoji={addRecentCustomEmoji}
/>
</div>
</div>
{activeVoiceRecording && (
<Button
round
color="danger"
className="cancel"
onClick={stopRecordingVoice}
ariaLabel="Cancel voice recording"
>
<i className="icon-delete" />
</Button>
)}
<Button
ref={mainButtonRef}
round
color="secondary"
className={buildClassName(mainButtonState, !isReady && 'not-ready', activeVoiceRecording && 'recording')}
disabled={areVoiceMessagesNotAllowed}
allowDisabledClick
ariaLabel={lang(sendButtonAriaLabel)}
onClick={mainButtonHandler}
onContextMenu={
mainButtonState === MainButtonState.Send && canShowCustomSendMenu ? handleContextMenu : undefined
}
>
<i className="icon-send" />
<i className="icon-schedule" />
<i className="icon-microphone-alt" />
<i className="icon-check" />
</Button>
{canShowCustomSendMenu && (
<CustomSendMenu
isOpen={isCustomSendMenuOpen}
onSendSilent={!isChatWithSelf ? handleSendSilent : undefined}
onSendSchedule={!shouldSchedule ? handleSendScheduled : undefined}
onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide}
isSavedMessages={isChatWithSelf}
/>
)}
{calendar}
</div>
);
};
export default memo(withGlobal<OwnProps>(
(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, threadId);
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: (
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));