import type { ApiChat, ApiInputMessageReplyInfo, ApiMessage, ApiMessageEntityCustomEmoji, ApiMessageForwardInfo, ApiMessageOutgoingStatus, ApiPeer, ApiRestrictionReason, ApiSponsoredMessage, ApiStickerSetInfo, MediaContainer, } from '../../api/types'; import type { ChatTranslatedMessages, MessageListType, TextSummary, ThreadId, TranslationTone, } from '../../types'; import type { IAllowedAttachmentOptions } from '../helpers'; import type { GlobalState, TabArgs, } from '../types'; import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types'; import { GENERAL_TOPIC_ID, SERVICE_NOTIFICATIONS_USER_ID, WEB_APP_PLATFORM, } from '../../config'; import { IS_TRANSLATION_SUPPORTED } from '../../util/browser/windowEnvironment'; import { isUserId } from '../../util/entities/ids'; import { getCurrentTabId } from '../../util/establishMultitabRole'; import { findLast } from '../../util/iteratees'; import { getMessageKey, isLocalMessageId } from '../../util/keys/messageKey'; import { parseTranslationCacheKey } from '../../util/keys/translationKey'; import { isIpRevealingMedia } from '../../util/media/ipRevealingMedia'; import { MEMO_EMPTY_ARRAY } from '../../util/memo'; import { getServerTime } from '../../util/serverTime'; import { getDocumentExtension } from '../../components/common/helpers/documentInfo'; import { API_GENERAL_ID_LIMIT } from '../../limits'; import { canSendReaction, getAllowedAttachmentOptions, getCanPostInChat, getHasAdminRight, getIsSavedDialog, getMessageAudio, getMessageDocument, getMessageLink, getMessagePaidMedia, getMessagePhoto, getMessageVideo, getMessageVoice, getSendingState, getWebPagePhoto, getWebPageVideo, hasMessageTtl, isActionMessage, isChatBasicGroup, isChatChannel, isChatGroup, isChatSuperGroup, isCommonBoxChat, isExpiredMessage, isForwardedMessage, isMessageDocumentSticker, isMessageFailed, isMessageLocal, isMessageTranslatable, isOwnMessage, isServiceNotificationMessage, isUserRightBanned, prepareMessageReplyInfo, } from '../helpers'; import { getMessageReplyInfo } from '../helpers/replies'; import { selectChat, selectChatFullInfo, selectChatLastMessageId, selectIsChatRestricted, selectIsChatWithBot, selectIsChatWithSelf, selectRequestedChatTranslationLanguage, } from './chats'; import { selectCurrentLimit } from './limits'; import { selectMessageDownloadableMedia } from './media'; import { selectPeer, selectPeerPaidMessagesStars } from './peers'; import { selectPeerStory } from './stories'; import { selectCustomEmoji, selectIsStickerFavorite } from './symbols'; import { selectTabState } from './tabs'; import { selectEditingId, selectEditingScheduledId, selectTabThreadParam, selectThreadByMessage, selectThreadInfo, selectThreadLocalState, selectThreadLocalStateParam, selectThreadReadState, } from './threads'; import { selectTopic, selectTopicFromMessage } from './topics'; import { selectBot, selectIsUserChatProtected, selectUser, selectUserStatus, } from './users'; 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 selectListedIds(global: T, chatId: string, threadId: ThreadId) { return selectThreadLocalStateParam(global, chatId, threadId, 'listedIds'); } export function selectOutlyingListByMessageId( global: T, chatId: string, threadId: ThreadId, messageId: number, ) { const outlyingLists = selectOutlyingLists(global, chatId, threadId); if (!outlyingLists) { return undefined; } return outlyingLists.find((list) => { return list[0] <= messageId && list[list.length - 1] >= messageId; }); } export function selectOutlyingLists( global: T, chatId: string, threadId: ThreadId, ) { return selectThreadLocalStateParam(global, chatId, threadId, 'outlyingLists'); } export function selectCurrentMessageIds( global: T, chatId: string, threadId: ThreadId, 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: ThreadId, ...[tabId = getCurrentTabId()]: TabArgs ) { return selectTabThreadParam(global, chatId, threadId, 'viewportIds', tabId); } export function selectPinnedIds(global: T, chatId: string, threadId: ThreadId) { return selectThreadLocalStateParam(global, chatId, threadId, 'pinnedIds'); } export function selectScheduledIds(global: T, chatId: string, threadId: ThreadId) { return selectThreadLocalStateParam(global, chatId, threadId, 'scheduledIds'); } export function selectFirstMessageId(global: T, chatId: string, threadId: ThreadId) { return selectThreadLocalStateParam(global, chatId, threadId, 'firstMessageId'); } 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: ThreadId, ...[tabId = getCurrentTabId()]: TabArgs ) { const viewportIds = selectViewportIds(global, chatId, threadId, tabId); if (!viewportIds || !viewportIds.length) { return true; } const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId); let lastMessageId: number; if (threadId === MAIN_THREAD_ID) { const id = selectChatLastMessageId(global, chatId); if (!id) { return true; } lastMessageId = id; } else if (isSavedDialog) { const id = selectChatLastMessageId(global, String(threadId), 'saved'); if (!id) { return true; } lastMessageId = id; } else { const threadInfo = selectThreadInfo(global, chatId, threadId); if (!threadInfo || !threadInfo.lastMessageId) { if (!threadInfo?.threadId) return undefined; // No messages in thread, except for the thread message itself lastMessageId = Number(threadInfo?.threadId); } else { lastMessageId = threadInfo.lastMessageId; } } // Edge case: outgoing `lastMessage` is updated with a delay to optimize animation if (isLocalMessageId(lastMessageId) && !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 selectQuickReplyMessage(global: T, messageId: number) { return global.quickReplies.messagesById[messageId]; } export function selectEditingMessage( global: T, chatId: string, threadId: ThreadId, 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 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, currentThreadId: ThreadId, ...[tabId = getCurrentTabId()]: TabArgs ) { const focusedId = selectFocusedMessageId(global, message.chatId, tabId); const threadId = selectTabState(global, tabId).focusedMessage?.threadId; if (currentThreadId !== threadId) return false; return focusedId ? focusedId === message.id || focusedId === message.previousLocalId : false; } export function selectIsMessageUnread( global: T, chatId: string, threadId: ThreadId, messageId: number, messageListType: MessageListType, ) { const { lastReadOutboxMessageId } = selectThreadReadState(global, chatId, threadId) || {}; const message = selectChatMessage(global, chatId, messageId); if (!message) { return false; } return messageListType === 'scheduled' || isMessageLocal(message) || !lastReadOutboxMessageId || lastReadOutboxMessageId < message.id; } export function selectOutgoingStatus( global: T, chatId: string, threadId: ThreadId, messageId: number, messageListType: MessageListType, ): ApiMessageOutgoingStatus { if (!selectIsMessageUnread(global, chatId, threadId, messageId, messageListType)) { return 'read'; } const message = selectChatMessage(global, chatId, messageId); if (!message) { return 'failed'; // Should never happen } return getSendingState(message); } export function selectSender(global: T, message: ApiMessage): ApiPeer | undefined { const { senderId } = message; const chat = selectChat(global, message.chatId); const currentUser = selectUser(global, global.currentUserId!); if (!senderId) { return message.isOutgoing ? currentUser : chat; } if (chat && isChatChannel(chat) && !chat.areProfilesShown) return chat; return selectPeer(global, senderId); } export function getSendersFromSelectedMessages( global: T, chatId: string, messageIds: number[], ) { return messageIds.map((id) => { const message = selectChatMessage(global, chatId, id); return message && selectSender(global, message); }).filter(Boolean); } export function selectSenderFromMessage( global: T, chatId: string, messageId: number, ): ApiPeer | undefined { const message = selectChatMessage(global, chatId, messageId); return message && selectSender(global, message); } export function selectSenderFromHeader( global: T, header: ApiMessageForwardInfo, ) { const { fromId } = header; if (fromId) { return selectPeer(global, fromId); } return undefined; } export function selectForwardedSender( global: T, message: ApiMessage, ): ApiPeer | undefined { const isStoryForward = Boolean(message.content.storyData); if (isStoryForward) { const peerId = message.content.storyData!.peerId; return selectPeer(global, peerId); } const { forwardInfo } = message; if (!forwardInfo) { return undefined; } if (forwardInfo.isChannelPost && forwardInfo.fromChatId) { return selectChat(global, forwardInfo.fromChatId); } if (forwardInfo.hiddenUserName) { return undefined; } if (forwardInfo.fromId) { return selectPeer(global, forwardInfo.fromId); } if (forwardInfo.savedFromPeerId) { return selectPeer(global, forwardInfo.savedFromPeerId); } return undefined; } export function selectPoll(global: T, pollId: string) { return global.messages.pollById[pollId]; } export function selectPollFromMessage(global: T, message: MediaContainer) { if (!message.content.pollId) return undefined; return selectPoll(global, message.content.pollId); } export function selectWebPage(global: T, webPageId: string) { return global.messages.webPageById[webPageId]; } export function selectWebPageFromMessage(global: T, message: MediaContainer) { if (!message.content.webPage) return undefined; return selectWebPage(global, message.content.webPage.id); } export function selectFullWebPage(global: T, webPageId: string) { const webPage = selectWebPage(global, webPageId); if (!webPage || webPage.webpageType !== 'full') return undefined; return webPage; } export function selectFullWebPageFromMessage(global: T, message: MediaContainer) { if (!message.content.webPage) return undefined; return selectFullWebPage(global, message.content.webPage.id); } const MAX_MESSAGES_TO_DELETE_OWNER_TOPIC = 10; export function selectCanDeleteOwnerTopic(global: T, chatId: string, topicId: number) { const topic = selectTopic(global, chatId, topicId); if (topic && !topic.isOwner) return false; const localThreadState = selectThreadLocalState(global, chatId, topicId); if (!localThreadState) return false; const { listedIds } = localThreadState; 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.isBotForum || chat.isCreator || getHasAdminRight(chat, 'deleteMessages') || (chat.isForum && selectCanDeleteOwnerTopic(global, chat.id, topicId)); } export function selectCanReplyToMessage(global: T, message: ApiMessage, threadId: ThreadId) { const chat = selectChat(global, message.chatId); const isRestricted = selectIsChatRestricted(global, message.chatId); if (!chat || isRestricted || chat.isForbidden) return false; const isLocal = isMessageLocal(message); const isServiceNotification = isServiceNotificationMessage(message); if (isLocal || isServiceNotification) return false; const threadInfo = selectThreadInfo(global, message.chatId, threadId); const isMessageThread = Boolean(!threadInfo?.isCommentsInfo && threadInfo?.fromChannelId); const chatFullInfo = selectChatFullInfo(global, chat.id); const topic = selectTopic(global, chat.id, threadId); const canPostInChat = getCanPostInChat(chat, topic, isMessageThread, chatFullInfo); if (!canPostInChat) return false; const messageTopic = selectTopicFromMessage(global, message); return !messageTopic || !messageTopic.isClosed || messageTopic.isOwner || getHasAdminRight(chat, 'manageTopics'); } export function selectCanForwardMessage(global: T, message: ApiMessage) { const isLocal = isMessageLocal(message); const isServiceNotification = isServiceNotificationMessage(message); const isAction = isActionMessage(message); const hasTtl = hasMessageTtl(message); const { content } = message; const webPage = selectFullWebPageFromMessage(global, message); const story = content.storyData ? selectPeerStory(global, content.storyData.peerId, content.storyData.id) : (webPage?.story ? selectPeerStory(global, webPage.story.peerId, webPage.story.id) : undefined ); const isChatProtected = selectIsChatProtected(global, message.chatId); const isStoryForwardForbidden = story && ('isDeleted' in story || ('noForwards' in story && story.noForwards)); const canForward = ( !isLocal && !isAction && !isChatProtected && !isStoryForwardForbidden && (message.isForwardingAllowed || isServiceNotification) && !hasTtl ); return canForward; } // This selector is slow and not to be used within lists (e.g. Message component) export function selectAllowedMessageActionsSlow( global: T, message: ApiMessage, threadId: ThreadId, ) { const chat = selectChat(global, message.chatId); const isRestricted = selectIsChatRestricted(global, message.chatId); if (!chat || isRestricted) { return {}; } const chatFullInfo = selectChatFullInfo(global, message.chatId); const isPrivate = isUserId(chat.id); const isChatWithSelf = selectIsChatWithSelf(global, message.chatId); const isBasicGroup = isChatBasicGroup(chat); const isSuperGroup = isChatSuperGroup(chat); const isChannel = isChatChannel(chat); const isBotChat = Boolean(selectBot(global, chat.id)); const isLocal = isMessageLocal(message); const isFailed = isMessageFailed(message); const isServiceNotification = isServiceNotificationMessage(message); const isOwn = isOwnMessage(message); const isForwarded = isForwardedMessage(message); const isAction = isActionMessage(message); const hasTtl = hasMessageTtl(message); const { content } = message; const isDocumentSticker = isMessageDocumentSticker(message); const isBoostMessage = message.content.action?.type === 'boostApply'; const isMonoforum = chat.isMonoforum; // https://github.com/telegramdesktop/tdesktop/blob/6627de646022af1394134974477109cd1439e1bb/Telegram/SourceFiles/data/data_peer_values.cpp#L367C2-L372C3 const canPinMessage = (() => { if (isPrivate || chat.isCreator) return true; if (isChannel) { return getHasAdminRight(chat, 'editMessages'); } const hasPinMessageRight = getHasAdminRight(chat, 'pinMessages'); const isPinMessageRightBanned = isUserRightBanned(chat, 'pinMessages', chatFullInfo); if (isSuperGroup) { const hasUsernameOrGeo = chat.hasUsername || chat.hasGeo; return (hasPinMessageRight || !hasUsernameOrGeo) && !isPinMessageRightBanned; } if (isBasicGroup) { return !chat.isForbidden && !chat.isNotJoined && hasPinMessageRight && !isPinMessageRightBanned; } return hasPinMessageRight; })(); // https://github.com/telegramdesktop/tdesktop/blob/335095a332607c41a8d20b47e61f5bbd66366d4b/Telegram/SourceFiles/data/data_peer.cpp#L653 const canEditMessagesIndefinitely = (() => { if (content.todo) return true; if (isPrivate) return isChatWithSelf; if (isBasicGroup) return false; if (isSuperGroup) return canPinMessage; if (isChannel) return chat.isCreator || getHasAdminRight(chat, 'editMessages'); return false; })(); const isMessageEditable = ( ( canEditMessagesIndefinitely || getServerTime() - message.date < (global.config?.editTimeLimit || Infinity) ) && !( content.sticker || content.contact || content.pollId || content.action || (content.video?.isRound) || content.location || content.invoice || content.giveaway || content.giveawayResults || isDocumentSticker || content.dice ) && !isForwarded && !message.viaBotId && !chat.isForbidden ); const isSavedDialog = getIsSavedDialog(chat.id, threadId, global.currentUserId); const canReply = selectCanReplyToMessage(global, message, threadId); const canReplyGlobally = canReply || (!isSavedDialog && !isLocal && !isServiceNotification && (isSuperGroup || isBasicGroup || isChatChannel(chat))); let canPin = !isLocal && !isServiceNotification && !isAction && canPinMessage && !isSavedDialog; let canUnpin = false; const pinnedMessageIds = selectPinnedIds(global, chat.id, threadId); if (canPin) { canUnpin = Boolean(pinnedMessageIds && pinnedMessageIds.includes(message.id)); canPin = !canUnpin; } const canNotDeleteBoostMessage = isBoostMessage && isOwn && !chat.isCreator && !getHasAdminRight(chat, 'deleteMessages'); const canDelete = (!isLocal || isFailed) && !isServiceNotification && !canNotDeleteBoostMessage && ( isPrivate || isOwn || isBasicGroup || chat.isCreator || getHasAdminRight(chat, 'deleteMessages') ); const canReport = !isPrivate && !isOwn; const canDeleteForAll = canDelete && !chat.isForbidden && ( (isPrivate && !isChatWithSelf && !isBotChat && !content.dice) || (isBasicGroup && ( isOwn || getHasAdminRight(chat, 'deleteMessages') || chat.isCreator )) ); const hasMessageEditRight = isOwn || (isChannel && (chat.isCreator || getHasAdminRight(chat, 'editMessages'))); const canEdit = !isLocal && !isAction && isMessageEditable && hasMessageEditRight; 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 = !isLocal && !isAction && (isChannel || isSuperGroup) && !isMonoforum; const canSelect = !isLocal && !isAction; const canDownload = selectMessageDownloadableMedia(global, message) && !hasTtl; const canSaveGif = message.content.video?.isGif; const poll = content.pollId ? selectPoll(global, content.pollId) : undefined; const hasChosenPollAnswer = Boolean( poll && Object.values(poll.results.resultByOption || {}).some((result) => result.isChosen), ); const canRevote = poll && !poll.summary.isClosed && !poll.summary.isRevoteDisabled && hasChosenPollAnswer; const canClosePoll = hasMessageEditRight && poll && !poll.summary.isClosed && !isForwarded; const noOptions = [ canReply, canReplyGlobally, canEdit, canPin, canUnpin, canReport, canDelete, canDeleteForAll, canFaveSticker, canUnfaveSticker, canCopy, canCopyLink, canSelect, canDownload, canSaveGif, canRevote, canClosePoll, ].every((ability) => !ability); return { noOptions, canReply, canReplyGlobally, canEdit, canPin, canUnpin, canReport, canDelete, canDeleteForAll, canFaveSticker, canUnfaveSticker, canCopy, canCopyLink, canSelect, canDownload, canSaveGif, canRevote, canClosePoll, }; } export function selectCanCopyMessageLink( global: T, message: ApiMessage, ) { const chat = selectChat(global, message.chatId); if (!chat || selectIsChatRestricted(global, message.chatId)) return false; const isLocal = isMessageLocal(message); const isAction = isActionMessage(message); const isChannel = isChatChannel(chat); const isSuperGroup = isChatSuperGroup(chat); return !isLocal && !isAction && (isChannel || isSuperGroup) && !chat.isMonoforum; } export function selectCanDeleteMessages( global: T, chatId: string, threadId: ThreadId, messageIds: number[], ) { const chatMessages = selectChatMessages(global, chatId); if (messageIds.length > API_GENERAL_ID_LIMIT) { return {}; } const messageActions = messageIds .map((id) => chatMessages[id] && selectAllowedMessageActionsSlow(global, chatMessages[id], threadId)) .filter(Boolean); return { canDelete: messageActions.every((actions) => actions.canDelete), canDeleteForAll: messageActions.every((actions) => actions.canDeleteForAll), }; } export function selectCanDeleteSelectedMessages( global: T, messageIds?: number[], ...[tabId = getCurrentTabId()]: TabArgs ) { const { messageIds: selectedMessageIds } = selectTabState(global, tabId).selectedMessages || {}; const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {}; const messageIdList = messageIds?.length ? messageIds : selectedMessageIds; if (!chatId || !threadId || !messageIdList) { return {}; } return selectCanDeleteMessages(global, chatId, threadId, messageIdList); } 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] && selectAllowedMessageActionsSlow(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] && selectAllowedMessageActionsSlow(global, chatMessages[id], threadId)) .filter(Boolean); return messageActions.some((actions) => actions.canDownload); } export function selectActiveDownloads( global: T, ...[tabId = getCurrentTabId()]: TabArgs ) { return selectTabState(global, tabId).activeDownloads; } export function selectUploadProgress(global: T, message: ApiMessage) { return global.fileUploads.byMessageKey[getMessageKey(message)]?.progress; } export function selectRealLastReadId(global: T, chatId: string, threadId: ThreadId) { const readState = selectThreadReadState(global, chatId, threadId); if (!readState) { return undefined; } // `lastReadInboxMessageId` is empty for new chats if (!readState.lastReadInboxMessageId) { // For new comments, mark thread start as the last read if (threadId !== MAIN_THREAD_ID && readState.unreadCount && typeof threadId === 'number') { return threadId; } return undefined; } const lastMessageId = selectChatLastMessageId(global, chatId); if (!lastMessageId || readState.unreadCount) { return readState.lastReadInboxMessageId; } return lastMessageId; } export function selectFirstUnreadId( global: T, chatId: string, threadId: ThreadId, ) { const chat = selectChat(global, chatId); if (threadId === MAIN_THREAD_ID) { if (!chat) { return undefined; } } else { const threadInfo = selectThreadInfo(global, chatId, threadId); const readState = selectThreadReadState(global, chatId, threadId); if (!threadInfo || !readState || (threadInfo.lastMessageId !== undefined && threadInfo.lastMessageId === readState.lastReadInboxMessageId)) { return undefined; } } const outlyingLists = selectOutlyingLists(global, chatId, threadId); const listedIds = selectListedIds(global, chatId, threadId); const byId = selectChatMessages(global, chatId); if (!byId || !(outlyingLists?.length || 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 (outlyingLists?.length) { const found = outlyingLists.map((list) => findAfterLastReadId(list)).filter(Boolean)[0]; if (found) { return found; } } if (listedIds) { const found = findAfterLastReadId(listedIds); if (found) { return found; } } return undefined; } export function selectIsForwardModalOpen( global: T, ...[tabId = getCurrentTabId()]: TabArgs ) { const { isShareMessageModalShown } = selectTabState(global, tabId); return Boolean(isShareMessageModalShown); } export function selectCommonBoxChatId(global: T, messageId: number) { const fromLastMessage = Object.values(global.chats.byId).find((chat) => ( isCommonBoxChat(chat) && selectChatLastMessageId(global, chat.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: 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) => { const message = chatMessages[id]; return message && selectShouldDisplayReplyKeyboard(global, message); }); const replyHideMessageId = findLast(viewportIds, (id) => { const message = chatMessages[id]; return message && selectShouldHideReplyKeyboard(global, message); }); if (messageId && replyHideMessageId && replyHideMessageId > messageId) { return undefined; } return messageId ? chatMessages[messageId] : undefined; } function selectShouldHideReplyKeyboard(global: T, message: ApiMessage) { const { shouldHideKeyboardButtons, isHideKeyboardSelective, isMentioned, } = message; if (!shouldHideKeyboardButtons) return false; const replyToMessageId = getMessageReplyInfo(message)?.replyToMsgId; 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, } = message; if (!keyboardButtons || shouldHideKeyboardButtons) return false; const replyToMessageId = getMessageReplyInfo(message)?.replyToMsgId; 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 | ApiSponsoredMessage, ) { const chat = selectChat(global, message.chatId); if (!chat) { return undefined; } const sender = 'id' in message ? selectSender(global, message) : undefined; const webPage = selectWebPageFromMessage(global, message); const isPhoto = Boolean(getMessagePhoto(message) || getWebPagePhoto(webPage)); const isVideo = Boolean(getMessageVideo(message) || getWebPageVideo(webPage)); 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?: ApiPeer; }) { const isMediaFromContact = Boolean(sender && ( selectIsChatWithSelf(global, sender.id) || selectUser(global, sender.id)?.isContact )); return Boolean( (isMediaFromContact && canAutoLoadMediaFromContacts) || (!isMediaFromContact && canAutoLoadMediaInPrivateChats && isUserId(chat.id)) || (canAutoLoadMediaInGroups && isChatGroup(chat)) || (canAutoLoadMediaInChannels && isChatChannel(chat)), ); } 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) || hasMessageTtl(message) || getMessagePaidMedia(message) )); } export function selectIsChatProtected(global: T, chatId: string) { const chat = selectChat(global, chatId); if (!chat) return false; if (chat.isProtected || (isUserId(chatId) && selectIsUserChatProtected(global, chatId))) { return true; } return false; } export function selectHasProtectedMessage(global: T, chatId: string, messageIds?: number[]) { if (selectIsChatProtected(global, chatId)) { 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 (selectIsChatProtected(global, chatId)) { return false; } if (!messageIds) { return false; } const messages = selectChatMessages(global, chatId); return messageIds .map((id) => messages[id]) .every((message) => message && !hasMessageTtl(message) && (message.isForwardingAllowed || isServiceNotificationMessage(message))); } export function selectHasIpRevealingMedia(global: T, chatId: string, messageIds: number[]) { const messages = selectChatMessages(global, chatId); return messageIds .map((id) => messages[id]) .some((message) => { if (!message) return false; const document = getMessageDocument(message); if (!document) return false; const extension = getDocumentExtension(document); if (!extension) return false; return isIpRevealingMedia({ mimeType: document.mimeType, extension }); }); } export function selectSponsoredMessage(global: T, chatId: string) { const message = global.messages.sponsoredByChatId[chatId]; 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 = selectChatFullInfo(global, chatId)?.enabledReactions; if (!chatReactions || !canSendReaction(defaultReaction, chatReactions)) { return undefined; } return defaultReaction; } export function selectMaxUserReactions(global: T): number { return selectCurrentLimit(global, 'maxReactions'); } // 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 selectCanSchedule( global: T, ...[tabId = getCurrentTabId()]: TabArgs ) { const chatId = selectCurrentMessageList(global, tabId)?.chatId; const paidMessagesStars = chatId ? selectPeerPaidMessagesStars(global, chatId) : undefined; return !paidMessagesStars; } export function selectCanScheduleUntilOnline(global: T, id: string) { const isChatWithSelf = selectIsChatWithSelf(global, id); const chatBot = selectBot(global, id); const paidMessagesStars = selectPeerPaidMessagesStars(global, id); return Boolean(!paidMessagesStars && !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) => selectCustomEmoji(global, 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); return 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[]); } 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) || Boolean(message.content.video?.isRound); }); } export function selectMessageSummary( global: T, chatId: string, messageId: number, toLanguageCode?: string, ): TextSummary | undefined { const message = selectChatMessage(global, chatId, messageId); if (!message?.summaryLanguageCode) return undefined; if (toLanguageCode && toLanguageCode !== message.summaryLanguageCode) { const messageTranslations = selectMessageTranslations(global, chatId, toLanguageCode); return messageTranslations[messageId]?.summary; } return global.messages.byChatId[chatId].summaryById[messageId]; } export function selectChatTranslations( global: T, chatId: string, ): ChatTranslatedMessages { return global.translations.byChatId[chatId]; } export function selectMessageTranslations( global: T, chatId: string, cacheKey: string, ) { return selectChatTranslations(global, chatId)?.byLangCode[cacheKey] || {}; } export function selectRequestedMessageTranslationLanguage( global: T, chatId: string, messageId: number, ...[tabId = getCurrentTabId()]: TabArgs ): string | undefined { const requestedInChat = selectTabState(global, tabId).requestedTranslations.byChatId[chatId]; return requestedInChat?.toLanguage || requestedInChat?.manualMessages?.[messageId]; } export function selectRequestedMessageTranslationTone( global: T, chatId: string, messageId: number, ...[tabId = getCurrentTabId()]: TabArgs ): TranslationTone | undefined { const requestedInChat = selectTabState(global, tabId).requestedTranslations.byChatId[chatId]; if (requestedInChat?.toLanguage) { return requestedInChat.tone || 'neutral'; } const cacheKey = requestedInChat?.manualMessages?.[messageId]; if (!cacheKey) return undefined; const { tone } = parseTranslationCacheKey(cacheKey); return tone; } export function selectReplyCanBeSentToChat( global: T, toChatId: string, fromChatId: string, replyInfo: ApiInputMessageReplyInfo, ) { if (!replyInfo.replyToMsgId) return false; const fromRealChatId = replyInfo?.replyToPeerId ?? fromChatId; if (toChatId === fromRealChatId) return true; const chatMessages = selectChatMessages(global, fromRealChatId); const message = chatMessages[replyInfo.replyToMsgId]; return !isExpiredMessage(message); } export function selectForwardsCanBeSentToChat( global: T, toChatId: string, ...[tabId = getCurrentTabId()]: TabArgs ) { const { messageIds, storyId, fromChatId } = selectTabState(global, tabId).forwardMessages; const chat = selectChat(global, toChatId); if ((!messageIds && !storyId) || !chat) return false; if (storyId) { return true; } const chatFullInfo = selectChatFullInfo(global, toChatId); const chatMessages = selectChatMessages(global, fromChatId!); const isSavedMessages = toChatId ? selectIsChatWithSelf(global, toChatId) : undefined; const isChatWithBot = toChatId ? selectIsChatWithBot(global, toChatId) : undefined; const options = getAllowedAttachmentOptions(chat, chatFullInfo, isChatWithBot, isSavedMessages); return !messageIds!.some((messageId) => сheckMessageSendingDenied(chatMessages[messageId], options)); } function сheckMessageSendingDenied(message: ApiMessage, options: IAllowedAttachmentOptions) { const isVoice = message.content.voice; const isRoundVideo = message.content.video?.isRound; const isPhoto = message.content.photo; const isGif = message.content.video?.isGif; const isVideo = message.content.video && !isRoundVideo && !isGif; const isAudio = message.content.audio; const isDocument = message.content.document; const isSticker = message.content.sticker; const isPlainText = message.content.text && !isVoice && !isRoundVideo && !isSticker && !isDocument && !isAudio && !isVideo && !isPhoto && !isGif; return (isVoice && !options.canSendVoices) || (isRoundVideo && !options.canSendRoundVideos) || (isSticker && !options.canSendStickers) || (isDocument && !options.canSendDocuments) || (isAudio && !options.canSendAudios) || (isVideo && !options.canSendVideos) || (isPhoto && !options.canSendPhotos) || (isGif && !options.canSendGifs) || (isPlainText && !options.canSendPlainText); } export function selectCanTranslateMessage( global: T, message: ApiMessage, detectedLanguage?: string, ...[tabId = getCurrentTabId()]: TabArgs ) { const { canTranslate: isTranslationEnabled, doNotTranslate } = global.settings.byKey; const canTranslateLanguage = !detectedLanguage || !doNotTranslate.includes(detectedLanguage); const isTranslatable = isMessageTranslatable(message); // Separate translations are disabled when chat translation enabled const chatRequestedLanguage = selectRequestedChatTranslationLanguage(global, message.chatId, tabId); return IS_TRANSLATION_SUPPORTED && isTranslationEnabled && canTranslateLanguage && isTranslatable && !chatRequestedLanguage; } export function selectTopicLink( global: T, chatId: string, topicId?: ThreadId, ) { const chat = selectChat(global, chatId); if (!chat || !chat.isForum || chat.isBotForum) { return undefined; } return getMessageLink(chat, topicId); } export function selectMessageReplyInfo( global: T, chatId: string, threadId: ThreadId, additionalReplyInfo?: ApiInputMessageReplyInfo, ) { const chat = selectChat(global, chatId); if (!chat) return undefined; return prepareMessageReplyInfo(threadId, additionalReplyInfo); } export function selectReplyMessage(global: T, message: ApiMessage) { const { replyToMsgId, replyToPeerId } = getMessageReplyInfo(message) || {}; const replyMessage = replyToMsgId ? selectChatMessage(global, replyToPeerId || message.chatId, replyToMsgId) : undefined; return replyMessage; } export function selectActiveRestrictionReasons( global: T, restrictionReasons?: ApiRestrictionReason[], ): ApiRestrictionReason[] { if (!restrictionReasons) return []; const { ignoreRestrictionReasons } = global.appConfig; return restrictionReasons.filter((reason) => { const isForCurrentPlatform = reason.platform === 'all' || reason.platform === WEB_APP_PLATFORM; if (!isForCurrentPlatform) return false; const shouldIgnore = ignoreRestrictionReasons?.includes(reason.reason); return !shouldIgnore; }); }