1342 lines
36 KiB
TypeScript
1342 lines
36 KiB
TypeScript
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
|
|
|
import type { GlobalActions } from '../../types';
|
|
import type {
|
|
ApiAttachment,
|
|
ApiChat,
|
|
ApiMessage,
|
|
ApiMessageEntity,
|
|
ApiNewPoll,
|
|
ApiOnProgress,
|
|
ApiSticker,
|
|
ApiUser,
|
|
ApiVideo,
|
|
} from '../../../api/types';
|
|
import {
|
|
MAIN_THREAD_ID,
|
|
MESSAGE_DELETED,
|
|
} from '../../../api/types';
|
|
import { LoadMoreDirection } from '../../../types';
|
|
|
|
import {
|
|
MAX_MEDIA_FILES_FOR_ALBUM,
|
|
MESSAGE_LIST_SLICE,
|
|
RE_TELEGRAM_LINK,
|
|
RE_TG_LINK,
|
|
RE_TME_LINK,
|
|
SERVICE_NOTIFICATIONS_USER_ID,
|
|
} from '../../../config';
|
|
import { IS_IOS } from '../../../util/environment';
|
|
import { callApi, cancelApiProgress } from '../../../api/gramjs';
|
|
import {
|
|
areSortedArraysIntersecting, buildCollectionByKey, split, unique,
|
|
} from '../../../util/iteratees';
|
|
import {
|
|
addUsers,
|
|
addChatMessagesById,
|
|
replaceThreadParam,
|
|
safeReplaceViewportIds,
|
|
updateChatMessage,
|
|
addChats,
|
|
updateListedIds,
|
|
updateOutlyingIds,
|
|
replaceScheduledMessages,
|
|
updateThreadInfos,
|
|
updateChat,
|
|
updateThreadUnreadFromForwardedMessage,
|
|
updateSponsoredMessage,
|
|
} from '../../reducers';
|
|
import {
|
|
selectChat,
|
|
selectChatMessage,
|
|
selectCurrentMessageList,
|
|
selectFocusedMessageId,
|
|
selectCurrentChat,
|
|
selectListedIds,
|
|
selectOutlyingIds,
|
|
selectViewportIds,
|
|
selectRealLastReadId,
|
|
selectReplyingToId,
|
|
selectEditingId,
|
|
selectDraft,
|
|
selectThreadOriginChat,
|
|
selectThreadTopMessageId,
|
|
selectEditingScheduledId,
|
|
selectEditingMessage,
|
|
selectScheduledMessage,
|
|
selectNoWebPage,
|
|
selectFirstUnreadId,
|
|
selectUser,
|
|
selectSendAs,
|
|
selectSponsoredMessage,
|
|
selectIsCurrentUserPremium,
|
|
selectForwardsContainVoiceMessages,
|
|
} from '../../selectors';
|
|
import {
|
|
debounce, onTickEnd, rafPromise,
|
|
} from '../../../util/schedulers';
|
|
import {
|
|
getMessageOriginalId, getUserFullName, isDeletedUser, isServiceNotificationMessage, isUserBot,
|
|
} from '../../helpers';
|
|
import { getTranslation } from '../../../util/langProvider';
|
|
import { ensureProtocol } from '../../../util/ensureProtocol';
|
|
|
|
const AUTOLOGIN_TOKEN_KEY = 'autologin_token';
|
|
|
|
const uploadProgressCallbacks = new Map<number, ApiOnProgress>();
|
|
|
|
const runDebouncedForMarkRead = debounce((cb) => cb(), 500, false);
|
|
|
|
addActionHandler('loadViewportMessages', (global, actions, payload) => {
|
|
const {
|
|
direction = LoadMoreDirection.Around,
|
|
isBudgetPreload = false,
|
|
} = payload || {};
|
|
|
|
let { chatId, threadId } = payload || {};
|
|
|
|
if (!chatId) {
|
|
const currentMessageList = selectCurrentMessageList(global);
|
|
if (!currentMessageList) {
|
|
return undefined;
|
|
}
|
|
|
|
chatId = currentMessageList.chatId;
|
|
threadId = currentMessageList.threadId;
|
|
}
|
|
|
|
const chat = selectChat(global, chatId);
|
|
// TODO Revise if `chat.isRestricted` check is needed
|
|
if (!chat || chat.isRestricted) {
|
|
return undefined;
|
|
}
|
|
|
|
const viewportIds = selectViewportIds(global, chatId, threadId);
|
|
const listedIds = selectListedIds(global, chatId, threadId);
|
|
const outlyingIds = selectOutlyingIds(global, chatId, threadId);
|
|
|
|
if (!viewportIds || !viewportIds.length || direction === LoadMoreDirection.Around) {
|
|
const offsetId = selectFocusedMessageId(global, chatId) || selectRealLastReadId(global, chatId, threadId);
|
|
const isOutlying = Boolean(offsetId && listedIds && !listedIds.includes(offsetId));
|
|
const historyIds = (isOutlying ? outlyingIds : listedIds) || [];
|
|
const {
|
|
newViewportIds, areSomeLocal, areAllLocal,
|
|
} = getViewportSlice(historyIds, offsetId, LoadMoreDirection.Around);
|
|
|
|
if (areSomeLocal && newViewportIds.length >= MESSAGE_LIST_SLICE) {
|
|
global = safeReplaceViewportIds(global, chatId, threadId, newViewportIds);
|
|
}
|
|
|
|
if (!areAllLocal) {
|
|
onTickEnd(() => {
|
|
void loadViewportMessages(chat, threadId, offsetId, LoadMoreDirection.Around, isOutlying, isBudgetPreload);
|
|
});
|
|
}
|
|
} else {
|
|
const offsetId = direction === LoadMoreDirection.Backwards ? viewportIds[0] : viewportIds[viewportIds.length - 1];
|
|
const isOutlying = Boolean(outlyingIds);
|
|
const historyIds = (isOutlying ? outlyingIds : listedIds)!;
|
|
const {
|
|
newViewportIds, areSomeLocal, areAllLocal,
|
|
} = getViewportSlice(historyIds, offsetId, direction);
|
|
|
|
if (areSomeLocal) {
|
|
global = safeReplaceViewportIds(global, chatId, threadId, newViewportIds);
|
|
}
|
|
|
|
onTickEnd(() => {
|
|
void loadWithBudget(actions, areAllLocal, isOutlying, isBudgetPreload, chat, threadId, direction, offsetId);
|
|
});
|
|
|
|
if (isBudgetPreload) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
return global;
|
|
});
|
|
|
|
async function loadWithBudget(
|
|
actions: GlobalActions,
|
|
areAllLocal: boolean, isOutlying: boolean, isBudgetPreload: boolean,
|
|
chat: ApiChat, threadId: number, direction: LoadMoreDirection, offsetId?: number,
|
|
) {
|
|
if (!areAllLocal) {
|
|
await loadViewportMessages(
|
|
chat, threadId, offsetId, direction, isOutlying, isBudgetPreload,
|
|
);
|
|
}
|
|
|
|
if (!isBudgetPreload) {
|
|
actions.loadViewportMessages({
|
|
chatId: chat.id, threadId, direction, isBudgetPreload: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
addActionHandler('loadMessage', async (global, actions, payload) => {
|
|
const {
|
|
chatId, messageId, replyOriginForId, threadUpdate,
|
|
} = payload!;
|
|
|
|
const chat = selectChat(global, chatId);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
const message = await loadMessage(chat, messageId, replyOriginForId);
|
|
if (message && threadUpdate) {
|
|
const { lastMessageId, isDeleting } = threadUpdate;
|
|
|
|
setGlobal(updateThreadUnreadFromForwardedMessage(
|
|
getGlobal(),
|
|
message,
|
|
chatId,
|
|
lastMessageId,
|
|
isDeleting,
|
|
));
|
|
}
|
|
});
|
|
|
|
addActionHandler('sendMessage', (global, actions, payload) => {
|
|
const currentMessageList = selectCurrentMessageList(global);
|
|
if (!currentMessageList) {
|
|
return undefined;
|
|
}
|
|
|
|
const { chatId, threadId, type } = currentMessageList;
|
|
|
|
if (type === 'scheduled' && !payload.scheduledAt) {
|
|
return {
|
|
...global,
|
|
messages: {
|
|
...global.messages,
|
|
contentToBeScheduled: payload,
|
|
},
|
|
};
|
|
}
|
|
|
|
const chat = selectChat(global, chatId)!;
|
|
|
|
const params = {
|
|
...payload,
|
|
chat,
|
|
replyingTo: selectReplyingToId(global, chatId, threadId),
|
|
noWebPage: selectNoWebPage(global, chatId, threadId),
|
|
sendAs: selectSendAs(global, chatId),
|
|
};
|
|
|
|
actions.setReplyingToId({ messageId: undefined });
|
|
actions.clearWebPagePreview({ chatId, threadId, value: false });
|
|
|
|
const isSingle = !payload.attachments || payload.attachments.length <= 1;
|
|
const isGrouped = !isSingle && payload.attachments && payload.attachments.length > 1;
|
|
|
|
if (isSingle) {
|
|
const { attachments, ...restParams } = params;
|
|
sendMessage({
|
|
...restParams,
|
|
attachment: attachments ? attachments[0] : undefined,
|
|
});
|
|
} else if (isGrouped) {
|
|
const {
|
|
text, entities, attachments, ...commonParams
|
|
} = params;
|
|
const groupedAttachments = split(attachments as ApiAttachment[], MAX_MEDIA_FILES_FOR_ALBUM);
|
|
for (let i = 0; i < groupedAttachments.length; i++) {
|
|
const [firstAttachment, ...restAttachments] = groupedAttachments[i];
|
|
const groupedId = `${Date.now()}${i}`;
|
|
|
|
sendMessage({
|
|
...commonParams,
|
|
text: i === 0 ? text : undefined,
|
|
entities: i === 0 ? entities : undefined,
|
|
attachment: firstAttachment,
|
|
groupedId: restAttachments.length > 0 ? groupedId : undefined,
|
|
});
|
|
|
|
restAttachments.forEach((attachment: ApiAttachment) => {
|
|
sendMessage({
|
|
...commonParams,
|
|
attachment,
|
|
groupedId,
|
|
});
|
|
});
|
|
}
|
|
} else {
|
|
const {
|
|
text, entities, attachments, replyingTo, ...commonParams
|
|
} = params;
|
|
|
|
if (text) {
|
|
sendMessage({
|
|
...commonParams,
|
|
text,
|
|
entities,
|
|
replyingTo,
|
|
});
|
|
}
|
|
|
|
attachments.forEach((attachment: ApiAttachment) => {
|
|
sendMessage({
|
|
...commonParams,
|
|
attachment,
|
|
});
|
|
});
|
|
}
|
|
|
|
return undefined;
|
|
});
|
|
|
|
addActionHandler('editMessage', (global, actions, payload) => {
|
|
const { serverTimeOffset } = global;
|
|
const { text, entities } = payload!;
|
|
|
|
const currentMessageList = selectCurrentMessageList(global);
|
|
if (!currentMessageList) {
|
|
return;
|
|
}
|
|
|
|
const { chatId, threadId, type: messageListType } = currentMessageList;
|
|
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), serverTimeOffset,
|
|
});
|
|
|
|
actions.setEditingId({ messageId: undefined });
|
|
});
|
|
|
|
addActionHandler('cancelSendingMessage', (global, actions, payload) => {
|
|
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', async (global, actions, payload) => {
|
|
const { chatId, threadId, draft } = payload!;
|
|
if (!draft) {
|
|
return;
|
|
}
|
|
|
|
const { text, entities } = draft;
|
|
const chat = selectChat(global, chatId)!;
|
|
const user = selectUser(global, chatId)!;
|
|
if (user && isDeletedUser(user)) return;
|
|
|
|
if (threadId === MAIN_THREAD_ID) {
|
|
const result = await callApi('saveDraft', {
|
|
chat,
|
|
text,
|
|
entities,
|
|
replyToMsgId: selectReplyingToId(global, chatId, threadId),
|
|
});
|
|
|
|
if (!result) {
|
|
draft.isLocal = true;
|
|
}
|
|
}
|
|
global = getGlobal();
|
|
|
|
global = replaceThreadParam(global, chatId, threadId, 'draft', draft);
|
|
global = updateChat(global, chatId, { draftDate: Math.round(Date.now() / 1000) });
|
|
|
|
setGlobal(global);
|
|
});
|
|
|
|
addActionHandler('clearDraft', (global, actions, payload) => {
|
|
const { chatId, threadId, localOnly } = payload!;
|
|
if (!selectDraft(global, chatId, threadId)) {
|
|
return undefined;
|
|
}
|
|
|
|
const chat = selectChat(global, chatId)!;
|
|
|
|
if (!localOnly && threadId === MAIN_THREAD_ID) {
|
|
void callApi('clearDraft', chat);
|
|
}
|
|
|
|
global = replaceThreadParam(global, chatId, threadId, 'draft', undefined);
|
|
global = updateChat(global, chatId, { draftDate: undefined });
|
|
|
|
return global;
|
|
});
|
|
|
|
addActionHandler('toggleMessageWebPage', (global, actions, payload) => {
|
|
const { chatId, threadId, noWebPage } = payload!;
|
|
|
|
return replaceThreadParam(global, chatId, threadId, 'noWebPage', noWebPage);
|
|
});
|
|
|
|
addActionHandler('pinMessage', (global, actions, payload) => {
|
|
const chat = selectCurrentChat(global);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
const {
|
|
messageId, isUnpin, isOneSide, isSilent,
|
|
} = payload!;
|
|
|
|
void callApi('pinMessage', {
|
|
chat, messageId, isUnpin, isOneSide, isSilent,
|
|
});
|
|
});
|
|
|
|
addActionHandler('unpinAllMessages', (global, actions, payload) => {
|
|
const chat = selectChat(global, payload.chatId);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
void unpinAllMessages(chat);
|
|
});
|
|
|
|
async function unpinAllMessages(chat: ApiChat) {
|
|
await callApi('unpinAllMessages', { chat });
|
|
let global = getGlobal();
|
|
global = replaceThreadParam(global, chat.id, MAIN_THREAD_ID, 'pinnedIds', []);
|
|
setGlobal(global);
|
|
}
|
|
|
|
addActionHandler('deleteMessages', (global, actions, payload) => {
|
|
const { messageIds, shouldDeleteForAll } = payload!;
|
|
const currentMessageList = selectCurrentMessageList(global);
|
|
if (!currentMessageList) {
|
|
return;
|
|
}
|
|
const { chatId, threadId } = currentMessageList;
|
|
const chat = selectChat(global, chatId)!;
|
|
|
|
void callApi('deleteMessages', { chat, messageIds, shouldDeleteForAll });
|
|
|
|
const editingId = selectEditingId(global, chatId, threadId);
|
|
if (messageIds.includes(editingId)) {
|
|
actions.setEditingId({ messageId: undefined });
|
|
}
|
|
});
|
|
|
|
addActionHandler('deleteScheduledMessages', (global, actions, payload) => {
|
|
const { messageIds } = payload!;
|
|
const currentMessageList = selectCurrentMessageList(global);
|
|
if (!currentMessageList) {
|
|
return;
|
|
}
|
|
|
|
const { chatId } = currentMessageList;
|
|
const chat = selectChat(global, chatId)!;
|
|
|
|
void callApi('deleteScheduledMessages', { chat, messageIds });
|
|
|
|
const editingId = selectEditingScheduledId(global, chatId);
|
|
if (messageIds.includes(editingId)) {
|
|
actions.setEditingId({ messageId: undefined });
|
|
}
|
|
});
|
|
|
|
addActionHandler('deleteHistory', async (global, actions, payload) => {
|
|
const { chatId, shouldDeleteForAll } = payload!;
|
|
const chat = selectChat(global, chatId);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
await callApi('deleteHistory', { chat, shouldDeleteForAll });
|
|
|
|
const activeChat = selectCurrentMessageList(global);
|
|
if (activeChat && activeChat.chatId === chatId) {
|
|
actions.openChat({ id: undefined });
|
|
}
|
|
});
|
|
|
|
addActionHandler('reportMessages', async (global, actions, payload) => {
|
|
const {
|
|
messageIds, reason, description,
|
|
} = payload!;
|
|
const currentMessageList = selectCurrentMessageList(global);
|
|
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
|
|
? getTranslation('ReportPeer.AlertSuccess')
|
|
: 'An error occurred while submitting your report. Please, try again later.',
|
|
});
|
|
});
|
|
|
|
addActionHandler('sendMessageAction', async (global, actions, payload) => {
|
|
const { action, chatId, threadId } = payload!;
|
|
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) => {
|
|
const { serverTimeOffset } = global;
|
|
const currentMessageList = selectCurrentMessageList(global);
|
|
if (!currentMessageList) {
|
|
return undefined;
|
|
}
|
|
|
|
const { chatId, threadId } = currentMessageList;
|
|
const chat = selectThreadOriginChat(global, chatId, threadId);
|
|
if (!chat) {
|
|
return undefined;
|
|
}
|
|
|
|
const { maxId } = payload!;
|
|
|
|
runDebouncedForMarkRead(() => {
|
|
void callApi('markMessageListRead', {
|
|
serverTimeOffset, chat, threadId, maxId,
|
|
});
|
|
});
|
|
|
|
// TODO Support local marking read for threads
|
|
if (threadId !== MAIN_THREAD_ID) {
|
|
return undefined;
|
|
}
|
|
|
|
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);
|
|
const minId = selectFirstUnreadId(global, chatId, threadId);
|
|
if (!viewportIds || !minId || !chat.unreadCount) {
|
|
return global;
|
|
}
|
|
|
|
const readCount = countSortedIds(viewportIds!, minId, maxId);
|
|
if (!readCount) {
|
|
return global;
|
|
}
|
|
|
|
return updateChat(global, chatId, {
|
|
lastReadInboxMessageId: maxId,
|
|
unreadCount: Math.max(0, chat.unreadCount - readCount),
|
|
});
|
|
});
|
|
|
|
addActionHandler('markMessagesRead', (global, actions, payload) => {
|
|
const chat = selectCurrentChat(global);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
const { messageIds } = payload!;
|
|
|
|
void callApi('markMessagesRead', { chat, messageIds });
|
|
});
|
|
|
|
addActionHandler('loadWebPagePreview', (global, actions, payload) => {
|
|
const { text } = payload!;
|
|
void loadWebPagePreview(text);
|
|
});
|
|
|
|
addActionHandler('clearWebPagePreview', (global) => {
|
|
if (!global.webPagePreview) {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
...global,
|
|
webPagePreview: undefined,
|
|
};
|
|
});
|
|
|
|
addActionHandler('sendPollVote', (global, actions, payload) => {
|
|
const { chatId, messageId, options } = payload!;
|
|
const chat = selectChat(global, chatId);
|
|
|
|
if (chat) {
|
|
void callApi('sendPollVote', { chat, messageId, options });
|
|
}
|
|
});
|
|
|
|
addActionHandler('cancelPollVote', (global, actions, payload) => {
|
|
const { chatId, messageId } = payload!;
|
|
const chat = selectChat(global, chatId);
|
|
|
|
if (chat) {
|
|
void callApi('sendPollVote', { chat, messageId, options: [] });
|
|
}
|
|
});
|
|
|
|
addActionHandler('closePoll', (global, actions, payload) => {
|
|
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', (global, actions, payload) => {
|
|
const {
|
|
chat, messageId, option, offset, limit, shouldResetVoters,
|
|
} = payload!;
|
|
|
|
void loadPollOptionResults(chat, messageId, option, offset, limit, shouldResetVoters);
|
|
});
|
|
|
|
addActionHandler('loadExtendedMedia', (global, actions, payload) => {
|
|
const { chatId, ids } = payload;
|
|
const chat = selectChat(global, chatId);
|
|
if (chat) {
|
|
void callApi('fetchExtendedMedia', { chat, ids });
|
|
}
|
|
});
|
|
|
|
addActionHandler('forwardMessages', (global, action, payload) => {
|
|
const {
|
|
fromChatId, messageIds, toChatId, withMyScore, noAuthors, noCaptions,
|
|
} = global.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) {
|
|
return;
|
|
}
|
|
|
|
const { isSilent, scheduledAt } = payload;
|
|
const sendAs = selectSendAs(global, toChatId!);
|
|
|
|
const realMessages = messages.filter((m) => !isServiceNotificationMessage(m));
|
|
if (realMessages.length) {
|
|
void callApi('forwardMessages', {
|
|
fromChat,
|
|
toChat,
|
|
messages: realMessages,
|
|
serverTimeOffset: getGlobal().serverTimeOffset,
|
|
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;
|
|
|
|
void sendMessage({
|
|
chat: toChat,
|
|
text,
|
|
entities,
|
|
sticker,
|
|
poll,
|
|
isSilent,
|
|
scheduledAt,
|
|
sendAs,
|
|
});
|
|
});
|
|
|
|
setGlobal({
|
|
...getGlobal(),
|
|
forwardMessages: {},
|
|
});
|
|
});
|
|
|
|
addActionHandler('loadScheduledHistory', (global, actions, payload) => {
|
|
const { chatId } = payload;
|
|
const chat = selectChat(global, chatId);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
void loadScheduledHistory(chat);
|
|
});
|
|
|
|
addActionHandler('sendScheduledMessages', (global, actions, payload) => {
|
|
const {
|
|
chatId, id,
|
|
} = payload!;
|
|
|
|
const chat = selectChat(global, chatId);
|
|
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
void callApi('sendScheduledMessages', {
|
|
chat,
|
|
ids: [id],
|
|
});
|
|
});
|
|
|
|
addActionHandler('rescheduleMessage', (global, actions, payload) => {
|
|
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', (global, actions, payload) => {
|
|
const { chatId, threadId } = payload;
|
|
const chat = selectThreadOriginChat(global, chatId, threadId);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
void callApi('requestThreadInfoUpdate', { chat, threadId });
|
|
});
|
|
|
|
addActionHandler('transcribeAudio', async (global, actions, payload) => {
|
|
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 = updateChatMessage(getGlobal(), chatId, messageId, {
|
|
transcriptionId: result,
|
|
isTranscriptionError: !result,
|
|
});
|
|
|
|
setGlobal(global);
|
|
});
|
|
|
|
addActionHandler('loadCustomEmojis', async (global, actions, payload) => {
|
|
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();
|
|
setGlobal({
|
|
...global,
|
|
customEmojis: {
|
|
...global.customEmojis,
|
|
byId: {
|
|
...global.customEmojis.byId,
|
|
...buildCollectionByKey(customEmoji, 'id'),
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
async function loadWebPagePreview(message: string) {
|
|
const webPagePreview = await callApi('fetchWebPagePreview', { message });
|
|
|
|
setGlobal({
|
|
...getGlobal(),
|
|
webPagePreview,
|
|
});
|
|
}
|
|
|
|
async function loadViewportMessages(
|
|
chat: ApiChat,
|
|
threadId: number,
|
|
offsetId: number | undefined,
|
|
direction: LoadMoreDirection,
|
|
isOutlying = false,
|
|
isBudgetPreload = false,
|
|
) {
|
|
const chatId = chat.id;
|
|
|
|
let addOffset: number | undefined;
|
|
switch (direction) {
|
|
case LoadMoreDirection.Backwards:
|
|
addOffset = undefined;
|
|
break;
|
|
case LoadMoreDirection.Around:
|
|
addOffset = -(Math.round(MESSAGE_LIST_SLICE / 2) + 1);
|
|
break;
|
|
case LoadMoreDirection.Forwards:
|
|
addOffset = -(MESSAGE_LIST_SLICE + 1);
|
|
break;
|
|
}
|
|
|
|
const result = await callApi('fetchMessages', {
|
|
chat: selectThreadOriginChat(getGlobal(), chatId, threadId)!,
|
|
offsetId,
|
|
addOffset,
|
|
limit: MESSAGE_LIST_SLICE,
|
|
threadId,
|
|
});
|
|
|
|
if (!result) {
|
|
return;
|
|
}
|
|
|
|
const {
|
|
messages, users, chats, threadInfos,
|
|
} = result;
|
|
|
|
let 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);
|
|
|
|
global = addChatMessagesById(global, chatId, byId);
|
|
global = isOutlying
|
|
? updateOutlyingIds(global, chatId, threadId, ids)
|
|
: updateListedIds(global, chatId, threadId, ids);
|
|
|
|
global = addUsers(global, buildCollectionByKey(users, 'id'));
|
|
global = addChats(global, buildCollectionByKey(chats, 'id'));
|
|
global = updateThreadInfos(global, chatId, threadInfos);
|
|
|
|
let listedIds = selectListedIds(global, chatId, threadId);
|
|
const outlyingIds = selectOutlyingIds(global, chatId, threadId);
|
|
|
|
if (isOutlying && listedIds && outlyingIds) {
|
|
if (!outlyingIds.length || areSortedArraysIntersecting(listedIds, outlyingIds)) {
|
|
global = updateListedIds(global, chatId, threadId, outlyingIds);
|
|
listedIds = selectListedIds(global, chatId, threadId);
|
|
global = replaceThreadParam(global, chatId, threadId, 'outlyingIds', undefined);
|
|
isOutlying = false;
|
|
}
|
|
}
|
|
|
|
if (!isBudgetPreload) {
|
|
const historyIds = isOutlying ? outlyingIds! : listedIds!;
|
|
const { newViewportIds } = getViewportSlice(historyIds, offsetId, direction);
|
|
global = safeReplaceViewportIds(global, chatId, threadId, newViewportIds!);
|
|
}
|
|
|
|
setGlobal(global);
|
|
}
|
|
|
|
async function loadMessage(chat: ApiChat, messageId: number, replyOriginForId: number) {
|
|
const result = await callApi('fetchMessage', { chat, messageId });
|
|
if (!result) {
|
|
return undefined;
|
|
}
|
|
|
|
if (result === MESSAGE_DELETED) {
|
|
if (replyOriginForId) {
|
|
let global = getGlobal();
|
|
const replyMessage = selectChatMessage(global, chat.id, replyOriginForId);
|
|
global = updateChatMessage(global, chat.id, replyOriginForId, {
|
|
...replyMessage,
|
|
replyToMessageId: undefined,
|
|
});
|
|
setGlobal(global);
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
let 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 indexForDirection = isBackwards ? index : (index + 1) || length;
|
|
const from = indexForDirection - MESSAGE_LIST_SLICE;
|
|
const to = indexForDirection + MESSAGE_LIST_SLICE - 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(params: {
|
|
chat: ApiChat;
|
|
text?: string;
|
|
entities?: ApiMessageEntity[];
|
|
replyingTo?: number;
|
|
attachment?: ApiAttachment;
|
|
sticker?: ApiSticker;
|
|
gif?: ApiVideo;
|
|
poll?: ApiNewPoll;
|
|
serverTimeOffset?: number;
|
|
isSilent?: boolean;
|
|
scheduledAt?: number;
|
|
sendAs?: ApiChat | ApiUser;
|
|
replyingToTopId?: number;
|
|
}) {
|
|
let localId: number | undefined;
|
|
const progressCallback = params.attachment ? (progress: number, messageLocalId: number) => {
|
|
if (!uploadProgressCallbacks.has(messageLocalId)) {
|
|
localId = messageLocalId;
|
|
uploadProgressCallbacks.set(messageLocalId, progressCallback!);
|
|
}
|
|
|
|
const global = getGlobal();
|
|
|
|
setGlobal({
|
|
...global,
|
|
fileUploads: {
|
|
byMessageLocalId: {
|
|
...global.fileUploads.byMessageLocalId,
|
|
[messageLocalId]: { progress },
|
|
},
|
|
},
|
|
});
|
|
} : undefined;
|
|
|
|
// @optimization
|
|
if (params.replyingTo || IS_IOS) {
|
|
await rafPromise();
|
|
}
|
|
|
|
const global = getGlobal();
|
|
params.serverTimeOffset = global.serverTimeOffset;
|
|
const currentMessageList = selectCurrentMessageList(global);
|
|
if (!currentMessageList) {
|
|
return;
|
|
}
|
|
const { threadId } = currentMessageList;
|
|
|
|
if (!params.replyingTo && threadId !== MAIN_THREAD_ID) {
|
|
params.replyingTo = selectThreadTopMessageId(global, params.chat.id, threadId)!;
|
|
}
|
|
|
|
if (params.replyingTo && !params.replyingToTopId && threadId !== MAIN_THREAD_ID) {
|
|
params.replyingToTopId = selectThreadTopMessageId(global, params.chat.id, threadId)!;
|
|
}
|
|
|
|
await callApi('sendMessage', params, progressCallback);
|
|
|
|
if (progressCallback && localId) {
|
|
uploadProgressCallbacks.delete(localId);
|
|
}
|
|
}
|
|
|
|
async function loadPollOptionResults(
|
|
chat: ApiChat,
|
|
messageId: number,
|
|
option: string,
|
|
offset?: string,
|
|
limit?: number,
|
|
shouldResetVoters?: boolean,
|
|
) {
|
|
const result = await callApi('loadPollOptionResults', {
|
|
chat, messageId, option, offset, limit,
|
|
});
|
|
|
|
if (!result) {
|
|
return;
|
|
}
|
|
|
|
let global = getGlobal();
|
|
|
|
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
|
const { voters } = global.pollResults;
|
|
|
|
setGlobal({
|
|
...global,
|
|
pollResults: {
|
|
...global.pollResults,
|
|
voters: {
|
|
...voters,
|
|
[option]: unique([
|
|
...(!shouldResetVoters && voters && voters[option] ? voters[option] : []),
|
|
...(result && result.users.map((user) => user.id)),
|
|
]),
|
|
},
|
|
offsets: {
|
|
...(global.pollResults.offsets ? global.pollResults.offsets : {}),
|
|
[option]: result.nextOffset || '',
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
addActionHandler('loadPinnedMessages', (global, actions, payload) => {
|
|
const { chatId } = payload;
|
|
const chat = selectChat(global, chatId);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
void loadPinnedMessages(chat);
|
|
});
|
|
|
|
addActionHandler('loadSeenBy', async (global, actions, payload) => {
|
|
const { chatId, messageId } = payload;
|
|
const chat = selectChat(global, chatId);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
const result = await callApi('fetchSeenBy', { chat, messageId });
|
|
if (!result) {
|
|
return;
|
|
}
|
|
|
|
setGlobal(updateChatMessage(getGlobal(), chatId, messageId, {
|
|
seenByUserIds: result,
|
|
}));
|
|
});
|
|
|
|
addActionHandler('saveDefaultSendAs', (global, actions, payload) => {
|
|
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 updateChat(global, chatId, {
|
|
fullInfo: {
|
|
...chat.fullInfo,
|
|
sendAsId,
|
|
},
|
|
});
|
|
});
|
|
|
|
addActionHandler('loadSendAs', async (global, actions, payload) => {
|
|
const { chatId } = payload;
|
|
const chat = selectChat(global, chatId);
|
|
if (!chat) {
|
|
return;
|
|
}
|
|
|
|
const result = await callApi('fetchSendAs', { chat });
|
|
if (!result) {
|
|
setGlobal(updateChat(getGlobal(), chatId, {
|
|
sendAsPeerIds: [],
|
|
}));
|
|
|
|
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);
|
|
});
|
|
|
|
async function loadPinnedMessages(chat: ApiChat) {
|
|
const result = await callApi('fetchPinnedMessages', { chat });
|
|
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);
|
|
|
|
let global = getGlobal();
|
|
global = addChatMessagesById(global, chat.id, byId);
|
|
global = replaceThreadParam(global, chat.id, MAIN_THREAD_ID, 'pinnedIds', ids);
|
|
global = addUsers(global, buildCollectionByKey(users, 'id'));
|
|
global = addChats(global, buildCollectionByKey(chats, 'id'));
|
|
setGlobal(global);
|
|
}
|
|
|
|
async function loadScheduledHistory(chat: ApiChat) {
|
|
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);
|
|
|
|
let global = getGlobal();
|
|
global = replaceScheduledMessages(global, chat.id, byId);
|
|
global = replaceThreadParam(global, chat.id, MAIN_THREAD_ID, 'scheduledIds', ids);
|
|
setGlobal(global);
|
|
}
|
|
|
|
addActionHandler('loadSponsoredMessages', async (global, actions, payload) => {
|
|
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) => {
|
|
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) => {
|
|
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) => {
|
|
const { messageIds } = payload;
|
|
|
|
const chat = selectCurrentChat(global);
|
|
if (!chat) return;
|
|
|
|
const unreadMentions = (chat.unreadMentions || []).filter((id) => !messageIds.includes(id));
|
|
global = updateChat(global, chat.id, {
|
|
unreadMentions,
|
|
});
|
|
|
|
setGlobal(global);
|
|
|
|
actions.markMessagesRead({ messageIds });
|
|
});
|
|
|
|
addActionHandler('focusNextMention', (global, actions) => {
|
|
const chat = selectCurrentChat(global);
|
|
|
|
if (!chat?.unreadMentions) return;
|
|
|
|
actions.focusMessage({ chatId: chat.id, messageId: chat.unreadMentions[0] });
|
|
});
|
|
|
|
addActionHandler('readAllMentions', (global) => {
|
|
const chat = selectCurrentChat(global);
|
|
if (!chat) return undefined;
|
|
|
|
callApi('readAllMentions', { chat });
|
|
|
|
return updateChat(global, chat.id, {
|
|
unreadMentionsCount: undefined,
|
|
unreadMentions: undefined,
|
|
});
|
|
});
|
|
|
|
addActionHandler('openUrl', (global, actions, payload) => {
|
|
const { url, shouldSkipModal } = payload;
|
|
const urlWithProtocol = ensureProtocol(url)!;
|
|
|
|
if (urlWithProtocol.match(RE_TME_LINK) || urlWithProtocol.match(RE_TG_LINK)) {
|
|
actions.openTelegramLink({ url });
|
|
return;
|
|
}
|
|
|
|
const { appConfig } = global;
|
|
if (appConfig) {
|
|
const parsedUrl = new URL(urlWithProtocol);
|
|
|
|
if (appConfig.autologinDomains.includes(parsedUrl.hostname)) {
|
|
parsedUrl.searchParams.set(AUTOLOGIN_TOKEN_KEY, appConfig.autologinToken);
|
|
window.open(parsedUrl.href, '_blank', 'noopener');
|
|
return;
|
|
}
|
|
|
|
if (appConfig.urlAuthDomains.includes(parsedUrl.hostname)) {
|
|
actions.requestLinkUrlAuth({ url });
|
|
return;
|
|
}
|
|
}
|
|
|
|
const shouldDisplayModal = !urlWithProtocol.match(RE_TELEGRAM_LINK) && !shouldSkipModal;
|
|
|
|
if (shouldDisplayModal) {
|
|
actions.toggleSafeLinkModal({ url: urlWithProtocol });
|
|
} else {
|
|
window.open(urlWithProtocol, '_blank', 'noopener');
|
|
}
|
|
});
|
|
|
|
addActionHandler('setForwardChatId', async (global, actions, payload) => {
|
|
const { id } = payload;
|
|
let user = selectUser(global, id);
|
|
if (user && selectForwardsContainVoiceMessages(global)) {
|
|
if (!user.fullInfo) {
|
|
const { accessHash } = user;
|
|
user = await callApi('fetchFullUser', { id, accessHash });
|
|
}
|
|
|
|
if (user?.fullInfo!.noVoiceMessages) {
|
|
actions.showDialog({
|
|
data: {
|
|
message: getTranslation('VoiceMessagesRestrictedByPrivacy', getUserFullName(user)),
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
setGlobal({
|
|
...global,
|
|
forwardMessages: {
|
|
...global.forwardMessages,
|
|
toChatId: id,
|
|
isModalShown: false,
|
|
},
|
|
});
|
|
|
|
actions.openChat({ id });
|
|
actions.closeMediaViewer();
|
|
actions.exitMessageSelectMode();
|
|
});
|
|
|
|
addActionHandler('forwardToSavedMessages', (global, actions) => {
|
|
setGlobal({
|
|
...global,
|
|
forwardMessages: {
|
|
...global.forwardMessages,
|
|
toChatId: global.currentUserId,
|
|
},
|
|
});
|
|
|
|
actions.exitMessageSelectMode();
|
|
actions.forwardMessages({ isSilent: true });
|
|
});
|
|
|
|
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;
|
|
}
|