949 lines
28 KiB
TypeScript

import type { RequiredGlobalActions } from '../../index';
import { addActionHandler, getGlobal, setGlobal } from '../../index';
import type {
ApiChat,
ApiMessage, ApiPollResult, ApiReactions, ApiThreadInfo,
} from '../../../api/types';
import type {
ActiveEmojiInteraction, ActionReturnType, GlobalState, RequiredGlobalState,
} from '../../types';
import { MAIN_THREAD_ID } from '../../../api/types';
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
import { pickTruthy, unique } from '../../../util/iteratees';
import { areDeepEqual } from '../../../util/areDeepEqual';
import { notifyAboutMessage } from '../../../util/notifications';
import {
updateChat,
deleteChatMessages,
updateChatMessage,
updateListedIds,
addViewportId,
updateThreadInfo,
replaceThreadParam,
updateScheduledMessage,
deleteChatScheduledMessages,
updateThreadUnreadFromForwardedMessage,
updateTopic,
deleteTopic,
} from '../../reducers';
import {
selectChatMessage,
selectChatMessages,
selectIsViewportNewest,
selectListedIds,
selectChatMessageByPollId,
selectCommonBoxChatId,
selectIsChatListed,
selectThreadInfo,
selectThreadByMessage,
selectPinnedIds,
selectScheduledMessage,
selectChatScheduledMessages,
selectIsMessageInCurrentMessageList,
selectScheduledIds,
selectCurrentMessageList,
selectViewportIds,
selectFirstUnreadId,
selectChat,
selectIsServiceChatReady,
selectLocalAnimatedEmojiEffect,
selectLocalAnimatedEmoji,
selectThreadIdFromMessage,
selectTopicFromMessage,
selectTabState,
} from '../../selectors';
import {
getMessageContent, isUserId, isMessageLocal, getMessageText, checkIfHasUnreadReactions,
} from '../../helpers';
import { onTickEnd } from '../../../util/schedulers';
import { updateUnreadReactions } from '../../reducers/reactions';
import { updateTabState } from '../../reducers/tabs';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
const ANIMATION_DELAY = 350;
addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
switch (update['@type']) {
case 'newMessage': {
const {
chatId, id, message, shouldForceReply,
} = update;
global = updateWithLocalMedia(global, chatId, id, message);
global = updateListedAndViewportIds(global, actions, message as ApiMessage);
if (message.repliesThreadInfo) {
global = updateThreadInfo(
global,
message.repliesThreadInfo.chatId,
message.repliesThreadInfo.threadId,
message.repliesThreadInfo,
);
}
const newMessage = selectChatMessage(global, chatId, id)!;
const chat = selectChat(global, chatId);
if (chat?.isForum
&& newMessage.isTopicReply
&& !selectTopicFromMessage(global, newMessage)
&& newMessage.replyToMessageId) {
actions.loadTopicById({ chatId, topicId: newMessage.replyToMessageId });
}
Object.values(global.byTabId).forEach(({ id: tabId }) => {
const isLocal = isMessageLocal(message as ApiMessage);
if (selectIsMessageInCurrentMessageList(global, chatId, message as ApiMessage, tabId)) {
if (isLocal && message.isOutgoing && !(message.content?.action)) {
const currentMessageList = selectCurrentMessageList(global, tabId);
if (currentMessageList) {
// We do not use `actions.focusLastMessage` as it may be set with a delay (see below)
actions.focusMessage({
chatId,
threadId: currentMessageList.threadId,
messageId: message.id!,
noHighlight: true,
isResizingContainer: true,
tabId,
});
}
}
const { threadInfo } = selectThreadByMessage(global, message as ApiMessage) || {};
if (threadInfo) {
actions.requestThreadInfoUpdate({ chatId, threadId: threadInfo.threadId });
}
// @perf Wait until scroll animation finishes or simply rely on delivery status update
// (which is itself delayed)
if (!isLocal) {
setTimeout(() => {
global = getGlobal();
if (shouldForceReply) {
global = replaceThreadParam(global, chatId, MAIN_THREAD_ID, 'replyingToId', id);
}
global = updateChatLastMessage(global, chatId, newMessage);
setGlobal(global);
}, ANIMATION_DELAY);
}
} else {
global = updateChatLastMessage(global, chatId, newMessage);
}
});
setGlobal(global);
// Edge case: New message in an old (not loaded) chat.
if (!selectIsChatListed(global, chatId)) {
actions.loadTopChats();
}
break;
}
case 'updateStartEmojiInteraction': {
Object.values(global.byTabId).forEach(({ id: tabId }) => {
const { chatId: currentChatId } = selectCurrentMessageList(global, tabId) || {};
if (currentChatId !== update.id) return;
const message = selectChatMessage(global, currentChatId, update.messageId);
if (!message) return;
// Workaround for a weird behavior when interaction is received after watching reaction
if (getMessageText(message) !== update.emoji) return;
const localEmoji = selectLocalAnimatedEmoji(global, update.emoji);
const tabState = selectTabState(global, tabId);
global = updateTabState(global, {
activeEmojiInteractions: [...(tabState.activeEmojiInteractions || []), {
id: tabState.activeEmojiInteractions?.length || 0,
animatedEffect: localEmoji ? selectLocalAnimatedEmojiEffect(localEmoji) : update.emoji,
messageId: update.messageId,
} as ActiveEmojiInteraction],
}, tabId);
});
setGlobal(global);
break;
}
case 'newScheduledMessage': {
const { chatId, id, message } = update;
global = updateWithLocalMedia(global, chatId, id, message, true);
const scheduledIds = selectScheduledIds(global, chatId, MAIN_THREAD_ID) || [];
global = replaceThreadParam(global, chatId, MAIN_THREAD_ID, 'scheduledIds', unique([...scheduledIds, id]));
const threadId = selectThreadIdFromMessage(global, message);
if (threadId !== MAIN_THREAD_ID) {
const threadScheduledIds = selectScheduledIds(global, chatId, threadId) || [];
global = replaceThreadParam(global, chatId, threadId, 'scheduledIds', unique([...threadScheduledIds, id]));
}
setGlobal(global);
break;
}
case 'updateMessage': {
const { chatId, id, message } = update;
const currentMessage = selectChatMessage(global, chatId, id);
const chat = selectChat(global, chatId);
global = updateWithLocalMedia(global, chatId, id, message);
const newMessage = selectChatMessage(global, chatId, id)!;
if (message.repliesThreadInfo) {
global = updateThreadInfo(
global,
message.repliesThreadInfo.chatId,
message.repliesThreadInfo.threadId,
message.repliesThreadInfo,
);
}
if (currentMessage) {
global = updateChatLastMessage(global, chatId, newMessage);
}
if (message.reactions && chat) {
global = updateReactions(global, chatId, id, message.reactions, chat, newMessage.isOutgoing, currentMessage);
}
setGlobal(global);
break;
}
case 'updateScheduledMessage': {
const { chatId, id, message } = update;
const currentMessage = selectScheduledMessage(global, chatId, id);
if (!currentMessage) {
return;
}
global = updateWithLocalMedia(global, chatId, id, message, true);
const ids = Object.keys(selectChatScheduledMessages(global, chatId) || {}).map(Number).sort((a, b) => b - a);
global = replaceThreadParam(global, chatId, MAIN_THREAD_ID, 'scheduledIds', ids);
const threadId = selectThreadIdFromMessage(global, currentMessage);
if (threadId !== MAIN_THREAD_ID) {
const threadScheduledIds = selectScheduledIds(global, chatId, threadId) || [];
global = replaceThreadParam(global, chatId, threadId, 'scheduledIds', threadScheduledIds.sort((a, b) => b - a));
}
setGlobal(global);
break;
}
case 'updateMessageSendSucceeded': {
const { chatId, localId, message } = update;
global = updateListedAndViewportIds(global, actions, message as ApiMessage);
const currentMessage = selectChatMessage(global, chatId, localId);
global = deleteChatMessages(global, chatId, [localId]);
// Edge case for "Send When Online"
if (message.isScheduled) {
global = deleteChatScheduledMessages(global, chatId, [localId]);
}
global = updateChatMessage(global, chatId, message.id, {
...currentMessage,
...message,
previousLocalId: localId,
});
const newMessage = selectChatMessage(global, chatId, message.id)!;
global = updateChatLastMessage(global, chatId, newMessage);
const thread = selectThreadByMessage(global, message);
// For some reason Telegram requires to manually mark outgoing thread messages read
Object.values(global.byTabId).forEach(({ id: tabId }) => {
const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global, tabId) || {};
if (currentChatId !== chatId
|| (thread?.threadInfo?.threadId || MAIN_THREAD_ID) !== currentThreadId) {
return;
}
actions.markMessageListRead({ maxId: message.id, tabId });
});
if (thread?.threadInfo) {
global = replaceThreadParam(global, chatId, thread.threadInfo.threadId, 'threadInfo', {
...thread.threadInfo,
lastMessageId: message.id,
lastReadInboxMessageId: message.id,
});
}
setGlobal(global);
break;
}
case 'updateScheduledMessageSendSucceeded': {
const { chatId, localId, message } = update;
const scheduledIds = selectScheduledIds(global, chatId, MAIN_THREAD_ID) || [];
global = replaceThreadParam(global, chatId, MAIN_THREAD_ID, 'scheduledIds', [...scheduledIds, message.id]);
const threadId = selectThreadIdFromMessage(global, message);
if (threadId !== MAIN_THREAD_ID) {
const threadScheduledIds = selectScheduledIds(global, chatId, threadId) || [];
global = replaceThreadParam(global, chatId, threadId, 'scheduledIds', [...threadScheduledIds, message.id]);
}
const currentMessage = selectScheduledMessage(global, chatId, localId);
global = deleteChatScheduledMessages(global, chatId, [localId]);
global = updateScheduledMessage(global, chatId, message.id, {
...currentMessage,
...message,
previousLocalId: localId,
});
setGlobal(global);
break;
}
case 'updatePinnedIds': {
const { chatId, isPinned, messageIds } = update;
const messages = pickTruthy(selectChatMessages(global, chatId), messageIds);
const updatePerThread: Record<number, number[]> = {
[MAIN_THREAD_ID]: messageIds,
};
Object.values(messages).forEach((message) => {
const threadId = selectThreadIdFromMessage(global, message);
if (threadId === MAIN_THREAD_ID) return;
const currentUpdatedInThread = updatePerThread[threadId] || [];
currentUpdatedInThread.push(message.id);
updatePerThread[threadId] = currentUpdatedInThread;
});
Object.entries(updatePerThread).forEach(([threadId, ids]) => {
const pinnedIds = selectPinnedIds(global, chatId, MAIN_THREAD_ID) || [];
const newPinnedIds = isPinned
? unique(pinnedIds.concat(ids)).sort((a, b) => b - a)
: pinnedIds.filter((id) => !ids.includes(id));
global = replaceThreadParam(global, chatId, Number(threadId), 'pinnedIds', newPinnedIds);
});
setGlobal(global);
break;
}
case 'updateThreadInfo': {
const {
chatId, threadId, threadInfo, firstMessageId,
} = update;
const currentThreadInfo = selectThreadInfo(global, chatId, threadId);
const newThreadInfo = {
...currentThreadInfo,
...threadInfo,
};
if (!newThreadInfo.threadId) {
return;
}
global = updateThreadInfo(global, chatId, threadId, newThreadInfo as ApiThreadInfo);
if (firstMessageId) {
global = replaceThreadParam(global, chatId, threadId, 'firstMessageId', firstMessageId);
}
setGlobal(global);
break;
}
case 'resetMessages': {
const { id: chatId } = update;
const messagesById = selectChatMessages(global, chatId);
if (messagesById && !isUserId(chatId)) {
global = deleteChatMessages(global, chatId, Object.keys(messagesById).map(Number));
setGlobal(global);
actions.loadFullChat({ chatId, force: true, tabId: getCurrentTabId() });
}
break;
}
case 'deleteMessages': {
const { ids, chatId } = update;
deleteMessages(global, chatId, ids, actions);
break;
}
case 'deleteScheduledMessages': {
const { ids, chatId } = update;
deleteScheduledMessages(chatId, ids, actions, global);
break;
}
case 'deleteHistory': {
const { chatId } = update;
const chatMessages = global.messages.byChatId[chatId];
if (chatId === SERVICE_NOTIFICATIONS_USER_ID) {
global = {
...global,
serviceNotifications: global.serviceNotifications.map((notification) => ({
...notification,
isDeleted: true,
})),
};
setGlobal(global);
}
if (chatMessages) {
const ids = Object.keys(chatMessages.byId).map(Number);
global = getGlobal();
deleteMessages(global, chatId, ids, actions);
} else {
actions.requestChatUpdate({ chatId });
}
break;
}
case 'updateCommonBoxMessages': {
const { ids, messageUpdate } = update;
ids.forEach((id) => {
const chatId = selectCommonBoxChatId(global, id);
if (chatId) {
global = updateChatMessage(global, chatId, id, messageUpdate);
}
});
setGlobal(global);
break;
}
case 'updateChannelMessages': {
const { channelId, ids, messageUpdate } = update;
ids.forEach((id) => {
global = updateChatMessage(global, channelId, id, messageUpdate);
});
setGlobal(global);
break;
}
case 'updateMessagePoll': {
const { pollId, pollUpdate } = update;
const message = selectChatMessageByPollId(global, pollId);
if (message?.content.poll) {
const oldResults = message.content.poll.results;
let newResults = oldResults;
if (pollUpdate.results?.results) {
if (!oldResults.results || !pollUpdate.results.isMin) {
newResults = pollUpdate.results;
} else if (oldResults.results) {
newResults = {
...pollUpdate.results,
results: pollUpdate.results.results.map((result) => ({
...result,
isChosen: oldResults.results!.find((r) => r.option === result.option)?.isChosen,
})),
isMin: undefined,
};
}
}
const updatedPoll = { ...message.content.poll, ...pollUpdate, results: newResults };
global = updateChatMessage(
global,
message.chatId,
message.id,
{
content: {
...message.content,
poll: updatedPoll,
},
},
);
setGlobal(global);
}
break;
}
case 'updateMessagePollVote': {
const { pollId, userId, options } = update;
const message = selectChatMessageByPollId(global, pollId);
if (!message || !message.content.poll || !message.content.poll.results) {
break;
}
const { poll } = message.content;
const { recentVoterIds, totalVoters, results } = poll.results;
const newRecentVoterIds = recentVoterIds ? [...recentVoterIds] : [];
const newTotalVoters = totalVoters ? totalVoters + 1 : 1;
const newResults = results ? [...results] : [];
newRecentVoterIds.push(userId);
options.forEach((option) => {
const targetOptionIndex = newResults.findIndex((result) => result.option === option);
const targetOption = newResults[targetOptionIndex];
const updatedOption: ApiPollResult = targetOption ? { ...targetOption } : { option, votersCount: 0 };
updatedOption.votersCount += 1;
if (userId === global.currentUserId) {
updatedOption.isChosen = true;
}
if (targetOptionIndex) {
newResults[targetOptionIndex] = updatedOption;
} else {
newResults.push(updatedOption);
}
});
global = updateChatMessage(
global,
message.chatId,
message.id,
{
content: {
...message.content,
poll: {
...poll,
results: {
...poll.results,
recentVoterIds: newRecentVoterIds,
totalVoters: newTotalVoters,
results: newResults,
},
},
},
},
);
setGlobal(global);
break;
}
case 'updateServiceNotification': {
const { message } = update;
if (selectIsServiceChatReady(global)) {
actions.createServiceNotification({ message });
}
break;
}
case 'updateMessageReactions': {
const { chatId, id, reactions } = update;
const message = selectChatMessage(global, chatId, id);
const chat = selectChat(global, update.chatId);
if (!chat || !message) return;
global = updateReactions(global, chatId, id, reactions, chat, message.isOutgoing, message);
setGlobal(global);
break;
}
case 'updateMessageExtendedMedia': {
const {
chatId, id, media, preview,
} = update;
const message = selectChatMessage(global, chatId, id);
const chat = selectChat(global, update.chatId);
if (!chat || !message) return;
if (preview) {
if (!message.content.invoice) return;
global = updateChatMessage(global, chatId, id, {
content: {
...message.content,
invoice: {
...message.content.invoice,
extendedMedia: preview,
},
},
});
setGlobal(global);
} else if (media) {
global = updateChatMessage(global, chatId, id, {
content: {
...media,
},
});
setGlobal(global);
}
break;
}
case 'updateTranscribedAudio': {
const { transcriptionId, text, isPending } = update;
global = {
...global,
transcriptions: {
...global.transcriptions,
[transcriptionId]: {
...(global.transcriptions[transcriptionId] || {}),
transcriptionId,
text,
isPending,
},
},
};
setGlobal(global);
break;
}
}
});
function updateReactions<T extends GlobalState>(
global: T,
chatId: string,
id: number,
reactions: ApiReactions,
chat: ApiChat,
isOutgoing?: boolean,
message?: ApiMessage,
): T {
const currentReactions = message?.reactions;
// `updateMessageReactions` happens with an interval, so we try to avoid redundant global state updates
if (currentReactions && areDeepEqual(reactions, currentReactions)) {
return global;
}
global = updateChatMessage(global, chatId, id, { reactions });
if (!isOutgoing) {
return global;
}
const alreadyHasUnreadReaction = chat.unreadReactions?.includes(id);
// Only notify about added reactions, not removed ones
if (checkIfHasUnreadReactions(global, reactions) && !alreadyHasUnreadReaction) {
global = updateUnreadReactions(global, chatId, {
unreadReactionsCount: (chat?.unreadReactionsCount || 0) + 1,
unreadReactions: [...(chat?.unreadReactions || []), id],
});
const newMessage = selectChatMessage(global, chatId, id);
if (!chat || !newMessage) return global;
onTickEnd(() => {
notifyAboutMessage({
chat,
message: newMessage,
isReaction: true,
});
});
} else if (alreadyHasUnreadReaction) {
global = updateUnreadReactions(global, chatId, {
unreadReactionsCount: (chat?.unreadReactionsCount || 1) - 1,
unreadReactions: chat?.unreadReactions?.filter((i) => i !== id),
});
}
return global;
}
function updateWithLocalMedia(
global: RequiredGlobalState, chatId: string, id: number, message: Partial<ApiMessage>, isScheduled = false,
) {
// Preserve locally uploaded media.
const currentMessage = isScheduled
? selectScheduledMessage(global, chatId, id)
: selectChatMessage(global, chatId, id);
if (currentMessage && message.content) {
const {
photo, video, sticker, document,
} = getMessageContent(currentMessage);
if (photo && message.content.photo) {
message.content.photo.blobUrl = photo.blobUrl;
message.content.photo.thumbnail = photo.thumbnail;
} else if (video && message.content.video) {
message.content.video.blobUrl = video.blobUrl;
} else if (sticker && message.content.sticker) {
message.content.sticker.isPreloadedGlobally = sticker.isPreloadedGlobally;
} else if (document && message.content.document) {
message.content.document.previewBlobUrl = document.previewBlobUrl;
}
}
return isScheduled
? updateScheduledMessage(global, chatId, id, message)
: updateChatMessage(global, chatId, id, message);
}
function updateThreadUnread<T extends GlobalState>(
global: T, actions: RequiredGlobalActions, message: ApiMessage, isDeleting?: boolean,
) {
const { chatId } = message;
const { threadInfo } = selectThreadByMessage(global, message) || {};
if (!threadInfo && message.replyToMessageId) {
const originMessage = selectChatMessage(global, chatId, message.replyToMessageId);
if (originMessage) {
global = updateThreadUnreadFromForwardedMessage(global, originMessage, chatId, message.id, isDeleting);
} else {
actions.loadMessage({
chatId,
messageId: message.replyToMessageId,
threadUpdate: {
isDeleting,
lastMessageId: message.id,
},
});
}
}
return global;
}
function updateListedAndViewportIds<T extends GlobalState>(
global: T, actions: RequiredGlobalActions, message: ApiMessage,
) {
const { id, chatId } = message;
const { threadInfo, firstMessageId } = selectThreadByMessage(global, message) || {};
const chat = selectChat(global, chatId);
const isUnreadChatNotLoaded = chat?.unreadCount && !selectListedIds(global, chatId, MAIN_THREAD_ID);
global = updateThreadUnread(global, actions, message);
if (threadInfo) {
if (firstMessageId || !isMessageLocal(message)) {
global = updateListedIds(global, chatId, threadInfo.threadId, [id]);
Object.values(global.byTabId).forEach(({ id: tabId }) => {
if (selectIsViewportNewest(global, chatId, threadInfo.threadId, tabId)) {
global = addViewportId(global, chatId, threadInfo.threadId, id, tabId);
if (!firstMessageId) {
global = replaceThreadParam(global, chatId, threadInfo.threadId, 'firstMessageId', message.id);
}
}
});
}
global = replaceThreadParam(global, chatId, threadInfo.threadId, 'threadInfo', {
...threadInfo,
lastMessageId: message.id,
messagesCount: (threadInfo.messagesCount || 0) + 1,
});
}
if (isUnreadChatNotLoaded) {
return global;
}
global = updateListedIds(global, chatId, MAIN_THREAD_ID, [id]);
Object.values(global.byTabId).forEach(({ id: tabId }) => {
if (selectIsViewportNewest(global, chatId, MAIN_THREAD_ID, tabId)) {
// Always keep the first unread message in the viewport list
const firstUnreadId = selectFirstUnreadId(global, chatId, MAIN_THREAD_ID, tabId);
const candidateGlobal = addViewportId(global, chatId, MAIN_THREAD_ID, id, tabId);
const newViewportIds = selectViewportIds(candidateGlobal, chatId, MAIN_THREAD_ID, tabId);
if (!firstUnreadId || newViewportIds!.includes(firstUnreadId)) {
global = candidateGlobal;
}
}
});
return global;
}
function updateChatLastMessage<T extends GlobalState>(
global: T,
chatId: string,
message: ApiMessage,
force = false,
) {
const { chats } = global;
const chat = chats.byId[chatId];
const currentLastMessage = chat?.lastMessage;
const topic = chat?.isForum ? selectTopicFromMessage(global, message) : undefined;
if (topic) {
global = updateTopic(global, chatId, topic.id, {
lastMessageId: message.id,
});
}
if (currentLastMessage && !force) {
const isSameOrNewer = (
currentLastMessage.id === message.id || currentLastMessage.id === message.previousLocalId
) || message.id > currentLastMessage.id;
if (!isSameOrNewer) {
return global;
}
}
global = updateChat(global, chatId, { lastMessage: message });
return global;
}
function findLastMessage<T extends GlobalState>(global: T, chatId: string) {
const byId = selectChatMessages(global, chatId);
const listedIds = selectListedIds(global, chatId, MAIN_THREAD_ID);
if (!byId || !listedIds) {
return undefined;
}
let i = listedIds.length;
while (i--) {
const message = byId[listedIds[i]];
if (!message.isDeleting) {
return message;
}
}
return undefined;
}
function deleteMessages<T extends GlobalState>(
global: T, chatId: string | undefined, ids: number[], actions: RequiredGlobalActions,
) {
// Channel update
if (chatId) {
const chat = selectChat(global, chatId);
if (!chat) return;
ids.forEach((id) => {
global = updateChatMessage(global, chatId, id, {
isDeleting: true,
});
const newLastMessage = findLastMessage(global, chatId);
if (newLastMessage) {
global = updateChatLastMessage(global, chatId, newLastMessage, true);
}
if (chat.topics?.[id]) {
global = deleteTopic(global, chatId, id);
}
});
actions.requestChatUpdate({ chatId });
const threadIdsToUpdate: number[] = [];
ids.forEach((id) => {
const message = selectChatMessage(global, chatId, id);
if (!message) {
return;
}
global = updateThreadUnread(global, actions, message, true);
const threadId = selectThreadIdFromMessage(global, message);
if (threadId) {
threadIdsToUpdate.push(threadId);
}
});
setGlobal(global);
setTimeout(() => {
global = getGlobal();
global = deleteChatMessages(global, chatId, ids);
setGlobal(global);
unique(threadIdsToUpdate).forEach((threadId) => {
actions.requestThreadInfoUpdate({ chatId, threadId });
});
}, ANIMATION_DELAY);
return;
}
// Common box update
const chatsIdsToUpdate: string[] = [];
ids.forEach((id) => {
const commonBoxChatId = selectCommonBoxChatId(global, id);
if (commonBoxChatId) {
chatsIdsToUpdate.push(commonBoxChatId);
global = updateChatMessage(global, commonBoxChatId, id, {
isDeleting: true,
});
const newLastMessage = findLastMessage(global, commonBoxChatId);
if (newLastMessage) {
global = updateChatLastMessage(global, commonBoxChatId, newLastMessage, true);
}
setTimeout(() => {
global = getGlobal();
global = deleteChatMessages(global, commonBoxChatId, [id]);
setGlobal(global);
}, ANIMATION_DELAY);
}
});
setGlobal(global);
unique(chatsIdsToUpdate).forEach((id) => {
actions.requestChatUpdate({ chatId: id });
});
}
function deleteScheduledMessages<T extends GlobalState>(
chatId: string | undefined, ids: number[], actions: RequiredGlobalActions, global: T,
) {
if (!chatId) {
return;
}
ids.forEach((id) => {
global = updateScheduledMessage(global, chatId, id, {
isDeleting: true,
});
});
setGlobal(global);
setTimeout(() => {
global = getGlobal();
global = deleteChatScheduledMessages(global, chatId, ids);
const scheduledMessages = selectChatScheduledMessages(global, chatId);
global = replaceThreadParam(
global, chatId, MAIN_THREAD_ID, 'scheduledIds', Object.keys(scheduledMessages || {}).map(Number),
);
setGlobal(global);
}, ANIMATION_DELAY);
}