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 {
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,
};
}

View File

@ -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(

View File

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

View File

@ -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<OwnProps & StateProps> = ({
areEffectsSupported,
canPlayEffect,
shouldPlayEffect,
maxMessageLength,
}) => {
const {
sendMessage,
@ -871,7 +873,7 @@ const Composer: FC<OwnProps & StateProps> = ({
});
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<OwnProps>(
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<OwnProps>(
areEffectsSupported,
canPlayEffect,
shouldPlayEffect,
maxMessageLength,
};
},
)(Composer));

View File

@ -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(

View File

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

View File

@ -364,7 +364,7 @@ export const DEFAULT_LIMITS: Record<ApiLimitType, readonly [number, number]> = {
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<ApiLimitType, readonly [number, number]> = {
recommendedChannels: [10, 100],
savedDialogsPinned: [5, 100],
};
export const DEFAULT_MAX_MESSAGE_LENGTH = 4096;
export const ONE_TIME_MEDIA_TTL_SECONDS = 2147483647;

View File

@ -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,
});
});
})();
}

View File

@ -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 => {

View File

@ -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<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 {
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<T extends GlobalState>(
global: 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');
}
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)
export function selectAllowedMessageActionsSlow<T extends GlobalState>(
global: T, message: ApiMessage, threadId: ThreadId,
@ -651,7 +671,7 @@ export function selectAllowedMessageActionsSlow<T extends GlobalState>(
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<T extends GlobalState>(
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<T extends GlobalState>(
canReport,
canDelete,
canDeleteForAll,
canForward,
canFaveSticker,
canUnfaveSticker,
canCopy,
@ -765,7 +770,6 @@ export function selectAllowedMessageActionsSlow<T extends GlobalState>(
canReport,
canDelete,
canDeleteForAll,
canForward,
canFaveSticker,
canUnfaveSticker,
canCopy,
@ -790,6 +794,8 @@ export function selectCanDeleteSelectedMessages<T extends GlobalState>(
return {};
}
if (selectedMessageIds.length > API_GENERAL_ID_LIMIT) return {};
const messageActions = selectedMessageIds
.map((id) => chatMessages[id] && selectAllowedMessageActionsSlow(global, chatMessages[id], threadId))
.filter(Boolean);