2023-12-28 14:38:07 +01:00

488 lines
14 KiB
TypeScript

/* eslint-disable eslint-multitab-tt/no-immediate-global */
import { addCallback, removeCallback } from '../lib/teact/teactn';
import type { ActionReturnType, GlobalState, MessageList } from './types';
import { MAIN_THREAD_ID } from '../api/types';
import {
ALL_FOLDER_ID,
ANIMATION_LEVEL_MED,
ANIMATION_LEVEL_MIN,
ARCHIVED_FOLDER_ID,
DEBUG,
DEFAULT_LIMITS,
GLOBAL_STATE_CACHE_CHAT_LIST_LIMIT,
GLOBAL_STATE_CACHE_CHATS_WITH_MESSAGES_LIMIT,
GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT,
GLOBAL_STATE_CACHE_DISABLED,
GLOBAL_STATE_CACHE_KEY,
GLOBAL_STATE_CACHE_USER_LIST_LIMIT,
} from '../config';
import { getOrderedIds } from '../util/folderManager';
import {
compact, pick, pickTruthy, unique,
} from '../util/iteratees';
import { encryptSession } from '../util/passcode';
import { onBeforeUnload, onIdle, throttle } from '../util/schedulers';
import { hasStoredSession } from '../util/sessions';
import { isUserId } from './helpers';
import { addActionHandler, getGlobal } from './index';
import { INITIAL_GLOBAL_STATE, INITIAL_PERFORMANCE_STATE_MID, INITIAL_PERFORMANCE_STATE_MIN } from './initialState';
import { clearGlobalForLockScreen } from './reducers';
import {
selectChat,
selectChatMessages,
selectCurrentMessageList,
selectViewportIds,
selectVisibleUsers,
} from './selectors';
import { getIsMobile } from '../hooks/useAppLayout';
import { isHeavyAnimating } from '../hooks/useHeavyAnimationCheck';
const UPDATE_THROTTLE = 5000;
const updateCacheThrottled = throttle(() => onIdle(updateCache), UPDATE_THROTTLE, false);
let isCaching = false;
let unsubscribeFromBeforeUnload: NoneToVoidFunction | undefined;
export function initCache() {
if (GLOBAL_STATE_CACHE_DISABLED) {
return;
}
const resetCache = () => {
localStorage.removeItem(GLOBAL_STATE_CACHE_KEY);
if (!isCaching) {
return;
}
clearCaching();
};
addActionHandler('saveSession', (): ActionReturnType => {
if (isCaching) {
return;
}
setupCaching();
});
addActionHandler('reset', resetCache);
}
export function loadCache(initialState: GlobalState): GlobalState | undefined {
if (GLOBAL_STATE_CACHE_DISABLED) {
return undefined;
}
const cache = readCache(initialState);
if (cache.passcode.hasPasscode || hasStoredSession(true)) {
setupCaching();
return cache;
} else {
clearCaching();
return undefined;
}
}
export function setupCaching() {
isCaching = true;
unsubscribeFromBeforeUnload = onBeforeUnload(updateCache, true);
window.addEventListener('blur', updateCache);
addCallback(updateCacheThrottled);
}
export function clearCaching() {
isCaching = false;
removeCallback(updateCacheThrottled);
window.removeEventListener('blur', updateCache);
if (unsubscribeFromBeforeUnload) {
unsubscribeFromBeforeUnload();
}
}
function readCache(initialState: GlobalState): GlobalState {
if (DEBUG) {
// eslint-disable-next-line no-console
console.time('global-state-cache-read');
}
const json = localStorage.getItem(GLOBAL_STATE_CACHE_KEY);
const cached = json ? JSON.parse(json) as GlobalState : undefined;
if (DEBUG) {
// eslint-disable-next-line no-console
console.timeEnd('global-state-cache-read');
}
if (cached) {
migrateCache(cached, initialState);
}
const newState = {
...initialState,
...cached,
};
return newState;
}
export function migrateCache(cached: GlobalState, initialState: GlobalState) {
try {
unsafeMigrateCache(cached, initialState);
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
}
}
function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
const untypedCached = cached as any;
// Pre-fill settings with defaults
cached.settings.byKey = {
...initialState.settings.byKey,
...cached.settings.byKey,
};
cached.settings.themes = {
...initialState.settings.themes,
...cached.settings.themes,
};
cached.chatFolders = {
...initialState.chatFolders,
...cached.chatFolders,
};
if (!cached.settings.performance) {
if (cached.settings.byKey.animationLevel === ANIMATION_LEVEL_MIN) {
cached.settings.performance = INITIAL_PERFORMANCE_STATE_MIN;
} else if (cached.settings.byKey.animationLevel === ANIMATION_LEVEL_MED) {
cached.settings.performance = INITIAL_PERFORMANCE_STATE_MID;
} else {
cached.settings.performance = initialState.settings.performance;
}
}
cached.settings.performance = {
...initialState.settings.performance,
...cached.settings.performance,
};
if (cached.appConfig && !cached.appConfig.limits) {
cached.appConfig.limits = DEFAULT_LIMITS;
}
if (typeof cached.config?.defaultReaction === 'string') {
cached.config.defaultReaction = { emoticon: cached.config.defaultReaction };
}
if (typeof cached.availableReactions?.[0].reaction === 'string') {
cached.availableReactions = cached.availableReactions
.map((r) => ({ ...r, reaction: { emoticon: r.reaction as unknown as string } }));
}
if (!cached.archiveSettings) {
cached.archiveSettings = initialState.archiveSettings;
}
if (!cached.stories) {
cached.stories = initialState.stories;
}
if (!cached.stories.stealthMode) {
cached.stories.stealthMode = initialState.stories.stealthMode;
}
if (!cached.stories.byPeerId) {
cached.stories.byPeerId = initialState.stories.byPeerId;
cached.stories.orderedPeerIds = initialState.stories.orderedPeerIds;
}
if (!cached.chats.similarChannelsById) {
cached.chats.similarChannelsById = initialState.chats.similarChannelsById;
}
// Clear old color storage to optimize cache size
if (untypedCached?.appConfig?.peerColors) {
untypedCached.appConfig.peerColors = undefined;
untypedCached.appConfig.darkPeerColors = undefined;
}
}
function updateCache() {
const global = getGlobal();
if (!isCaching || global.isLoggingOut || isHeavyAnimating()) {
return;
}
forceUpdateCache();
}
export function forceUpdateCache(noEncrypt = false) {
const global = getGlobal();
const { hasPasscode, isScreenLocked } = global.passcode;
const serializedGlobal = serializeGlobal(global);
if (hasPasscode) {
if (!isScreenLocked && !noEncrypt) {
void encryptSession(undefined, serializedGlobal);
}
const serializedGlobalClean = JSON.stringify(clearGlobalForLockScreen(global, false));
localStorage.setItem(GLOBAL_STATE_CACHE_KEY, serializedGlobalClean);
return;
}
localStorage.setItem(GLOBAL_STATE_CACHE_KEY, serializedGlobal);
}
export function serializeGlobal<T extends GlobalState>(global: T) {
const reducedGlobal: GlobalState = {
...INITIAL_GLOBAL_STATE,
...pick(global, [
'appConfig',
'authState',
'authPhoneNumber',
'authRememberMe',
'authNearestCountry',
'currentUserId',
'contactList',
'topPeers',
'topInlineBots',
'recentEmojis',
'recentCustomEmojis',
'topReactions',
'recentReactions',
'push',
'serviceNotifications',
'attachmentSettings',
'leftColumnWidth',
'archiveSettings',
'mediaViewer',
'audioPlayer',
'shouldShowContextMenuHint',
'trustedBotIds',
'recentlyFoundChatIds',
'peerColors',
]),
lastIsChatInfoShown: !getIsMobile() ? global.lastIsChatInfoShown : undefined,
customEmojis: reduceCustomEmojis(global),
users: reduceUsers(global),
chats: reduceChats(global),
messages: reduceMessages(global),
settings: reduceSettings(global),
chatFolders: reduceChatFolders(global),
groupCalls: reduceGroupCalls(global),
availableReactions: reduceAvailableReactions(global),
passcode: pick(global.passcode, [
'isScreenLocked',
'hasPasscode',
'invalidAttemptsCount',
'timeoutUntil',
]),
};
return JSON.stringify(reducedGlobal);
}
function reduceCustomEmojis<T extends GlobalState>(global: T): GlobalState['customEmojis'] {
const { lastRendered, byId } = global.customEmojis;
const idsToSave = lastRendered.slice(0, GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT);
const byIdToSave = pick(byId, idsToSave);
return {
byId: byIdToSave,
lastRendered: idsToSave,
forEmoji: {},
added: {},
statusRecent: {},
};
}
function reduceUsers<T extends GlobalState>(global: T): GlobalState['users'] {
const { users: { byId, statusesById, fullInfoById }, currentUserId } = global;
const currentChatIds = compact(
Object.values(global.byTabId)
.map(({ id: tabId }) => selectCurrentMessageList(global, tabId)),
).map(({ chatId }) => chatId).filter((chatId) => isUserId(chatId));
const visibleUserIds = unique(compact(Object.values(global.byTabId)
.flatMap(({ id: tabId }) => selectVisibleUsers(global, tabId)?.map((u) => u.id) || [])));
const chatStoriesUserIds = currentChatIds
.flatMap((chatId) => Object.values(selectChatMessages(global, chatId) || {}))
.map((message) => message.content.storyData?.peerId || message.content.webPage?.story?.peerId)
.filter((id): id is string => Boolean(id) && isUserId(id));
const idsToSave = unique([
...currentUserId ? [currentUserId] : [],
...currentChatIds,
...chatStoriesUserIds,
...visibleUserIds || [],
...global.topPeers.userIds || [],
...getOrderedIds(ALL_FOLDER_ID)?.filter(isUserId) || [],
...getOrderedIds(ARCHIVED_FOLDER_ID)?.filter(isUserId) || [],
...global.contactList?.userIds || [],
...global.recentlyFoundChatIds?.filter(isUserId) || [],
...Object.keys(byId),
]).slice(0, GLOBAL_STATE_CACHE_USER_LIST_LIMIT);
return {
byId: pick(byId, idsToSave),
statusesById: pick(statusesById, idsToSave),
fullInfoById: pick(fullInfoById, idsToSave),
};
}
function reduceChats<T extends GlobalState>(global: T): GlobalState['chats'] {
const { chats: { byId }, currentUserId } = global;
const currentChatIds = compact(
Object.values(global.byTabId)
.map(({ id: tabId }): MessageList | undefined => {
return selectCurrentMessageList(global, tabId);
}),
).map(({ chatId }) => chatId);
const messagesChatIds = compact(Object.values(global.byTabId).flatMap(({ id: tabId }) => {
const messageList = selectCurrentMessageList(global, tabId);
if (!messageList) return undefined;
const messages = selectChatMessages(global, messageList.chatId);
const viewportIds = selectViewportIds(global, messageList.chatId, messageList.threadId, tabId);
return viewportIds?.map((id) => {
const message = messages[id];
const content = message?.content;
const replyPeer = message.replyInfo?.type === 'message' && message.replyInfo.replyToPeerId;
return content.storyData?.peerId || content.webPage?.story?.peerId || replyPeer;
});
}));
const idsToSave = unique([
...currentUserId ? [currentUserId] : [],
...currentChatIds,
...messagesChatIds,
...getOrderedIds(ALL_FOLDER_ID) || [],
...getOrderedIds(ARCHIVED_FOLDER_ID) || [],
...global.recentlyFoundChatIds || [],
...Object.keys(byId),
]).slice(0, GLOBAL_STATE_CACHE_CHAT_LIST_LIMIT);
return {
...global.chats,
similarChannelsById: {},
isFullyLoaded: {},
byId: pick(global.chats.byId, idsToSave),
fullInfoById: pick(global.chats.fullInfoById, idsToSave),
};
}
function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages'] {
const { currentUserId } = global;
const byChatId: GlobalState['messages']['byChatId'] = {};
const currentChatIds = compact(
Object.values(global.byTabId)
.map(({ id: tabId }) => selectCurrentMessageList(global, tabId)),
).map(({ chatId }) => chatId);
const forumPanelChatIds = compact(
Object.values(global.byTabId)
.map(({ forumPanelChatId }) => forumPanelChatId),
);
const chatIdsToSave = unique([
...currentChatIds,
...currentUserId ? [currentUserId] : [],
...forumPanelChatIds,
...getOrderedIds(ALL_FOLDER_ID)?.slice(0, GLOBAL_STATE_CACHE_CHATS_WITH_MESSAGES_LIMIT) || [],
]);
chatIdsToSave.forEach((chatId) => {
const current = global.messages.byChatId[chatId];
if (!current) {
return;
}
const chat = selectChat(global, chatId);
const threadIds = unique(compact(Object.values(global.byTabId).map(({ id: tabId }) => {
const { chatId: tabChatId, threadId } = selectCurrentMessageList(global, tabId) || {};
if (!tabChatId || tabChatId !== chatId || !threadId || threadId === MAIN_THREAD_ID) {
return undefined;
}
return threadId;
}).concat(
Object.values(global.messages.byChatId[chatId].threadsById || {})
.map(({ threadInfo }) => (threadInfo?.isCommentsInfo ? threadInfo?.originMessageId : undefined)),
)));
const threadIdsToSave = threadIds.length ? [MAIN_THREAD_ID, ...threadIds] : [MAIN_THREAD_ID];
const threadsToSave = pickTruthy(current.threadsById, threadIdsToSave);
if (!Object.keys(threadsToSave).length) {
return;
}
const viewportIdsToSave = unique(Object.values(threadsToSave).flatMap((thread) => thread.lastViewportIds || []));
const lastMessageIdsToSave = chat?.topics
? Object.values(chat.topics).map(({ lastMessageId }) => lastMessageId) : [];
const byId = pick(current.byId, viewportIdsToSave.concat(lastMessageIdsToSave));
const threadsById = Object.keys(threadsToSave).reduce((acc, key) => {
const thread = threadsToSave[Number(key)];
acc[Number(key)] = {
...thread,
listedIds: thread.lastViewportIds,
pinnedIds: undefined,
typingStatus: undefined,
};
return acc;
}, {} as GlobalState['messages']['byChatId'][string]['threadsById']);
byChatId[chatId] = {
byId,
threadsById,
};
});
return {
byChatId,
sponsoredByChatId: {},
};
}
function reduceSettings<T extends GlobalState>(global: T): GlobalState['settings'] {
const { byKey, themes, performance } = global.settings;
return {
byKey,
themes,
performance,
privacy: {},
notifyExceptions: {},
};
}
function reduceChatFolders<T extends GlobalState>(global: T): GlobalState['chatFolders'] {
return {
...global.chatFolders,
};
}
function reduceGroupCalls<T extends GlobalState>(global: T): GlobalState['groupCalls'] {
return {
...global.groupCalls,
byId: {},
activeGroupCallId: undefined,
};
}
function reduceAvailableReactions(global: GlobalState): GlobalState['availableReactions'] {
return global.availableReactions
?.map((r) => pick(r, ['reaction', 'staticIcon', 'title', 'isInactive']));
}