1799 lines
51 KiB
TypeScript
1799 lines
51 KiB
TypeScript
import type {
|
|
ApiAttachment,
|
|
ApiChat,
|
|
ApiInputMessageReplyInfo,
|
|
ApiInputReplyInfo,
|
|
ApiInputStoryReplyInfo,
|
|
ApiMessage,
|
|
ApiMessageEntity,
|
|
ApiNewPoll,
|
|
ApiOnProgress,
|
|
ApiPeer,
|
|
ApiSticker,
|
|
ApiStory,
|
|
ApiStorySkipped,
|
|
ApiVideo,
|
|
} from '../../../api/types';
|
|
import type { RequiredGlobalActions } from '../../index';
|
|
import type {
|
|
ActionReturnType, ApiDraft, GlobalState, TabArgs,
|
|
} from '../../types';
|
|
import { MAIN_THREAD_ID, MESSAGE_DELETED } from '../../../api/types';
|
|
import { LoadMoreDirection } from '../../../types';
|
|
|
|
import {
|
|
GIF_MIME_TYPE,
|
|
MAX_MEDIA_FILES_FOR_ALBUM,
|
|
MESSAGE_LIST_SLICE,
|
|
RE_TELEGRAM_LINK,
|
|
RE_TG_LINK,
|
|
RE_TME_LINK,
|
|
SERVICE_NOTIFICATIONS_USER_ID,
|
|
SUPPORTED_AUDIO_CONTENT_TYPES,
|
|
SUPPORTED_IMAGE_CONTENT_TYPES,
|
|
SUPPORTED_VIDEO_CONTENT_TYPES,
|
|
} from '../../../config';
|
|
import { ensureProtocol } from '../../../util/ensureProtocol';
|
|
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
|
import {
|
|
areSortedArraysIntersecting, buildCollectionByKey, omit, split, unique,
|
|
} from '../../../util/iteratees';
|
|
import { translate } from '../../../util/langProvider';
|
|
import { debounce, onTickEnd, rafPromise } from '../../../util/schedulers';
|
|
import { IS_IOS } from '../../../util/windowEnvironment';
|
|
import { callApi, cancelApiProgress } from '../../../api/gramjs';
|
|
import {
|
|
getMessageOriginalId,
|
|
getUserFullName, isChatChannel,
|
|
isDeletedUser, isMessageLocal,
|
|
isServiceNotificationMessage,
|
|
isUserBot,
|
|
} from '../../helpers';
|
|
import {
|
|
addActionHandler, getActions, getGlobal, setGlobal,
|
|
} from '../../index';
|
|
import {
|
|
addChatMessagesById,
|
|
addChats,
|
|
addUsers,
|
|
removeOutlyingList,
|
|
removeRequestedMessageTranslation,
|
|
replaceScheduledMessages,
|
|
replaceSettings,
|
|
replaceThreadParam,
|
|
safeReplacePinnedIds,
|
|
safeReplaceViewportIds,
|
|
updateChat,
|
|
updateChatFullInfo,
|
|
updateChatMessage,
|
|
updateListedIds,
|
|
updateMessageTranslation,
|
|
updateOutlyingLists,
|
|
updateRequestedMessageTranslation,
|
|
updateSponsoredMessage,
|
|
updateThreadInfo,
|
|
updateThreadInfos,
|
|
updateThreadUnreadFromForwardedMessage,
|
|
updateTopic,
|
|
} from '../../reducers';
|
|
import { updateTabState } from '../../reducers/tabs';
|
|
import {
|
|
selectChat,
|
|
selectChatFullInfo,
|
|
selectChatMessage,
|
|
selectCurrentChat,
|
|
selectCurrentMessageList,
|
|
selectCurrentViewedStory,
|
|
selectDraft,
|
|
selectEditingId,
|
|
selectEditingMessage,
|
|
selectEditingScheduledId,
|
|
selectFirstMessageId,
|
|
selectFirstUnreadId,
|
|
selectFocusedMessageId,
|
|
selectForwardsCanBeSentToChat,
|
|
selectForwardsContainVoiceMessages,
|
|
selectIsCurrentUserPremium,
|
|
selectLanguageCode,
|
|
selectListedIds,
|
|
selectMessageReplyInfo,
|
|
selectNoWebPage,
|
|
selectOutlyingListByMessageId,
|
|
selectPeerStory,
|
|
selectPinnedIds,
|
|
selectRealLastReadId,
|
|
selectScheduledMessage,
|
|
selectSendAs,
|
|
selectSponsoredMessage,
|
|
selectTabState,
|
|
selectThreadIdFromMessage,
|
|
selectThreadOriginChat,
|
|
selectThreadTopMessageId,
|
|
selectTranslationLanguage,
|
|
selectUser,
|
|
selectUserFullInfo,
|
|
selectViewportIds,
|
|
} from '../../selectors';
|
|
import { deleteMessages } from '../apiUpdaters/messages';
|
|
|
|
const AUTOLOGIN_TOKEN_KEY = 'autologin_token';
|
|
|
|
const uploadProgressCallbacks = new Map<number, ApiOnProgress>();
|
|
|
|
const runDebouncedForMarkRead = debounce((cb) => cb(), 500, false);
|
|
|
|
addActionHandler('loadViewportMessages', (global, actions, payload): ActionReturnType => {
|
|
const {
|
|
direction = LoadMoreDirection.Around,
|
|
isBudgetPreload = false,
|
|
shouldForceRender = false,
|
|
tabId = getCurrentTabId(),
|
|
} = payload || {};
|
|
|
|
let { chatId, threadId } = payload || {};
|
|
|
|
if (!chatId || !threadId) {
|
|
const currentMessageList = selectCurrentMessageList(global, tabId);
|
|
if (!currentMessageList) {
|
|
return;
|
|
}
|
|
|
|
chatId = currentMessageList.chatId;
|
|
threadId = currentMessageList.threadId;
|
|
}
|
|
|
|
const chat = selectChat(global, chatId);
|
|
// TODO Revise if `chat.isRestricted` check is needed
|
|
if (!chat || chat.isRestricted) {
|
|
return;
|
|
}
|
|
|
|
const viewportIds = selectViewportIds(global, chatId, threadId, tabId);
|
|
const listedIds = selectListedIds(global, chatId, threadId);
|
|
|
|
if (!viewportIds || !viewportIds.length || direction === LoadMoreDirection.Around) {
|
|
const offsetId = selectFocusedMessageId(global, chatId, tabId) || selectRealLastReadId(global, chatId, threadId);
|
|
const isOutlying = Boolean(offsetId && listedIds && !listedIds.includes(offsetId));
|
|
const historyIds = (isOutlying
|
|
? selectOutlyingListByMessageId(global, chatId, threadId, offsetId!)
|
|
: listedIds) || [];
|
|
const {
|
|
newViewportIds, areSomeLocal, areAllLocal,
|
|
} = getViewportSlice(historyIds, offsetId, LoadMoreDirection.Around);
|
|
|
|
if (areSomeLocal) {
|
|
global = safeReplaceViewportIds(global, chatId, threadId, newViewportIds, tabId);
|
|
}
|
|
|
|
if (!areAllLocal) {
|
|
onTickEnd(() => {
|
|
void loadViewportMessages(
|
|
global, chat, threadId!, offsetId, LoadMoreDirection.Around, isOutlying, isBudgetPreload, tabId,
|
|
);
|
|
});
|
|
}
|
|
} else {
|
|
const offsetId = direction === LoadMoreDirection.Backwards ? viewportIds[0] : viewportIds[viewportIds.length - 1];
|
|
const isOutlying = Boolean(listedIds && !listedIds.includes(offsetId));
|
|
const historyIds = (isOutlying
|
|
? selectOutlyingListByMessageId(global, chatId, threadId, offsetId) : listedIds)!;
|
|
const {
|
|
newViewportIds, areSomeLocal, areAllLocal,
|
|
} = getViewportSlice(historyIds, offsetId, direction);
|
|
|
|
if (areSomeLocal) {
|
|
global = safeReplaceViewportIds(global, chatId, threadId, newViewportIds, tabId);
|
|
}
|
|
|
|
onTickEnd(() => {
|
|
void loadWithBudget(
|
|
global, actions, areAllLocal, isOutlying, isBudgetPreload, chat, threadId!, direction, offsetId, tabId,
|
|
);
|
|
});
|
|
|
|
if (isBudgetPreload) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
setGlobal(global, { forceOnHeavyAnimation: shouldForceRender });
|
|
});
|
|
|
|
async function loadWithBudget<T extends GlobalState>(
|
|
global: T,
|
|
actions: RequiredGlobalActions,
|
|
areAllLocal: boolean, isOutlying: boolean, isBudgetPreload: boolean,
|
|
chat: ApiChat, threadId: number, direction: LoadMoreDirection, offsetId?: number,
|
|
...[tabId = getCurrentTabId()]: TabArgs<T>
|
|
) {
|
|
if (!areAllLocal) {
|
|
await loadViewportMessages(
|
|
global, chat, threadId, offsetId, direction, isOutlying, isBudgetPreload, tabId,
|
|
);
|
|
}
|
|
|
|
if (!isBudgetPreload) {
|
|
actions.loadViewportMessages({
|
|
chatId: chat.id, threadId, direction, isBudgetPreload: true, tabId,
|
|
});
|
|
}
|
|
}
|
|
|
|
addActionHandler('loadMessage', async (global, actions, payload): Promise<void> => {
|
|
const {
|
|
chatId, messageId, replyOriginForId, threadUpdate,
|
|
} = payload!;
|
|
|
|
const chat = selectChat(global, chatId);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
const message = await loadMessage(global, chat, messageId, replyOriginForId);
|
|
if (message && threadUpdate) {
|
|
const { lastMessageId, isDeleting } = threadUpdate;
|
|
global = getGlobal();
|
|
|
|
global = updateThreadUnreadFromForwardedMessage(
|
|
global,
|
|
message,
|
|
chatId,
|
|
lastMessageId,
|
|
isDeleting,
|
|
);
|
|
setGlobal(global);
|
|
}
|
|
});
|
|
|
|
addActionHandler('sendMessage', (global, actions, payload): ActionReturnType => {
|
|
const { messageList, tabId = getCurrentTabId() } = payload;
|
|
|
|
const { storyId, peerId: storyPeerId } = selectCurrentViewedStory(global, tabId);
|
|
const isStoryReply = Boolean(storyId && storyPeerId);
|
|
|
|
if (!messageList && !isStoryReply) {
|
|
return undefined;
|
|
}
|
|
|
|
let { chatId, threadId, type } = messageList || {};
|
|
if (isStoryReply) {
|
|
chatId = storyPeerId!;
|
|
threadId = MAIN_THREAD_ID;
|
|
type = 'thread';
|
|
}
|
|
|
|
payload = omit(payload, ['tabId']);
|
|
|
|
if (type === 'scheduled' && !payload.scheduledAt) {
|
|
return updateTabState(global, {
|
|
contentToBeScheduled: payload,
|
|
}, tabId);
|
|
}
|
|
|
|
const chat = selectChat(global, chatId!)!;
|
|
const draftReplyInfo = !isStoryReply ? selectDraft(global, chatId!, threadId!)?.replyInfo : undefined;
|
|
|
|
const storyReplyInfo = isStoryReply ? {
|
|
type: 'story',
|
|
userId: storyPeerId!,
|
|
storyId: storyId!,
|
|
} satisfies ApiInputStoryReplyInfo : undefined;
|
|
|
|
const messageReplyInfo = selectMessageReplyInfo(global, chatId!, threadId!, draftReplyInfo);
|
|
|
|
const replyInfo = storyReplyInfo || messageReplyInfo;
|
|
|
|
const params = {
|
|
...payload,
|
|
chat,
|
|
replyInfo,
|
|
noWebPage: selectNoWebPage(global, chatId!, threadId!),
|
|
sendAs: selectSendAs(global, chatId!),
|
|
};
|
|
|
|
if (!isStoryReply) {
|
|
actions.resetDraftReplyInfo({ tabId });
|
|
actions.clearWebPagePreview({ tabId });
|
|
}
|
|
|
|
const isSingle = !payload.attachments || payload.attachments.length <= 1;
|
|
const isGrouped = !isSingle && payload.shouldGroupMessages;
|
|
|
|
if (isSingle) {
|
|
const { attachments, ...restParams } = params;
|
|
sendMessage(global, {
|
|
...restParams,
|
|
attachment: attachments ? attachments[0] : undefined,
|
|
});
|
|
} else if (isGrouped) {
|
|
const {
|
|
text, entities, attachments, ...commonParams
|
|
} = params;
|
|
const byType = splitAttachmentsByType(attachments!);
|
|
|
|
byType.forEach((group, groupIndex) => {
|
|
const groupedAttachments = split(group as ApiAttachment[], MAX_MEDIA_FILES_FOR_ALBUM);
|
|
for (let i = 0; i < groupedAttachments.length; i++) {
|
|
const [firstAttachment, ...restAttachments] = groupedAttachments[i];
|
|
const groupedId = `${Date.now()}${groupIndex}${i}`;
|
|
|
|
const isFirst = i === 0 && groupIndex === 0;
|
|
|
|
sendMessage(global, {
|
|
...commonParams,
|
|
text: isFirst ? text : undefined,
|
|
entities: isFirst ? entities : undefined,
|
|
attachment: firstAttachment,
|
|
groupedId: restAttachments.length > 0 ? groupedId : undefined,
|
|
});
|
|
|
|
restAttachments.forEach((attachment: ApiAttachment) => {
|
|
sendMessage(global, {
|
|
...commonParams,
|
|
attachment,
|
|
groupedId,
|
|
});
|
|
});
|
|
}
|
|
});
|
|
} else {
|
|
const {
|
|
text, entities, attachments, replyInfo: replyToForFirstMessage, ...commonParams
|
|
} = params;
|
|
|
|
if (text) {
|
|
sendMessage(global, {
|
|
...commonParams,
|
|
text,
|
|
entities,
|
|
replyInfo: replyToForFirstMessage,
|
|
});
|
|
}
|
|
|
|
attachments?.forEach((attachment: ApiAttachment) => {
|
|
sendMessage(global, {
|
|
...commonParams,
|
|
attachment,
|
|
});
|
|
});
|
|
}
|
|
|
|
return undefined;
|
|
});
|
|
|
|
addActionHandler('sendInviteMessages', async (global, actions, payload): Promise<void> => {
|
|
const { chatId, userIds, tabId = getCurrentTabId() } = payload;
|
|
const chatFullInfo = selectChatFullInfo(global, chatId);
|
|
if (!chatFullInfo?.inviteLink) {
|
|
return undefined;
|
|
}
|
|
const userFullNames: string[] = [];
|
|
await Promise.all(userIds.map((userId) => {
|
|
const chat = selectChat(global, userId);
|
|
if (!chat) {
|
|
return undefined;
|
|
}
|
|
const userFullName = getUserFullName(selectUser(global, userId));
|
|
if (userFullName) {
|
|
userFullNames.push(userFullName);
|
|
}
|
|
return sendMessage(global, {
|
|
chat,
|
|
text: chatFullInfo.inviteLink,
|
|
});
|
|
}));
|
|
return actions.showNotification({
|
|
message: translate('Conversation.ShareLinkTooltip.Chat.One', userFullNames.join(', ')),
|
|
tabId,
|
|
});
|
|
});
|
|
|
|
addActionHandler('editMessage', (global, actions, payload): ActionReturnType => {
|
|
const {
|
|
messageList, text, entities, tabId = getCurrentTabId(),
|
|
} = payload;
|
|
|
|
if (!messageList) {
|
|
return;
|
|
}
|
|
|
|
const { chatId, threadId, type: messageListType } = messageList;
|
|
const chat = selectChat(global, chatId);
|
|
const message = selectEditingMessage(global, chatId, threadId, messageListType);
|
|
if (!chat || !message) {
|
|
return;
|
|
}
|
|
|
|
void callApi('editMessage', {
|
|
chat, message, text, entities, noWebPage: selectNoWebPage(global, chatId, threadId),
|
|
});
|
|
|
|
actions.setEditingId({ messageId: undefined, tabId });
|
|
});
|
|
|
|
addActionHandler('cancelSendingMessage', (global, actions, payload): ActionReturnType => {
|
|
const { chatId, messageId } = payload!;
|
|
const message = selectChatMessage(global, chatId, messageId);
|
|
const progressCallback = message && uploadProgressCallbacks.get(getMessageOriginalId(message));
|
|
if (progressCallback) {
|
|
cancelApiProgress(progressCallback);
|
|
}
|
|
|
|
actions.apiUpdate({
|
|
'@type': 'deleteMessages',
|
|
ids: [messageId],
|
|
chatId,
|
|
});
|
|
});
|
|
|
|
addActionHandler('saveDraft', (global, actions, payload): ActionReturnType => {
|
|
const {
|
|
chatId, threadId, text,
|
|
} = payload;
|
|
if (!text) {
|
|
return;
|
|
}
|
|
|
|
const currentDraft = selectDraft(global, chatId, threadId);
|
|
|
|
const newDraft: ApiDraft = {
|
|
text,
|
|
replyInfo: currentDraft?.replyInfo,
|
|
};
|
|
|
|
saveDraft(global, chatId, threadId, newDraft);
|
|
});
|
|
|
|
addActionHandler('clearDraft', (global, actions, payload): ActionReturnType => {
|
|
const {
|
|
chatId, threadId = MAIN_THREAD_ID, isLocalOnly, shouldKeepReply,
|
|
} = payload;
|
|
const currentDraft = selectDraft(global, chatId, threadId);
|
|
if (!currentDraft) {
|
|
return;
|
|
}
|
|
|
|
const currentReplyInfo = currentDraft.replyInfo;
|
|
|
|
const newDraft: ApiDraft | undefined = shouldKeepReply && currentReplyInfo ? {
|
|
replyInfo: currentReplyInfo,
|
|
} : undefined;
|
|
|
|
if (!isLocalOnly) {
|
|
saveDraft(global, chatId, threadId, newDraft);
|
|
}
|
|
});
|
|
|
|
addActionHandler('updateDraftReplyInfo', (global, actions, payload): ActionReturnType => {
|
|
const { tabId = getCurrentTabId(), ...update } = payload;
|
|
const currentMessageList = selectCurrentMessageList(global, tabId);
|
|
if (!currentMessageList) {
|
|
return;
|
|
}
|
|
|
|
const { chatId, threadId } = currentMessageList;
|
|
|
|
const currentDraft = selectDraft(global, chatId, threadId);
|
|
|
|
const updatedReplyInfo = {
|
|
type: 'message',
|
|
...currentDraft?.replyInfo,
|
|
...update,
|
|
} as ApiInputMessageReplyInfo;
|
|
|
|
if (!updatedReplyInfo.replyToMsgId) return;
|
|
|
|
const newDraft: ApiDraft = {
|
|
...currentDraft,
|
|
replyInfo: updatedReplyInfo,
|
|
};
|
|
|
|
saveDraft(global, chatId, threadId, newDraft);
|
|
});
|
|
|
|
addActionHandler('resetDraftReplyInfo', (global, actions, payload): ActionReturnType => {
|
|
const { tabId = getCurrentTabId() } = payload || {};
|
|
const currentMessageList = selectCurrentMessageList(global, tabId);
|
|
if (!currentMessageList) {
|
|
return;
|
|
}
|
|
const { chatId, threadId } = currentMessageList;
|
|
|
|
const currentDraft = selectDraft(global, chatId, threadId);
|
|
const newDraft: ApiDraft | undefined = !currentDraft?.text ? undefined : {
|
|
...currentDraft,
|
|
replyInfo: undefined,
|
|
};
|
|
|
|
saveDraft(global, chatId, threadId, newDraft);
|
|
});
|
|
|
|
async function saveDraft<T extends GlobalState>(global: T, chatId: string, threadId: number, draft?: ApiDraft) {
|
|
const chat = selectChat(global, chatId);
|
|
const user = selectUser(global, chatId);
|
|
if (!chat || (user && isDeletedUser(user))) return;
|
|
|
|
const replyInfo = selectMessageReplyInfo(global, chatId, threadId, draft?.replyInfo);
|
|
|
|
const newDraft: ApiDraft | undefined = draft ? {
|
|
...draft,
|
|
replyInfo,
|
|
date: Math.floor(Date.now() / 1000),
|
|
isLocal: true,
|
|
} : undefined;
|
|
|
|
global = replaceThreadParam(global, chatId, threadId, 'draft', newDraft);
|
|
global = updateChat(global, chatId, { draftDate: newDraft?.date });
|
|
|
|
setGlobal(global);
|
|
|
|
const result = await callApi('saveDraft', {
|
|
chat,
|
|
draft: newDraft,
|
|
});
|
|
|
|
if (result && newDraft) {
|
|
newDraft.isLocal = false;
|
|
}
|
|
|
|
global = getGlobal();
|
|
global = replaceThreadParam(global, chatId, threadId, 'draft', newDraft);
|
|
global = updateChat(global, chatId, { draftDate: newDraft?.date });
|
|
|
|
setGlobal(global);
|
|
}
|
|
|
|
addActionHandler('toggleMessageWebPage', (global, actions, payload): ActionReturnType => {
|
|
const { chatId, threadId, noWebPage } = payload!;
|
|
|
|
return replaceThreadParam(global, chatId, threadId, 'noWebPage', noWebPage);
|
|
});
|
|
|
|
addActionHandler('pinMessage', (global, actions, payload): ActionReturnType => {
|
|
const {
|
|
messageId, isUnpin, isOneSide, isSilent, tabId = getCurrentTabId(),
|
|
} = payload;
|
|
|
|
const chat = selectCurrentChat(global, tabId);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
void callApi('pinMessage', {
|
|
chat, messageId, isUnpin, isOneSide, isSilent,
|
|
});
|
|
});
|
|
|
|
addActionHandler('unpinAllMessages', async (global, actions, payload): Promise<void> => {
|
|
const { chatId, threadId } = payload;
|
|
const chat = selectChat(global, chatId);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
const topId = selectThreadTopMessageId(global, chatId, threadId);
|
|
await callApi('unpinAllMessages', { chat, threadId: topId });
|
|
|
|
global = getGlobal();
|
|
const pinnedIds = selectPinnedIds(global, chatId, threadId);
|
|
pinnedIds?.forEach((id) => {
|
|
global = updateChatMessage(global, chatId, id, { isPinned: false });
|
|
});
|
|
global = replaceThreadParam(global, chat.id, MAIN_THREAD_ID, 'pinnedIds', []);
|
|
setGlobal(global);
|
|
});
|
|
|
|
addActionHandler('deleteMessages', (global, actions, payload): ActionReturnType => {
|
|
const { messageIds, shouldDeleteForAll, tabId = getCurrentTabId() } = payload!;
|
|
const currentMessageList = selectCurrentMessageList(global, tabId);
|
|
if (!currentMessageList) {
|
|
return;
|
|
}
|
|
const { chatId, threadId } = currentMessageList;
|
|
const chat = selectChat(global, chatId)!;
|
|
const messageIdsToDelete = messageIds.filter((id) => {
|
|
const message = selectChatMessage(global, chatId, id);
|
|
return message && !isMessageLocal(message);
|
|
});
|
|
|
|
// Only local messages
|
|
if (!messageIdsToDelete.length && messageIds.length) {
|
|
deleteMessages(global, isChatChannel(chat) ? chatId : undefined, messageIds, actions);
|
|
return;
|
|
}
|
|
|
|
void callApi('deleteMessages', { chat, messageIds: messageIdsToDelete, shouldDeleteForAll });
|
|
|
|
const editingId = selectEditingId(global, chatId, threadId);
|
|
if (editingId && messageIds.includes(editingId)) {
|
|
actions.setEditingId({ messageId: undefined, tabId });
|
|
}
|
|
});
|
|
|
|
addActionHandler('deleteScheduledMessages', (global, actions, payload): ActionReturnType => {
|
|
const { messageIds, tabId = getCurrentTabId() } = payload;
|
|
const currentMessageList = selectCurrentMessageList(global, tabId);
|
|
if (!currentMessageList) {
|
|
return;
|
|
}
|
|
|
|
const { chatId } = currentMessageList;
|
|
const chat = selectChat(global, chatId)!;
|
|
|
|
void callApi('deleteScheduledMessages', { chat, messageIds });
|
|
|
|
const editingId = selectEditingScheduledId(global, chatId);
|
|
if (editingId && messageIds.includes(editingId)) {
|
|
actions.setEditingId({ messageId: undefined, tabId });
|
|
}
|
|
});
|
|
|
|
addActionHandler('deleteHistory', async (global, actions, payload): Promise<void> => {
|
|
const { chatId, shouldDeleteForAll, tabId = getCurrentTabId() } = payload!;
|
|
const chat = selectChat(global, chatId);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
await callApi('deleteHistory', { chat, shouldDeleteForAll });
|
|
|
|
global = getGlobal();
|
|
const activeChat = selectCurrentMessageList(global, tabId);
|
|
if (activeChat && activeChat.chatId === chatId) {
|
|
actions.openChat({ id: undefined, tabId });
|
|
}
|
|
|
|
// Delete chat from folders
|
|
const folders = global.chatFolders.byId;
|
|
Object.values(folders).forEach((folder) => {
|
|
if (folder.includedChatIds.includes(chatId) || folder.pinnedChatIds?.includes(chatId)) {
|
|
const newIncludedChatIds = folder.includedChatIds.filter((id) => id !== chatId);
|
|
const newPinnedChatIds = folder.pinnedChatIds?.filter((id) => id !== chatId);
|
|
|
|
const updatedFolder = {
|
|
...folder,
|
|
includedChatIds: newIncludedChatIds,
|
|
pinnedChatIds: newPinnedChatIds,
|
|
};
|
|
|
|
callApi('editChatFolder', {
|
|
id: folder.id,
|
|
folderUpdate: updatedFolder,
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
addActionHandler('reportMessages', async (global, actions, payload): Promise<void> => {
|
|
const {
|
|
messageIds, reason, description, tabId = getCurrentTabId(),
|
|
} = payload!;
|
|
const currentMessageList = selectCurrentMessageList(global, tabId);
|
|
if (!currentMessageList) {
|
|
return;
|
|
}
|
|
|
|
const { chatId } = currentMessageList;
|
|
const chat = selectChat(global, chatId)!;
|
|
|
|
const result = await callApi('reportMessages', {
|
|
peer: chat, messageIds, reason, description,
|
|
});
|
|
|
|
actions.showNotification({
|
|
message: result
|
|
? translate('ReportPeer.AlertSuccess')
|
|
: 'An error occurred while submitting your report. Please, try again later.',
|
|
tabId,
|
|
});
|
|
});
|
|
|
|
addActionHandler('sendMessageAction', async (global, actions, payload): Promise<void> => {
|
|
const { action, chatId, threadId } = payload!;
|
|
if (global.connectionState !== 'connectionStateReady') return;
|
|
if (chatId === global.currentUserId) return; // Message actions are disabled in Saved Messages
|
|
|
|
const chat = selectChat(global, chatId)!;
|
|
if (!chat) return;
|
|
const user = selectUser(global, chatId);
|
|
if (user && (isUserBot(user) || isDeletedUser(user))) return;
|
|
|
|
await callApi('sendMessageAction', {
|
|
peer: chat, threadId, action,
|
|
});
|
|
});
|
|
|
|
addActionHandler('markMessageListRead', (global, actions, payload): ActionReturnType => {
|
|
const { maxId, tabId = getCurrentTabId() } = payload!;
|
|
|
|
const currentMessageList = selectCurrentMessageList(global, tabId);
|
|
if (!currentMessageList) {
|
|
return undefined;
|
|
}
|
|
|
|
const { chatId, threadId } = currentMessageList;
|
|
const chat = selectChat(global, chatId);
|
|
if (!chat) {
|
|
return undefined;
|
|
}
|
|
|
|
runDebouncedForMarkRead(() => {
|
|
void callApi('markMessageListRead', {
|
|
chat, threadId, maxId,
|
|
});
|
|
});
|
|
|
|
if (chatId === SERVICE_NOTIFICATIONS_USER_ID) {
|
|
global = {
|
|
...global,
|
|
serviceNotifications: global.serviceNotifications.map((notification) => {
|
|
return notification.isUnread && notification.id <= maxId ? { ...notification, isUnread: false } : notification;
|
|
}),
|
|
};
|
|
}
|
|
|
|
const viewportIds = selectViewportIds(global, chatId, threadId, tabId);
|
|
const minId = selectFirstUnreadId(global, chatId, threadId);
|
|
if (!viewportIds || !minId || !chat.unreadCount) {
|
|
return global;
|
|
}
|
|
|
|
const readCount = countSortedIds(viewportIds!, minId, maxId);
|
|
if (!readCount) {
|
|
return global;
|
|
}
|
|
|
|
if (chat.isForum && chat.topics?.[threadId]) {
|
|
const topic = chat.topics[threadId];
|
|
global = updateThreadInfo(global, chatId, threadId, {
|
|
lastReadInboxMessageId: maxId,
|
|
});
|
|
const newTopicUnreadCount = Math.max(0, topic.unreadCount - readCount);
|
|
if (newTopicUnreadCount === 0) {
|
|
global = updateChat(global, chatId, {
|
|
unreadCount: Math.max(0, chat.unreadCount - 1),
|
|
});
|
|
}
|
|
return updateTopic(global, chatId, threadId, {
|
|
unreadCount: newTopicUnreadCount,
|
|
});
|
|
}
|
|
|
|
// TODO Support local marking read for comments
|
|
if (threadId !== MAIN_THREAD_ID) {
|
|
return undefined;
|
|
}
|
|
|
|
return updateChat(global, chatId, {
|
|
lastReadInboxMessageId: maxId,
|
|
unreadCount: Math.max(0, chat.unreadCount - readCount),
|
|
});
|
|
});
|
|
|
|
addActionHandler('markMessagesRead', (global, actions, payload): ActionReturnType => {
|
|
const { messageIds, tabId = getCurrentTabId() } = payload!;
|
|
|
|
const chat = selectCurrentChat(global, tabId);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
void callApi('markMessagesRead', { chat, messageIds });
|
|
});
|
|
|
|
addActionHandler('loadWebPagePreview', async (global, actions, payload): Promise<void> => {
|
|
const { text, tabId = getCurrentTabId() } = payload;
|
|
|
|
const webPagePreview = await callApi('fetchWebPagePreview', { text });
|
|
|
|
global = getGlobal();
|
|
global = updateTabState(global, {
|
|
webPagePreview,
|
|
}, tabId);
|
|
setGlobal(global);
|
|
});
|
|
|
|
addActionHandler('clearWebPagePreview', (global, actions, payload): ActionReturnType => {
|
|
const { tabId = getCurrentTabId() } = payload || {};
|
|
if (!selectTabState(global, tabId).webPagePreview) {
|
|
return undefined;
|
|
}
|
|
|
|
return updateTabState(global, {
|
|
webPagePreview: undefined,
|
|
}, tabId);
|
|
});
|
|
|
|
addActionHandler('sendPollVote', (global, actions, payload): ActionReturnType => {
|
|
const { chatId, messageId, options } = payload!;
|
|
const chat = selectChat(global, chatId);
|
|
|
|
if (chat) {
|
|
void callApi('sendPollVote', { chat, messageId, options });
|
|
}
|
|
});
|
|
|
|
addActionHandler('cancelPollVote', (global, actions, payload): ActionReturnType => {
|
|
const { chatId, messageId } = payload!;
|
|
const chat = selectChat(global, chatId);
|
|
|
|
if (chat) {
|
|
void callApi('sendPollVote', { chat, messageId, options: [] });
|
|
}
|
|
});
|
|
|
|
addActionHandler('closePoll', (global, actions, payload): ActionReturnType => {
|
|
const { chatId, messageId } = payload;
|
|
const chat = selectChat(global, chatId);
|
|
const poll = selectChatMessage(global, chatId, messageId)?.content.poll;
|
|
if (chat && poll) {
|
|
void callApi('closePoll', { chat, messageId, poll });
|
|
}
|
|
});
|
|
|
|
addActionHandler('loadPollOptionResults', async (global, actions, payload): Promise<void> => {
|
|
const {
|
|
chat, messageId, option, offset, limit, shouldResetVoters, tabId = getCurrentTabId(),
|
|
} = payload!;
|
|
|
|
const result = await callApi('loadPollOptionResults', {
|
|
chat, messageId, option, offset, limit,
|
|
});
|
|
|
|
if (!result) {
|
|
return;
|
|
}
|
|
|
|
global = getGlobal();
|
|
|
|
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
|
global = addChats(global, buildCollectionByKey(result.chats, 'id'));
|
|
|
|
const tabState = selectTabState(global, tabId);
|
|
const { pollResults } = tabState;
|
|
const { voters } = tabState.pollResults;
|
|
|
|
global = updateTabState(global, {
|
|
pollResults: {
|
|
...pollResults,
|
|
voters: {
|
|
...voters,
|
|
[option]: unique([
|
|
...(!shouldResetVoters && voters?.[option] ? voters[option] : []),
|
|
...result.votes.map((vote) => vote.peerId),
|
|
]),
|
|
},
|
|
offsets: {
|
|
...(pollResults.offsets ? pollResults.offsets : {}),
|
|
[option]: result.nextOffset || '',
|
|
},
|
|
},
|
|
}, tabId);
|
|
setGlobal(global);
|
|
});
|
|
|
|
addActionHandler('loadExtendedMedia', (global, actions, payload): ActionReturnType => {
|
|
const { chatId, ids } = payload;
|
|
const chat = selectChat(global, chatId);
|
|
if (chat) {
|
|
void callApi('fetchExtendedMedia', { chat, ids });
|
|
}
|
|
});
|
|
|
|
addActionHandler('forwardMessages', (global, actions, payload): ActionReturnType => {
|
|
const {
|
|
isSilent, scheduledAt, tabId = getCurrentTabId(),
|
|
} = payload;
|
|
|
|
const {
|
|
fromChatId, messageIds, toChatId, withMyScore, noAuthors, noCaptions, toThreadId,
|
|
} = selectTabState(global, tabId).forwardMessages;
|
|
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
|
const fromChat = fromChatId ? selectChat(global, fromChatId) : undefined;
|
|
const toChat = toChatId ? selectChat(global, toChatId) : undefined;
|
|
const messages = fromChatId && messageIds
|
|
? messageIds
|
|
.sort((a, b) => a - b)
|
|
.map((id) => selectChatMessage(global, fromChatId, id)).filter(Boolean)
|
|
: undefined;
|
|
|
|
if (!fromChat || !toChat || !messages || (toThreadId && !toChat.isForum)) {
|
|
return;
|
|
}
|
|
|
|
const sendAs = selectSendAs(global, toChatId!);
|
|
|
|
const realMessages = messages.filter((m) => !isServiceNotificationMessage(m));
|
|
if (realMessages.length) {
|
|
(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,
|
|
});
|
|
})();
|
|
}
|
|
|
|
messages
|
|
.filter((m) => isServiceNotificationMessage(m))
|
|
.forEach((message) => {
|
|
const { text, entities } = message.content.text || {};
|
|
const { sticker, poll } = message.content;
|
|
|
|
const replyInfo = selectMessageReplyInfo(global, toChat.id, toThreadId);
|
|
|
|
void sendMessage(global, {
|
|
chat: toChat,
|
|
replyInfo,
|
|
text,
|
|
entities,
|
|
sticker,
|
|
poll,
|
|
isSilent,
|
|
scheduledAt,
|
|
sendAs,
|
|
});
|
|
});
|
|
|
|
global = getGlobal();
|
|
global = updateTabState(global, {
|
|
forwardMessages: {},
|
|
}, tabId);
|
|
setGlobal(global);
|
|
});
|
|
|
|
addActionHandler('loadScheduledHistory', async (global, actions, payload): Promise<void> => {
|
|
const { chatId } = payload;
|
|
const chat = selectChat(global, chatId);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
const result = await callApi('fetchScheduledHistory', { chat });
|
|
if (!result) {
|
|
return;
|
|
}
|
|
|
|
const { messages } = result;
|
|
|
|
const byId = buildCollectionByKey(messages, 'id');
|
|
const ids = Object.keys(byId).map(Number).sort((a, b) => b - a);
|
|
|
|
global = getGlobal();
|
|
global = replaceScheduledMessages(global, chat.id, byId);
|
|
global = replaceThreadParam(global, chat.id, MAIN_THREAD_ID, 'scheduledIds', ids);
|
|
if (chat?.isForum) {
|
|
const scheduledPerThread: Record<number, number[]> = {};
|
|
messages.forEach((message) => {
|
|
const threadId = selectThreadIdFromMessage(global, message);
|
|
const scheduledInThread = scheduledPerThread[threadId] || [];
|
|
scheduledInThread.push(message.id);
|
|
scheduledPerThread[threadId] = scheduledInThread;
|
|
});
|
|
|
|
Object.entries(scheduledPerThread).forEach(([threadId, scheduledIds]) => {
|
|
global = replaceThreadParam(global, chat.id, Number(threadId), 'scheduledIds', scheduledIds);
|
|
});
|
|
}
|
|
setGlobal(global);
|
|
});
|
|
|
|
addActionHandler('sendScheduledMessages', (global, actions, payload): ActionReturnType => {
|
|
const {
|
|
chatId, id,
|
|
} = payload!;
|
|
|
|
const chat = selectChat(global, chatId);
|
|
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
void callApi('sendScheduledMessages', {
|
|
chat,
|
|
ids: [id],
|
|
});
|
|
});
|
|
|
|
addActionHandler('rescheduleMessage', (global, actions, payload): ActionReturnType => {
|
|
const {
|
|
chatId, messageId, scheduledAt,
|
|
} = payload!;
|
|
|
|
const chat = selectChat(global, chatId);
|
|
const message = chat && selectScheduledMessage(global, chat.id, messageId);
|
|
if (!chat || !message) {
|
|
return;
|
|
}
|
|
|
|
void callApi('rescheduleMessage', {
|
|
chat,
|
|
message,
|
|
scheduledAt,
|
|
});
|
|
});
|
|
|
|
addActionHandler('requestThreadInfoUpdate', async (global, actions, payload): Promise<void> => {
|
|
const { chatId, threadId } = payload;
|
|
const chat = selectChat(global, chatId);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
const originChannelId = selectThreadOriginChat(global, chatId, threadId)?.id;
|
|
|
|
const result = await callApi('requestThreadInfoUpdate', { chat, threadId, originChannelId });
|
|
if (!result) return;
|
|
global = getGlobal();
|
|
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
|
setGlobal(global);
|
|
});
|
|
|
|
addActionHandler('transcribeAudio', async (global, actions, payload): Promise<void> => {
|
|
const { messageId, chatId } = payload;
|
|
|
|
const chat = selectChat(global, chatId);
|
|
|
|
if (!chat) return;
|
|
|
|
global = updateChatMessage(global, chatId, messageId, {
|
|
transcriptionId: '',
|
|
});
|
|
|
|
setGlobal(global);
|
|
|
|
const result = await callApi('transcribeAudio', { chat, messageId });
|
|
|
|
global = getGlobal();
|
|
global = updateChatMessage(global, chatId, messageId, {
|
|
transcriptionId: result,
|
|
isTranscriptionError: !result,
|
|
});
|
|
|
|
setGlobal(global);
|
|
});
|
|
|
|
addActionHandler('loadCustomEmojis', async (global, actions, payload): Promise<void> => {
|
|
const { ids, ignoreCache } = payload;
|
|
const newCustomEmojiIds = ignoreCache ? ids
|
|
: unique(ids.filter((documentId) => !global.customEmojis.byId[documentId]));
|
|
const customEmoji = await callApi('fetchCustomEmoji', {
|
|
documentId: newCustomEmojiIds,
|
|
});
|
|
if (!customEmoji) return;
|
|
|
|
global = getGlobal();
|
|
global = {
|
|
...global,
|
|
customEmojis: {
|
|
...global.customEmojis,
|
|
byId: {
|
|
...global.customEmojis.byId,
|
|
...buildCollectionByKey(customEmoji, 'id'),
|
|
},
|
|
},
|
|
};
|
|
setGlobal(global);
|
|
});
|
|
|
|
async function loadViewportMessages<T extends GlobalState>(
|
|
global: T,
|
|
chat: ApiChat,
|
|
threadId: number,
|
|
offsetId: number | undefined,
|
|
direction: LoadMoreDirection,
|
|
isOutlying = false,
|
|
isBudgetPreload = false,
|
|
...[tabId = getCurrentTabId()]: TabArgs<T>
|
|
) {
|
|
const chatId = chat.id;
|
|
|
|
let addOffset: number | undefined;
|
|
let sliceSize = MESSAGE_LIST_SLICE;
|
|
switch (direction) {
|
|
case LoadMoreDirection.Backwards:
|
|
if (offsetId) {
|
|
addOffset = -1;
|
|
sliceSize += 1;
|
|
} else {
|
|
addOffset = undefined;
|
|
}
|
|
break;
|
|
case LoadMoreDirection.Around:
|
|
addOffset = -(Math.round(MESSAGE_LIST_SLICE / 2) + 1);
|
|
break;
|
|
case LoadMoreDirection.Forwards:
|
|
addOffset = -(MESSAGE_LIST_SLICE + 1);
|
|
if (offsetId) {
|
|
sliceSize += 1;
|
|
}
|
|
break;
|
|
}
|
|
|
|
global = getGlobal();
|
|
const result = await callApi('fetchMessages', {
|
|
chat: selectChat(global, chatId)!,
|
|
offsetId,
|
|
addOffset,
|
|
limit: sliceSize,
|
|
threadId,
|
|
});
|
|
|
|
if (!result) {
|
|
return;
|
|
}
|
|
|
|
const {
|
|
messages, users, chats, repliesThreadInfos,
|
|
} = result;
|
|
|
|
global = getGlobal();
|
|
|
|
const localMessages = chatId === SERVICE_NOTIFICATIONS_USER_ID
|
|
? global.serviceNotifications.filter(({ isDeleted }) => !isDeleted).map(({ message }) => message)
|
|
: [];
|
|
const allMessages = ([] as ApiMessage[]).concat(messages, localMessages);
|
|
const byId = buildCollectionByKey(allMessages, 'id');
|
|
const ids = Object.keys(byId).map(Number);
|
|
|
|
if (threadId !== MAIN_THREAD_ID) {
|
|
const threadFirstMessageId = selectFirstMessageId(global, chatId, threadId) || {};
|
|
if ((!ids[0] || threadFirstMessageId === ids[0]) && threadFirstMessageId !== threadId) {
|
|
ids.unshift(threadId);
|
|
}
|
|
}
|
|
|
|
global = addChatMessagesById(global, chatId, byId);
|
|
global = isOutlying
|
|
? updateOutlyingLists(global, chatId, threadId, ids)
|
|
: updateListedIds(global, chatId, threadId, ids);
|
|
|
|
global = addUsers(global, buildCollectionByKey(users, 'id'));
|
|
global = addChats(global, buildCollectionByKey(chats, 'id'));
|
|
global = updateThreadInfos(global, repliesThreadInfos);
|
|
|
|
let listedIds = selectListedIds(global, chatId, threadId);
|
|
const outlyingList = offsetId ? selectOutlyingListByMessageId(global, chatId, threadId, offsetId) : undefined;
|
|
|
|
if (isOutlying && listedIds && outlyingList) {
|
|
if (!outlyingList.length || areSortedArraysIntersecting(listedIds, outlyingList)) {
|
|
global = updateListedIds(global, chatId, threadId, outlyingList);
|
|
listedIds = selectListedIds(global, chatId, threadId);
|
|
global = removeOutlyingList(global, chatId, threadId, outlyingList);
|
|
isOutlying = false;
|
|
}
|
|
}
|
|
|
|
if (!isBudgetPreload) {
|
|
const historyIds = isOutlying ? outlyingList! : listedIds!;
|
|
const { newViewportIds } = getViewportSlice(historyIds, offsetId, direction);
|
|
global = safeReplaceViewportIds(global, chatId, threadId, newViewportIds!, tabId);
|
|
}
|
|
|
|
setGlobal(global);
|
|
}
|
|
|
|
async function loadMessage<T extends GlobalState>(
|
|
global: T, chat: ApiChat, messageId: number, replyOriginForId?: number,
|
|
) {
|
|
const result = await callApi('fetchMessage', { chat, messageId });
|
|
if (!result) {
|
|
return undefined;
|
|
}
|
|
|
|
if (result === MESSAGE_DELETED) {
|
|
if (replyOriginForId) {
|
|
global = getGlobal();
|
|
const replyMessage = selectChatMessage(global, chat.id, replyOriginForId);
|
|
global = updateChatMessage(global, chat.id, replyOriginForId, {
|
|
...replyMessage,
|
|
replyInfo: undefined,
|
|
});
|
|
setGlobal(global);
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
global = getGlobal();
|
|
global = updateChatMessage(global, chat.id, messageId, result.message);
|
|
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
|
setGlobal(global);
|
|
|
|
return result.message;
|
|
}
|
|
|
|
function findClosestIndex(sourceIds: number[], offsetId: number) {
|
|
if (offsetId < sourceIds[0]) {
|
|
return 0;
|
|
}
|
|
|
|
if (offsetId > sourceIds[sourceIds.length - 1]) {
|
|
return sourceIds.length - 1;
|
|
}
|
|
|
|
return sourceIds.findIndex((id, i) => (
|
|
id === offsetId
|
|
|| (id < offsetId && sourceIds[i + 1] > offsetId)
|
|
));
|
|
}
|
|
|
|
function getViewportSlice(
|
|
sourceIds: number[],
|
|
offsetId: number | undefined,
|
|
direction: LoadMoreDirection,
|
|
) {
|
|
const { length } = sourceIds;
|
|
const index = offsetId ? findClosestIndex(sourceIds, offsetId) : -1;
|
|
const isBackwards = direction === LoadMoreDirection.Backwards;
|
|
const isAround = direction === LoadMoreDirection.Around;
|
|
const indexForDirection = isBackwards ? index : (index + 1) || length;
|
|
const sliceSize = isAround ? Math.round(MESSAGE_LIST_SLICE / 2) : MESSAGE_LIST_SLICE;
|
|
const from = indexForDirection - sliceSize;
|
|
const to = indexForDirection + sliceSize - 1;
|
|
const newViewportIds = sourceIds.slice(Math.max(0, from), to + 1);
|
|
|
|
let areSomeLocal;
|
|
let areAllLocal;
|
|
switch (direction) {
|
|
case LoadMoreDirection.Backwards:
|
|
areSomeLocal = indexForDirection >= 0;
|
|
areAllLocal = from >= 0;
|
|
break;
|
|
case LoadMoreDirection.Forwards:
|
|
areSomeLocal = indexForDirection < length;
|
|
areAllLocal = to <= length - 1;
|
|
break;
|
|
case LoadMoreDirection.Around:
|
|
default:
|
|
areSomeLocal = newViewportIds.length > 0;
|
|
areAllLocal = newViewportIds.length === MESSAGE_LIST_SLICE;
|
|
break;
|
|
}
|
|
|
|
return { newViewportIds, areSomeLocal, areAllLocal };
|
|
}
|
|
|
|
async function sendMessage<T extends GlobalState>(global: T, params: {
|
|
chat: ApiChat;
|
|
text?: string;
|
|
entities?: ApiMessageEntity[];
|
|
replyInfo?: ApiInputReplyInfo;
|
|
attachment?: ApiAttachment;
|
|
sticker?: ApiSticker;
|
|
story?: ApiStory | ApiStorySkipped;
|
|
gif?: ApiVideo;
|
|
poll?: ApiNewPoll;
|
|
isSilent?: boolean;
|
|
scheduledAt?: number;
|
|
sendAs?: ApiPeer;
|
|
groupedId?: string;
|
|
}) {
|
|
let localId: number | undefined;
|
|
const progressCallback = params.attachment ? (progress: number, messageLocalId: number) => {
|
|
if (!uploadProgressCallbacks.has(messageLocalId)) {
|
|
localId = messageLocalId;
|
|
uploadProgressCallbacks.set(messageLocalId, progressCallback!);
|
|
}
|
|
|
|
global = getGlobal();
|
|
|
|
global = {
|
|
...global,
|
|
fileUploads: {
|
|
byMessageLocalId: {
|
|
...global.fileUploads.byMessageLocalId,
|
|
[messageLocalId]: { progress },
|
|
},
|
|
},
|
|
};
|
|
setGlobal(global);
|
|
} : undefined;
|
|
|
|
// @optimization
|
|
if (params.replyInfo || IS_IOS) {
|
|
await rafPromise();
|
|
}
|
|
|
|
await callApi('sendMessage', params, progressCallback);
|
|
|
|
if (progressCallback && localId) {
|
|
uploadProgressCallbacks.delete(localId);
|
|
}
|
|
}
|
|
|
|
addActionHandler('loadPinnedMessages', async (global, actions, payload): Promise<void> => {
|
|
const { chatId, threadId } = payload;
|
|
const chat = selectChat(global, chatId);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
const result = await callApi('fetchPinnedMessages', { chat, threadId });
|
|
if (!result) {
|
|
return;
|
|
}
|
|
|
|
const { messages, chats, users } = result;
|
|
|
|
const byId = buildCollectionByKey(messages, 'id');
|
|
const ids = Object.keys(byId).map(Number).sort((a, b) => b - a);
|
|
|
|
global = getGlobal();
|
|
global = addChatMessagesById(global, chat.id, byId);
|
|
global = safeReplacePinnedIds(global, chat.id, threadId, ids);
|
|
global = addUsers(global, buildCollectionByKey(users, 'id'));
|
|
global = addChats(global, buildCollectionByKey(chats, 'id'));
|
|
setGlobal(global);
|
|
});
|
|
|
|
addActionHandler('loadSeenBy', async (global, actions, payload): Promise<void> => {
|
|
const { chatId, messageId } = payload;
|
|
const chat = selectChat(global, chatId);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
const result = await callApi('fetchSeenBy', { chat, messageId });
|
|
if (!result) {
|
|
return;
|
|
}
|
|
|
|
global = getGlobal();
|
|
global = updateChatMessage(global, chatId, messageId, {
|
|
seenByDates: result,
|
|
});
|
|
setGlobal(global);
|
|
});
|
|
|
|
addActionHandler('saveDefaultSendAs', (global, actions, payload): ActionReturnType => {
|
|
const { chatId, sendAsId } = payload;
|
|
const chat = selectChat(global, chatId);
|
|
const sendAsChat = selectChat(global, sendAsId) || selectUser(global, sendAsId);
|
|
if (!chat || !sendAsChat) {
|
|
return undefined;
|
|
}
|
|
|
|
void callApi('saveDefaultSendAs', { sendAs: sendAsChat, chat });
|
|
|
|
return updateChatFullInfo(global, chatId, { sendAsId });
|
|
});
|
|
|
|
addActionHandler('loadSendAs', async (global, actions, payload): Promise<void> => {
|
|
const { chatId } = payload;
|
|
const chat = selectChat(global, chatId);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
const result = await callApi('fetchSendAs', { chat });
|
|
if (!result) {
|
|
global = getGlobal();
|
|
global = updateChat(global, chatId, {
|
|
sendAsPeerIds: [],
|
|
});
|
|
setGlobal(global);
|
|
|
|
return;
|
|
}
|
|
|
|
global = getGlobal();
|
|
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
|
global = addChats(global, buildCollectionByKey(result.chats, 'id'));
|
|
global = updateChat(global, chatId, { sendAsPeerIds: result.sendAs });
|
|
setGlobal(global);
|
|
});
|
|
|
|
addActionHandler('loadSponsoredMessages', async (global, actions, payload): Promise<void> => {
|
|
const { chatId } = payload;
|
|
const chat = selectChat(global, chatId);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
const result = await callApi('fetchSponsoredMessages', { chat });
|
|
if (!result) {
|
|
return;
|
|
}
|
|
|
|
global = getGlobal();
|
|
global = updateSponsoredMessage(global, chatId, result.messages[0]);
|
|
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
|
global = addChats(global, buildCollectionByKey(result.chats, 'id'));
|
|
setGlobal(global);
|
|
});
|
|
|
|
addActionHandler('viewSponsoredMessage', (global, actions, payload): ActionReturnType => {
|
|
const { chatId } = payload;
|
|
const chat = selectChat(global, chatId);
|
|
const message = selectSponsoredMessage(global, chatId);
|
|
if (!chat || !message) {
|
|
return;
|
|
}
|
|
|
|
void callApi('viewSponsoredMessage', { chat, random: message.randomId });
|
|
});
|
|
|
|
addActionHandler('fetchUnreadMentions', async (global, actions, payload): Promise<void> => {
|
|
const { chatId, offsetId } = payload;
|
|
const chat = selectChat(global, chatId);
|
|
if (!chat) return;
|
|
|
|
const result = await callApi('fetchUnreadMentions', { chat, offsetId });
|
|
|
|
if (!result) return;
|
|
|
|
const { messages, chats, users } = result;
|
|
|
|
const byId = buildCollectionByKey(messages, 'id');
|
|
const ids = Object.keys(byId).map(Number);
|
|
|
|
global = getGlobal();
|
|
global = addChatMessagesById(global, chat.id, byId);
|
|
global = addUsers(global, buildCollectionByKey(users, 'id'));
|
|
global = addChats(global, buildCollectionByKey(chats, 'id'));
|
|
global = updateChat(global, chatId, {
|
|
unreadMentions: [...(chat.unreadMentions || []), ...ids],
|
|
});
|
|
|
|
setGlobal(global);
|
|
});
|
|
|
|
addActionHandler('markMentionsRead', (global, actions, payload): ActionReturnType => {
|
|
const { messageIds, tabId = getCurrentTabId() } = payload;
|
|
|
|
const chat = selectCurrentChat(global, tabId);
|
|
if (!chat) return;
|
|
|
|
const unreadMentions = (chat.unreadMentions || []).filter((id) => !messageIds.includes(id));
|
|
global = updateChat(global, chat.id, {
|
|
unreadMentions,
|
|
});
|
|
|
|
setGlobal(global);
|
|
|
|
actions.markMessagesRead({ messageIds, tabId });
|
|
});
|
|
|
|
addActionHandler('focusNextMention', (global, actions, payload): ActionReturnType => {
|
|
const { tabId = getCurrentTabId() } = payload || {};
|
|
|
|
const chat = selectCurrentChat(global, tabId);
|
|
|
|
if (!chat?.unreadMentions) return;
|
|
|
|
actions.focusMessage({ chatId: chat.id, messageId: chat.unreadMentions[0], tabId });
|
|
});
|
|
|
|
addActionHandler('readAllMentions', (global, actions, payload): ActionReturnType => {
|
|
const { tabId = getCurrentTabId() } = payload || {};
|
|
|
|
const chat = selectCurrentChat(global, tabId);
|
|
if (!chat) return undefined;
|
|
|
|
callApi('readAllMentions', { chat });
|
|
|
|
return updateChat(global, chat.id, {
|
|
unreadMentionsCount: undefined,
|
|
unreadMentions: undefined,
|
|
});
|
|
});
|
|
|
|
addActionHandler('openUrl', (global, actions, payload): ActionReturnType => {
|
|
const { url, shouldSkipModal, tabId = getCurrentTabId() } = payload;
|
|
const urlWithProtocol = ensureProtocol(url)!;
|
|
const isStoriesViewerOpen = Boolean(selectTabState(global, tabId).storyViewer.peerId);
|
|
|
|
if (urlWithProtocol.match(RE_TME_LINK) || urlWithProtocol.match(RE_TG_LINK)) {
|
|
if (isStoriesViewerOpen) {
|
|
actions.closeStoryViewer({ tabId });
|
|
}
|
|
|
|
actions.openTelegramLink({ url, tabId });
|
|
return;
|
|
}
|
|
|
|
const { appConfig, config } = global;
|
|
if (appConfig) {
|
|
const parsedUrl = new URL(urlWithProtocol);
|
|
|
|
if (config?.autologinToken && appConfig.autologinDomains.includes(parsedUrl.hostname)) {
|
|
parsedUrl.searchParams.set(AUTOLOGIN_TOKEN_KEY, config.autologinToken);
|
|
window.open(parsedUrl.href, '_blank', 'noopener');
|
|
return;
|
|
}
|
|
|
|
if (appConfig.urlAuthDomains.includes(parsedUrl.hostname)) {
|
|
if (isStoriesViewerOpen) {
|
|
actions.closeStoryViewer({ tabId });
|
|
}
|
|
|
|
actions.requestLinkUrlAuth({ url, tabId });
|
|
return;
|
|
}
|
|
}
|
|
|
|
const shouldDisplayModal = !urlWithProtocol.match(RE_TELEGRAM_LINK) && !shouldSkipModal;
|
|
|
|
if (shouldDisplayModal) {
|
|
actions.toggleSafeLinkModal({ url: urlWithProtocol, tabId });
|
|
} else {
|
|
window.open(urlWithProtocol, '_blank', 'noopener');
|
|
}
|
|
});
|
|
|
|
addActionHandler('setForwardChatOrTopic', async (global, actions, payload): Promise<void> => {
|
|
const { chatId, topicId, tabId = getCurrentTabId() } = payload;
|
|
let user = selectUser(global, chatId);
|
|
if (user && selectForwardsContainVoiceMessages(global, tabId)) {
|
|
let fullInfo = selectUserFullInfo(global, chatId);
|
|
if (!fullInfo) {
|
|
const { accessHash } = user;
|
|
const result = await callApi('fetchFullUser', { id: chatId, accessHash });
|
|
global = getGlobal();
|
|
user = result?.user;
|
|
fullInfo = result?.fullInfo;
|
|
}
|
|
|
|
if (fullInfo!.noVoiceMessages) {
|
|
actions.showDialog({
|
|
data: {
|
|
message: translate('VoiceMessagesRestrictedByPrivacy', getUserFullName(user)),
|
|
},
|
|
tabId,
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!selectForwardsCanBeSentToChat(global, chatId, tabId)) {
|
|
actions.showAllowedMessageTypesNotification({ chatId, tabId });
|
|
return;
|
|
}
|
|
|
|
global = updateTabState(global, {
|
|
forwardMessages: {
|
|
...selectTabState(global, tabId).forwardMessages,
|
|
toChatId: chatId,
|
|
toThreadId: topicId,
|
|
isModalShown: false,
|
|
},
|
|
}, tabId);
|
|
setGlobal(global);
|
|
|
|
actions.openChat({ id: chatId, threadId: topicId, tabId });
|
|
actions.closeMediaViewer({ tabId });
|
|
actions.exitMessageSelectMode({ tabId });
|
|
});
|
|
|
|
addActionHandler('forwardToSavedMessages', (global, actions, payload): ActionReturnType => {
|
|
const { tabId = getCurrentTabId() } = payload || {};
|
|
global = updateTabState(global, {
|
|
forwardMessages: {
|
|
...selectTabState(global, tabId).forwardMessages,
|
|
toChatId: global.currentUserId,
|
|
},
|
|
}, tabId);
|
|
setGlobal(global);
|
|
|
|
actions.exitMessageSelectMode({ tabId });
|
|
actions.forwardMessages({ isSilent: true, tabId });
|
|
});
|
|
|
|
addActionHandler('forwardStory', (global, actions, payload): ActionReturnType => {
|
|
const { toChatId, tabId = getCurrentTabId() } = payload || {};
|
|
|
|
const { fromChatId, storyId } = selectTabState(global, tabId).forwardMessages;
|
|
const fromChat = fromChatId ? selectChat(global, fromChatId) : undefined;
|
|
const toChat = toChatId ? selectChat(global, toChatId) : undefined;
|
|
const story = fromChatId && storyId
|
|
? selectPeerStory(global, fromChatId, storyId)
|
|
: undefined;
|
|
|
|
if (!fromChat || !toChat || !story || 'isDeleted' in story) {
|
|
return;
|
|
}
|
|
|
|
const { text, entities } = (story as ApiStory).content.text || {};
|
|
void sendMessage(global, {
|
|
chat: toChat,
|
|
text,
|
|
entities,
|
|
story,
|
|
});
|
|
|
|
global = getGlobal();
|
|
global = updateTabState(global, {
|
|
forwardMessages: {},
|
|
}, tabId);
|
|
setGlobal(global);
|
|
});
|
|
|
|
addActionHandler('requestMessageTranslation', (global, actions, payload): ActionReturnType => {
|
|
const {
|
|
chatId, id, toLanguageCode = selectTranslationLanguage(global), tabId = getCurrentTabId(),
|
|
} = payload;
|
|
|
|
global = updateRequestedMessageTranslation(global, chatId, id, toLanguageCode, tabId);
|
|
global = replaceSettings(global, {
|
|
translationLanguage: toLanguageCode,
|
|
});
|
|
|
|
return global;
|
|
});
|
|
|
|
addActionHandler('showOriginalMessage', (global, actions, payload): ActionReturnType => {
|
|
const {
|
|
chatId, id, tabId = getCurrentTabId(),
|
|
} = payload;
|
|
|
|
global = removeRequestedMessageTranslation(global, chatId, id, tabId);
|
|
|
|
return global;
|
|
});
|
|
|
|
addActionHandler('markMessagesTranslationPending', (global, actions, payload): ActionReturnType => {
|
|
const {
|
|
chatId, messageIds, toLanguageCode = selectLanguageCode(global),
|
|
} = payload;
|
|
|
|
messageIds.forEach((id) => {
|
|
global = updateMessageTranslation(global, chatId, id, toLanguageCode, {
|
|
isPending: true,
|
|
});
|
|
});
|
|
|
|
return global;
|
|
});
|
|
|
|
addActionHandler('translateMessages', (global, actions, payload): ActionReturnType => {
|
|
const {
|
|
chatId, messageIds, toLanguageCode = selectLanguageCode(global),
|
|
} = payload;
|
|
|
|
const chat = selectChat(global, chatId);
|
|
if (!chat) return undefined;
|
|
|
|
actions.markMessagesTranslationPending({ chatId, messageIds, toLanguageCode });
|
|
|
|
callApi('translateText', {
|
|
chat,
|
|
messageIds,
|
|
toLanguageCode,
|
|
});
|
|
|
|
return global;
|
|
});
|
|
|
|
// https://github.com/telegramdesktop/tdesktop/blob/11906297d82b6ff57b277da5251d2e6eb3d8b6d0/Telegram/SourceFiles/api/api_views.cpp#L22
|
|
const SEND_VIEWS_TIMEOUT = 1000;
|
|
let viewsIncrementTimeout: number | undefined;
|
|
let idsToIncrementViews: Record<string, Set<number>> = {};
|
|
|
|
function incrementViews() {
|
|
if (viewsIncrementTimeout) {
|
|
clearTimeout(viewsIncrementTimeout);
|
|
viewsIncrementTimeout = undefined;
|
|
}
|
|
|
|
// eslint-disable-next-line eslint-multitab-tt/no-getactions-in-actions
|
|
const { loadMessageViews } = getActions();
|
|
Object.entries(idsToIncrementViews).forEach(([chatId, ids]) => {
|
|
loadMessageViews({ chatId, ids: Array.from(ids), shouldIncrement: true });
|
|
});
|
|
|
|
idsToIncrementViews = {};
|
|
}
|
|
addActionHandler('scheduleForViewsIncrement', (global, actions, payload): ActionReturnType => {
|
|
const { ids, chatId } = payload;
|
|
|
|
if (!viewsIncrementTimeout) {
|
|
setTimeout(incrementViews, SEND_VIEWS_TIMEOUT);
|
|
}
|
|
|
|
if (!idsToIncrementViews[chatId]) {
|
|
idsToIncrementViews[chatId] = new Set();
|
|
}
|
|
|
|
ids.forEach((id) => {
|
|
idsToIncrementViews[chatId].add(id);
|
|
});
|
|
});
|
|
|
|
addActionHandler('loadMessageViews', async (global, actions, payload): Promise<void> => {
|
|
const { chatId, ids, shouldIncrement } = payload;
|
|
|
|
const chat = selectChat(global, chatId);
|
|
if (!chat) return;
|
|
|
|
const result = await callApi('fetchMessageViews', {
|
|
chat,
|
|
ids,
|
|
shouldIncrement,
|
|
});
|
|
|
|
if (!result) return;
|
|
|
|
global = getGlobal();
|
|
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
|
global = addChats(global, buildCollectionByKey(result.chats, 'id'));
|
|
result.viewsInfo.forEach((update) => {
|
|
global = updateChatMessage(global, chatId, update.id, {
|
|
views: update.views,
|
|
forwards: update.forwards,
|
|
});
|
|
|
|
const message = selectChatMessage(global, chatId, update.id);
|
|
if (!message) return;
|
|
|
|
const repliesChatId = message.repliesThreadInfo?.chatId;
|
|
const threadId = message.repliesThreadInfo?.threadId;
|
|
if (!repliesChatId || !threadId) return;
|
|
|
|
global = updateThreadInfo(global, repliesChatId, threadId, {
|
|
messagesCount: update.messagesCount,
|
|
recentReplierIds: update.recentReplierIds,
|
|
lastMessageId: update.maxId,
|
|
lastReadInboxMessageId: update.readMaxId,
|
|
});
|
|
});
|
|
|
|
setGlobal(global);
|
|
});
|
|
|
|
function countSortedIds(ids: number[], from: number, to: number) {
|
|
let count = 0;
|
|
|
|
for (let i = 0, l = ids.length; i < l; i++) {
|
|
if (ids[i] >= from && ids[i] <= to) {
|
|
count++;
|
|
}
|
|
|
|
if (ids[i] >= to) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
function splitAttachmentsByType(attachments: ApiAttachment[]) {
|
|
return attachments.reduce((acc, attachment, index, arr) => {
|
|
if (index === 0) {
|
|
acc.push([attachment]);
|
|
return acc;
|
|
}
|
|
|
|
const type = getAttachmentType(attachment);
|
|
const previousType = getAttachmentType(arr[index - 1]);
|
|
if (type === previousType) {
|
|
acc[acc.length - 1].push(attachment);
|
|
} else {
|
|
acc.push([attachment]);
|
|
}
|
|
|
|
return acc;
|
|
}, [] as ApiAttachment[][]);
|
|
}
|
|
|
|
function getAttachmentType(attachment: ApiAttachment) {
|
|
const {
|
|
shouldSendAsFile, mimeType,
|
|
} = attachment;
|
|
if (shouldSendAsFile) return 'file';
|
|
if (mimeType === GIF_MIME_TYPE) return 'gif';
|
|
if (SUPPORTED_IMAGE_CONTENT_TYPES.has(mimeType) || SUPPORTED_VIDEO_CONTENT_TYPES.has(mimeType)) return 'media';
|
|
if (SUPPORTED_AUDIO_CONTENT_TYPES.has(mimeType)) return 'audio';
|
|
if (attachment.voice) return 'voice';
|
|
return 'file';
|
|
}
|