diff --git a/src/api/gramjs/apiBuilders/misc.ts b/src/api/gramjs/apiBuilders/misc.ts index f727e8647..16c93c815 100644 --- a/src/api/gramjs/apiBuilders/misc.ts +++ b/src/api/gramjs/apiBuilders/misc.ts @@ -228,14 +228,21 @@ export function buildApiUrlAuthResult(result: GramJs.TypeUrlAuthResult): ApiUrlA } export function buildApiConfig(config: GramJs.Config): ApiConfig { - const defaultReaction = config.reactionsDefault && buildApiReaction(config.reactionsDefault); + const { + testMode, expires, gifSearchUsername, chatSizeMax, autologinToken, reactionsDefault, + messageLengthMax, editTimeLimit, forwardedCountMax, + } = config; + const defaultReaction = reactionsDefault && buildApiReaction(reactionsDefault); return { - isTestServer: config.testMode, - expiresAt: config.expires, - gifSearchUsername: config.gifSearchUsername, + isTestServer: testMode, + expiresAt: expires, + gifSearchUsername, defaultReaction, - maxGroupSize: config.chatSizeMax, - autologinToken: config.autologinToken, + maxGroupSize: chatSizeMax, + autologinToken, + maxMessageLength: messageLengthMax, + editTimeLimit, + maxForwardedCount: forwardedCountMax, }; } diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 07501629f..9a647f5b0 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -1963,7 +1963,10 @@ function handleMultipleLocalMessagesUpdate( return true; }); - handleGramJsUpdate(otherUpdates); + // Illegal monkey patching. Easier than creating mock update object + update.updates = otherUpdates; + + handleGramJsUpdate(update); } function handleLocalMessageUpdate( diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 5deb1fe12..89894b82a 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -249,6 +249,9 @@ export interface ApiConfig { maxGroupSize: number; autologinToken?: string; isTestServer?: boolean; + maxMessageLength: number; + editTimeLimit: number; + maxForwardedCount: number; } export type ApiPeerColorSet = string[]; diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx index 2f2a0258e..aa7e3fea3 100644 --- a/src/components/common/Composer.tsx +++ b/src/components/common/Composer.tsx @@ -44,6 +44,7 @@ import { MAIN_THREAD_ID } from '../../api/types'; import { BASE_EMOJI_KEYWORD_LANG, + DEFAULT_MAX_MESSAGE_LENGTH, EDITABLE_INPUT_MODAL_ID, HEART_REACTION, MAX_UPLOAD_FILEPART_SIZE, @@ -270,6 +271,7 @@ type StateProps = areEffectsSupported?: boolean; canPlayEffect?: boolean; shouldPlayEffect?: boolean; + maxMessageLength: number; }; enum MainButtonState { @@ -291,7 +293,6 @@ const SCREEN_WIDTH_TO_HIDE_PLACEHOLDER = 600; // px const MOBILE_KEYBOARD_HIDE_DELAY_MS = 100; const SELECT_MODE_TRANSITION_MS = 200; -const MESSAGE_MAX_LENGTH = 4096; const SENDING_ANIMATION_DURATION = 350; const MOUNT_ANIMATION_DURATION = 430; @@ -384,6 +385,7 @@ const Composer: FC = ({ areEffectsSupported, canPlayEffect, shouldPlayEffect, + maxMessageLength, }) => { const { sendMessage, @@ -871,7 +873,7 @@ const Composer: FC = ({ }); const validateTextLength = useLastCallback((text: string, isAttachmentModal?: boolean) => { - const maxLength = isAttachmentModal ? captionLimit : MESSAGE_MAX_LENGTH; + const maxLength = isAttachmentModal ? captionLimit : maxMessageLength; if (text?.length > maxLength) { const extraLength = text.length - maxLength; showDialog({ @@ -2145,6 +2147,8 @@ export default memo(withGlobal( const effect = effectId ? global.availableEffectById[effectId] : undefined; const effectReactions = global.reactions.effectReactions; + const maxMessageLength = global.config?.maxMessageLength || DEFAULT_MAX_MESSAGE_LENGTH; + return { availableReactions: global.reactions.availableReactions, topReactions: type === 'story' ? global.reactions.topReactions : undefined, @@ -2220,6 +2224,7 @@ export default memo(withGlobal( areEffectsSupported, canPlayEffect, shouldPlayEffect, + maxMessageLength, }; }, )(Composer)); diff --git a/src/components/middle/composer/hooks/useClipboardPaste.ts b/src/components/middle/composer/hooks/useClipboardPaste.ts index 2ea6bf400..6b2e1e3c2 100644 --- a/src/components/middle/composer/hooks/useClipboardPaste.ts +++ b/src/components/middle/composer/hooks/useClipboardPaste.ts @@ -16,8 +16,6 @@ import getFilesFromDataTransferItems from '../helpers/getFilesFromDataTransferIt import useOldLang from '../../../../hooks/useOldLang'; -const MAX_MESSAGE_LENGTH = 4096; - const TYPE_HTML = 'text/html'; const DOCUMENT_TYPE_WORD = 'urn:schemas-microsoft-com:office:word'; const NAMESPACE_PREFIX_WORD = 'xmlns:w'; @@ -49,7 +47,7 @@ const useClipboardPaste = ( return; } - const pastedText = e.clipboardData.getData('text').substring(0, MAX_MESSAGE_LENGTH); + const pastedText = e.clipboardData.getData('text'); const html = e.clipboardData.getData('text/html'); let pastedFormattedText = html ? parseHtmlAsFormattedText( diff --git a/src/components/middle/message/ContextMenuContainer.tsx b/src/components/middle/message/ContextMenuContainer.tsx index 2d1135f56..c2d23cea0 100644 --- a/src/components/middle/message/ContextMenuContainer.tsx +++ b/src/components/middle/message/ContextMenuContainer.tsx @@ -44,6 +44,7 @@ import { import { selectActiveDownloads, selectAllowedMessageActionsSlow, + selectCanForwardMessage, selectCanPlayAnimatedEmojis, selectCanScheduleUntilOnline, selectCanTranslateMessage, @@ -732,7 +733,6 @@ export default memo(withGlobal( canDelete, canReport, canEdit, - canForward, canFaveSticker, canUnfaveSticker, canCopy, @@ -743,6 +743,7 @@ export default memo(withGlobal( canRevote, canClosePoll, } = (threadId && selectAllowedMessageActionsSlow(global, message, threadId)) || {}; + const canForward = selectCanForwardMessage(global, message); const userStatus = isPrivate ? selectUserStatus(global, chat.id) : undefined; const isOwn = isOwnMessage(message); diff --git a/src/config.ts b/src/config.ts index 089d9b52e..7571003ca 100644 --- a/src/config.ts +++ b/src/config.ts @@ -364,7 +364,7 @@ export const DEFAULT_LIMITS: Record = { dialogFiltersChats: [100, 200], dialogFilters: [10, 20], dialogFolderPinned: [5, 10], - captionLength: [1024, 2048], + captionLength: [1024, 4096], channels: [500, 1000], channelsPublic: [10, 20], aboutLength: [70, 140], @@ -373,6 +373,7 @@ export const DEFAULT_LIMITS: Record = { recommendedChannels: [10, 100], savedDialogsPinned: [5, 100], }; +export const DEFAULT_MAX_MESSAGE_LENGTH = 4096; export const ONE_TIME_MEDIA_TTL_SECONDS = 2147483647; diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 6397bccf9..7f81c91f1 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -62,6 +62,7 @@ import { isMessageLocal, isServiceNotificationMessage, isUserBot, + splitMessagesForForwarding, } from '../../helpers'; import { isApiPeerUser } from '../../helpers/peers'; import { @@ -99,6 +100,7 @@ import { } from '../../reducers'; import { updateTabState } from '../../reducers/tabs'; import { + selectCanForwardMessage, selectChat, selectChatFullInfo, selectChatLastMessageId, @@ -1146,23 +1148,29 @@ addActionHandler('forwardMessages', (global, actions, payload): ActionReturnType const lastMessageId = selectChatLastMessageId(global, toChat.id); const [realMessages, serviceMessages] = partition(messages, (m) => !isServiceNotificationMessage(m)); - if (realMessages.length) { + const forwardableRealMessages = realMessages.filter((message) => selectCanForwardMessage(global, message)); + if (forwardableRealMessages.length) { + const messageBatches = global.config?.maxForwardedCount + ? splitMessagesForForwarding(forwardableRealMessages, global.config.maxForwardedCount) + : [forwardableRealMessages]; (async () => { await rafPromise(); // Wait one frame for any previous `sendMessage` to be processed - callApi('forwardMessages', { - fromChat, - toChat, - toThreadId, - messages: realMessages, - isSilent, - scheduledAt, - sendAs, - withMyScore, - noAuthors, - noCaptions, - isCurrentUserPremium, - wasDrafted: Boolean(draft), - lastMessageId, + messageBatches.forEach((batch) => { + callApi('forwardMessages', { + fromChat, + toChat, + toThreadId, + messages: batch, + isSilent, + scheduledAt, + sendAs, + withMyScore, + noAuthors, + noCaptions, + isCurrentUserPremium, + wasDrafted: Boolean(draft), + lastMessageId, + }); }); })(); } diff --git a/src/global/actions/ui/messages.ts b/src/global/actions/ui/messages.ts index 6c9d84b00..454872a48 100644 --- a/src/global/actions/ui/messages.ts +++ b/src/global/actions/ui/messages.ts @@ -50,8 +50,10 @@ import { import { updateTabState } from '../../reducers/tabs'; import { selectAllowedMessageActionsSlow, + selectCanForwardMessage, selectChat, selectChatLastMessageId, + selectChatMessage, selectChatMessages, selectChatScheduledMessages, selectCurrentChat, @@ -610,7 +612,16 @@ addActionHandler('openForwardMenuForSelectedMessages', (global, actions, payload const { chatId: fromChatId, messageIds } = tabState.selectedMessages; - actions.openForwardMenu({ fromChatId, messageIds, tabId }); + const forwardableMessageIds = messageIds.filter((id) => { + const message = selectChatMessage(global, fromChatId, id); + return message && selectCanForwardMessage(global, message); + }); + + if (!forwardableMessageIds.length) { + return; + } + + actions.openForwardMenu({ fromChatId, messageIds: forwardableMessageIds, tabId }); }); addActionHandler('cancelMediaDownload', (global, actions, payload): ActionReturnType => { diff --git a/src/global/helpers/messages.ts b/src/global/helpers/messages.ts index b1e8417b9..07989cdbc 100644 --- a/src/global/helpers/messages.ts +++ b/src/global/helpers/messages.ts @@ -408,3 +408,34 @@ export function getMessageLink(peer: ApiPeer, topicId?: ThreadId, messageId?: nu const messagePart = messageId ? `/${messageId}` : ''; return `${TME_LINK_PREFIX}${chatPart}${topicPart}${messagePart}`; } + +export function splitMessagesForForwarding(messages: ApiMessage[], limit: number): ApiMessage[][] { + const result: ApiMessage[][] = []; + let currentArr: ApiMessage[] = []; + + // Group messages by `groupedId` + messages.reduce((acc, message) => { + const lastGroup = acc[acc.length - 1]; + if (message.groupedId && lastGroup?.[0]?.groupedId === message.groupedId) { + lastGroup.push(message); + return acc; + } + + acc.push([message]); + return acc; + }, []).forEach((batch) => { + // Fit them into `limit` size + if (currentArr.length + batch.length > limit) { + result.push(currentArr); + currentArr = []; + } + + currentArr.push(...batch); + }); + + if (currentArr.length) { + result.push(currentArr); + } + + return result; +} diff --git a/src/global/selectors/messages.ts b/src/global/selectors/messages.ts index d00b92aa5..b057c2e82 100644 --- a/src/global/selectors/messages.ts +++ b/src/global/selectors/messages.ts @@ -22,7 +22,7 @@ import type { import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types'; import { - ANONYMOUS_USER_ID, GENERAL_TOPIC_ID, SERVICE_NOTIFICATIONS_USER_ID, + ANONYMOUS_USER_ID, API_GENERAL_ID_LIMIT, GENERAL_TOPIC_ID, SERVICE_NOTIFICATIONS_USER_ID, } from '../../config'; import { getCurrentTabId } from '../../util/establishMultitabRole'; import { findLast } from '../../util/iteratees'; @@ -81,8 +81,6 @@ import { selectBot, selectIsCurrentUserPremium, selectUser, selectUserStatus, } from './users'; -const MESSAGE_EDIT_ALLOWED_TIME = 172800; // 48 hours - export function selectCurrentMessageList( global: T, ...[tabId = getCurrentTabId()]: TabArgs @@ -607,6 +605,28 @@ export function selectCanReplyToMessage(global: T, messag 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 story = content.storyData + ? selectPeerStory(global, content.storyData.peerId, content.storyData.id) + : (content.webPage?.story + ? selectPeerStory(global, content.webPage.story.peerId, content.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, @@ -651,7 +671,7 @@ export function selectAllowedMessageActionsSlow( const isMessageEditable = ( ( canEditMessagesIndefinitely - || getServerTime() - message.date < MESSAGE_EDIT_ALLOWED_TIME + || 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 @@ -702,20 +722,6 @@ export function selectAllowedMessageActionsSlow( const canEdit = !isLocal && !isAction && isMessageEditable && hasMessageEditRight; - const story = content.storyData - ? selectPeerStory(global, content.storyData.peerId, content.storyData.id) - : (content.webPage?.story - ? selectPeerStory(global, content.webPage.story.peerId, content.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 - ); - const hasSticker = Boolean(message.content.sticker); const hasFavoriteSticker = hasSticker && selectIsStickerFavorite(global, message.content.sticker!); const canFaveSticker = !isAction && hasSticker && !hasFavoriteSticker; @@ -743,7 +749,6 @@ export function selectAllowedMessageActionsSlow( canReport, canDelete, canDeleteForAll, - canForward, canFaveSticker, canUnfaveSticker, canCopy, @@ -765,7 +770,6 @@ export function selectAllowedMessageActionsSlow( canReport, canDelete, canDeleteForAll, - canForward, canFaveSticker, canUnfaveSticker, canCopy, @@ -790,6 +794,8 @@ export function selectCanDeleteSelectedMessages( return {}; } + if (selectedMessageIds.length > API_GENERAL_ID_LIMIT) return {}; + const messageActions = selectedMessageIds .map((id) => chatMessages[id] && selectAllowedMessageActionsSlow(global, chatMessages[id], threadId)) .filter(Boolean);