import type { Dialog } from './appMessagesManager'; import { App, DEBUG, MOUNT_CLASS_TO, UserAuth } from '../mtproto/mtproto_config'; import EventListenerBase from '../../helpers/eventListenerBase'; import rootScope from '../rootScope'; import sessionStorage from '../sessionStorage'; import { logger } from '../logger'; import type { AppUsersManager } from './appUsersManager'; import type { AppChatsManager } from './appChatsManager'; import type { AuthState } from '../../types'; import type FiltersStorage from '../storages/filters'; import type DialogsStorage from '../storages/dialogs'; import type { AppDraftsManager } from './appDraftsManager'; import { copy, setDeepProperty, validateInitObject } from '../../helpers/object'; import { getHeavyAnimationPromise } from '../../hooks/useHeavyAnimationCheck'; const REFRESH_EVERY = 24 * 60 * 60 * 1000; // 1 day const STATE_VERSION = App.version; export type State = Partial<{ dialogs: Dialog[], allDialogsLoaded: DialogsStorage['allDialogsLoaded'], chats: {[peerId: string]: ReturnType}, users: {[peerId: string]: ReturnType}, messages: any[], contactsList: number[], updates: Partial<{ seq: number, pts: number, date: number }>, filters: FiltersStorage['filters'], maxSeenMsgId: number, stateCreatedTime: number, recentEmoji: string[], topPeers: number[], recentSearch: number[], version: typeof STATE_VERSION, authState: AuthState, hiddenPinnedMessages: {[peerId: string]: number}, settings: { messagesTextSize: number, sendShortcut: 'enter' | 'ctrlEnter', animationsEnabled: boolean, autoDownload: { contacts: boolean private: boolean groups: boolean channels: boolean }, autoPlay: { gifs: boolean, videos: boolean }, stickers: { suggest: boolean, loop: boolean }, background: { type: 'color' | 'image' | 'default', blur: boolean, highlightningColor?: string, color?: string, slug?: string, } }, drafts: AppDraftsManager['drafts'] }>; export const STATE_INIT: State = { dialogs: [], allDialogsLoaded: {}, chats: {}, users: {}, messages: [], contactsList: [], updates: {}, filters: {}, maxSeenMsgId: 0, stateCreatedTime: Date.now(), recentEmoji: [], topPeers: [], recentSearch: [], version: STATE_VERSION, authState: { _: 'authStateSignIn' }, hiddenPinnedMessages: {}, settings: { messagesTextSize: 16, sendShortcut: 'enter', animationsEnabled: true, autoDownload: { contacts: true, private: true, groups: true, channels: true }, autoPlay: { gifs: true, videos: true }, stickers: { suggest: true, loop: true }, background: { type: 'image', blur: false, slug: 'ByxGo2lrMFAIAAAAmkJxZabh8eM', // * new blurred camomile } }, drafts: {} }; const ALL_KEYS = Object.keys(STATE_INIT) as any as Array; const REFRESH_KEYS = ['dialogs', 'allDialogsLoaded', 'messages', 'contactsList', 'stateCreatedTime', 'updates', 'maxSeenMsgId', 'filters', 'topPeers'] as any as Array; export class AppStateManager extends EventListenerBase<{ save: (state: State) => Promise }> { public static STATE_INIT = STATE_INIT; public loaded: Promise; private log = logger('STATE'/* , LogLevels.error */); private state: State; private savePromise: Promise; private tempId = 0; constructor() { super(); this.loadSavedState(); } public loadSavedState() { if(this.loaded) return this.loaded; //console.time('load state'); return this.loaded = new Promise((resolve) => { Promise.all(ALL_KEYS.concat('user_auth' as any).map(key => sessionStorage.get(key))).then((arr) => { let state: State = {}; // ! then can't store false values ALL_KEYS.forEach((key, idx) => { const value = arr[idx]; if(value !== undefined) { // @ts-ignore state[key] = value; } else { // @ts-ignore state[key] = copy(STATE_INIT[key]); } }); const time = Date.now(); if(state) { if(state.version !== STATE_VERSION) { state = copy(STATE_INIT); } else if((state.stateCreatedTime + REFRESH_EVERY) < time/* || true *//* && false */) { if(DEBUG) { this.log('will refresh state', state.stateCreatedTime, time); } REFRESH_KEYS.forEach(key => { // @ts-ignore state[key] = copy(STATE_INIT[key]); }); const users: typeof state['users'] = {}, chats: typeof state['chats'] = {}; if(state.recentSearch?.length) { state.recentSearch.forEach(peerId => { if(peerId < 0) chats[peerId] = state.chats[peerId]; else users[peerId] = state.users[peerId]; }); } state.users = users; state.chats = chats; } } validateInitObject(STATE_INIT, state); this.state = state; this.state.version = STATE_VERSION; // ! probably there is better place for it rootScope.settings = this.state.settings; if(DEBUG) { this.log('state res', state); } //return resolve(); const auth: UserAuth = arr[arr.length - 1] as any; if(auth) { // ! Warning ! DON'T delete this this.state.authState = {_: 'authStateSignedIn'}; rootScope.broadcast('user_auth', typeof(auth) !== 'number' ? (auth as any).id : auth); // * support old version } //console.timeEnd('load state'); resolve(this.state); }).catch(resolve).finally(() => { setInterval(() => { this.tempId++; this.saveState(); }, 10000); }); }); } public getState() { return this.state === undefined ? this.loadSavedState() : Promise.resolve(this.state); } public saveState() { if(this.state === undefined || this.savePromise) return; //return; const tempId = this.tempId; this.savePromise = getHeavyAnimationPromise().then(() => { return Promise.all(this.setListenerResult('save', this.state)) .then(() => getHeavyAnimationPromise()) .then(() => sessionStorage.set(this.state)) .then(() => { this.savePromise = null; if(this.tempId !== tempId) { this.saveState(); } }); }); //let perf = performance.now(); //this.log('saveState: event time:', performance.now() - perf); //const pinnedOrders = appMessagesManager.dialogsStorage.pinnedOrders; //perf = performance.now(); //this.log('saveState: storage set time:', performance.now() - perf); } public setByKey(key: string, value: any) { setDeepProperty(this.state, key, value); rootScope.broadcast('settings_updated', {key, value}); } public pushToState(key: T, value: State[T]) { this.state[key] = value; } public setPeer(peerId: number, peer: any) { const container = peerId > 0 ? this.state.users : this.state.chats; if(container.hasOwnProperty(peerId)) return; container[peerId] = peer; } public resetState() { for(let i in this.state) { // @ts-ignore this.state[i] = false; } sessionStorage.set(this.state).then(() => { location.reload(); }); } } //console.trace('appStateManager include'); const appStateManager = new AppStateManager(); MOUNT_CLASS_TO && (MOUNT_CLASS_TO.appStateManager = appStateManager); export default appStateManager;