diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 3f9e7614b..e9c0425c4 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -65,7 +65,7 @@ import { selectSendAs, selectSponsoredMessage, } from '../../selectors'; -import { debounce, rafPromise } from '../../../util/schedulers'; +import { debounce, onTickEnd, rafPromise } from '../../../util/schedulers'; import { isServiceNotificationMessage } from '../../helpers'; const uploadProgressCallbacks = new Map(); @@ -113,7 +113,9 @@ addActionHandler('loadViewportMessages', (global, actions, payload) => { } if (!areAllLocal) { - void loadViewportMessages(chat, threadId, offsetId, LoadMoreDirection.Around, isOutlying, isBudgetPreload); + onTickEnd(() => { + void loadViewportMessages(chat, threadId, offsetId, LoadMoreDirection.Around, isOutlying, isBudgetPreload); + }); } } else { const offsetId = direction === LoadMoreDirection.Backwards ? viewportIds[0] : viewportIds[viewportIds.length - 1]; @@ -127,7 +129,9 @@ addActionHandler('loadViewportMessages', (global, actions, payload) => { global = safeReplaceViewportIds(global, chatId, threadId, newViewportIds); } - void loadWithBudget(actions, areAllLocal, isOutlying, isBudgetPreload, chat, threadId, direction, offsetId); + onTickEnd(() => { + void loadWithBudget(actions, areAllLocal, isOutlying, isBudgetPreload, chat, threadId, direction, offsetId); + }); if (isBudgetPreload) { return undefined; @@ -149,8 +153,6 @@ async function loadWithBudget( } if (!isBudgetPreload) { - // Let reducer return and update global - await Promise.resolve(); actions.loadViewportMessages({ chatId: chat.id, threadId, direction, isBudgetPreload: true, }); diff --git a/src/global/actions/apiUpdaters/messages.ts b/src/global/actions/apiUpdaters/messages.ts index b86249585..f8820407c 100644 --- a/src/global/actions/apiUpdaters/messages.ts +++ b/src/global/actions/apiUpdaters/messages.ts @@ -47,6 +47,7 @@ import { import { getMessageContent, isUserId, isMessageLocal, getMessageText, checkIfReactionAdded, } from '../../helpers'; +import { onTickEnd } from '../../../util/schedulers'; const ANIMATION_DELAY = 350; @@ -497,10 +498,12 @@ addActionHandler('apiUpdate', (global, actions, update) => { if (shouldNotify) { const newMessage = selectChatMessage(global, chatId, id); if (!chat || !newMessage) return; - notifyAboutMessage({ - chat, - message: newMessage, - isReaction: true, + onTickEnd(() => { + notifyAboutMessage({ + chat, + message: newMessage, + isReaction: true, + }); }); } @@ -548,13 +551,15 @@ function updateThreadUnread(global: GlobalState, actions: GlobalActions, message if (originMessage) { global = updateThreadUnreadFromForwardedMessage(global, originMessage, chatId, message.id, isDeleting); } else { - actions.loadMessage({ - chatId, - messageId: message.replyToMessageId, - threadUpdate: { - isDeleting, - lastMessageId: message.id, - }, + onTickEnd(() => { + actions.loadMessage({ + chatId, + messageId: message.replyToMessageId, + threadUpdate: { + isDeleting, + lastMessageId: message.id, + }, + }); }); } } diff --git a/src/global/init.ts b/src/global/init.ts index cad5c2092..d717295ce 100644 --- a/src/global/init.ts +++ b/src/global/init.ts @@ -6,7 +6,10 @@ import { cloneDeep } from '../util/iteratees'; initCache(); -addActionHandler('init', () => { +addActionHandler('init', (global) => { const initial = cloneDeep(INITIAL_STATE); - return loadCache(initial) || initial; + return { + ...global, + ...(loadCache(initial) || initial), + }; }); diff --git a/src/lib/teact/teactn.tsx b/src/lib/teact/teactn.tsx index 29ca97e1a..b2c976a8e 100644 --- a/src/lib/teact/teactn.tsx +++ b/src/lib/teact/teactn.tsx @@ -13,7 +13,9 @@ import { isHeavyAnimating } from '../../hooks/useHeavyAnimationCheck'; export default React; -type GlobalState = AnyLiteral; +type GlobalState = + AnyLiteral + & { DEBUG_id: number }; type ActionNames = string; type ActionPayload = any; @@ -35,6 +37,13 @@ type MapStateToProps = ((global: GlobalState, ownProps: Ow let currentGlobal = {} as GlobalState; +// eslint-disable-next-line @typescript-eslint/naming-convention +let DEBUG_currentGlobalId: number | undefined; +// eslint-disable-next-line @typescript-eslint/naming-convention +const DEBUG_resetIdThrottled = throttleWithTickEnd(() => { + DEBUG_currentGlobalId = Math.random(); +}); + const actionHandlers: Record = {}; const callbacks: Function[] = [updateContainers]; const actions = {} as Actions; @@ -61,6 +70,12 @@ function runCallbacks(forceOnHeavyAnimation = false) { export function setGlobal(newGlobal?: GlobalState, options?: ActionOptions) { if (typeof newGlobal === 'object' && newGlobal !== currentGlobal) { + if (DEBUG) { + if (newGlobal.DEBUG_id !== DEBUG_currentGlobalId) { + throw new Error('[TeactN.setGlobal] Attempt to set an outdated global'); + } + } + currentGlobal = newGlobal; if (options?.forceSyncOnIOs) { runCallbacks(true); @@ -71,6 +86,12 @@ export function setGlobal(newGlobal?: GlobalState, options?: ActionOptions) { } export function getGlobal() { + if (DEBUG) { + DEBUG_currentGlobalId = Math.random(); + currentGlobal.DEBUG_id = DEBUG_currentGlobalId; + DEBUG_resetIdThrottled(); + } + return currentGlobal; } @@ -80,12 +101,12 @@ export function getActions() { function handleAction(name: string, payload?: ActionPayload, options?: ActionOptions) { actionHandlers[name]?.forEach((handler) => { - const response = handler(currentGlobal, actions, payload); + const response = handler(DEBUG ? getGlobal() : currentGlobal, actions, payload); if (!response || typeof response.then === 'function') { return; } - setGlobal(response, options); + setGlobal(response as GlobalState, options); }); } diff --git a/src/util/schedulers.ts b/src/util/schedulers.ts index 7db98a9c9..cffd92d9a 100644 --- a/src/util/schedulers.ts +++ b/src/util/schedulers.ts @@ -1,7 +1,6 @@ type Scheduler = typeof requestAnimationFrame - | typeof onTickEnd - | typeof runNow; + | typeof onTickEnd; export function debounce( fn: F, @@ -77,10 +76,6 @@ export function throttleWithTickEnd(fn: F) { return throttleWith(onTickEnd, fn); } -export function throttleWithNow(fn: F) { - return throttleWith(runNow, fn); -} - export function throttleWith(schedulerFn: Scheduler, fn: F) { let waiting = false; let args: Parameters; @@ -109,10 +104,6 @@ export function onIdle(cb: NoneToVoidFunction, timeout?: number) { } } -function runNow(fn: NoneToVoidFunction) { - fn(); -} - export const pause = (ms: number) => new Promise((resolve) => { setTimeout(() => resolve(), ms); });