import type { GlobalState, MessageListType, TabArgs, Thread, TabThread, } from '../types'; import type { ApiChat, ApiMessage, ApiMessageEntityCustomEmoji, ApiMessageOutgoingStatus, ApiStickerSetInfo, ApiUser, } from '../../api/types'; import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types'; import { GENERAL_TOPIC_ID, LOCAL_MESSAGE_MIN_ID, REPLIES_USER_ID, SERVICE_NOTIFICATIONS_USER_ID, } from '../../config'; import { selectChat, selectChatBot, selectIsChatWithSelf, } from './chats'; import { selectIsCurrentUserPremium, selectIsUserOrChatContact, selectUser, selectUserStatus, } from './users'; import { getCanPostInChat, getHasAdminRight, getMessageAudio, getMessageDocument, getMessageOriginalId, getMessagePhoto, getMessageVideo, getMessageVoice, getMessageWebPagePhoto, getMessageWebPageVideo, getSendingState, isActionMessage, isChatBasicGroup, isChatChannel, isChatGroup, isChatSuperGroup, isCommonBoxChat, isForwardedMessage, isMessageLocal, isOwnMessage, isServiceNotificationMessage, isUserId, isUserRightBanned, canSendReaction, } from '../helpers'; import { findLast } from '../../util/iteratees'; import { selectIsStickerFavorite } from './symbols'; import { getServerTime } from '../../util/serverTime'; import { MEMO_EMPTY_ARRAY } from '../../util/memo'; import { selectTabState } from './tabs'; import { getCurrentTabId } from '../../util/establishMultitabRole'; const MESSAGE_EDIT_ALLOWED_TIME = 172800; // 48 hours export function selectCurrentMessageList( global: T, ...[tabId = getCurrentTabId()]: TabArgs ) { const { messageLists } = selectTabState(global, tabId); if (messageLists.length) { return messageLists[messageLists.length - 1]; } return undefined; } export function selectCurrentChat( global: T, ...[tabId = getCurrentTabId()]: TabArgs ) { const { chatId } = selectCurrentMessageList(global, tabId) || {}; return chatId ? selectChat(global, chatId) : undefined; } export function selectChatMessages(global: T, chatId: string) { return global.messages.byChatId[chatId]?.byId; } export function selectChatScheduledMessages(global: T, chatId: string) { return global.scheduledMessages.byChatId[chatId]?.byId; } export function selectTabThreadParam( global: T, chatId: string, threadId: number, key: K, ...[tabId = getCurrentTabId()]: TabArgs ) { return selectTabState(global, tabId).tabThreads[chatId]?.[threadId]?.[key]; } export function selectThreadParam( global: T, chatId: string, threadId: number, key: K, ) { return selectThread(global, chatId, threadId)?.[key]; } export function selectThread( global: T, chatId: string, threadId: number, ) { const messageInfo = global.messages.byChatId[chatId]; if (!messageInfo) { return undefined; } const thread = messageInfo.threadsById[threadId]; if (!thread) { return undefined; } return thread; } export function selectListedIds(global: T, chatId: string, threadId: number) { return selectThreadParam(global, chatId, threadId, 'listedIds'); } export function selectOutlyingIds( global: T, chatId: string, threadId: number, ...[tabId = getCurrentTabId()]: TabArgs ) { return selectTabThreadParam(global, chatId, threadId, 'outlyingIds', tabId); } export function selectCurrentMessageIds( global: T, chatId: string, threadId: number, messageListType: MessageListType, ...[tabId = getCurrentTabId()]: TabArgs ) { switch (messageListType) { case 'thread': return selectViewportIds(global, chatId, threadId, tabId); case 'pinned': return selectPinnedIds(global, chatId, threadId); case 'scheduled': return selectScheduledIds(global, chatId, threadId); } return undefined; } export function selectViewportIds( global: T, chatId: string, threadId: number, ...[tabId = getCurrentTabId()]: TabArgs ) { return selectTabThreadParam(global, chatId, threadId, 'viewportIds', tabId); } export function selectPinnedIds(global: T, chatId: string, threadId: number) { return selectThreadParam(global, chatId, threadId, 'pinnedIds'); } export function selectScheduledIds(global: T, chatId: string, threadId: number) { return selectThreadParam(global, chatId, threadId, 'scheduledIds'); } export function selectScrollOffset( global: T, chatId: string, threadId: number, ...[tabId = getCurrentTabId()]: TabArgs ) { return selectTabThreadParam(global, chatId, threadId, 'scrollOffset', tabId); } export function selectLastScrollOffset(global: T, chatId: string, threadId: number) { return selectThreadParam(global, chatId, threadId, 'lastScrollOffset'); } export function selectReplyingToId(global: T, chatId: string, threadId: number) { return selectThreadParam(global, chatId, threadId, 'replyingToId'); } export function selectEditingId(global: T, chatId: string, threadId: number) { return selectThreadParam(global, chatId, threadId, 'editingId'); } export function selectEditingDraft(global: T, chatId: string, threadId: number) { return selectThreadParam(global, chatId, threadId, 'editingDraft'); } export function selectEditingScheduledId(global: T, chatId: string) { return selectThreadParam(global, chatId, MAIN_THREAD_ID, 'editingScheduledId'); } export function selectEditingScheduledDraft(global: T, chatId: string) { return selectThreadParam(global, chatId, MAIN_THREAD_ID, 'editingScheduledDraft'); } export function selectDraft(global: T, chatId: string, threadId: number) { return selectThreadParam(global, chatId, threadId, 'draft'); } export function selectNoWebPage(global: T, chatId: string, threadId: number) { return selectThreadParam(global, chatId, threadId, 'noWebPage'); } export function selectThreadInfo(global: T, chatId: string, threadId: number) { return selectThreadParam(global, chatId, threadId, 'threadInfo'); } export function selectFirstMessageId(global: T, chatId: string, threadId: number) { return selectThreadParam(global, chatId, threadId, 'firstMessageId'); } export function selectReplyStack( global: T, chatId: string, threadId: number, ...[tabId = getCurrentTabId()]: TabArgs ) { return selectTabThreadParam(global, chatId, threadId, 'replyStack', tabId); } export function selectThreadMessagesCount(global: GlobalState, chatId: string, threadId: number) { const chat = selectChat(global, chatId); const threadInfo = selectThreadInfo(global, chatId, threadId); if (!chat || !threadInfo || threadInfo.messagesCount === undefined) return undefined; // In forum topics first message is ignored, but not in General if (chat.isForum && threadId !== GENERAL_TOPIC_ID) return threadInfo.messagesCount - 1; return threadInfo.messagesCount; } export function selectThreadOriginChat(global: T, chatId: string, threadId: number) { if (threadId === MAIN_THREAD_ID) { return selectChat(global, chatId); } const threadInfo = selectThreadInfo(global, chatId, threadId); return selectChat(global, threadInfo?.originChannelId || chatId); } export function selectThreadTopMessageId(global: T, chatId: string, threadId: number) { if (threadId === MAIN_THREAD_ID) { return undefined; } const chat = selectChat(global, chatId); if (chat?.isForum) { return threadId; } const threadInfo = selectThreadInfo(global, chatId, threadId); if (!threadInfo) { return undefined; } return threadInfo.topMessageId; } export function selectThreadByMessage(global: T, message: ApiMessage) { const threadId = selectThreadIdFromMessage(global, message); if (!threadId || threadId === MAIN_THREAD_ID) { return undefined; } return global.messages.byChatId[message.chatId].threadsById[threadId]; } export function selectIsMessageInCurrentMessageList( global: T, chatId: string, message: ApiMessage, ...[tabId = getCurrentTabId()]: TabArgs ) { const currentMessageList = selectCurrentMessageList(global, tabId); if (!currentMessageList) { return false; } const { threadInfo } = selectThreadByMessage(global, message) || {}; return ( chatId === currentMessageList.chatId && ( (currentMessageList.threadId === MAIN_THREAD_ID) || (threadInfo && currentMessageList.threadId === threadInfo.threadId) ) ); } export function selectIsViewportNewest( global: T, chatId: string, threadId: number, ...[tabId = getCurrentTabId()]: TabArgs ) { const viewportIds = selectViewportIds(global, chatId, threadId, tabId); if (!viewportIds || !viewportIds.length) { return true; } let lastMessageId: number; if (threadId === MAIN_THREAD_ID) { const chat = selectChat(global, chatId); if (!chat || !chat.lastMessage) { return true; } lastMessageId = chat.lastMessage.id; } else { const threadInfo = selectThreadInfo(global, chatId, threadId); if (!threadInfo || !threadInfo.lastMessageId) { return undefined; } lastMessageId = threadInfo.lastMessageId; } // Edge case: outgoing `lastMessage` is updated with a delay to optimize animation if (lastMessageId > LOCAL_MESSAGE_MIN_ID && !selectChatMessage(global, chatId, lastMessageId)) { return true; } return viewportIds[viewportIds.length - 1] >= lastMessageId; } export function selectChatMessage(global: T, chatId: string, messageId: number) { const chatMessages = selectChatMessages(global, chatId); return chatMessages ? chatMessages[messageId] : undefined; } export function selectScheduledMessage(global: T, chatId: string, messageId: number) { const chatMessages = selectChatScheduledMessages(global, chatId); return chatMessages ? chatMessages[messageId] : undefined; } export function selectEditingMessage( global: T, chatId: string, threadId: number, messageListType: MessageListType, ) { if (messageListType === 'scheduled') { const messageId = selectEditingScheduledId(global, chatId); return messageId ? selectScheduledMessage(global, chatId, messageId) : undefined; } else { const messageId = selectEditingId(global, chatId, threadId); return messageId ? selectChatMessage(global, chatId, messageId) : undefined; } } export function selectChatMessageByPollId(global: T, pollId: string) { let messageWithPoll: ApiMessage | undefined; // eslint-disable-next-line no-restricted-syntax for (const chatMessages of Object.values(global.messages.byChatId)) { const { byId } = chatMessages; messageWithPoll = Object.values(byId).find((message) => { return message.content.poll && message.content.poll.id === pollId; }); if (messageWithPoll) { break; } } return messageWithPoll; } export function selectFocusedMessageId( global: T, chatId: string, ...[tabId = getCurrentTabId()]: TabArgs ) { const { chatId: focusedChatId, messageId } = selectTabState(global, tabId).focusedMessage || {}; return focusedChatId === chatId ? messageId : undefined; } export function selectIsMessageFocused( global: T, message: ApiMessage, ...[tabId = getCurrentTabId()]: TabArgs ) { const focusedId = selectFocusedMessageId(global, message.chatId, tabId); return focusedId ? focusedId === message.id || focusedId === message.previousLocalId : false; } export function selectIsMessageUnread(global: T, message: ApiMessage) { const { lastReadOutboxMessageId } = selectChat(global, message.chatId) || {}; return isMessageLocal(message) || !lastReadOutboxMessageId || lastReadOutboxMessageId < message.id; } export function selectOutgoingStatus( global: T, message: ApiMessage, isScheduledList = false, ): ApiMessageOutgoingStatus { if (!selectIsMessageUnread(global, message) && !isScheduledList) { return 'read'; } return getSendingState(message); } export function selectSender(global: T, message: ApiMessage): ApiUser | ApiChat | undefined { const { senderId } = message; if (!senderId) { return undefined; } return isUserId(senderId) ? selectUser(global, senderId) : selectChat(global, senderId); } export function selectReplySender(global: T, message: ApiMessage, isForwarded = false) { if (isForwarded) { const { senderUserId, hiddenUserName } = message.forwardInfo || {}; if (senderUserId) { return isUserId(senderUserId) ? selectUser(global, senderUserId) : selectChat(global, senderUserId); } if (hiddenUserName) return undefined; } const { senderId } = message; if (!senderId) { return undefined; } return isUserId(senderId) ? selectUser(global, senderId) : selectChat(global, senderId); } export function selectForwardedSender( global: T, message: ApiMessage, ): ApiUser | ApiChat | undefined { const { forwardInfo } = message; if (!forwardInfo) { return undefined; } if (forwardInfo.isChannelPost && forwardInfo.fromChatId) { return selectChat(global, forwardInfo.fromChatId); } else if (forwardInfo.senderUserId) { return selectUser(global, forwardInfo.senderUserId) || selectChat(global, forwardInfo.senderUserId); } return undefined; } const MAX_MESSAGES_TO_DELETE_OWNER_TOPIC = 10; export function selectCanDeleteOwnerTopic(global: T, chatId: string, topicId: number) { const chat = selectChat(global, chatId); if (!chat) { return false; } if (chat.topics?.[topicId] && !chat.topics?.[topicId].isOwner) return false; const thread = global.messages.byChatId[chatId]?.threadsById[topicId]; if (!thread) return false; const { listedIds } = thread; if (!listedIds // Plus one for root message || listedIds.length + 1 >= MAX_MESSAGES_TO_DELETE_OWNER_TOPIC) { return false; } const hasNotOutgoingMessages = listedIds.some((messageId) => { const message = selectChatMessage(global, chatId, messageId); return !message || !message.isOutgoing; }); return !hasNotOutgoingMessages; } export function selectCanDeleteTopic(global: T, chatId: string, topicId: number) { const chat = selectChat(global, chatId); if (!chat) return false; if (topicId === GENERAL_TOPIC_ID) return false; return chat.isCreator || getHasAdminRight(chat, 'deleteMessages') || (chat.isForum && selectCanDeleteOwnerTopic(global, chat.id, topicId)); } export function selectThreadIdFromMessage(global: T, message: ApiMessage): number { const chat = selectChat(global, message.chatId); const { replyToMessageId, replyToTopMessageId, isTopicReply, content, } = message; if ('action' in content && content.action?.type === 'topicCreate') { return message.id; } // TODO ignore only basic group if reply threads are added if (!chat?.isForum) return MAIN_THREAD_ID; if (!isTopicReply) return GENERAL_TOPIC_ID; return replyToTopMessageId || replyToMessageId || GENERAL_TOPIC_ID; } export function selectTopicFromMessage(global: T, message: ApiMessage) { const { chatId } = message; const chat = selectChat(global, chatId); if (!chat?.isForum) return undefined; const threadId = selectThreadIdFromMessage(global, message); return chat.topics?.[threadId]; } export function selectAllowedMessageActions(global: T, message: ApiMessage, threadId: number) { const chat = selectChat(global, message.chatId); if (!chat || chat.isRestricted) { return {}; } const isPrivate = isUserId(chat.id); const isChatWithSelf = selectIsChatWithSelf(global, message.chatId); const isBasicGroup = isChatBasicGroup(chat); const isSuperGroup = isChatSuperGroup(chat); const isChannel = isChatChannel(chat); const isLocal = isMessageLocal(message); const isServiceNotification = isServiceNotificationMessage(message); const isOwn = isOwnMessage(message); const isAction = isActionMessage(message); const { content } = message; const messageTopic = selectTopicFromMessage(global, message); const canEditMessagesIndefinitely = isChatWithSelf || (isSuperGroup && getHasAdminRight(chat, 'pinMessages')) || (isChannel && getHasAdminRight(chat, 'editMessages')); const isMessageEditable = ( ( canEditMessagesIndefinitely || getServerTime() - message.date < MESSAGE_EDIT_ALLOWED_TIME ) && !( content.sticker || content.contact || content.poll || content.action || content.audio || (content.video?.isRound) || content.location || content.invoice ) && !isForwardedMessage(message) && !message.viaBotId && !chat.isForbidden ); const canReply = !isLocal && !isServiceNotification && !chat.isForbidden && getCanPostInChat(chat, threadId) && (!messageTopic || !messageTopic.isClosed || messageTopic.isOwner || getHasAdminRight(chat, 'manageTopics')); const hasPinPermission = isPrivate || ( chat.isCreator || (!isChannel && !isUserRightBanned(chat, 'pinMessages')) || getHasAdminRight(chat, 'pinMessages') ); let canPin = !isLocal && !isServiceNotification && !isAction && hasPinPermission; let canUnpin = false; const pinnedMessageIds = selectPinnedIds(global, chat.id, threadId); if (canPin) { canUnpin = Boolean(pinnedMessageIds && pinnedMessageIds.includes(message.id)); canPin = !canUnpin; } const canDelete = !isLocal && !isServiceNotification && ( isPrivate || isOwn || isBasicGroup || chat.isCreator || getHasAdminRight(chat, 'deleteMessages') ); const canReport = !isPrivate && !isOwn; const canDeleteForAll = canDelete && !chat.isForbidden && ( (isPrivate && !isChatWithSelf) || (isBasicGroup && ( isOwn || getHasAdminRight(chat, 'deleteMessages') || chat.isCreator )) ); const canEdit = !isLocal && !isAction && isMessageEditable && ( isOwn || (isChannel && (chat.isCreator || getHasAdminRight(chat, 'editMessages'))) ); const isChatProtected = selectIsChatProtected(global, message.chatId); const canForward = ( !isLocal && !isAction && !isChatProtected && (message.isForwardingAllowed || isServiceNotification) ); const hasSticker = Boolean(message.content.sticker); const hasFavoriteSticker = hasSticker && selectIsStickerFavorite(global, message.content.sticker!); const canFaveSticker = !isAction && hasSticker && !hasFavoriteSticker; const canUnfaveSticker = !isAction && hasFavoriteSticker; const canCopy = !isAction; const canCopyLink = !isAction && (isChannel || isSuperGroup); const canSelect = !isAction; const canDownload = Boolean(content.webPage?.document || content.webPage?.video || content.webPage?.photo || content.audio || content.voice || content.photo || content.video || content.document || content.sticker); const canSaveGif = message.content.video?.isGif; const poll = content.poll; const canRevote = !poll?.summary.closed && !poll?.summary.quiz && poll?.results.results?.some((r) => r.isChosen); const canClosePoll = isOwn && poll && !poll.summary.closed; const noOptions = [ canReply, canEdit, canPin, canUnpin, canReport, canDelete, canDeleteForAll, canForward, canFaveSticker, canUnfaveSticker, canCopy, canCopyLink, canSelect, canDownload, canSaveGif, canRevote, canClosePoll, ].every((ability) => !ability); return { noOptions, canReply, canEdit, canPin, canUnpin, canReport, canDelete, canDeleteForAll, canForward, canFaveSticker, canUnfaveSticker, canCopy, canCopyLink, canSelect, canDownload, canSaveGif, canRevote, canClosePoll, }; } // This selector always returns a new object which can not be safely used in shallow-equal checks export function selectCanDeleteSelectedMessages( global: T, ...[tabId = getCurrentTabId()]: TabArgs ) { const { messageIds: selectedMessageIds } = selectTabState(global, tabId).selectedMessages || {}; const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {}; const chatMessages = chatId && selectChatMessages(global, chatId); if (!chatMessages || !selectedMessageIds || !threadId) { return {}; } const messageActions = selectedMessageIds .map((id) => chatMessages[id] && selectAllowedMessageActions(global, chatMessages[id], threadId)) .filter(Boolean); return { canDelete: messageActions.every((actions) => actions.canDelete), canDeleteForAll: messageActions.every((actions) => actions.canDeleteForAll), }; } export function selectCanReportSelectedMessages( global: T, ...[tabId = getCurrentTabId()]: TabArgs ) { const { messageIds: selectedMessageIds } = selectTabState(global, tabId).selectedMessages || {}; const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {}; const chatMessages = chatId && selectChatMessages(global, chatId); if (!chatMessages || !selectedMessageIds || !threadId) { return false; } const messageActions = selectedMessageIds .map((id) => chatMessages[id] && selectAllowedMessageActions(global, chatMessages[id], threadId)) .filter(Boolean); return messageActions.every((actions) => actions.canReport); } export function selectCanDownloadSelectedMessages( global: T, ...[tabId = getCurrentTabId()]: TabArgs ) { const { messageIds: selectedMessageIds } = selectTabState(global, tabId).selectedMessages || {}; const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {}; const chatMessages = chatId && selectChatMessages(global, chatId); if (!chatMessages || !selectedMessageIds || !threadId) { return false; } const messageActions = selectedMessageIds .map((id) => chatMessages[id] && selectAllowedMessageActions(global, chatMessages[id], threadId)) .filter(Boolean); return messageActions.some((actions) => actions.canDownload); } export function selectIsDownloading( global: T, message: ApiMessage, ...[tabId = getCurrentTabId()]: TabArgs ) { const activeInChat = selectTabState(global, tabId).activeDownloads.byChatId[message.chatId]; return activeInChat ? activeInChat.includes(message.id) : false; } export function selectActiveDownloadIds( global: T, chatId: string, ...[tabId = getCurrentTabId()]: TabArgs ) { return selectTabState(global, tabId).activeDownloads.byChatId[chatId] || MEMO_EMPTY_ARRAY; } export function selectUploadProgress(global: T, message: ApiMessage) { return global.fileUploads.byMessageLocalId[getMessageOriginalId(message)]?.progress; } export function selectRealLastReadId(global: T, chatId: string, threadId: number) { if (threadId === MAIN_THREAD_ID) { const chat = selectChat(global, chatId); if (!chat) { return undefined; } // `lastReadInboxMessageId` is empty for new chats if (!chat.lastReadInboxMessageId) { return undefined; } if (!chat.lastMessage) { return chat.lastReadInboxMessageId; } if (isMessageLocal(chat.lastMessage)) { return chat.lastMessage.id; } // Some previously read messages may be deleted return Math.min(chat.lastMessage.id, chat.lastReadInboxMessageId); } else { const threadInfo = selectThreadInfo(global, chatId, threadId); if (!threadInfo) { return undefined; } if (!threadInfo.lastReadInboxMessageId) { return threadInfo.topMessageId; } // Some previously read messages may be deleted return Math.min(threadInfo.lastReadInboxMessageId, threadInfo.lastMessageId || Infinity); } } export function selectFirstUnreadId( global: T, chatId: string, threadId: number, ...[tabId = getCurrentTabId()]: TabArgs ) { const chat = selectChat(global, chatId); if (threadId === MAIN_THREAD_ID) { if (!chat) { return undefined; } } else { const threadInfo = selectThreadInfo(global, chatId, threadId); if (!threadInfo || (threadInfo.lastMessageId !== undefined && threadInfo.lastMessageId === threadInfo.lastReadInboxMessageId)) { return undefined; } } const outlyingIds = selectOutlyingIds(global, chatId, threadId, tabId); const listedIds = selectListedIds(global, chatId, threadId); const byId = selectChatMessages(global, chatId); if (!byId || !(outlyingIds || listedIds)) { return undefined; } const lastReadId = selectRealLastReadId(global, chatId, threadId); if (!lastReadId && chat && chat.isNotJoined) { return undefined; } const lastReadServiceNotificationId = chatId === SERVICE_NOTIFICATIONS_USER_ID ? global.serviceNotifications.reduce((max, notification) => { return !notification.isUnread && notification.id > max ? notification.id : max; }, -1) : -1; function findAfterLastReadId(listIds: number[]) { return listIds.find((id) => { return ( (!lastReadId || id > lastReadId) && byId[id] && (!byId[id].isOutgoing || byId[id].isFromScheduled) && id > lastReadServiceNotificationId ); }); } if (outlyingIds) { const found = findAfterLastReadId(outlyingIds); if (found) { return found; } } if (listedIds) { const found = findAfterLastReadId(listedIds); if (found) { return found; } } return undefined; } export function selectIsPollResultsOpen( global: T, ...[tabId = getCurrentTabId()]: TabArgs ) { const { pollResults } = selectTabState(global, tabId); return Boolean(pollResults.messageId); } export function selectIsCreateTopicPanelOpen( global: T, ...[tabId = getCurrentTabId()]: TabArgs ) { const { createTopicPanel } = selectTabState(global, tabId); return Boolean(createTopicPanel); } export function selectIsEditTopicPanelOpen( global: T, ...[tabId = getCurrentTabId()]: TabArgs ) { const { editTopicPanel } = selectTabState(global, tabId); return Boolean(editTopicPanel); } export function selectIsForwardModalOpen( global: T, ...[tabId = getCurrentTabId()]: TabArgs ) { const { forwardMessages } = selectTabState(global, tabId); return Boolean(forwardMessages.isModalShown); } export function selectCommonBoxChatId(global: T, messageId: number) { const fromLastMessage = Object.values(global.chats.byId).find((chat) => ( isCommonBoxChat(chat) && chat.lastMessage && chat.lastMessage.id === messageId )); if (fromLastMessage) { return fromLastMessage.id; } const { byChatId } = global.messages; return Object.keys(byChatId).find((chatId) => { const chat = selectChat(global, chatId); return chat && isCommonBoxChat(chat) && byChatId[chat.id].byId[messageId]; }); } export function selectIsInSelectMode( global: T, ...[tabId = getCurrentTabId()]: TabArgs ) { const { selectedMessages } = selectTabState(global, tabId); return Boolean(selectedMessages); } export function selectIsMessageSelected( global: T, messageId: number, ...[tabId = getCurrentTabId()]: TabArgs ) { const { messageIds } = selectTabState(global, tabId).selectedMessages || {}; if (!messageIds) { return false; } return messageIds.includes(messageId); } export function selectForwardedMessageIdsByGroupId( global: T, chatId: string, groupedId: string, ) { const chatMessages = selectChatMessages(global, chatId); if (!chatMessages) { return undefined; } return Object.values(chatMessages) .filter((message) => message.groupedId === groupedId && message.forwardInfo) .map(({ forwardInfo }) => forwardInfo!.fromMessageId); } export function selectMessageIdsByGroupId(global: T, chatId: string, groupedId: string) { const chatMessages = selectChatMessages(global, chatId); if (!chatMessages) { return undefined; } return Object.keys(chatMessages) .map(Number) .filter((id) => chatMessages[id].groupedId === groupedId); } export function selectIsDocumentGroupSelected( global: T, chatId: string, groupedId: string, ...[tabId = getCurrentTabId()]: TabArgs ) { const { messageIds: selectedIds } = selectTabState(global, tabId).selectedMessages || {}; if (!selectedIds) { return false; } const groupIds = selectMessageIdsByGroupId(global, chatId, groupedId); return groupIds && groupIds.every((id) => selectedIds.includes(id)); } export function selectSelectedMessagesCount( global: T, ...[tabId = getCurrentTabId()]: TabArgs ) { const { messageIds } = selectTabState(global, tabId).selectedMessages || {}; return messageIds ? messageIds.length : 0; } export function selectNewestMessageWithBotKeyboardButtons( global: T, chatId: string, threadId = MAIN_THREAD_ID, ...[tabId = getCurrentTabId()]: TabArgs ): ApiMessage | undefined { const chat = selectChat(global, chatId); if (!chat) { return undefined; } const chatMessages = selectChatMessages(global, chatId); const viewportIds = selectViewportIds(global, chatId, threadId, tabId); if (!chatMessages || !viewportIds) { return undefined; } const messageId = findLast(viewportIds, (id) => selectShouldDisplayReplyKeyboard(global, chatMessages[id])); const replyHideMessageId = findLast(viewportIds, (id) => selectShouldHideReplyKeyboard(global, chatMessages[id])); if (messageId && replyHideMessageId && replyHideMessageId > messageId) { return undefined; } return messageId ? chatMessages[messageId] : undefined; } function selectShouldHideReplyKeyboard(global: T, message: ApiMessage) { const { shouldHideKeyboardButtons, isHideKeyboardSelective, replyToMessageId, isMentioned, } = message; if (!shouldHideKeyboardButtons) return false; if (isHideKeyboardSelective) { if (isMentioned) return true; if (!replyToMessageId) return false; const replyMessage = selectChatMessage(global, message.chatId, replyToMessageId); return Boolean(replyMessage?.senderId === global.currentUserId); } return true; } function selectShouldDisplayReplyKeyboard(global: T, message: ApiMessage) { const { keyboardButtons, shouldHideKeyboardButtons, isKeyboardSelective, isMentioned, replyToMessageId, } = message; if (!keyboardButtons || shouldHideKeyboardButtons) return false; if (isKeyboardSelective) { if (isMentioned) return true; if (!replyToMessageId) return false; const replyMessage = selectChatMessage(global, message.chatId, replyToMessageId); return Boolean(replyMessage?.senderId === global.currentUserId); } return true; } export function selectCanAutoLoadMedia(global: T, message: ApiMessage) { const chat = selectChat(global, message.chatId); if (!chat) { return undefined; } const sender = selectSender(global, message); const isPhoto = Boolean(getMessagePhoto(message) || getMessageWebPagePhoto(message)); const isVideo = Boolean(getMessageVideo(message) || getMessageWebPageVideo(message)); const isFile = Boolean(getMessageAudio(message) || getMessageVoice(message) || getMessageDocument(message)); const { canAutoLoadPhotoFromContacts, canAutoLoadPhotoInPrivateChats, canAutoLoadPhotoInGroups, canAutoLoadPhotoInChannels, canAutoLoadVideoFromContacts, canAutoLoadVideoInPrivateChats, canAutoLoadVideoInGroups, canAutoLoadVideoInChannels, canAutoLoadFileFromContacts, canAutoLoadFileInPrivateChats, canAutoLoadFileInGroups, canAutoLoadFileInChannels, } = global.settings.byKey; if (isPhoto) { return canAutoLoadMedia({ global, chat, sender, canAutoLoadMediaFromContacts: canAutoLoadPhotoFromContacts, canAutoLoadMediaInPrivateChats: canAutoLoadPhotoInPrivateChats, canAutoLoadMediaInGroups: canAutoLoadPhotoInGroups, canAutoLoadMediaInChannels: canAutoLoadPhotoInChannels, }); } if (isVideo) { return canAutoLoadMedia({ global, chat, sender, canAutoLoadMediaFromContacts: canAutoLoadVideoFromContacts, canAutoLoadMediaInPrivateChats: canAutoLoadVideoInPrivateChats, canAutoLoadMediaInGroups: canAutoLoadVideoInGroups, canAutoLoadMediaInChannels: canAutoLoadVideoInChannels, }); } if (isFile) { return canAutoLoadMedia({ global, chat, sender, canAutoLoadMediaFromContacts: canAutoLoadFileFromContacts, canAutoLoadMediaInPrivateChats: canAutoLoadFileInPrivateChats, canAutoLoadMediaInGroups: canAutoLoadFileInGroups, canAutoLoadMediaInChannels: canAutoLoadFileInChannels, }); } return true; } function canAutoLoadMedia({ global, chat, sender, canAutoLoadMediaFromContacts, canAutoLoadMediaInPrivateChats, canAutoLoadMediaInGroups, canAutoLoadMediaInChannels, }: { global: T; chat: ApiChat; canAutoLoadMediaFromContacts: boolean; canAutoLoadMediaInPrivateChats: boolean; canAutoLoadMediaInGroups: boolean; canAutoLoadMediaInChannels: boolean; sender?: ApiChat | ApiUser; }) { const isMediaFromContact = Boolean(sender && ( sender.id === global.currentUserId || selectIsUserOrChatContact(global, sender) )); return Boolean( (isMediaFromContact && canAutoLoadMediaFromContacts) || (!isMediaFromContact && canAutoLoadMediaInPrivateChats && isUserId(chat.id)) || (canAutoLoadMediaInGroups && isChatGroup(chat)) || (canAutoLoadMediaInChannels && isChatChannel(chat)), ); } export function selectCanAutoPlayMedia(global: T, message: ApiMessage) { const video = getMessageVideo(message) || getMessageWebPageVideo(message); if (!video) { return undefined; } const { canAutoPlayVideos, canAutoPlayGifs, } = global.settings.byKey; const asGif = video.isGif || video.isRound; return (canAutoPlayVideos && !asGif) || (canAutoPlayGifs && asGif); } export function selectShouldLoopStickers(global: T) { return global.settings.byKey.shouldLoopStickers; } export function selectLastServiceNotification(global: T) { const { serviceNotifications } = global; const maxId = Math.max(...serviceNotifications.map(({ id }) => id)); return serviceNotifications.find(({ id, isDeleted }) => !isDeleted && id === maxId); } export function selectIsMessageProtected(global: T, message?: ApiMessage) { return Boolean(message && (message.isProtected || selectIsChatProtected(global, message.chatId))); } export function selectIsChatProtected(global: T, chatId: string) { return selectChat(global, chatId)?.isProtected || false; } export function selectHasProtectedMessage(global: T, chatId: string, messageIds?: number[]) { if (selectChat(global, chatId)?.isProtected) { return true; } if (!messageIds) { return false; } const messages = selectChatMessages(global, chatId); return messageIds.some((messageId) => messages[messageId]?.isProtected); } export function selectCanForwardMessages(global: T, chatId: string, messageIds?: number[]) { if (selectChat(global, chatId)?.isProtected) { return false; } if (!messageIds) { return false; } const messages = selectChatMessages(global, chatId); return messageIds .map((id) => messages[id]) .every((message) => message.isForwardingAllowed || isServiceNotificationMessage(message)); } export function selectSponsoredMessage(global: T, chatId: string) { const chat = selectChat(global, chatId); const message = chat && isChatChannel(chat) ? global.messages.sponsoredByChatId[chatId] : undefined; return message && message.expiresAt >= Math.round(Date.now() / 1000) ? message : undefined; } export function selectDefaultReaction(global: T, chatId: string) { if (chatId === SERVICE_NOTIFICATIONS_USER_ID) return undefined; const isPrivate = isUserId(chatId); const defaultReaction = global.config?.defaultReaction; if (!defaultReaction) { return undefined; } if (isPrivate) { return defaultReaction; } const chatReactions = selectChat(global, chatId)?.fullInfo?.enabledReactions; if (!chatReactions || !canSendReaction(defaultReaction, chatReactions)) { return undefined; } return defaultReaction; } export function selectMaxUserReactions(global: T): number { const isPremium = selectIsCurrentUserPremium(global); const { maxUserReactionsPremium = 3, maxUserReactionsDefault = 1 } = global.appConfig || {}; return isPremium ? maxUserReactionsPremium : maxUserReactionsDefault; } // Slow, not to be used in `withGlobal` export function selectVisibleUsers( global: T, ...[tabId = getCurrentTabId()]: TabArgs ) { const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {}; if (!chatId || !threadId) { return undefined; } const messageIds = selectTabThreadParam(global, chatId, threadId, 'viewportIds', tabId); if (!messageIds) { return undefined; } return messageIds.map((messageId) => { const { senderId } = selectChatMessage(global, chatId, messageId) || {}; return senderId ? selectUser(global, senderId) : undefined; }).filter(Boolean); } export function selectShouldSchedule( global: T, ...[tabId = getCurrentTabId()]: TabArgs ) { return selectCurrentMessageList(global, tabId)?.type === 'scheduled'; } export function selectCanScheduleUntilOnline(global: T, id: string) { const isChatWithSelf = selectIsChatWithSelf(global, id); const chatBot = id === REPLIES_USER_ID && selectChatBot(global, id); return Boolean( !isChatWithSelf && !chatBot && isUserId(id) && selectUserStatus(global, id)?.wasOnline, ); } export function selectCustomEmojis(message: ApiMessage) { const entities = message.content.text?.entities; return entities?.filter((entity): entity is ApiMessageEntityCustomEmoji => ( entity.type === ApiMessageEntityTypes.CustomEmoji )); } export function selectMessageCustomEmojiSets( global: T, message: ApiMessage, ): ApiStickerSetInfo[] | undefined { const customEmojis = selectCustomEmojis(message); if (!customEmojis) return MEMO_EMPTY_ARRAY; const documents = customEmojis.map((entity) => global.customEmojis.byId[entity.documentId]); // If some emoji still loading, do not return empty array if (!documents.every(Boolean)) return undefined; const sets = documents.map((doc) => doc.stickerSetInfo); const setsWithoutDuplicates = sets.reduce((acc, set) => { if ('shortName' in set) { if (acc.some((s) => 'shortName' in s && s.shortName === set.shortName)) { return acc; } } if ('id' in set) { if (acc.some((s) => 'id' in s && s.id === set.id)) { return acc; } } acc.push(set); // Optimization return acc; }, [] as ApiStickerSetInfo[]); return setsWithoutDuplicates; } export function selectForwardsContainVoiceMessages( global: T, ...[tabId = getCurrentTabId()]: TabArgs ) { const { messageIds, fromChatId } = selectTabState(global, tabId).forwardMessages; if (!messageIds) return false; const chatMessages = selectChatMessages(global, fromChatId!); return messageIds.some((messageId) => { const message = chatMessages[messageId]; return Boolean(message.content.voice) || message.content.video?.isRound; }); }