Messages: Handle 100+ messages forward (#5329)

This commit is contained in:
zubiden 2024-12-29 11:58:51 +01:00 committed by Alexander Zinchuk
parent ed761133bc
commit 5e4924189a
11 changed files with 124 additions and 50 deletions

View File

@ -228,14 +228,21 @@ export function buildApiUrlAuthResult(result: GramJs.TypeUrlAuthResult): ApiUrlA
} }
export function buildApiConfig(config: GramJs.Config): ApiConfig { 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 { return {
isTestServer: config.testMode, isTestServer: testMode,
expiresAt: config.expires, expiresAt: expires,
gifSearchUsername: config.gifSearchUsername, gifSearchUsername,
defaultReaction, defaultReaction,
maxGroupSize: config.chatSizeMax, maxGroupSize: chatSizeMax,
autologinToken: config.autologinToken, autologinToken,
maxMessageLength: messageLengthMax,
editTimeLimit,
maxForwardedCount: forwardedCountMax,
}; };
} }

View File

@ -1963,7 +1963,10 @@ function handleMultipleLocalMessagesUpdate(
return true; return true;
}); });
handleGramJsUpdate(otherUpdates); // Illegal monkey patching. Easier than creating mock update object
update.updates = otherUpdates;
handleGramJsUpdate(update);
} }
function handleLocalMessageUpdate( function handleLocalMessageUpdate(

View File

@ -249,6 +249,9 @@ export interface ApiConfig {
maxGroupSize: number; maxGroupSize: number;
autologinToken?: string; autologinToken?: string;
isTestServer?: boolean; isTestServer?: boolean;
maxMessageLength: number;
editTimeLimit: number;
maxForwardedCount: number;
} }
export type ApiPeerColorSet = string[]; export type ApiPeerColorSet = string[];

View File

@ -44,6 +44,7 @@ import { MAIN_THREAD_ID } from '../../api/types';
import { import {
BASE_EMOJI_KEYWORD_LANG, BASE_EMOJI_KEYWORD_LANG,
DEFAULT_MAX_MESSAGE_LENGTH,
EDITABLE_INPUT_MODAL_ID, EDITABLE_INPUT_MODAL_ID,
HEART_REACTION, HEART_REACTION,
MAX_UPLOAD_FILEPART_SIZE, MAX_UPLOAD_FILEPART_SIZE,
@ -270,6 +271,7 @@ type StateProps =
areEffectsSupported?: boolean; areEffectsSupported?: boolean;
canPlayEffect?: boolean; canPlayEffect?: boolean;
shouldPlayEffect?: boolean; shouldPlayEffect?: boolean;
maxMessageLength: number;
}; };
enum MainButtonState { enum MainButtonState {
@ -291,7 +293,6 @@ const SCREEN_WIDTH_TO_HIDE_PLACEHOLDER = 600; // px
const MOBILE_KEYBOARD_HIDE_DELAY_MS = 100; const MOBILE_KEYBOARD_HIDE_DELAY_MS = 100;
const SELECT_MODE_TRANSITION_MS = 200; const SELECT_MODE_TRANSITION_MS = 200;
const MESSAGE_MAX_LENGTH = 4096;
const SENDING_ANIMATION_DURATION = 350; const SENDING_ANIMATION_DURATION = 350;
const MOUNT_ANIMATION_DURATION = 430; const MOUNT_ANIMATION_DURATION = 430;
@ -384,6 +385,7 @@ const Composer: FC<OwnProps & StateProps> = ({
areEffectsSupported, areEffectsSupported,
canPlayEffect, canPlayEffect,
shouldPlayEffect, shouldPlayEffect,
maxMessageLength,
}) => { }) => {
const { const {
sendMessage, sendMessage,
@ -871,7 +873,7 @@ const Composer: FC<OwnProps & StateProps> = ({
}); });
const validateTextLength = useLastCallback((text: string, isAttachmentModal?: boolean) => { const validateTextLength = useLastCallback((text: string, isAttachmentModal?: boolean) => {
const maxLength = isAttachmentModal ? captionLimit : MESSAGE_MAX_LENGTH; const maxLength = isAttachmentModal ? captionLimit : maxMessageLength;
if (text?.length > maxLength) { if (text?.length > maxLength) {
const extraLength = text.length - maxLength; const extraLength = text.length - maxLength;
showDialog({ showDialog({
@ -2145,6 +2147,8 @@ export default memo(withGlobal<OwnProps>(
const effect = effectId ? global.availableEffectById[effectId] : undefined; const effect = effectId ? global.availableEffectById[effectId] : undefined;
const effectReactions = global.reactions.effectReactions; const effectReactions = global.reactions.effectReactions;
const maxMessageLength = global.config?.maxMessageLength || DEFAULT_MAX_MESSAGE_LENGTH;
return { return {
availableReactions: global.reactions.availableReactions, availableReactions: global.reactions.availableReactions,
topReactions: type === 'story' ? global.reactions.topReactions : undefined, topReactions: type === 'story' ? global.reactions.topReactions : undefined,
@ -2220,6 +2224,7 @@ export default memo(withGlobal<OwnProps>(
areEffectsSupported, areEffectsSupported,
canPlayEffect, canPlayEffect,
shouldPlayEffect, shouldPlayEffect,
maxMessageLength,
}; };
}, },
)(Composer)); )(Composer));

View File

@ -16,8 +16,6 @@ import getFilesFromDataTransferItems from '../helpers/getFilesFromDataTransferIt
import useOldLang from '../../../../hooks/useOldLang'; import useOldLang from '../../../../hooks/useOldLang';
const MAX_MESSAGE_LENGTH = 4096;
const TYPE_HTML = 'text/html'; const TYPE_HTML = 'text/html';
const DOCUMENT_TYPE_WORD = 'urn:schemas-microsoft-com:office:word'; const DOCUMENT_TYPE_WORD = 'urn:schemas-microsoft-com:office:word';
const NAMESPACE_PREFIX_WORD = 'xmlns:w'; const NAMESPACE_PREFIX_WORD = 'xmlns:w';
@ -49,7 +47,7 @@ const useClipboardPaste = (
return; 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'); const html = e.clipboardData.getData('text/html');
let pastedFormattedText = html ? parseHtmlAsFormattedText( let pastedFormattedText = html ? parseHtmlAsFormattedText(

View File

@ -44,6 +44,7 @@ import {
import { import {
selectActiveDownloads, selectActiveDownloads,
selectAllowedMessageActionsSlow, selectAllowedMessageActionsSlow,
selectCanForwardMessage,
selectCanPlayAnimatedEmojis, selectCanPlayAnimatedEmojis,
selectCanScheduleUntilOnline, selectCanScheduleUntilOnline,
selectCanTranslateMessage, selectCanTranslateMessage,
@ -732,7 +733,6 @@ export default memo(withGlobal<OwnProps>(
canDelete, canDelete,
canReport, canReport,
canEdit, canEdit,
canForward,
canFaveSticker, canFaveSticker,
canUnfaveSticker, canUnfaveSticker,
canCopy, canCopy,
@ -743,6 +743,7 @@ export default memo(withGlobal<OwnProps>(
canRevote, canRevote,
canClosePoll, canClosePoll,
} = (threadId && selectAllowedMessageActionsSlow(global, message, threadId)) || {}; } = (threadId && selectAllowedMessageActionsSlow(global, message, threadId)) || {};
const canForward = selectCanForwardMessage(global, message);
const userStatus = isPrivate ? selectUserStatus(global, chat.id) : undefined; const userStatus = isPrivate ? selectUserStatus(global, chat.id) : undefined;
const isOwn = isOwnMessage(message); const isOwn = isOwnMessage(message);

View File

@ -364,7 +364,7 @@ export const DEFAULT_LIMITS: Record<ApiLimitType, readonly [number, number]> = {
dialogFiltersChats: [100, 200], dialogFiltersChats: [100, 200],
dialogFilters: [10, 20], dialogFilters: [10, 20],
dialogFolderPinned: [5, 10], dialogFolderPinned: [5, 10],
captionLength: [1024, 2048], captionLength: [1024, 4096],
channels: [500, 1000], channels: [500, 1000],
channelsPublic: [10, 20], channelsPublic: [10, 20],
aboutLength: [70, 140], aboutLength: [70, 140],
@ -373,6 +373,7 @@ export const DEFAULT_LIMITS: Record<ApiLimitType, readonly [number, number]> = {
recommendedChannels: [10, 100], recommendedChannels: [10, 100],
savedDialogsPinned: [5, 100], savedDialogsPinned: [5, 100],
}; };
export const DEFAULT_MAX_MESSAGE_LENGTH = 4096;
export const ONE_TIME_MEDIA_TTL_SECONDS = 2147483647; export const ONE_TIME_MEDIA_TTL_SECONDS = 2147483647;

View File

@ -62,6 +62,7 @@ import {
isMessageLocal, isMessageLocal,
isServiceNotificationMessage, isServiceNotificationMessage,
isUserBot, isUserBot,
splitMessagesForForwarding,
} from '../../helpers'; } from '../../helpers';
import { isApiPeerUser } from '../../helpers/peers'; import { isApiPeerUser } from '../../helpers/peers';
import { import {
@ -99,6 +100,7 @@ import {
} from '../../reducers'; } from '../../reducers';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
import { import {
selectCanForwardMessage,
selectChat, selectChat,
selectChatFullInfo, selectChatFullInfo,
selectChatLastMessageId, selectChatLastMessageId,
@ -1146,23 +1148,29 @@ addActionHandler('forwardMessages', (global, actions, payload): ActionReturnType
const lastMessageId = selectChatLastMessageId(global, toChat.id); const lastMessageId = selectChatLastMessageId(global, toChat.id);
const [realMessages, serviceMessages] = partition(messages, (m) => !isServiceNotificationMessage(m)); 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 () => { (async () => {
await rafPromise(); // Wait one frame for any previous `sendMessage` to be processed await rafPromise(); // Wait one frame for any previous `sendMessage` to be processed
callApi('forwardMessages', { messageBatches.forEach((batch) => {
fromChat, callApi('forwardMessages', {
toChat, fromChat,
toThreadId, toChat,
messages: realMessages, toThreadId,
isSilent, messages: batch,
scheduledAt, isSilent,
sendAs, scheduledAt,
withMyScore, sendAs,
noAuthors, withMyScore,
noCaptions, noAuthors,
isCurrentUserPremium, noCaptions,
wasDrafted: Boolean(draft), isCurrentUserPremium,
lastMessageId, wasDrafted: Boolean(draft),
lastMessageId,
});
}); });
})(); })();
} }

View File

@ -50,8 +50,10 @@ import {
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
import { import {
selectAllowedMessageActionsSlow, selectAllowedMessageActionsSlow,
selectCanForwardMessage,
selectChat, selectChat,
selectChatLastMessageId, selectChatLastMessageId,
selectChatMessage,
selectChatMessages, selectChatMessages,
selectChatScheduledMessages, selectChatScheduledMessages,
selectCurrentChat, selectCurrentChat,
@ -610,7 +612,16 @@ addActionHandler('openForwardMenuForSelectedMessages', (global, actions, payload
const { chatId: fromChatId, messageIds } = tabState.selectedMessages; 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 => { addActionHandler('cancelMediaDownload', (global, actions, payload): ActionReturnType => {

View File

@ -408,3 +408,34 @@ export function getMessageLink(peer: ApiPeer, topicId?: ThreadId, messageId?: nu
const messagePart = messageId ? `/${messageId}` : ''; const messagePart = messageId ? `/${messageId}` : '';
return `${TME_LINK_PREFIX}${chatPart}${topicPart}${messagePart}`; 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<ApiMessage[][]>((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;
}

View File

@ -22,7 +22,7 @@ import type {
import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types'; import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types';
import { 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'; } from '../../config';
import { getCurrentTabId } from '../../util/establishMultitabRole'; import { getCurrentTabId } from '../../util/establishMultitabRole';
import { findLast } from '../../util/iteratees'; import { findLast } from '../../util/iteratees';
@ -81,8 +81,6 @@ import {
selectBot, selectIsCurrentUserPremium, selectUser, selectUserStatus, selectBot, selectIsCurrentUserPremium, selectUser, selectUserStatus,
} from './users'; } from './users';
const MESSAGE_EDIT_ALLOWED_TIME = 172800; // 48 hours
export function selectCurrentMessageList<T extends GlobalState>( export function selectCurrentMessageList<T extends GlobalState>(
global: T, global: T,
...[tabId = getCurrentTabId()]: TabArgs<T> ...[tabId = getCurrentTabId()]: TabArgs<T>
@ -607,6 +605,28 @@ export function selectCanReplyToMessage<T extends GlobalState>(global: T, messag
return !messageTopic || !messageTopic.isClosed || messageTopic.isOwner || getHasAdminRight(chat, 'manageTopics'); 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 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) // This selector is slow and not to be used within lists (e.g. Message component)
export function selectAllowedMessageActionsSlow<T extends GlobalState>( export function selectAllowedMessageActionsSlow<T extends GlobalState>(
global: T, message: ApiMessage, threadId: ThreadId, global: T, message: ApiMessage, threadId: ThreadId,
@ -651,7 +671,7 @@ export function selectAllowedMessageActionsSlow<T extends GlobalState>(
const isMessageEditable = ( const isMessageEditable = (
( (
canEditMessagesIndefinitely canEditMessagesIndefinitely
|| getServerTime() - message.date < MESSAGE_EDIT_ALLOWED_TIME || getServerTime() - message.date < (global.config?.editTimeLimit || Infinity)
) && !( ) && !(
content.sticker || content.contact || content.pollId || content.action content.sticker || content.contact || content.pollId || content.action
|| (content.video?.isRound) || content.location || content.invoice || content.giveaway || content.giveawayResults || (content.video?.isRound) || content.location || content.invoice || content.giveaway || content.giveawayResults
@ -702,20 +722,6 @@ export function selectAllowedMessageActionsSlow<T extends GlobalState>(
const canEdit = !isLocal && !isAction && isMessageEditable && hasMessageEditRight; 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 hasSticker = Boolean(message.content.sticker);
const hasFavoriteSticker = hasSticker && selectIsStickerFavorite(global, message.content.sticker!); const hasFavoriteSticker = hasSticker && selectIsStickerFavorite(global, message.content.sticker!);
const canFaveSticker = !isAction && hasSticker && !hasFavoriteSticker; const canFaveSticker = !isAction && hasSticker && !hasFavoriteSticker;
@ -743,7 +749,6 @@ export function selectAllowedMessageActionsSlow<T extends GlobalState>(
canReport, canReport,
canDelete, canDelete,
canDeleteForAll, canDeleteForAll,
canForward,
canFaveSticker, canFaveSticker,
canUnfaveSticker, canUnfaveSticker,
canCopy, canCopy,
@ -765,7 +770,6 @@ export function selectAllowedMessageActionsSlow<T extends GlobalState>(
canReport, canReport,
canDelete, canDelete,
canDeleteForAll, canDeleteForAll,
canForward,
canFaveSticker, canFaveSticker,
canUnfaveSticker, canUnfaveSticker,
canCopy, canCopy,
@ -790,6 +794,8 @@ export function selectCanDeleteSelectedMessages<T extends GlobalState>(
return {}; return {};
} }
if (selectedMessageIds.length > API_GENERAL_ID_LIMIT) return {};
const messageActions = selectedMessageIds const messageActions = selectedMessageIds
.map((id) => chatMessages[id] && selectAllowedMessageActionsSlow(global, chatMessages[id], threadId)) .map((id) => chatMessages[id] && selectAllowedMessageActionsSlow(global, chatMessages[id], threadId))
.filter(Boolean); .filter(Boolean);