1514 lines
49 KiB
TypeScript
1514 lines
49 KiB
TypeScript
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<T extends GlobalState>(
|
||
global: T,
|
||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
const { messageLists } = selectTabState(global, tabId);
|
||
|
||
if (messageLists.length) {
|
||
return messageLists[messageLists.length - 1];
|
||
}
|
||
|
||
return undefined;
|
||
}
|
||
|
||
export function selectCurrentChat<T extends GlobalState>(
|
||
global: T,
|
||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
const { chatId } = selectCurrentMessageList(global, tabId) || {};
|
||
|
||
return chatId ? selectChat(global, chatId) : undefined;
|
||
}
|
||
|
||
export function selectChatMessages<T extends GlobalState>(global: T, chatId: string) {
|
||
return global.messages.byChatId[chatId]?.byId;
|
||
}
|
||
|
||
export function selectChatScheduledMessages<T extends GlobalState>(global: T, chatId: string) {
|
||
return global.scheduledMessages.byChatId[chatId]?.byId;
|
||
}
|
||
|
||
export function selectListedIds<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||
return selectThreadLocalStateParam(global, chatId, threadId, 'listedIds');
|
||
}
|
||
|
||
export function selectOutlyingListByMessageId<T extends GlobalState>(
|
||
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<T extends GlobalState>(
|
||
global: T, chatId: string, threadId: ThreadId,
|
||
) {
|
||
return selectThreadLocalStateParam(global, chatId, threadId, 'outlyingLists');
|
||
}
|
||
|
||
export function selectCurrentMessageIds<T extends GlobalState>(
|
||
global: T,
|
||
chatId: string, threadId: ThreadId, messageListType: MessageListType,
|
||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
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<T extends GlobalState>(
|
||
global: T, chatId: string, threadId: ThreadId, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
return selectTabThreadParam(global, chatId, threadId, 'viewportIds', tabId);
|
||
}
|
||
|
||
export function selectPinnedIds<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||
return selectThreadLocalStateParam(global, chatId, threadId, 'pinnedIds');
|
||
}
|
||
|
||
export function selectScheduledIds<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||
return selectThreadLocalStateParam(global, chatId, threadId, 'scheduledIds');
|
||
}
|
||
|
||
export function selectFirstMessageId<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||
return selectThreadLocalStateParam(global, chatId, threadId, 'firstMessageId');
|
||
}
|
||
|
||
export function selectIsMessageInCurrentMessageList<T extends GlobalState>(
|
||
global: T, chatId: string, message: ApiMessage,
|
||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
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<T extends GlobalState>(
|
||
global: T, chatId: string, threadId: ThreadId,
|
||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
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<T extends GlobalState>(global: T, chatId: string, messageId: number) {
|
||
const chatMessages = selectChatMessages(global, chatId);
|
||
|
||
return chatMessages ? chatMessages[messageId] : undefined;
|
||
}
|
||
|
||
export function selectScheduledMessage<T extends GlobalState>(global: T, chatId: string, messageId: number) {
|
||
const chatMessages = selectChatScheduledMessages(global, chatId);
|
||
|
||
return chatMessages ? chatMessages[messageId] : undefined;
|
||
}
|
||
|
||
export function selectQuickReplyMessage<T extends GlobalState>(global: T, messageId: number) {
|
||
return global.quickReplies.messagesById[messageId];
|
||
}
|
||
|
||
export function selectEditingMessage<T extends GlobalState>(
|
||
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<T extends GlobalState>(
|
||
global: T, chatId: string, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
const { chatId: focusedChatId, messageId } = selectTabState(global, tabId).focusedMessage || {};
|
||
|
||
return focusedChatId === chatId ? messageId : undefined;
|
||
}
|
||
|
||
export function selectIsMessageFocused<T extends GlobalState>(
|
||
global: T, message: ApiMessage, currentThreadId: ThreadId,
|
||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
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<T extends GlobalState>(
|
||
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<T extends GlobalState>(
|
||
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<T extends GlobalState>(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<T extends GlobalState>(
|
||
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<T extends GlobalState>(
|
||
global: T,
|
||
chatId: string,
|
||
messageId: number,
|
||
): ApiPeer | undefined {
|
||
const message = selectChatMessage(global, chatId, messageId);
|
||
|
||
return message && selectSender(global, message);
|
||
}
|
||
|
||
export function selectSenderFromHeader<T extends GlobalState>(
|
||
global: T,
|
||
header: ApiMessageForwardInfo,
|
||
) {
|
||
const { fromId } = header;
|
||
if (fromId) {
|
||
return selectPeer(global, fromId);
|
||
}
|
||
|
||
return undefined;
|
||
}
|
||
|
||
export function selectForwardedSender<T extends GlobalState>(
|
||
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<T extends GlobalState>(global: T, pollId: string) {
|
||
return global.messages.pollById[pollId];
|
||
}
|
||
|
||
export function selectPollFromMessage<T extends GlobalState>(global: T, message: MediaContainer) {
|
||
if (!message.content.pollId) return undefined;
|
||
return selectPoll(global, message.content.pollId);
|
||
}
|
||
|
||
export function selectWebPage<T extends GlobalState>(global: T, webPageId: string) {
|
||
return global.messages.webPageById[webPageId];
|
||
}
|
||
|
||
export function selectWebPageFromMessage<T extends GlobalState>(global: T, message: MediaContainer) {
|
||
if (!message.content.webPage) return undefined;
|
||
return selectWebPage(global, message.content.webPage.id);
|
||
}
|
||
|
||
export function selectFullWebPage<T extends GlobalState>(global: T, webPageId: string) {
|
||
const webPage = selectWebPage(global, webPageId);
|
||
if (!webPage || webPage.webpageType !== 'full') return undefined;
|
||
return webPage;
|
||
}
|
||
|
||
export function selectFullWebPageFromMessage<T extends GlobalState>(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<T extends GlobalState>(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<T extends GlobalState>(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<T extends GlobalState>(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<T extends GlobalState>(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<T extends GlobalState>(
|
||
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<T extends GlobalState>(
|
||
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<T extends GlobalState>(
|
||
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<T extends GlobalState>(
|
||
global: T,
|
||
messageIds?: number[],
|
||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
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<T extends GlobalState>(
|
||
global: T,
|
||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
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<T extends GlobalState>(
|
||
global: T,
|
||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
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<T extends GlobalState>(
|
||
global: T,
|
||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
return selectTabState(global, tabId).activeDownloads;
|
||
}
|
||
|
||
export function selectUploadProgress<T extends GlobalState>(global: T, message: ApiMessage) {
|
||
return global.fileUploads.byMessageKey[getMessageKey(message)]?.progress;
|
||
}
|
||
|
||
export function selectRealLastReadId<T extends GlobalState>(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<T extends GlobalState>(
|
||
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<T extends GlobalState>(
|
||
global: T,
|
||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
const { isShareMessageModalShown } = selectTabState(global, tabId);
|
||
return Boolean(isShareMessageModalShown);
|
||
}
|
||
|
||
export function selectCommonBoxChatId<T extends GlobalState>(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<T extends GlobalState>(
|
||
global: T,
|
||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
const { selectedMessages } = selectTabState(global, tabId);
|
||
|
||
return Boolean(selectedMessages);
|
||
}
|
||
|
||
export function selectIsMessageSelected<T extends GlobalState>(
|
||
global: T, messageId: number,
|
||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
const { messageIds } = selectTabState(global, tabId).selectedMessages || {};
|
||
if (!messageIds) {
|
||
return false;
|
||
}
|
||
|
||
return messageIds.includes(messageId);
|
||
}
|
||
|
||
export function selectForwardedMessageIdsByGroupId<T extends GlobalState>(
|
||
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<T extends GlobalState>(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<T extends GlobalState>(
|
||
global: T, chatId: string, groupedId: string,
|
||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
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<T extends GlobalState>(
|
||
global: T,
|
||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
const { messageIds } = selectTabState(global, tabId).selectedMessages || {};
|
||
|
||
return messageIds ? messageIds.length : 0;
|
||
}
|
||
|
||
export function selectNewestMessageWithBotKeyboardButtons<T extends GlobalState>(
|
||
global: T, chatId: string, threadId: ThreadId = MAIN_THREAD_ID,
|
||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
): 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<T extends GlobalState>(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<T extends GlobalState>(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<T extends GlobalState>(
|
||
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<T extends GlobalState>({
|
||
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<T extends GlobalState>(global: T) {
|
||
const { serviceNotifications } = global;
|
||
const maxId = Math.max(...serviceNotifications.map(({ id }) => id));
|
||
|
||
return serviceNotifications.find(({ id, isDeleted }) => !isDeleted && id === maxId);
|
||
}
|
||
|
||
export function selectIsMessageProtected<T extends GlobalState>(global: T, message?: ApiMessage) {
|
||
return Boolean(message && (
|
||
message.isProtected || selectIsChatProtected(global, message.chatId) || hasMessageTtl(message)
|
||
|| getMessagePaidMedia(message)
|
||
));
|
||
}
|
||
|
||
export function selectIsChatProtected<T extends GlobalState>(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<T extends GlobalState>(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<T extends GlobalState>(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<T extends GlobalState>(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<T extends GlobalState>(global: T, chatId: string) {
|
||
const message = global.messages.sponsoredByChatId[chatId];
|
||
|
||
return message && message.expiresAt >= Math.round(Date.now() / 1000) ? message : undefined;
|
||
}
|
||
|
||
export function selectDefaultReaction<T extends GlobalState>(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<T extends GlobalState>(global: T): number {
|
||
return selectCurrentLimit(global, 'maxReactions');
|
||
}
|
||
|
||
// Slow, not to be used in `withGlobal`
|
||
export function selectVisibleUsers<T extends GlobalState>(
|
||
global: T,
|
||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
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<T extends GlobalState>(
|
||
global: T,
|
||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
return selectCurrentMessageList(global, tabId)?.type === 'scheduled';
|
||
}
|
||
|
||
export function selectCanSchedule<T extends GlobalState>(
|
||
global: T,
|
||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
const chatId = selectCurrentMessageList(global, tabId)?.chatId;
|
||
const paidMessagesStars = chatId ? selectPeerPaidMessagesStars(global, chatId) : undefined;
|
||
return !paidMessagesStars;
|
||
}
|
||
|
||
export function selectCanScheduleUntilOnline<T extends GlobalState>(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<T extends GlobalState>(
|
||
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<T extends GlobalState>(
|
||
global: T,
|
||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
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<T extends GlobalState>(
|
||
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<T extends GlobalState>(
|
||
global: T, chatId: string,
|
||
): ChatTranslatedMessages {
|
||
return global.translations.byChatId[chatId];
|
||
}
|
||
|
||
export function selectMessageTranslations<T extends GlobalState>(
|
||
global: T, chatId: string, cacheKey: string,
|
||
) {
|
||
return selectChatTranslations(global, chatId)?.byLangCode[cacheKey] || {};
|
||
}
|
||
|
||
export function selectRequestedMessageTranslationLanguage<T extends GlobalState>(
|
||
global: T, chatId: string, messageId: number, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
): string | undefined {
|
||
const requestedInChat = selectTabState(global, tabId).requestedTranslations.byChatId[chatId];
|
||
return requestedInChat?.toLanguage || requestedInChat?.manualMessages?.[messageId];
|
||
}
|
||
|
||
export function selectRequestedMessageTranslationTone<T extends GlobalState>(
|
||
global: T, chatId: string, messageId: number, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
): 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<T extends GlobalState>(
|
||
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<T extends GlobalState>(
|
||
global: T,
|
||
toChatId: string,
|
||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
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<T extends GlobalState>(
|
||
global: T, message: ApiMessage, detectedLanguage?: string, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||
) {
|
||
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<T extends GlobalState>(
|
||
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<T extends GlobalState>(
|
||
global: T, chatId: string, threadId: ThreadId, additionalReplyInfo?: ApiInputMessageReplyInfo,
|
||
) {
|
||
const chat = selectChat(global, chatId);
|
||
if (!chat) return undefined;
|
||
|
||
return prepareMessageReplyInfo(threadId, additionalReplyInfo);
|
||
}
|
||
|
||
export function selectReplyMessage<T extends GlobalState>(global: T, message: ApiMessage) {
|
||
const { replyToMsgId, replyToPeerId } = getMessageReplyInfo(message) || {};
|
||
const replyMessage = replyToMsgId
|
||
? selectChatMessage(global, replyToPeerId || message.chatId, replyToMsgId) : undefined;
|
||
|
||
return replyMessage;
|
||
}
|
||
|
||
export function selectActiveRestrictionReasons<T extends GlobalState>(
|
||
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;
|
||
});
|
||
}
|