2025-03-01 18:02:02 +01:00

3314 lines
87 KiB
TypeScript

import type {
ApiChat, ApiChatFolder, ApiChatlistExportedInvite,
ApiChatMember, ApiError, ApiMissingInvitedUser,
} from '../../../api/types';
import type { RequiredGlobalActions } from '../../index';
import type {
ActionReturnType, GlobalState, TabArgs,
} from '../../types';
import { MAIN_THREAD_ID } from '../../../api/types';
import {
ChatCreationProgress,
type ChatListType,
ManagementProgress,
NewChatMembersProgress,
SettingsScreens,
type ThreadId,
} from '../../../types';
import {
ALL_FOLDER_ID,
ARCHIVED_FOLDER_ID,
CHAT_LIST_LOAD_SLICE,
DEBUG,
GLOBAL_SUGGESTED_CHANNELS_ID,
RE_TG_LINK,
SAVED_FOLDER_ID,
SERVICE_NOTIFICATIONS_USER_ID,
TME_WEB_DOMAINS,
TMP_CHAT_ID,
TOP_CHAT_MESSAGES_PRELOAD_LIMIT,
TOPICS_SLICE,
TOPICS_SLICE_SECOND_LOAD,
} from '../../../config';
import { copyTextToClipboard } from '../../../util/clipboard';
import { formatShareText, processDeepLink } from '../../../util/deeplink';
import { isDeepLink } from '../../../util/deepLinkParser';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { getOrderedIds } from '../../../util/folderManager';
import {
buildCollectionByKey, omit, pick, unique,
} from '../../../util/iteratees';
import { isLocalMessageId } from '../../../util/keys/messageKey';
import * as langProvider from '../../../util/oldLangProvider';
import { debounce, pause, throttle } from '../../../util/schedulers';
import { extractCurrentThemeParams } from '../../../util/themeStyle';
import { callApi } from '../../../api/gramjs';
import {
getIsSavedDialog,
isChatArchived,
isChatBasicGroup,
isChatChannel,
isChatSuperGroup,
isUserBot,
} from '../../helpers';
import {
addActionHandler, getGlobal, setGlobal,
} from '../../index';
import {
addChatListIds,
addChatMembers,
addChats,
addMessages,
addSimilarBots,
addUsers,
addUserStatuses,
deleteChatMessages,
deletePeerPhoto,
deleteTopic,
leaveChat,
removeChatFromChatLists,
replaceChatFullInfo,
replaceChatListIds,
replaceChatListLoadingParameters,
replaceMessages,
replaceSimilarChannels,
replaceThreadParam,
replaceUserStatuses,
toggleSimilarChannels,
updateChat,
updateChatFullInfo,
updateChatLastMessageId,
updateChatListSecondaryInfo,
updateChats,
updateChatsLastMessageId,
updateListedTopicIds,
updateManagementProgress,
updateMissingInvitedUsers,
updatePeerFullInfo,
updateThread,
updateThreadInfo,
updateTopic,
updateTopics,
updateUser,
updateUsers,
} from '../../reducers';
import { updateGroupCall } from '../../reducers/calls';
import { updateTabState } from '../../reducers/tabs';
import {
selectChat,
selectChatByUsername,
selectChatFolder,
selectChatFullInfo,
selectChatLastMessageId,
selectChatListLoadingParameters,
selectChatListType,
selectChatMessages,
selectCurrentChat,
selectCurrentMessageList,
selectDraft,
selectIsChatPinned,
selectIsChatWithSelf,
selectLastServiceNotification,
selectPeer,
selectSimilarChannelIds,
selectStickerSet,
selectSupportChat,
selectTabState,
selectThread,
selectThreadInfo,
selectTopic,
selectTopics,
selectTopicsInfo,
selectUser,
selectUserByPhoneNumber,
} from '../../selectors';
import { selectGroupCall } from '../../selectors/calls';
import { selectCurrentLimit } from '../../selectors/limits';
const TOP_CHAT_MESSAGES_PRELOAD_INTERVAL = 100;
const INFINITE_LOOP_MARKER = 100;
const CHATLIST_LIMIT_ERROR_LIST = new Set([
'FILTERS_TOO_MUCH',
'CHATLISTS_TOO_MUCH',
'INVITES_TOO_MUCH',
]);
const runThrottledForLoadTopChats = throttle((cb) => cb(), 3000, true);
const runDebouncedForLoadFullChat = debounce((cb) => cb(), 500, false, true);
addActionHandler('preloadTopChatMessages', async (global, actions): Promise<void> => {
const preloadedChatIds = new Set<string>();
for (let i = 0; i < TOP_CHAT_MESSAGES_PRELOAD_LIMIT; i++) {
await pause(TOP_CHAT_MESSAGES_PRELOAD_INTERVAL);
global = getGlobal();
const currentChatIds = Object.values(global.byTabId)
// eslint-disable-next-line @typescript-eslint/no-loop-func
.map(({ id: tabId }) => selectCurrentMessageList(global, tabId)?.chatId)
.filter(Boolean);
const folderAllOrderedIds = getOrderedIds(ALL_FOLDER_ID);
const nextChatId = folderAllOrderedIds?.find((id) => !currentChatIds.includes(id) && !preloadedChatIds.has(id));
if (!nextChatId) {
return;
}
preloadedChatIds.add(nextChatId);
actions.loadViewportMessages({ chatId: nextChatId, threadId: MAIN_THREAD_ID, tabId: getCurrentTabId() });
}
});
function abortChatRequests(chatId: string, threadId?: ThreadId) {
callApi('abortChatRequests', { chatId, threadId });
}
function abortChatRequestsForCurrentChat<T extends GlobalState>(
global: T, newChatId?: string, newThreadId?: ThreadId,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const currentMessageList = selectCurrentMessageList(global, tabId);
const currentChatId = currentMessageList?.chatId;
const currentThreadId = currentMessageList?.threadId;
if (currentChatId && (currentChatId !== newChatId || currentThreadId !== newThreadId)) {
const [isChatOpened, isThreadOpened] = Object.values(global.byTabId)
.reduce(([accHasChatOpened, accHasThreadOpened], { id: otherTabId }) => {
if (otherTabId === tabId || (accHasChatOpened && accHasThreadOpened)) {
return [accHasChatOpened, accHasThreadOpened];
}
const otherMessageList = selectCurrentMessageList(global, otherTabId);
const isSameChat = otherMessageList?.chatId === currentChatId;
const isSameThread = isSameChat && otherMessageList?.threadId === currentThreadId;
return [accHasChatOpened || isSameChat, accHasThreadOpened || isSameThread];
}, [currentChatId === newChatId, false]);
const shouldAbortChatRequests = !isChatOpened || !isThreadOpened;
if (shouldAbortChatRequests) {
abortChatRequests(currentChatId, isChatOpened ? currentThreadId : undefined);
}
}
}
addActionHandler('openChat', (global, actions, payload): ActionReturnType => {
const {
id, type, noForumTopicPanel, shouldReplaceHistory, shouldReplaceLast,
tabId = getCurrentTabId(),
} = payload;
actions.processOpenChatOrThread({
chatId: id,
type,
threadId: MAIN_THREAD_ID,
noForumTopicPanel,
shouldReplaceHistory,
shouldReplaceLast,
tabId,
});
abortChatRequestsForCurrentChat(global, id, MAIN_THREAD_ID, tabId);
if (!id || id === TMP_CHAT_ID) {
return;
}
const chat = selectChat(global, id);
if (chat?.hasUnreadMark) {
actions.toggleChatUnread({ id });
}
const isChatOnlySummary = !selectChatLastMessageId(global, id);
if (!chat) {
if (selectIsChatWithSelf(global, id)) {
void callApi('fetchChat', { type: 'self' });
} else {
const user = selectUser(global, id);
if (user) {
void callApi('fetchChat', { type: 'user', user });
}
}
} else if (isChatOnlySummary && !chat.isMin) {
actions.requestChatUpdate({ chatId: id });
}
});
addActionHandler('openSavedDialog', (global, actions, payload): ActionReturnType => {
const { chatId, tabId = getCurrentTabId(), ...otherParams } = payload;
actions.openThread({
chatId: global.currentUserId!,
threadId: chatId,
tabId,
...otherParams,
});
});
addActionHandler('openThread', async (global, actions, payload): Promise<void> => {
const {
type, isComments, noForumTopicPanel, shouldReplaceHistory, shouldReplaceLast,
focusMessageId,
tabId = getCurrentTabId(),
} = payload;
let { chatId } = payload;
let threadId: ThreadId | undefined;
let loadingChatId: string;
let loadingThreadId: ThreadId;
if (!isComments) {
loadingChatId = payload.chatId;
threadId = payload.threadId;
loadingThreadId = threadId;
const originalChat = selectChat(global, loadingChatId);
if (threadId === MAIN_THREAD_ID) {
actions.openChat({
id: chatId,
type,
noForumTopicPanel,
shouldReplaceHistory,
shouldReplaceLast,
tabId,
});
return;
} else if (originalChat?.isForum || (chatId && getIsSavedDialog(chatId, threadId, global.currentUserId))) {
actions.processOpenChatOrThread({
chatId,
type,
threadId,
isComments,
noForumTopicPanel,
shouldReplaceHistory,
shouldReplaceLast,
tabId,
});
return;
}
} else {
const { originChannelId, originMessageId } = payload;
loadingChatId = originChannelId;
loadingThreadId = originMessageId;
}
const chat = selectChat(global, loadingChatId);
const threadInfo = selectThreadInfo(global, loadingChatId, loadingThreadId);
const thread = selectThread(global, loadingChatId, loadingThreadId);
if (!chat) return;
abortChatRequestsForCurrentChat(global, loadingChatId, loadingThreadId, tabId);
if (chatId
&& threadInfo?.threadId
&& (isComments || (thread?.listedIds?.length && thread.listedIds.includes(Number(threadInfo.threadId))))) {
global = updateTabState(global, {
loadingThread: undefined,
}, tabId);
setGlobal(global);
actions.processOpenChatOrThread({
chatId,
type,
threadId: threadInfo.threadId,
isComments,
noForumTopicPanel,
shouldReplaceHistory,
shouldReplaceLast,
tabId,
});
return;
}
let { loadingThread } = selectTabState(global, tabId);
if (loadingThread) {
abortChatRequests(loadingThread.loadingChatId, loadingThread.loadingMessageId);
}
global = updateTabState(global, {
loadingThread: {
loadingChatId,
loadingMessageId: Number(loadingThreadId),
},
}, tabId);
setGlobal(global);
const openPreviousChat = () => {
// eslint-disable-next-line eslint-multitab-tt/no-immediate-global
const currentGlobal = getGlobal();
if (isComments
|| selectCurrentMessageList(currentGlobal, tabId)?.chatId !== loadingChatId
|| selectCurrentMessageList(currentGlobal, tabId)?.threadId !== loadingThreadId) {
return;
}
actions.openPreviousChat({ tabId });
};
if (!isComments) {
actions.processOpenChatOrThread({
chatId,
type,
threadId: threadId!,
tabId,
isComments,
noForumTopicPanel,
shouldReplaceHistory,
shouldReplaceLast,
});
}
const result = await callApi('fetchDiscussionMessage', {
chat: selectChat(global, loadingChatId)!,
messageId: Number(loadingThreadId),
});
global = getGlobal();
loadingThread = selectTabState(global, tabId).loadingThread;
if (loadingThread?.loadingChatId !== loadingChatId || loadingThread?.loadingMessageId !== loadingThreadId) {
openPreviousChat();
return;
}
if (!result) {
global = updateTabState(global, {
loadingThread: undefined,
}, tabId);
setGlobal(global);
actions.showNotification({
message: langProvider.oldTranslate(isComments ? 'ChannelPostDeleted' : 'lng_message_not_found'),
tabId,
});
openPreviousChat();
return;
}
threadId ??= result.threadId;
chatId ??= result.chatId;
if (!chatId) {
openPreviousChat();
return;
}
global = getGlobal();
global = addMessages(global, result.messages);
if (isComments) {
global = updateThreadInfo(global, loadingChatId, loadingThreadId, {
threadId,
});
global = updateThreadInfo(global, chatId, threadId, {
isCommentsInfo: false,
threadId,
chatId,
fromChannelId: loadingChatId,
fromMessageId: loadingThreadId,
...(threadInfo
&& pick(threadInfo, ['messagesCount', 'lastMessageId', 'lastReadInboxMessageId', 'recentReplierIds'])),
});
}
global = updateThread(global, chatId, threadId, {
firstMessageId: result.firstMessageId,
});
setGlobal(global);
if (focusMessageId) {
actions.focusMessage({
chatId,
threadId: threadId!,
messageId: focusMessageId,
tabId,
});
}
actions.loadViewportMessages({
chatId,
threadId,
tabId,
onError: () => {
global = getGlobal();
global = updateTabState(global, {
loadingThread: undefined,
}, tabId);
setGlobal(global);
actions.showNotification({
message: langProvider.oldTranslate('Group.ErrorAccessDenied'),
tabId,
});
},
onLoaded: () => {
global = getGlobal();
loadingThread = selectTabState(global, tabId).loadingThread;
if (loadingThread?.loadingChatId !== loadingChatId || loadingThread?.loadingMessageId !== loadingThreadId) {
return;
}
global = updateTabState(global, {
loadingThread: undefined,
}, tabId);
setGlobal(global);
actions.processOpenChatOrThread({
chatId,
type,
threadId: threadId!,
tabId,
isComments,
noForumTopicPanel,
shouldReplaceHistory,
shouldReplaceLast,
});
},
});
});
addActionHandler('openLinkedChat', async (global, actions, payload): Promise<void> => {
const { id, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, id);
if (!chat) {
return;
}
const chatFullInfo = await callApi('fetchFullChat', chat);
if (chatFullInfo?.fullInfo?.linkedChatId) {
actions.openChat({ id: chatFullInfo.fullInfo.linkedChatId, tabId });
}
});
addActionHandler('openSupportChat', async (global, actions, payload): Promise<void> => {
const { tabId = getCurrentTabId() } = payload || {};
const chat = selectSupportChat(global);
if (chat) {
actions.openChat({ id: chat.id, shouldReplaceHistory: true, tabId });
return;
}
actions.openChat({ id: TMP_CHAT_ID, shouldReplaceHistory: true, tabId });
const result = await callApi('fetchChat', { type: 'support' });
if (result) {
actions.openChat({ id: result.chatId, shouldReplaceHistory: true, tabId });
}
});
addActionHandler('loadAllChats', async (global, actions, payload): Promise<void> => {
const { whenFirstBatchDone } = payload;
const listType = payload.listType;
let isCallbackFired = false;
let i = 0;
while (!global.chats.isFullyLoaded[listType]) {
if (i++ >= INFINITE_LOOP_MARKER) {
if (DEBUG) {
// eslint-disable-next-line no-console
console.error('`actions/loadAllChats`: Infinite loop detected');
}
return;
}
global = getGlobal();
if (global.connectionState !== 'connectionStateReady' || global.authState !== 'authorizationStateReady') {
return;
}
await loadChats(
listType,
true,
);
if (!isCallbackFired) {
await whenFirstBatchDone?.();
isCallbackFired = true;
}
global = getGlobal();
}
});
addActionHandler('loadFullChat', (global, actions, payload): ActionReturnType => {
const {
chatId, force, withPhotos,
} = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
const loadChat = async () => {
await loadFullChat(global, actions, chat);
if (withPhotos) {
actions.loadMoreProfilePhotos({ peerId: chatId, shouldInvalidateCache: true });
}
};
if (force) {
void loadChat();
} else {
runDebouncedForLoadFullChat(loadChat);
}
});
addActionHandler('loadTopChats', (): ActionReturnType => {
runThrottledForLoadTopChats(() => {
loadChats('active', undefined, true);
loadChats('archived', undefined, true);
});
});
addActionHandler('requestChatUpdate', (global, actions, payload): ActionReturnType => {
const { chatId } = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
void callApi('requestChatUpdate', {
chat,
...(chatId === SERVICE_NOTIFICATIONS_USER_ID && {
lastLocalMessage: selectLastServiceNotification(global)?.message,
}),
});
});
addActionHandler('requestSavedDialogUpdate', async (global, actions, payload): Promise<void> => {
const { chatId } = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
const result = await callApi('fetchMessages', {
chat,
isSavedDialog: true,
limit: 1,
});
if (!result) return;
global = getGlobal();
global = addMessages(global, result.messages);
if (result.messages.length) {
global = updateChatLastMessageId(global, chatId, result.messages[0].id, 'saved');
global = addChatListIds(global, 'saved', [chatId]);
setGlobal(global);
} else {
global = removeChatFromChatLists(global, chatId, 'saved');
setGlobal(global);
Object.values(global.byTabId).forEach(({ id: tabId }) => {
const currentMessageList = selectCurrentMessageList(global, tabId);
if (!currentMessageList) return;
const { chatId: tabChatId, threadId } = currentMessageList;
if (selectIsChatWithSelf(global, tabChatId) && threadId === chatId) {
actions.openChat({ id: undefined, tabId });
}
});
}
});
addActionHandler('updateChatMutedState', (global, actions, payload): ActionReturnType => {
const { chatId, muteUntil = 0 } = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
const isMuted = payload.isMuted ?? muteUntil > 0;
global = updateChat(global, chatId, { isMuted });
setGlobal(global);
void callApi('updateChatMutedState', { chat, isMuted, muteUntil });
});
addActionHandler('updateTopicMutedState', (global, actions, payload): ActionReturnType => {
const { chatId, topicId, muteUntil = 0 } = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
const isMuted = payload.isMuted ?? muteUntil > 0;
global = updateTopic(global, chatId, topicId, { isMuted });
setGlobal(global);
void callApi('updateTopicMutedState', {
chat, topicId, isMuted, muteUntil,
});
});
addActionHandler('createChannel', async (global, actions, payload): Promise<void> => {
const {
title, about, photo, memberIds, tabId = getCurrentTabId(),
} = payload;
const users = (memberIds as string[])
.map((id) => selectUser(global, id))
.filter(Boolean);
global = updateTabState(global, {
chatCreation: {
progress: ChatCreationProgress.InProgress,
},
}, tabId);
setGlobal(global);
let createdChannel: ApiChat | undefined;
let missingInvitedUsers: ApiMissingInvitedUser[] | undefined;
try {
const result = await callApi('createChannel', { title, about, users });
createdChannel = result?.channel;
missingInvitedUsers = result?.missingUsers;
} catch (error) {
global = getGlobal();
global = updateTabState(global, {
chatCreation: {
progress: ChatCreationProgress.Error,
},
}, tabId);
setGlobal(global);
if ((error as ApiError).message === 'CHANNELS_TOO_MUCH') {
actions.openLimitReachedModal({ limit: 'channels', tabId });
} else {
actions.showDialog({ data: { ...(error as ApiError), hasErrorKey: true }, tabId });
}
}
if (!createdChannel) {
return;
}
const { id: channelId, accessHash } = createdChannel;
global = getGlobal();
global = updateChat(global, channelId, createdChannel);
global = updateTabState(global, {
chatCreation: {
...selectTabState(global, tabId).chatCreation,
progress: createdChannel ? ChatCreationProgress.Complete : ChatCreationProgress.Error,
},
}, tabId);
setGlobal(global);
actions.openChat({ id: channelId, shouldReplaceHistory: true, tabId });
if (missingInvitedUsers) {
global = getGlobal();
global = updateMissingInvitedUsers(global, channelId, missingInvitedUsers, tabId);
setGlobal(global);
}
if (channelId && accessHash && photo) {
await callApi('editChatPhoto', { chatId: channelId, accessHash, photo });
}
});
addActionHandler('joinChannel', async (global, actions, payload): Promise<void> => {
const { chatId, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
const { id: channelId, accessHash } = chat;
if (!(channelId && accessHash)) {
return;
}
try {
await callApi('joinChannel', { channelId, accessHash });
} catch (error) {
if ((error as ApiError).message === 'CHANNELS_TOO_MUCH') {
actions.openLimitReachedModal({ limit: 'channels', tabId });
} else {
actions.showDialog({ data: { ...(error as ApiError), hasErrorKey: true }, tabId });
}
}
});
addActionHandler('deleteChatUser', (global, actions, payload): ActionReturnType => {
const {
chatId, userId, shouldRevokeHistory, tabId = getCurrentTabId(),
} = payload;
const chat = selectChat(global, chatId);
const user = selectUser(global, userId);
if (!chat || !user) {
return;
}
global = leaveChat(global, chatId);
setGlobal(global);
if (selectCurrentMessageList(global, tabId)?.chatId === chatId) {
actions.openChat({ id: undefined, tabId });
}
void callApi('deleteChatUser', { chat, user, shouldRevokeHistory });
});
addActionHandler('deleteChat', (global, actions, payload): ActionReturnType => {
const { chatId, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
global = leaveChat(global, chatId);
setGlobal(global);
if (selectCurrentMessageList(global, tabId)?.chatId === chatId) {
actions.openChat({ id: undefined, tabId });
}
void callApi('deleteChat', { chatId: chat.id });
});
addActionHandler('leaveChannel', async (global, actions, payload): Promise<void> => {
const { chatId, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
global = leaveChat(global, chatId);
setGlobal(global);
if (selectCurrentMessageList(global, tabId)?.chatId === chatId) {
actions.openChat({ id: undefined, tabId });
}
const { id: channelId, accessHash } = chat;
if (channelId && accessHash) {
await callApi('leaveChannel', { channelId, accessHash });
global = getGlobal();
const chatMessages = selectChatMessages(global, chatId);
const localMessageIds = Object.keys(chatMessages).map(Number).filter(isLocalMessageId);
global = deleteChatMessages(global, chatId, localMessageIds);
setGlobal(global);
}
});
addActionHandler('deleteChannel', (global, actions, payload): ActionReturnType => {
const { chatId, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
global = leaveChat(global, chatId);
setGlobal(global);
if (selectCurrentMessageList(global, tabId)?.chatId === chatId) {
actions.openChat({ id: undefined, tabId });
}
const { id: channelId, accessHash } = chat;
if (channelId && accessHash) {
void callApi('deleteChannel', { channelId, accessHash });
}
});
addActionHandler('createGroupChat', async (global, actions, payload): Promise<void> => {
const {
title, memberIds, photo, tabId = getCurrentTabId(),
} = payload;
const users = (memberIds as string[])
.map((id) => selectUser(global, id))
.filter(Boolean);
global = updateTabState(global, {
chatCreation: {
progress: ChatCreationProgress.InProgress,
},
}, tabId);
setGlobal(global);
try {
const { chat: createdChat, missingUsers } = await callApi('createGroupChat', {
title,
users,
}) ?? {};
if (!createdChat) {
return;
}
const { id: chatId } = createdChat;
global = getGlobal();
global = updateChat(global, chatId, createdChat);
global = updateTabState(global, {
chatCreation: {
...selectTabState(global, tabId).chatCreation,
progress: createdChat ? ChatCreationProgress.Complete : ChatCreationProgress.Error,
},
}, tabId);
setGlobal(global);
actions.openChat({
id: chatId,
shouldReplaceHistory: true,
tabId,
});
if (missingUsers) {
global = getGlobal();
global = updateMissingInvitedUsers(global, chatId, missingUsers, tabId);
setGlobal(global);
}
if (chatId && photo) {
await callApi('editChatPhoto', {
chatId,
photo,
});
}
} catch (err) {
if ((err as ApiError).message === 'USERS_TOO_FEW') {
global = getGlobal();
global = updateTabState(global, {
chatCreation: {
...selectTabState(global, tabId).chatCreation,
progress: ChatCreationProgress.Error,
error: 'CreateGroupError',
},
}, tabId);
setGlobal(global);
}
}
});
addActionHandler('toggleChatPinned', (global, actions, payload): ActionReturnType => {
const { id, folderId, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, id);
if (!chat) {
return;
}
const limit = selectCurrentLimit(global, 'dialogFolderPinned');
if (folderId) {
const folder = selectChatFolder(global, folderId);
if (folder) {
const shouldBePinned = !selectIsChatPinned(global, id, folderId);
const { pinnedChatIds, includedChatIds } = folder;
const newPinnedIds = shouldBePinned
? [id, ...(pinnedChatIds || [])]
: (pinnedChatIds || []).filter((pinnedId) => pinnedId !== id);
// With both Pin and Unpin we need to re-add a user to the included group
const newIncludedChatIds = [id, ...includedChatIds];
void callApi('editChatFolder', {
id: folderId,
folderUpdate: {
...folder,
pinnedChatIds: newPinnedIds,
includedChatIds: newIncludedChatIds,
},
});
}
} else {
const listType = selectChatListType(global, id);
const isPinned = selectIsChatPinned(global, id, listType === 'archived' ? ARCHIVED_FOLDER_ID : undefined);
const ids = global.chats.orderedPinnedIds[listType === 'archived' ? 'archived' : 'active'];
if ((ids?.length || 0) >= limit && !isPinned) {
actions.openLimitReachedModal({
limit: 'dialogFolderPinned',
tabId,
});
return;
}
void callApi('toggleChatPinned', { chat, shouldBePinned: !isPinned });
}
});
addActionHandler('toggleChatArchived', (global, actions, payload): ActionReturnType => {
const { id } = payload;
const chat = selectChat(global, id);
if (chat) {
void callApi('toggleChatArchived', {
chat,
folderId: isChatArchived(chat) ? 0 : ARCHIVED_FOLDER_ID,
});
}
});
addActionHandler('toggleSavedDialogPinned', (global, actions, payload): ActionReturnType => {
const { id, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, id);
if (!chat) {
return;
}
const limit = selectCurrentLimit(global, 'savedDialogsPinned');
const isPinned = selectIsChatPinned(global, id, SAVED_FOLDER_ID);
const ids = global.chats.orderedPinnedIds.saved;
if ((ids?.length || 0) >= limit && !isPinned) {
actions.openLimitReachedModal({
limit: 'savedDialogsPinned',
tabId,
});
return;
}
void callApi('toggleSavedDialogPinned', { chat, shouldBePinned: !isPinned });
});
addActionHandler('loadChatFolders', async (global): Promise<void> => {
const chatFolders = await callApi('fetchChatFolders');
if (chatFolders) {
global = getGlobal();
global = {
...global,
chatFolders: {
...global.chatFolders,
...chatFolders,
},
};
setGlobal(global);
}
});
addActionHandler('loadRecommendedChatFolders', async (global): Promise<void> => {
const recommendedChatFolders = await callApi('fetchRecommendedChatFolders');
if (recommendedChatFolders) {
global = getGlobal();
global = {
...global,
chatFolders: {
...global.chatFolders,
recommended: recommendedChatFolders,
},
};
setGlobal(global);
}
});
addActionHandler('editChatFolders', (global, actions, payload): ActionReturnType => {
const {
chatId, idsToRemove, idsToAdd, tabId = getCurrentTabId(),
} = payload;
const limit = selectCurrentLimit(global, 'dialogFiltersChats');
const isLimitReached = idsToAdd
.some((id) => selectChatFolder(global, id)!.includedChatIds.length >= limit);
if (isLimitReached) {
actions.openLimitReachedModal({ limit: 'dialogFiltersChats', tabId });
return;
}
idsToRemove.forEach(async (id) => {
const folder = selectChatFolder(global, id);
if (folder) {
await callApi('editChatFolder', {
id,
folderUpdate: {
...folder,
pinnedChatIds: folder.pinnedChatIds?.filter((pinnedId) => pinnedId !== chatId),
includedChatIds: folder.includedChatIds.filter((includedId) => includedId !== chatId),
},
});
}
});
idsToAdd.forEach(async (id) => {
const folder = selectChatFolder(global, id);
if (folder) {
await callApi('editChatFolder', {
id,
folderUpdate: {
...folder,
includedChatIds: folder.includedChatIds.concat(chatId),
},
});
}
});
});
addActionHandler('editChatFolder', (global, actions, payload): ActionReturnType => {
const { id, folderUpdate } = payload;
const folder = selectChatFolder(global, id);
if (folder) {
void callApi('editChatFolder', {
id,
folderUpdate: {
id,
emoticon: folder.emoticon,
pinnedChatIds: folder.pinnedChatIds,
...folderUpdate,
},
});
}
});
addActionHandler('addChatFolder', async (global, actions, payload): Promise<void> => {
const { folder, tabId = getCurrentTabId() } = payload;
const { orderedIds, byId } = global.chatFolders;
const limit = selectCurrentLimit(global, 'dialogFilters');
if (Object.keys(byId).length >= limit) {
actions.openLimitReachedModal({
limit: 'dialogFilters',
tabId,
});
return;
}
const maxId = Math.max(...(orderedIds || []), ARCHIVED_FOLDER_ID);
// Clear fields from recommended folders
const { id: recommendedId, description, ...newFolder } = folder;
const newId = maxId + 1;
const folderUpdate = {
id: newId,
...newFolder,
};
await callApi('editChatFolder', {
id: newId,
folderUpdate,
});
// Update called from the above `callApi` is throttled, but we need to apply changes immediately
actions.apiUpdate({
'@type': 'updateChatFolder',
id: newId,
folder: folderUpdate,
});
actions.requestNextSettingsScreen({
foldersAction: {
type: 'setFolderId',
payload: maxId + 1,
},
tabId,
});
if (!description) {
return;
}
global = getGlobal();
const { recommended } = global.chatFolders;
if (recommended) {
global = {
...global,
chatFolders: {
...global.chatFolders,
recommended: recommended.filter(({ id }) => id !== recommendedId),
},
};
setGlobal(global);
}
});
addActionHandler('sortChatFolders', async (global, actions, payload): Promise<void> => {
const { folderIds } = payload;
const result = await callApi('sortChatFolders', folderIds);
if (result) {
global = getGlobal();
global = {
...global,
chatFolders: {
...global.chatFolders,
orderedIds: folderIds,
},
};
setGlobal(global);
}
});
addActionHandler('deleteChatFolder', async (global, actions, payload): Promise<void> => {
const { id } = payload;
const folder = selectChatFolder(global, id);
if (folder) {
await callApi('deleteChatFolder', id);
}
});
addActionHandler('toggleChatUnread', (global, actions, payload): ActionReturnType => {
const { id } = payload;
const chat = selectChat(global, id);
if (chat) {
if (chat.unreadCount) {
void callApi('markMessageListRead', { chat, threadId: MAIN_THREAD_ID });
} else {
void callApi('toggleDialogUnread', {
chat,
hasUnreadMark: !chat.hasUnreadMark,
});
}
}
});
addActionHandler('markTopicRead', (global, actions, payload): ActionReturnType => {
const { chatId, topicId } = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
const topic = selectTopic(global, chatId, topicId);
const lastTopicMessageId = topic?.lastMessageId;
if (!lastTopicMessageId) return;
void callApi('markMessageListRead', {
chat,
threadId: topicId,
maxId: lastTopicMessageId,
});
global = getGlobal();
global = updateTopic(global, chatId, topicId, {
unreadCount: 0,
});
global = updateThreadInfo(global, chatId, topicId, {
lastReadInboxMessageId: lastTopicMessageId,
});
setGlobal(global);
});
addActionHandler('checkChatInvite', async (global, actions, payload): Promise<void> => {
const { hash, tabId = getCurrentTabId() } = payload;
const result = await callApi('checkChatInvite', hash);
if (!result) {
return;
}
global = getGlobal();
if (result.users) {
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
}
if (result.chat) {
global = addChats(global, buildCollectionByKey([result.chat], 'id'));
setGlobal(global);
actions.openChat({ id: result.chat.id, tabId });
return;
}
if (result.invite.subscriptionFormId) {
global = updateTabState(global, {
starsPayment: {
inputInvoice: {
type: 'chatInviteSubscription',
hash,
},
subscriptionInfo: result.invite,
status: 'pending',
},
}, tabId);
setGlobal(global);
return;
}
global = updateTabState(global, {
chatInviteModal: {
hash,
inviteInfo: result.invite,
},
}, tabId);
setGlobal(global);
});
addActionHandler('openChatByPhoneNumber', async (global, actions, payload): Promise<void> => {
const {
phoneNumber, startAttach, attach, text, tabId = getCurrentTabId(),
} = payload;
// Open temporary empty chat to make the click response feel faster
actions.openChat({ id: TMP_CHAT_ID, tabId });
const chat = await fetchChatByPhoneNumber(global, phoneNumber);
if (!chat) {
actions.openPreviousChat({ tabId });
actions.showNotification({
message: langProvider.oldTranslate('lng_username_by_phone_not_found').replace('{phone}', phoneNumber),
tabId,
});
return;
}
if (text) {
actions.openChatWithDraft({ chatId: chat.id, text: { text }, tabId });
} else {
actions.openChat({ id: chat.id, tabId });
}
if (attach) {
global = getGlobal();
openAttachMenuFromLink(global, actions, chat.id, attach, startAttach, tabId);
}
});
addActionHandler('openTelegramLink', async (global, actions, payload): Promise<void> => {
const {
url,
shouldIgnoreCache,
tabId = getCurrentTabId(),
} = payload;
const {
openChatByPhoneNumber,
checkChatInvite,
openStickerSet,
openChatWithDraft,
joinVoiceChatByLink,
openInvoice,
checkChatlistInvite,
openChatByUsername: openChatByUsernameAction,
openStoryViewerByUsername,
checkGiftCode,
} = actions;
if (isDeepLink(url)) {
const isProcessed = processDeepLink(url);
if (isProcessed || url.match(RE_TG_LINK)) {
return;
}
}
const uri = new URL(url.toLowerCase().startsWith('http') ? url : `https://${url}`);
if (TME_WEB_DOMAINS.has(uri.hostname) && uri.pathname === '/') {
window.open(uri.toString(), '_blank', 'noopener');
return;
}
const hostname = TME_WEB_DOMAINS.has(uri.hostname) ? 't.me' : uri.hostname;
const hostParts = hostname.split('.');
if (hostParts.length > 3) return;
const adaptedPathname = uri.pathname.replace(/^\/?s\//, '');
const pathname = hostParts.length === 3 ? `${hostParts[0]}/${adaptedPathname}` : adaptedPathname;
const [part1, part2, part3] = pathname.split('/').filter(Boolean).map((part) => decodeURI(part));
const params = Object.fromEntries(uri.searchParams);
let hash: string | undefined;
if (part1 === 'joinchat') {
hash = part2;
}
const storyId = part2 === 's' && (Number(part3) || undefined);
if (part1.match(/^\+([0-9]+)(\?|$)/)) {
openChatByPhoneNumber({
phoneNumber: part1.substr(1, part1.length - 1),
startAttach: params.startattach,
attach: params.attach,
text: params.text,
tabId,
});
return;
}
if (storyId) {
openStoryViewerByUsername({
username: part1,
storyId,
tabId,
});
return;
}
if (part1.startsWith(' ') || part1.startsWith('+')) {
hash = part1.substr(1, part1.length - 1);
}
if (hash) {
checkChatInvite({ hash, tabId });
return;
}
if (part1 === 'addstickers' || part1 === 'addemoji') {
openStickerSet({
stickerSetInfo: {
shortName: part2,
},
shouldIgnoreCache,
tabId,
});
return;
}
if (part1 === 'share') {
const text = formatShareText(params.url, params.text);
openChatWithDraft({ text, tabId });
return;
}
if (part1 === 'addlist') {
const slug = part2;
checkChatlistInvite({ slug, tabId });
return;
}
if (part1 === 'giftcode') {
const slug = part2;
checkGiftCode({ slug, tabId });
return;
}
const chatOrChannelPostId = part2 || undefined;
const messageId = part3 ? Number(part3) : undefined;
const commentId = params.comment ? Number(params.comment) : undefined;
const isWebApp = await checkWebAppExists(global, part1, part2);
const shouldTryOpenChat = (part1 && !part2) || Number.isInteger(Number(part2)) || isWebApp;
if (params.hasOwnProperty('voicechat') || params.hasOwnProperty('livestream')) {
joinVoiceChatByLink({
username: part1,
inviteHash: params.voicechat || params.livestream,
tabId,
});
} else if (part1.startsWith('$')) {
openInvoice({
type: 'slug',
slug: part1.substring(1),
tabId,
});
} else if (part1 === 'invoice') {
openInvoice({
type: 'slug',
slug: part2,
tabId,
});
} else if (shouldTryOpenChat) {
openChatByUsernameAction({
username: part1,
messageId: messageId || Number(chatOrChannelPostId),
threadId: messageId ? Number(chatOrChannelPostId) : undefined,
commentId,
startParam: params.start,
startAttach: params.startattach,
attach: params.attach,
startApp: params.startapp,
mode: params.mode,
originalParts: [part1, part2, part3],
tabId,
});
} else {
actions.openUrl({
url, shouldSkipModal: true, tabId, ignoreDeepLinks: true,
});
}
});
addActionHandler('processBoostParameters', async (global, actions, payload): Promise<void> => {
const { usernameOrId, isPrivate, tabId = getCurrentTabId() } = payload;
let chat: ApiChat | undefined;
if (isPrivate) {
chat = selectChat(global, usernameOrId);
if (!chat) {
actions.showNotification({ message: { key: 'PrivateChannelInaccessible' }, tabId });
return;
}
} else {
chat = await fetchChatByUsername(global, usernameOrId);
if (!chat) {
actions.showNotification({ message: { key: 'NoUsernameFound' }, tabId });
return;
}
}
if (!isChatChannel(chat) && !isChatSuperGroup(chat)) {
actions.openChat({ id: chat.id, tabId });
return;
}
actions.openBoostModal({
chatId: chat.id,
tabId,
});
});
addActionHandler('acceptChatInvite', async (global, actions, payload): Promise<void> => {
const { hash, tabId = getCurrentTabId() } = payload;
const result = await callApi('importChatInvite', { hash });
if (!result) {
return;
}
actions.openChat({ id: result.id, tabId });
});
addActionHandler('openChatByUsername', async (global, actions, payload): Promise<void> => {
const {
username, messageId, commentId, startParam, startAttach, attach, threadId, originalParts, startApp, mode,
text, onChatChanged, choose, ref, timestamp,
tabId = getCurrentTabId(),
} = payload;
const chat = selectCurrentChat(global, tabId);
const webAppName = originalParts?.[1];
const isWebApp = webAppName && !Number(webAppName) && !originalParts?.[2];
if (!commentId) {
if (startAttach === undefined && messageId && !startParam && !ref
&& chat?.usernames?.some((c) => c.username === username)) {
actions.focusMessage({
chatId: chat.id, threadId, messageId, timestamp, tabId,
});
return;
}
if (startAttach !== undefined && choose) {
actions.processAttachBotParameters({
username,
filter: choose,
startParam: startAttach || startApp,
tabId,
});
return;
}
if (startApp !== undefined && !webAppName) {
const theme = extractCurrentThemeParams();
const chatByUsername = await fetchChatByUsername(global, username);
global = getGlobal();
const user = chatByUsername && selectUser(global, chatByUsername.id);
if (!chatByUsername || !chat || !user?.hasMainMiniApp) return;
actions.requestMainWebView({
botId: chatByUsername.id,
peerId: chat.id,
theme,
startParam: startApp,
mode,
tabId,
});
return;
}
if (!isWebApp) {
await openChatByUsername(
global, actions, {
username,
threadId,
channelPostId: messageId,
startParam,
ref,
startAttach,
attach,
text,
timestamp,
}, tabId,
);
if (onChatChanged) {
// @ts-ignore
actions[onChatChanged.action](onChatChanged.payload);
}
return;
}
}
const usernameChat = selectChatByUsername(global, username);
if (commentId && messageId && usernameChat) {
actions.openThread({
isComments: true,
originChannelId: usernameChat.id,
originMessageId: messageId,
tabId,
focusMessageId: commentId,
});
if (timestamp) {
actions.openMediaFromTimestamp({
chatId: usernameChat.id,
messageId: commentId,
timestamp,
tabId,
});
}
return;
}
if (!isWebApp) actions.openChat({ id: TMP_CHAT_ID, tabId });
const chatByUsername = await fetchChatByUsername(global, username);
if (!chatByUsername) return;
if (isWebApp && chatByUsername) {
const theme = extractCurrentThemeParams();
actions.requestAppWebView({
appName: webAppName,
botId: chatByUsername.id,
tabId,
startApp,
mode,
theme,
});
return;
}
if (!messageId) return;
actions.openThread({
isComments: true,
originChannelId: chatByUsername.id,
originMessageId: messageId,
tabId,
focusMessageId: commentId,
});
if (timestamp) {
actions.openMediaFromTimestamp({
chatId: chatByUsername.id,
messageId: commentId || messageId!,
timestamp,
tabId,
});
}
if (onChatChanged) {
// @ts-ignore
actions[onChatChanged.action](onChatChanged.payload);
}
});
addActionHandler('openPrivateChannel', (global, actions, payload): ActionReturnType => {
const {
id, commentId, messageId, threadId, timestamp, tabId = getCurrentTabId(),
} = payload;
const chat = selectChat(global, id);
if (!chat) {
actions.showNotification({
message: {
key: 'PrivateChannelInaccessible',
},
tabId,
});
return;
}
if (!commentId && !messageId && !threadId) {
actions.openChat({ id, tabId });
return;
}
if (timestamp) {
actions.openMediaFromTimestamp({
chatId: id,
messageId: commentId || messageId!,
timestamp,
tabId,
});
}
if (commentId && messageId) {
actions.openThread({
isComments: true,
originChannelId: id,
originMessageId: messageId,
tabId,
focusMessageId: commentId,
});
return;
}
openChatWithParams(global, actions, chat, {
messageId,
threadId,
timestamp,
}, tabId);
});
addActionHandler('togglePreHistoryHidden', async (global, actions, payload): Promise<void> => {
const {
chatId, isEnabled,
tabId = getCurrentTabId(),
} = payload;
const chat = await ensureIsSuperGroup(global, actions, chatId, tabId);
if (!chat) {
return;
}
global = getGlobal();
global = updateChatFullInfo(global, chat.id, { isPreHistoryHidden: isEnabled });
setGlobal(global);
void callApi('togglePreHistoryHidden', { chat, isEnabled });
});
addActionHandler('updateChatDefaultBannedRights', (global, actions, payload): ActionReturnType => {
const { chatId, bannedRights } = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
void callApi('updateChatDefaultBannedRights', { chat, bannedRights });
});
addActionHandler('updateChatMemberBannedRights', async (global, actions, payload): Promise<void> => {
const {
chatId, userId, bannedRights,
tabId = getCurrentTabId(),
} = payload;
const user = selectUser(global, userId);
if (!user) {
return;
}
const chat = await ensureIsSuperGroup(global, actions, chatId, tabId);
if (!chat) return;
await callApi('updateChatMemberBannedRights', { chat, user, bannedRights });
global = getGlobal();
const updatedFullInfo = selectChatFullInfo(global, chat.id);
if (!updatedFullInfo) {
return;
}
const { members, kickedMembers } = updatedFullInfo;
const isBanned = Boolean(bannedRights.viewMessages);
const isUnblocked = !Object.keys(bannedRights).length;
global = updateChatFullInfo(global, chat.id, {
...(members && isBanned && {
members: members.filter((m) => m.userId !== userId),
}),
...(members && !isBanned && {
members: members.map((m) => (
m.userId === userId
? { ...m, bannedRights }
: m
)),
}),
...(isUnblocked && kickedMembers && {
kickedMembers: kickedMembers.filter((m) => m.userId !== userId),
}),
});
setGlobal(global);
});
addActionHandler('updateChatAdmin', async (global, actions, payload): Promise<void> => {
const {
chatId, userId, adminRights, customTitle,
tabId = getCurrentTabId(),
} = payload;
const user = selectUser(global, userId);
if (!user) {
return;
}
const chat = await ensureIsSuperGroup(global, actions, chatId, tabId);
if (!chat) return;
await callApi('updateChatAdmin', {
chat, user, adminRights, customTitle,
});
const chatAfterUpdate = await callApi('fetchFullChat', chat);
if (!chatAfterUpdate?.fullInfo) {
return;
}
const { adminMembersById } = chatAfterUpdate.fullInfo;
const isDismissed = !Object.keys(adminRights).length;
let newAdminMembersById: Record<string, ApiChatMember> | undefined;
if (adminMembersById) {
if (isDismissed) {
const { [userId]: remove, ...rest } = adminMembersById;
newAdminMembersById = rest;
} else {
newAdminMembersById = {
...adminMembersById,
[userId]: {
...adminMembersById[userId],
adminRights,
customTitle,
},
};
}
}
if (newAdminMembersById) {
global = getGlobal();
global = updateChatFullInfo(global, chat.id, { adminMembersById: newAdminMembersById });
setGlobal(global);
}
});
addActionHandler('updateChat', async (global, actions, payload): Promise<void> => {
const {
chatId, title, about, photo, tabId = getCurrentTabId(),
} = payload;
const chat = selectChat(global, chatId);
const fullInfo = selectChatFullInfo(global, chatId);
if (!chat) {
return;
}
global = getGlobal();
global = updateManagementProgress(global, ManagementProgress.InProgress, tabId);
setGlobal(global);
await Promise.all([
chat.title !== title
? callApi('updateChatTitle', chat, title)
: undefined,
fullInfo?.about !== about
? callApi('updateChatAbout', chat, about)
: undefined,
photo
? callApi('editChatPhoto', { chatId, accessHash: chat.accessHash, photo })
: undefined,
]);
global = getGlobal();
global = updateManagementProgress(global, ManagementProgress.Complete, tabId);
setGlobal(global);
if (photo) {
actions.loadFullChat({ chatId, withPhotos: true });
}
});
addActionHandler('updateChatPhoto', async (global, actions, payload): Promise<void> => {
const { photo, chatId } = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
await callApi('editChatPhoto', {
chatId,
accessHash: chat.accessHash,
photo,
});
actions.loadFullChat({ chatId, withPhotos: true });
});
addActionHandler('deleteChatPhoto', async (global, actions, payload): Promise<void> => {
const { photo, chatId } = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
let isDeleted;
if (photo.id === chat.avatarPhotoId) {
isDeleted = await callApi('editChatPhoto', {
chatId,
accessHash: chat.accessHash,
});
} else {
isDeleted = await callApi('deleteProfilePhotos', [photo]);
}
if (!isDeleted) return;
global = getGlobal();
global = deletePeerPhoto(global, chatId, photo.id);
setGlobal(global);
actions.loadFullChat({ chatId, withPhotos: true });
});
addActionHandler('toggleSignatures', (global, actions, payload): ActionReturnType => {
const { chatId, areProfilesEnabled, areSignaturesEnabled } = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
void callApi('toggleSignatures', { chat, areProfilesEnabled, areSignaturesEnabled });
});
addActionHandler('loadGroupsForDiscussion', async (global): Promise<void> => {
const groups = await callApi('fetchGroupsForDiscussion');
if (!groups) {
return;
}
const addedById = groups.reduce((result, group) => {
if (group && !group.isForum) {
result[group.id] = group;
}
return result;
}, {} as Record<string, ApiChat>);
global = getGlobal();
global = {
...global,
chats: {
...global.chats,
forDiscussionIds: Object.keys(addedById),
},
};
setGlobal(global);
});
addActionHandler('linkDiscussionGroup', async (global, actions, payload): Promise<void> => {
const { channelId, chatId, tabId = getCurrentTabId() } = payload || {};
const channel = selectChat(global, channelId);
if (!channel) {
return;
}
const chat = await ensureIsSuperGroup(global, actions, chatId, tabId);
if (!chat) return;
let fullInfo = selectChatFullInfo(global, chat.id);
if (!fullInfo) {
const fullChat = await callApi('fetchFullChat', chat);
if (!fullChat) {
return;
}
fullInfo = fullChat.fullInfo;
}
if (fullInfo!.isPreHistoryHidden) {
global = getGlobal();
global = updateChatFullInfo(global, chat.id, { isPreHistoryHidden: false });
setGlobal(global);
await callApi('togglePreHistoryHidden', { chat, isEnabled: false });
}
void callApi('setDiscussionGroup', { channel, chat });
});
addActionHandler('unlinkDiscussionGroup', async (global, actions, payload): Promise<void> => {
const { channelId } = payload;
const channel = selectChat(global, channelId);
if (!channel) {
return;
}
const fullInfo = selectChatFullInfo(global, channelId);
let chat: ApiChat | undefined;
if (fullInfo?.linkedChatId) {
chat = selectChat(global, fullInfo.linkedChatId);
}
await callApi('setDiscussionGroup', { channel });
if (chat) {
global = getGlobal();
loadFullChat(global, actions, chat);
}
});
addActionHandler('setActiveChatFolder', (global, actions, payload): ActionReturnType => {
const { activeChatFolder, tabId = getCurrentTabId() } = payload;
const maxFolders = selectCurrentLimit(global, 'dialogFilters');
const isBlocked = activeChatFolder + 1 > maxFolders;
if (isBlocked) {
actions.openLimitReachedModal({
limit: 'dialogFilters',
tabId,
});
return undefined;
}
return updateTabState(global, {
activeChatFolder,
}, tabId);
});
addActionHandler('resetOpenChatWithDraft', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
requestedDraft: undefined,
}, tabId);
});
addActionHandler('loadMoreMembers', async (global, actions, payload): Promise<void> => {
const { tabId = getCurrentTabId() } = payload || {};
const { chatId } = selectCurrentMessageList(global, tabId) || {};
const chat = chatId ? selectChat(global, chatId) : undefined;
if (!chat || isChatBasicGroup(chat)) {
return;
}
const offset = selectChatFullInfo(global, chat.id)?.members?.length;
if (offset !== undefined && chat.membersCount !== undefined && offset >= chat.membersCount) return;
const result = await callApi('fetchMembers', { chat, offset });
if (!result) {
return;
}
const { members, userStatusesById } = result;
if (!members || !members.length) {
return;
}
global = getGlobal();
global = addUserStatuses(global, userStatusesById);
global = addChatMembers(global, chat, members);
setGlobal(global);
});
addActionHandler('addChatMembers', async (global, actions, payload): Promise<void> => {
const { chatId, memberIds, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, chatId);
const users = memberIds.map((userId) => selectUser(global, userId)).filter(Boolean);
if (!chat || !users.length) {
return;
}
actions.setNewChatMembersDialogState({ newChatMembersProgress: NewChatMembersProgress.Loading, tabId });
const missingUsers = await callApi('addChatMembers', chat, users);
if (missingUsers) {
global = getGlobal();
global = updateMissingInvitedUsers(global, chatId, missingUsers, tabId);
setGlobal(global);
}
actions.setNewChatMembersDialogState({ newChatMembersProgress: NewChatMembersProgress.Closed, tabId });
global = getGlobal();
loadFullChat(global, actions, chat);
});
addActionHandler('deleteChatMember', async (global, actions, payload): Promise<void> => {
const { chatId, userId } = payload;
const chat = selectChat(global, chatId);
const user = selectUser(global, userId);
if (!chat || !user) {
return;
}
await callApi('deleteChatMember', chat, user);
global = getGlobal();
loadFullChat(global, actions, chat);
});
addActionHandler('toggleIsProtected', (global, actions, payload): ActionReturnType => {
const { chatId, isProtected } = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
void callApi('toggleIsProtected', { chat, isProtected });
});
addActionHandler('setChatEnabledReactions', async (global, actions, payload): Promise<void> => {
const {
chatId, enabledReactions, reactionsLimit,
} = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
await callApi('setChatEnabledReactions', {
chat,
enabledReactions,
reactionsLimit,
});
global = getGlobal();
void loadFullChat(global, actions, chat);
});
addActionHandler('fetchChat', (global, actions, payload): ActionReturnType => {
const { chatId } = payload;
const chat = selectChat(global, chatId);
if (chat) {
return;
}
if (selectIsChatWithSelf(global, chatId)) {
void callApi('fetchChat', { type: 'self' });
} else {
const user = selectUser(global, chatId);
if (user) {
void callApi('fetchChat', { type: 'user', user });
}
}
});
addActionHandler('loadChatSettings', async (global, actions, payload): Promise<void> => {
const { chatId } = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
const result = await callApi('fetchChatSettings', chat);
if (!result) return;
const { settings } = result;
global = getGlobal();
global = updateChat(global, chat.id, { settings });
setGlobal(global);
});
addActionHandler('toggleJoinToSend', async (global, actions, payload): Promise<void> => {
const { chatId, isEnabled } = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
if (!isChatSuperGroup(chat) && !isChatChannel(chat)) return;
await callApi('toggleJoinToSend', chat, isEnabled);
});
addActionHandler('toggleJoinRequest', async (global, actions, payload): Promise<void> => {
const { chatId, isEnabled } = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
if (!isChatSuperGroup(chat) && !isChatChannel(chat)) return;
await callApi('toggleJoinRequest', chat, isEnabled);
});
addActionHandler('openForumPanel', (global, actions, payload): ActionReturnType => {
const { chatId, tabId = getCurrentTabId() } = payload;
actions.toggleStoryRibbon({ isShown: false, tabId });
actions.toggleStoryRibbon({ isShown: false, isArchived: true, tabId });
return updateTabState(global, {
forumPanelChatId: chatId,
}, tabId);
});
addActionHandler('closeForumPanel', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
forumPanelChatId: undefined,
}, tabId);
});
addActionHandler('processAttachBotParameters', async (global, actions, payload): Promise<void> => {
const {
username, filter, startParam, tabId = getCurrentTabId(),
} = payload;
const bot = await getAttachBotOrNotify(global, actions, username, tabId);
if (!bot) return;
const isForChat = Boolean(filter);
if (!isForChat) {
actions.callAttachBot({
isFromSideMenu: true,
bot,
startParam,
tabId,
});
return;
}
global = getGlobal();
const { attachMenu: { bots } } = global;
if (!bots[bot.id]) {
global = updateTabState(global, {
requestedAttachBotInstall: {
bot,
onConfirm: {
action: 'requestAttachBotInChat',
payload: {
bot,
filter,
startParam,
},
},
},
}, tabId);
setGlobal(global);
return;
}
actions.requestAttachBotInChat({
bot,
filter,
startParam,
tabId,
});
});
addActionHandler('loadTopics', async (global, actions, payload): Promise<void> => {
const { chatId, force } = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
const topicsInfo = selectTopicsInfo(global, chatId);
if (!force && topicsInfo?.listedTopicIds && topicsInfo.listedTopicIds.length === topicsInfo.totalCount) {
return;
}
const offsetTopic = !force ? topicsInfo?.listedTopicIds?.reduce((acc, el) => {
const topic = selectTopic(global, chatId, el);
const accTopic = selectTopic(global, chatId, acc);
if (!topic) return acc;
if (!accTopic || topic.lastMessageId < accTopic.lastMessageId) {
return el;
}
return acc;
}) : undefined;
const { id: offsetTopicId, date: offsetDate, lastMessageId: offsetId } = (offsetTopic
&& selectTopic(global, chatId, offsetTopic)) || {};
const result = await callApi('fetchTopics', {
chat, offsetTopicId, offsetId, offsetDate, limit: offsetTopicId ? TOPICS_SLICE : TOPICS_SLICE_SECOND_LOAD,
});
if (!result) return;
global = getGlobal();
global = addMessages(global, result.messages);
global = updateTopics(global, chatId, result.count, result.topics);
global = updateListedTopicIds(global, chatId, result.topics.map((topic) => topic.id));
Object.entries(result.draftsById || {}).forEach(([threadId, draft]) => {
global = replaceThreadParam(global, chatId, Number(threadId), 'draft', draft);
});
Object.entries(result.readInboxMessageIdByTopicId || {}).forEach(([topicId, messageId]) => {
global = updateThreadInfo(global, chatId, Number(topicId), { lastReadInboxMessageId: messageId });
});
setGlobal(global);
});
addActionHandler('loadTopicById', async (global, actions, payload): Promise<void> => {
const { chatId, topicId } = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
const result = await callApi('fetchTopicById', { chat, topicId });
if (!result) {
if ('tabId' in payload && payload.shouldCloseChatOnError) {
const { tabId = getCurrentTabId() } = payload;
actions.openChat({ id: undefined, tabId });
}
return;
}
global = getGlobal();
global = addMessages(global, result.messages);
global = updateTopic(global, chatId, topicId, result.topic);
setGlobal(global);
});
addActionHandler('toggleForum', async (global, actions, payload): Promise<void> => {
const { chatId, isEnabled, tabId = getCurrentTabId() } = payload;
const chat = await ensureIsSuperGroup(global, actions, chatId, tabId);
if (!chat) {
return;
}
let result: true | undefined;
try {
result = await callApi('toggleForum', { chat, isEnabled });
} catch (error) {
if ((error as ApiError).message === 'FLOOD') {
actions.showNotification({ message: langProvider.oldTranslate('FloodWait'), tabId });
} else {
actions.showDialog({ data: { ...(error as ApiError), hasErrorKey: true }, tabId });
}
}
if (result) {
global = getGlobal();
global = updateChat(global, chat.id, { isForum: isEnabled });
setGlobal(global);
if (!isEnabled) {
actions.closeForumPanel({ tabId });
} else {
actions.openForumPanel({ chatId: chat.id, tabId });
}
}
});
addActionHandler('toggleParticipantsHidden', async (global, actions, payload): Promise<void> => {
const { chatId, isEnabled } = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
const prevIsEnabled = selectChatFullInfo(global, chat.id)?.areParticipantsHidden;
global = updateChatFullInfo(global, chatId, { areParticipantsHidden: isEnabled });
setGlobal(global);
const result = await callApi('toggleParticipantsHidden', { chat, isEnabled });
if (!result && prevIsEnabled !== undefined) {
global = getGlobal();
global = updateChatFullInfo(global, chatId, { areParticipantsHidden: prevIsEnabled });
setGlobal(global);
}
});
addActionHandler('createTopic', async (global, actions, payload): Promise<void> => {
const {
chatId, title, iconColor, iconEmojiId,
tabId = getCurrentTabId(),
} = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
if (selectTabState(global, tabId).createTopicPanel) {
global = updateTabState(global, {
createTopicPanel: {
chatId,
isLoading: true,
},
}, tabId);
setGlobal(global);
}
const topicId = await callApi('createTopic', {
chat, title, iconColor, iconEmojiId,
});
if (topicId) {
actions.openThread({
chatId, threadId: topicId, shouldReplaceHistory: true, tabId,
});
}
actions.closeCreateTopicPanel({ tabId });
});
addActionHandler('deleteTopic', async (global, actions, payload): Promise<void> => {
const { chatId, topicId } = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
await callApi('deleteTopic', { chat, topicId });
global = getGlobal();
global = deleteTopic(global, chatId, topicId);
setGlobal(global);
});
addActionHandler('editTopic', async (global, actions, payload): Promise<void> => {
const {
chatId, topicId, tabId = getCurrentTabId(), ...rest
} = payload;
const chat = selectChat(global, chatId);
const topic = selectTopic(global, chatId, topicId);
if (!chat || !topic) return;
if (selectTabState(global, tabId).editTopicPanel) {
global = updateTabState(global, {
editTopicPanel: {
chatId,
topicId,
isLoading: true,
},
}, tabId);
setGlobal(global);
}
const result = await callApi('editTopic', { chat, topicId, ...rest });
if (!result) return;
global = getGlobal();
global = updateTopic(global, chatId, topicId, rest);
setGlobal(global);
actions.closeEditTopicPanel({ tabId });
});
addActionHandler('toggleTopicPinned', (global, actions, payload): ActionReturnType => {
const {
chatId, topicId, isPinned, tabId = getCurrentTabId(),
} = payload;
const { topicsPinnedLimit } = global.appConfig || {};
const chat = selectChat(global, chatId);
const topics = selectTopics(global, chatId);
if (!chat || !topics || !topicsPinnedLimit) return;
if (isPinned && Object.values(topics).filter((topic) => topic.isPinned).length >= topicsPinnedLimit) {
actions.showNotification({
message: langProvider.oldTranslate('LimitReachedPinnedTopics', topicsPinnedLimit, 'i'),
tabId,
});
return;
}
void callApi('togglePinnedTopic', { chat, topicId, isPinned });
});
addActionHandler('checkChatlistInvite', async (global, actions, payload): Promise<void> => {
const { slug, tabId = getCurrentTabId() } = payload;
const result = await callApi('checkChatlistInvite', { slug });
if (!result) {
actions.showNotification({
message: langProvider.oldTranslate('lng_group_invite_bad_link'),
tabId,
});
return;
}
global = getGlobal();
global = updateTabState(global, {
chatlistModal: {
invite: result.invite,
},
}, tabId);
setGlobal(global);
});
addActionHandler('joinChatlistInvite', async (global, actions, payload): Promise<void> => {
const { invite, peerIds, tabId = getCurrentTabId() } = payload;
const peers = peerIds.map((peerId) => selectChat(global, peerId)).filter(Boolean);
const currentNotJoinedCount = peers.filter((peer) => peer.isNotJoined).length;
const existingFolder = 'folderId' in invite ? selectChatFolder(global, invite.folderId) : undefined;
const folderTitle = ('title' in invite ? invite.title : existingFolder?.title)!;
try {
const result = await callApi('joinChatlistInvite', { slug: invite.slug, peers });
if (!result) return;
if (existingFolder) {
actions.showNotification({
title: {
key: 'FolderLinkNotificationUpdatedTitle',
variables: {
title: folderTitle.text,
},
},
message: {
key: 'FolderLinkNotificationUpdatedSubtitle',
variables: {
count: currentNotJoinedCount,
},
options: {
pluralValue: currentNotJoinedCount,
},
},
tabId,
});
return;
}
actions.showNotification({
title: {
key: 'FolderLinkNotificationAddedTitle',
variables: {
title: folderTitle.text,
},
},
message: {
key: 'FolderLinkNotificationAddedSubtitle',
variables: {
count: currentNotJoinedCount,
},
options: {
pluralValue: currentNotJoinedCount,
},
},
tabId,
});
} catch (error) {
if ((error as ApiError).message === 'CHATLISTS_TOO_MUCH') {
actions.openLimitReachedModal({ limit: 'chatlistJoined', tabId });
} else {
actions.showDialog({ data: { ...(error as ApiError), hasErrorKey: true }, tabId });
}
}
});
addActionHandler('leaveChatlist', async (global, actions, payload): Promise<void> => {
const { folderId, peerIds, tabId = getCurrentTabId() } = payload;
const folder = selectChatFolder(global, folderId);
const peers = peerIds?.map((peerId) => selectChat(global, peerId)).filter(Boolean) || [];
const result = await callApi('leaveChatlist', { folderId, peers });
if (!result) return;
if (!folder) return;
actions.showNotification({
title: {
key: 'FolderLinkNotificationDeletedTitle',
variables: {
title: folder.title.text,
},
},
message: {
key: 'FolderLinkNotificationDeletedSubtitle',
variables: {
count: peers.length,
},
options: {
pluralValue: peers.length,
},
},
tabId,
});
});
addActionHandler('loadChatlistInvites', async (global, actions, payload): Promise<void> => {
const { folderId } = payload;
const result = await callApi('fetchChatlistInvites', { folderId });
if (!result) return;
global = getGlobal();
global = {
...global,
chatFolders: {
...global.chatFolders,
invites: {
...global.chatFolders.invites,
[folderId]: result.invites,
},
},
};
setGlobal(global);
});
addActionHandler('createChatlistInvite', async (global, actions, payload): Promise<void> => {
const { folderId, tabId = getCurrentTabId() } = payload;
const folder = selectChatFolder(global, folderId);
if (!folder) return;
global = updateTabState(global, {
shareFolderScreen: {
...selectTabState(global, tabId).shareFolderScreen!,
isLoading: true,
},
}, tabId);
setGlobal(global);
let result: { filter: ApiChatFolder; invite: ApiChatlistExportedInvite | undefined } | undefined;
try {
result = await callApi('createChalistInvite', {
folderId,
peers: folder.includedChatIds.concat(folder.pinnedChatIds || [])
.map((chatId) => selectChat(global, chatId) || selectUser(global, chatId)).filter(Boolean),
});
} catch (error) {
if (CHATLIST_LIMIT_ERROR_LIST.has((error as ApiError).message)) {
actions.openLimitReachedModal({ limit: 'chatlistInvites', tabId });
actions.requestNextSettingsScreen({ screen: SettingsScreens.Folders, tabId });
} else {
actions.showDialog({ data: { ...(error as ApiError), hasErrorKey: true }, tabId });
}
}
if (!result || !result.invite) return;
const { shareFolderScreen } = selectTabState(global, tabId);
if (!shareFolderScreen) return;
global = getGlobal();
global = {
...global,
chatFolders: {
...global.chatFolders,
byId: {
...global.chatFolders.byId,
[folderId]: {
...global.chatFolders.byId[folderId],
...result.filter,
},
},
invites: {
...global.chatFolders.invites,
[folderId]: [
...(global.chatFolders.invites[folderId] || []),
result.invite,
],
},
},
};
global = updateTabState(global, {
shareFolderScreen: {
...shareFolderScreen,
url: result.invite.url,
isLoading: false,
},
}, tabId);
setGlobal(global);
});
addActionHandler('editChatlistInvite', async (global, actions, payload): Promise<void> => {
const {
folderId, peerIds, url, tabId = getCurrentTabId(),
} = payload;
const slug = url.split('/').pop();
if (!slug) return;
const peers = peerIds
.map((chatId) => selectChat(global, chatId) || selectUser(global, chatId)).filter(Boolean);
global = updateTabState(global, {
shareFolderScreen: {
...selectTabState(global, tabId).shareFolderScreen!,
isLoading: true,
},
}, tabId);
setGlobal(global);
try {
const result = await callApi('editChatlistInvite', { folderId, slug, peers });
if (!result) {
return;
}
global = getGlobal();
global = {
...global,
chatFolders: {
...global.chatFolders,
invites: {
...global.chatFolders.invites,
[folderId]: global.chatFolders.invites[folderId]?.map((invite) => {
if (invite.url === url) {
return result;
}
return invite;
}),
},
},
};
setGlobal(global);
} catch (error) {
actions.showDialog({ data: { ...(error as ApiError), hasErrorKey: true }, tabId });
} finally {
global = getGlobal();
global = updateTabState(global, {
shareFolderScreen: {
...selectTabState(global, tabId).shareFolderScreen!,
isLoading: false,
},
}, tabId);
setGlobal(global);
}
});
addActionHandler('deleteChatlistInvite', async (global, actions, payload): Promise<void> => {
const { folderId, url } = payload;
const slug = url.split('/').pop();
if (!slug) return;
const result = await callApi('deleteChatlistInvite', { folderId, slug });
if (!result) return;
global = getGlobal();
global = {
...global,
chatFolders: {
...global.chatFolders,
invites: {
...global.chatFolders.invites,
[folderId]: global.chatFolders.invites[folderId]?.filter((invite) => invite.url !== url),
},
},
};
setGlobal(global);
});
addActionHandler('openDeleteChatFolderModal', async (global, actions, payload): Promise<void> => {
const { folderId, isConfirmedForChatlist, tabId = getCurrentTabId() } = payload;
const folder = selectChatFolder(global, folderId);
if (!folder) return;
if (folder.isChatList && (!folder.hasMyInvites || isConfirmedForChatlist)) {
const currentIds = getOrderedIds(folderId);
const suggestions = await callApi('fetchLeaveChatlistSuggestions', { folderId });
global = getGlobal();
global = updateTabState(global, {
chatlistModal: {
removal: {
folderId,
suggestedPeerIds: unique([...(suggestions || []), ...(currentIds || [])]),
},
},
}, tabId);
setGlobal(global);
return;
}
global = updateTabState(global, {
deleteFolderDialogModal: folderId,
}, tabId);
setGlobal(global);
});
addActionHandler('updateChatDetectedLanguage', (global, actions, payload): ActionReturnType => {
const { chatId, detectedLanguage } = payload;
global = getGlobal();
global = updateChat(global, chatId, {
detectedLanguage,
}, undefined, true);
return global;
});
addActionHandler('togglePeerTranslations', async (global, actions, payload): Promise<void> => {
const { chatId, isEnabled } = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
const result = await callApi('togglePeerTranslations', { chat, isEnabled });
if (result === undefined) return;
global = getGlobal();
global = updatePeerFullInfo(global, chatId, {
isTranslationDisabled: isEnabled ? undefined : true,
});
setGlobal(global);
});
addActionHandler('setViewForumAsMessages', (global, actions, payload): ActionReturnType => {
const { chatId, isEnabled } = payload;
const chat = selectChat(global, chatId);
if (!chat?.isForum || chat.isForumAsMessages === isEnabled) {
return;
}
global = updateChat(global, chatId, { isForumAsMessages: isEnabled || undefined });
setGlobal(global);
void callApi('setViewForumAsMessages', { chat, isEnabled });
});
addActionHandler('loadChannelRecommendations', async (global, actions, payload): Promise<void> => {
const { chatId } = payload;
const chat = chatId ? selectChat(global, chatId) : undefined;
if (chatId && !chat) {
return;
}
if (!chatId) {
const similarChannelIds = selectSimilarChannelIds(global, GLOBAL_SUGGESTED_CHANNELS_ID);
if (similarChannelIds) return; // Already cached
}
const result = await callApi('fetchChannelRecommendations', {
chat,
});
if (!result) {
return;
}
const { similarChannels, count } = result;
const chatsById = buildCollectionByKey(similarChannels, 'id');
global = getGlobal();
global = replaceSimilarChannels(global, chatId || GLOBAL_SUGGESTED_CHANNELS_ID, Object.keys(chatsById), count);
setGlobal(global);
});
addActionHandler('loadBotRecommendations', async (global, actions, payload): Promise<void> => {
const { userId } = payload;
const user = selectChat(global, userId);
if (!user) {
return;
}
const result = await callApi('fetchBotsRecommendations', {
user,
});
if (!result) {
return;
}
const { similarBots, count } = result;
const users = buildCollectionByKey(similarBots, 'id');
global = getGlobal();
global = addUsers(global, users);
global = addSimilarBots(global, userId, Object.keys(users), count);
setGlobal(global);
});
addActionHandler('toggleChannelRecommendations', (global, actions, payload): ActionReturnType => {
const { chatId } = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
global = toggleSimilarChannels(global, chatId);
setGlobal(global);
});
addActionHandler('resolveBusinessChatLink', async (global, actions, payload): Promise<void> => {
const { slug, tabId = getCurrentTabId() } = payload;
const result = await callApi('resolveBusinessChatLink', { slug });
if (!result) {
actions.showNotification({
message: langProvider.oldTranslate('BusinessLink.ErrorExpired'),
tabId,
});
return;
}
const { chatLink } = result;
actions.openChatWithDraft({
chatId: chatLink.chatId,
text: chatLink.text,
tabId,
});
});
addActionHandler('requestCollectibleInfo', async (global, actions, payload): Promise<void> => {
const {
type, collectible, peerId, tabId = getCurrentTabId(),
} = payload;
let inputCollectible;
if (type === 'phone') {
inputCollectible = { phone: collectible };
}
if (type === 'username') {
inputCollectible = { username: collectible };
}
if (!inputCollectible) return;
const result = await callApi('fetchCollectionInfo', inputCollectible);
if (!result) {
copyTextToClipboard(collectible);
return;
}
global = getGlobal();
global = updateTabState(global, {
collectibleInfoModal: {
...result,
type,
collectible,
peerId,
},
}, tabId);
setGlobal(global);
});
async function loadChats(
listType: ChatListType,
isFullDraftSync?: boolean,
shouldIgnorePagination?: boolean,
) {
// eslint-disable-next-line eslint-multitab-tt/no-immediate-global
let global = getGlobal();
let lastLocalServiceMessageId = selectLastServiceNotification(global)?.id;
const params = !shouldIgnorePagination ? selectChatListLoadingParameters(global, listType) : {};
const offsetPeer = params.nextOffsetPeerId ? selectPeer(global, params.nextOffsetPeerId) : undefined;
const offsetDate = params.nextOffsetDate;
const offsetId = params.nextOffsetId;
const isFirstBatch = !shouldIgnorePagination && !offsetPeer && !offsetDate && !offsetId;
const result = listType === 'saved' ? await callApi('fetchSavedChats', {
limit: CHAT_LIST_LOAD_SLICE,
offsetDate,
offsetId,
offsetPeer,
withPinned: isFirstBatch,
}) : await callApi('fetchChats', {
limit: CHAT_LIST_LOAD_SLICE,
offsetDate,
offsetId,
offsetPeer,
archived: listType === 'archived',
withPinned: isFirstBatch,
lastLocalServiceMessageId,
});
if (!result) {
return;
}
const { chatIds } = result;
global = getGlobal();
lastLocalServiceMessageId = selectLastServiceNotification(global)?.id;
const newChats = buildCollectionByKey(result.chats, 'id');
global = updateUsers(global, buildCollectionByKey(result.users, 'id'));
global = updateChats(global, newChats);
if (isFirstBatch) {
global = replaceChatListIds(global, listType, chatIds);
global = replaceUserStatuses(global, result.userStatusesById);
} else {
global = addChatListIds(global, listType, chatIds);
global = addUserStatuses(global, result.userStatusesById);
}
global = updateChatListSecondaryInfo(global, listType, result);
global = replaceMessages(global, result.messages);
global = updateChatsLastMessageId(global, result.lastMessageByChatId, listType);
if (!shouldIgnorePagination) {
global = replaceChatListLoadingParameters(
global, listType, result.nextOffsetId, result.nextOffsetPeerId, result.nextOffsetDate,
);
}
const idsToUpdateDraft = isFullDraftSync ? result.chatIds : Object.keys(result.draftsById);
idsToUpdateDraft.forEach((chatId) => {
const draft = result.draftsById[chatId];
const thread = selectThread(global, chatId, MAIN_THREAD_ID);
if (!draft && !thread) return;
if (!selectDraft(global, chatId, MAIN_THREAD_ID)?.isLocal) {
global = replaceThreadParam(
global, chatId, MAIN_THREAD_ID, 'draft', draft,
);
}
});
if ((chatIds.length === 0 || chatIds.length === result.totalChatCount) && !global.chats.isFullyLoaded[listType]) {
global = {
...global,
chats: {
...global.chats,
isFullyLoaded: {
...global.chats.isFullyLoaded,
[listType]: true,
},
},
};
}
setGlobal(global);
}
export async function loadFullChat<T extends GlobalState>(
global: T, actions: RequiredGlobalActions, chat: ApiChat,
) {
const result = await callApi('fetchFullChat', chat);
if (!result) {
return undefined;
}
const {
chats, userStatusesById, fullInfo, groupCall, membersCount, isForumAsMessages,
} = result;
global = getGlobal();
global = updateChats(global, buildCollectionByKey(chats, 'id'));
if (userStatusesById) {
global = addUserStatuses(global, userStatusesById);
}
if (groupCall) {
const existingGroupCall = selectGroupCall(global, groupCall.id!);
global = updateGroupCall(
global,
groupCall.id!,
omit(groupCall, ['connectionState', 'isLoaded']),
undefined,
existingGroupCall ? undefined : groupCall.participantsCount,
);
}
if (membersCount !== undefined) {
global = updateChat(global, chat.id, { membersCount });
}
if (chat.isForum) {
global = updateChat(global, chat.id, { isForumAsMessages });
}
global = replaceChatFullInfo(global, chat.id, fullInfo);
setGlobal(global);
const stickerSet = fullInfo.stickerSet;
const localSet = stickerSet && selectStickerSet(global, stickerSet);
if (stickerSet && !localSet) {
actions.loadStickers({
stickerSetInfo: {
id: stickerSet.id,
accessHash: stickerSet.accessHash,
},
});
}
const emojiSet = fullInfo.emojiSet;
const localEmojiSet = emojiSet && selectStickerSet(global, emojiSet);
if (emojiSet && !localEmojiSet) {
actions.loadStickers({
stickerSetInfo: {
id: emojiSet.id,
accessHash: emojiSet.accessHash,
},
});
}
return result;
}
export async function migrateChat<T extends GlobalState>(
global: T, actions: RequiredGlobalActions, chat: ApiChat,
...[tabId = getCurrentTabId()]: TabArgs<T>
): Promise<ApiChat | undefined> {
try {
const supergroup = await callApi('migrateChat', chat);
return supergroup;
} catch (error) {
if ((error as ApiError).message === 'CHANNELS_TOO_MUCH') {
actions.openLimitReachedModal({ limit: 'channels', tabId });
} else {
actions.showDialog({ data: { ...(error as ApiError), hasErrorKey: true }, tabId });
}
return undefined;
}
}
export async function fetchChatByUsername<T extends GlobalState>(
global: T,
username: string,
referrer?: string,
) {
global = getGlobal();
const localChat = !referrer ? selectChatByUsername(global, username) : undefined;
if (localChat && !localChat.isMin) {
return localChat;
}
const { chat, user } = await callApi('getChatByUsername', username, referrer) || {};
if (!chat) {
return undefined;
}
global = getGlobal();
global = updateChat(global, chat.id, chat);
if (user) {
global = updateUser(global, user.id, user);
}
setGlobal(global);
return chat;
}
export async function checkWebAppExists<T extends GlobalState>(
global: T, botName: string, appName: string,
) {
if (!botName || !appName) return false;
global = getGlobal();
const chatByUsername = await fetchChatByUsername(global, botName);
global = getGlobal();
const bot = chatByUsername && selectUser(global, chatByUsername.id);
const botApp = bot && await callApi('fetchBotApp', {
bot,
appName,
});
return Boolean(botApp);
}
export async function fetchChatByPhoneNumber<T extends GlobalState>(global: T, phoneNumber: string) {
global = getGlobal();
const localUser = selectUserByPhoneNumber(global, phoneNumber);
if (localUser && !localUser.isMin) {
return selectChat(global, localUser.id);
}
const { chat, user } = await callApi('getChatByPhoneNumber', phoneNumber) || {};
if (!chat) {
return undefined;
}
global = getGlobal();
global = updateChat(global, chat.id, chat);
if (user) {
global = updateUser(global, user.id, user);
}
setGlobal(global);
return chat;
}
async function getAttachBotOrNotify<T extends GlobalState>(
global: T, actions: RequiredGlobalActions, username: string,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const chat = await fetchChatByUsername(global, username);
if (!chat) return undefined;
global = getGlobal();
const user = selectUser(global, chat.id);
if (!user) return undefined;
const isBot = isUserBot(user);
if (!isBot) return undefined;
const result = await callApi('loadAttachBot', {
bot: user,
});
global = getGlobal();
if (!result) {
actions.showNotification({
message: langProvider.oldTranslate('WebApp.AddToAttachmentUnavailableError'),
tabId,
});
return undefined;
}
setGlobal(global);
return result.bot;
}
async function openChatByUsername<T extends GlobalState>(
global: T,
actions: RequiredGlobalActions,
params: {
username: string;
threadId?: ThreadId;
channelPostId?: number;
startParam?: string;
ref?: string;
startAttach?: string;
attach?: string;
text?: string;
timestamp?: number;
},
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const {
username, threadId, channelPostId, startParam, ref, startAttach, attach, text, timestamp,
} = params;
const currentChat = selectCurrentChat(global, tabId);
// Attach in the current chat
if (startAttach !== undefined && !attach) {
const bot = await getAttachBotOrNotify(global, actions, username, tabId);
if (!bot) return;
actions.callAttachBot({
bot,
chatId: currentChat?.id || bot.id,
startParam: startAttach,
tabId,
});
return;
}
const isCurrentChat = currentChat?.usernames?.some((c) => c.username === username);
if (!isCurrentChat) {
// Open temporary empty chat to make the click response feel faster
actions.openChat({ id: TMP_CHAT_ID, tabId });
}
const starRefStartPrefixes = global.appConfig?.starRefStartPrefixes;
let referrer = ref;
if (startParam && starRefStartPrefixes?.length) {
const prefix = starRefStartPrefixes.find((p) => startParam.startsWith(p));
if (prefix) {
referrer = startParam.slice(prefix.length);
}
}
const chat = await fetchChatByUsername(global, username, referrer);
if (!chat) {
if (!isCurrentChat) {
actions.openPreviousChat({ tabId });
actions.showNotification({ message: 'User does not exist', tabId });
}
return;
}
openChatWithParams(global, actions, chat, {
isCurrentChat,
threadId,
messageId: channelPostId,
startParam,
referrer,
startAttach,
attach,
text,
timestamp,
}, tabId);
}
async function openChatWithParams<T extends GlobalState>(
global: T,
actions: RequiredGlobalActions,
chat: ApiChat,
params: {
isCurrentChat?: boolean;
threadId?: ThreadId;
messageId?: number;
startParam?: string;
referrer?: string;
startAttach?: string;
attach?: string;
text?: string;
timestamp?: number;
},
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const {
isCurrentChat, threadId, messageId, startParam, referrer, startAttach, attach, text, timestamp,
} = params;
if (messageId) {
let isTopicProcessed = false;
// In forums, link to a topic start message should open the topic
if (chat.isForum && !threadId) {
let topic = selectTopics(global, chat.id)?.[messageId];
if (!topic) {
const topicResult = await callApi('fetchTopicById', { chat, topicId: messageId });
topic = topicResult?.topic;
}
if (topic) {
actions.openThread({
chatId: chat.id, threadId: topic.id, tabId,
});
isTopicProcessed = true;
}
}
if (!isTopicProcessed) {
actions.focusMessage({
chatId: chat.id, threadId, messageId, timestamp, tabId,
});
}
} else if (!isCurrentChat) {
actions.openThread({ chatId: chat.id, threadId: threadId ?? MAIN_THREAD_ID, tabId });
}
if (startParam && !referrer) {
actions.startBot({ botId: chat.id, param: startParam });
}
if (attach) {
global = getGlobal();
openAttachMenuFromLink(global, actions, chat.id, attach, startAttach, tabId);
}
if (text) {
actions.openChatWithDraft({ chatId: chat.id, text: { text }, tabId });
}
if (messageId && timestamp) {
actions.openMediaFromTimestamp({
chatId: chat.id, threadId, messageId, timestamp, tabId,
});
}
}
async function openAttachMenuFromLink<T extends GlobalState>(
global: T,
actions: RequiredGlobalActions,
chatId: string,
attach: string,
startAttach?: string | boolean,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
global = getGlobal();
const bot = await getAttachBotOrNotify(global, actions, attach, tabId);
if (!bot) return;
actions.callAttachBot({
bot,
chatId,
...(typeof startAttach === 'string' && { startParam: startAttach }),
tabId,
});
}
export async function ensureIsSuperGroup<T extends GlobalState>(
global: T,
actions: RequiredGlobalActions,
chatId: string,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const chat = selectChat(global, chatId);
if (!chat || !isChatBasicGroup(chat)) {
return chat;
}
const newChat = await migrateChat(global, actions, chat, tabId);
if (!newChat) {
return undefined;
}
actions.loadFullChat({ chatId: newChat.id });
actions.openChat({ id: newChat.id, tabId });
return newChat;
}