342 lines
9.9 KiB
TypeScript
342 lines
9.9 KiB
TypeScript
import {
|
|
addReducer, getGlobal, setGlobal, getDispatch,
|
|
} from '../../../lib/teact/teactn';
|
|
|
|
import {
|
|
ApiChat, ApiFormattedText, ApiUser, MAIN_THREAD_ID,
|
|
} from '../../../api/types';
|
|
import { GlobalActions } from '../../../global/types';
|
|
|
|
import {
|
|
CHAT_LIST_LOAD_SLICE, DEBUG, MESSAGE_LIST_SLICE,
|
|
} from '../../../config';
|
|
import { callApi } from '../../../api/gramjs';
|
|
import { buildCollectionByKey } from '../../../util/iteratees';
|
|
import {
|
|
replaceChatListIds,
|
|
replaceChats,
|
|
replaceUsers,
|
|
updateUsers,
|
|
updateChats,
|
|
updateChatListSecondaryInfo,
|
|
updateThreadInfos,
|
|
replaceThreadParam,
|
|
} from '../../reducers';
|
|
import {
|
|
selectUser, selectChat, selectCurrentMessageList, selectDraft, selectChatMessage, selectThreadInfo,
|
|
} from '../../selectors';
|
|
import { isChatPrivate } from '../../helpers';
|
|
|
|
addReducer('sync', (global, actions) => {
|
|
void sync(actions.afterSync);
|
|
});
|
|
|
|
addReducer('afterSync', (global, actions) => {
|
|
void afterSync(actions);
|
|
});
|
|
|
|
async function sync(afterSyncCallback: () => void) {
|
|
if (DEBUG) {
|
|
// eslint-disable-next-line no-console
|
|
console.log('>>> START SYNC');
|
|
}
|
|
|
|
await callApi('fetchCurrentUser');
|
|
|
|
// This fetches only active chats and clears archived chats, which will be fetched in `afterSync`
|
|
const savedUsers = await loadAndReplaceChats();
|
|
await loadAndReplaceMessages(savedUsers);
|
|
|
|
setGlobal({
|
|
...getGlobal(),
|
|
lastSyncTime: Date.now(),
|
|
});
|
|
|
|
if (DEBUG) {
|
|
// eslint-disable-next-line no-console
|
|
console.log('>>> FINISH SYNC');
|
|
}
|
|
|
|
afterSyncCallback();
|
|
}
|
|
|
|
async function afterSync(actions: GlobalActions) {
|
|
if (DEBUG) {
|
|
// eslint-disable-next-line no-console
|
|
console.log('>>> START AFTER-SYNC');
|
|
}
|
|
|
|
actions.loadFavoriteStickers();
|
|
|
|
await Promise.all([
|
|
loadAndUpdateUsers(),
|
|
loadAndReplaceArchivedChats(),
|
|
]);
|
|
|
|
await callApi('fetchCurrentUser');
|
|
|
|
if (DEBUG) {
|
|
// eslint-disable-next-line no-console
|
|
console.log('>>> FINISH AFTER-SYNC');
|
|
}
|
|
}
|
|
|
|
async function loadAndReplaceChats() {
|
|
const result = await callApi('fetchChats', {
|
|
limit: CHAT_LIST_LOAD_SLICE,
|
|
withPinned: true,
|
|
serverTimeOffset: getGlobal().serverTimeOffset,
|
|
});
|
|
if (!result) {
|
|
return undefined;
|
|
}
|
|
|
|
let global = getGlobal();
|
|
|
|
const { recentlyFoundChatIds } = global.globalSearch;
|
|
const { userIds: contactIds } = global.contactList || {};
|
|
const { currentUserId } = global;
|
|
|
|
const savedPrivateChatIds = [
|
|
...(recentlyFoundChatIds || []),
|
|
...(contactIds || []),
|
|
...(currentUserId ? [currentUserId] : []),
|
|
];
|
|
|
|
const savedUsers = savedPrivateChatIds
|
|
.map((id) => selectUser(global, id))
|
|
.filter<ApiUser>(Boolean as any);
|
|
|
|
const savedChats = savedPrivateChatIds
|
|
.map((id) => selectChat(global, id))
|
|
.filter<ApiChat>(Boolean as any);
|
|
|
|
const { chatId: currentChatId } = selectCurrentMessageList(global) || {};
|
|
if (currentChatId) {
|
|
const selectedChat = selectChat(global, currentChatId);
|
|
if (selectedChat && !savedPrivateChatIds.includes(currentChatId)) {
|
|
savedChats.push(selectedChat);
|
|
}
|
|
|
|
if (isChatPrivate(currentChatId)) {
|
|
const selectedChatUser = selectUser(global, currentChatId);
|
|
if (selectedChatUser && !savedPrivateChatIds.includes(currentChatId)) {
|
|
savedUsers.push(selectedChatUser);
|
|
}
|
|
}
|
|
}
|
|
|
|
savedUsers.push(...result.users);
|
|
savedChats.push(...result.chats);
|
|
|
|
global = replaceChats(global, buildCollectionByKey(savedChats, 'id'));
|
|
global = replaceChatListIds(global, 'active', result.chatIds);
|
|
|
|
global = {
|
|
...global,
|
|
chats: {
|
|
...global.chats,
|
|
},
|
|
};
|
|
|
|
global = updateChatListSecondaryInfo(global, 'active', result);
|
|
|
|
Object.keys(result.draftsById).map(Number).forEach((chatId) => {
|
|
global = replaceThreadParam(global, chatId, MAIN_THREAD_ID, 'draft', result.draftsById[chatId]);
|
|
});
|
|
|
|
Object.keys(result.replyingToById).map(Number).forEach((chatId) => {
|
|
global = replaceThreadParam(
|
|
global, chatId, MAIN_THREAD_ID, 'replyingToId', result.replyingToById[chatId],
|
|
);
|
|
});
|
|
|
|
setGlobal(global);
|
|
|
|
if (currentChatId && !global.chats.byId[currentChatId]) {
|
|
getDispatch().openChat({ id: undefined });
|
|
}
|
|
|
|
return savedUsers;
|
|
}
|
|
|
|
async function loadAndReplaceArchivedChats() {
|
|
const result = await callApi('fetchChats', {
|
|
limit: CHAT_LIST_LOAD_SLICE,
|
|
archived: true,
|
|
withPinned: true,
|
|
serverTimeOffset: getGlobal().serverTimeOffset,
|
|
});
|
|
|
|
if (!result) {
|
|
return;
|
|
}
|
|
|
|
let global = getGlobal();
|
|
global = updateUsers(global, buildCollectionByKey(result.users, 'id'));
|
|
global = updateChats(global, buildCollectionByKey(result.chats, 'id'));
|
|
global = replaceChatListIds(global, 'archived', result.chatIds);
|
|
global = updateChatListSecondaryInfo(global, 'archived', result);
|
|
setGlobal(global);
|
|
}
|
|
|
|
async function loadAndReplaceMessages(savedUsers?: ApiUser[]) {
|
|
let areMessagesLoaded = false;
|
|
let users = savedUsers || [];
|
|
|
|
let global = getGlobal();
|
|
const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global) || {};
|
|
|
|
// Memoize drafts
|
|
const draftChatIds = Object.keys(global.messages.byChatId).map(Number);
|
|
const draftsByChatId = draftChatIds.reduce<Record<number, ApiFormattedText>>((acc, chatId) => {
|
|
const draft = selectDraft(global, chatId, MAIN_THREAD_ID);
|
|
return draft ? { ...acc, [chatId]: draft } : acc;
|
|
}, {});
|
|
|
|
if (currentChatId) {
|
|
const result = await loadTopMessages(global.chats.byId[currentChatId]);
|
|
global = getGlobal();
|
|
const { chatId: newCurrentChatId } = selectCurrentMessageList(global) || {};
|
|
const threadInfo = currentThreadId && selectThreadInfo(global, currentChatId, currentThreadId);
|
|
|
|
if (result && newCurrentChatId === currentChatId) {
|
|
const currentMessageListInfo = global.messages.byChatId[currentChatId];
|
|
const byId = buildCollectionByKey(result.messages, 'id');
|
|
const listedIds = Object.keys(byId).map(Number);
|
|
|
|
global = {
|
|
...global,
|
|
messages: {
|
|
...global.messages,
|
|
byChatId: {
|
|
[currentChatId]: {
|
|
byId,
|
|
threadsById: {
|
|
[MAIN_THREAD_ID]: {
|
|
...(currentMessageListInfo?.threadsById[MAIN_THREAD_ID]),
|
|
listedIds,
|
|
viewportIds: listedIds,
|
|
outlyingIds: undefined,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
if (currentThreadId && threadInfo && threadInfo.originChannelId) {
|
|
const { originChannelId } = threadInfo;
|
|
const currentMessageListInfoOrigin = global.messages.byChatId[originChannelId];
|
|
const resultOrigin = await loadTopMessages(global.chats.byId[originChannelId]);
|
|
if (resultOrigin) {
|
|
const byIdOrigin = buildCollectionByKey(resultOrigin.messages, 'id');
|
|
const listedIdsOrigin = Object.keys(byIdOrigin)
|
|
.map(Number);
|
|
|
|
global = {
|
|
...global,
|
|
messages: {
|
|
...global.messages,
|
|
byChatId: {
|
|
...global.messages.byChatId,
|
|
[threadInfo.originChannelId]: {
|
|
byId: byIdOrigin,
|
|
threadsById: {
|
|
[MAIN_THREAD_ID]: {
|
|
...(currentMessageListInfoOrigin?.threadsById[MAIN_THREAD_ID]),
|
|
listedIds: listedIdsOrigin,
|
|
viewportIds: listedIdsOrigin,
|
|
outlyingIds: undefined,
|
|
},
|
|
},
|
|
},
|
|
[currentChatId]: {
|
|
...global.messages.byChatId[currentChatId],
|
|
threadsById: {
|
|
...global.messages.byChatId[currentChatId].threadsById,
|
|
[currentThreadId]: {
|
|
...(currentMessageListInfo?.threadsById[currentThreadId]),
|
|
outlyingIds: undefined,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
}
|
|
global = updateChats(global, buildCollectionByKey(result.chats, 'id'));
|
|
global = updateThreadInfos(global, currentChatId, result.threadInfos);
|
|
|
|
areMessagesLoaded = true;
|
|
users = Array.prototype.concat(users, result.users);
|
|
}
|
|
}
|
|
|
|
if (!areMessagesLoaded) {
|
|
global = {
|
|
...global,
|
|
messages: {
|
|
...global.messages,
|
|
byChatId: {},
|
|
},
|
|
};
|
|
}
|
|
|
|
// Restore drafts
|
|
Object.keys(draftsByChatId).map(Number).forEach((chatId) => {
|
|
global = replaceThreadParam(global, chatId, MAIN_THREAD_ID, 'draft', draftsByChatId[chatId]);
|
|
});
|
|
|
|
if (savedUsers) {
|
|
global = replaceUsers(global, buildCollectionByKey(users, 'id'));
|
|
} else if (users) {
|
|
// If `fetchChats` has failed for some reason, we don't have saved chats, thus we can not replace
|
|
global = updateUsers(global, buildCollectionByKey(users, 'id'));
|
|
}
|
|
|
|
setGlobal(global);
|
|
|
|
const { chatId: audioChatId, messageId: audioMessageId } = global.audioPlayer;
|
|
if (audioChatId && audioMessageId && !selectChatMessage(global, audioChatId, audioMessageId)) {
|
|
getDispatch().closeAudioPlayer();
|
|
}
|
|
}
|
|
|
|
async function loadAndUpdateUsers() {
|
|
let global = getGlobal();
|
|
const { recentlyFoundChatIds } = global.globalSearch;
|
|
const { userIds: contactIds } = global.contactList || {};
|
|
if (
|
|
(!contactIds || !contactIds.length)
|
|
&& (!recentlyFoundChatIds || !recentlyFoundChatIds.length)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const users = [
|
|
...(recentlyFoundChatIds || []),
|
|
...(contactIds || []),
|
|
].map((id) => selectUser(global, id)).filter<ApiUser>(Boolean as any);
|
|
|
|
const updatedUsers = await callApi('fetchUsers', { users });
|
|
if (!updatedUsers) {
|
|
return;
|
|
}
|
|
|
|
global = getGlobal();
|
|
global = updateUsers(global, buildCollectionByKey(updatedUsers, 'id'));
|
|
setGlobal(global);
|
|
}
|
|
|
|
function loadTopMessages(chat: ApiChat) {
|
|
return callApi('fetchMessages', {
|
|
chat,
|
|
threadId: MAIN_THREAD_ID,
|
|
offsetId: chat.lastReadInboxMessageId,
|
|
addOffset: -(Math.round(MESSAGE_LIST_SLICE / 2) + 1),
|
|
limit: MESSAGE_LIST_SLICE,
|
|
});
|
|
}
|