From 1f66947c044ed0cf46876a68fc6db6068bd19bc9 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 16 Mar 2026 14:01:54 +0100 Subject: [PATCH 1/5] Revert "Merge pull request #85198 from callstack-internal/revert-82764-freeze-non-top-screens" This reverts commit c43fe34816d9ddccb81d6821a9ec91c4f38ad0ed, reversing changes made to b2b5ac16f3a5381418768c9eaaf8cfb0b96c1de3. --- patches/react-native-screens/details.md | 8 +++++ ...+4.15.4+001+delay-freeze-until-paint.patch | 23 ++++++++++++ ...3.14+003+freeze-screen-below-focused.patch | 36 +++++++++++++++++++ patches/react-navigation/details.md | 10 +++++- .../createSplitNavigator/index.tsx | 1 + .../ScreenFreezeWrapper.tsx | 35 ++++++++++++++++++ .../index.tsx | 25 +++++++++++-- .../types/NavigatorComponent.ts | 1 + .../AttachmentModalBaseContent/index.tsx | 9 ++--- .../WorkspaceInviteMessageComponent.tsx | 2 +- src/setup/index.ts | 5 +++ 11 files changed, 147 insertions(+), 8 deletions(-) create mode 100644 patches/react-native-screens/details.md create mode 100644 patches/react-native-screens/react-native-screens+4.15.4+001+delay-freeze-until-paint.patch create mode 100644 patches/react-navigation/@react-navigation+native-stack+7.3.14+003+freeze-screen-below-focused.patch create mode 100644 src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeWrapper.tsx diff --git a/patches/react-native-screens/details.md b/patches/react-native-screens/details.md new file mode 100644 index 0000000000000..e07de39d8108d --- /dev/null +++ b/patches/react-native-screens/details.md @@ -0,0 +1,8 @@ +# `react-native-screens` patches + +### [react-native-screens+4.15.4+001+delay-freeze-until-paint.patch](react-native-screens+4.15.4+001+delay-freeze-until-paint.patch) + +- Reason: Adds `requestAnimationFrame` to `DelayedFreeze` so the screen freeze is deferred until after the current frame is painted. Without this, screens can be frozen while transitional UI states are still visible, causing a visual flicker. +- Upstream PR/issue: N/A +- E/App issue: https://github.com/Expensify/App/issues/33725 +- PR introducing patch: https://github.com/Expensify/App/pull/82764 diff --git a/patches/react-native-screens/react-native-screens+4.15.4+001+delay-freeze-until-paint.patch b/patches/react-native-screens/react-native-screens+4.15.4+001+delay-freeze-until-paint.patch new file mode 100644 index 0000000000000..34b81d29b89fb --- /dev/null +++ b/patches/react-native-screens/react-native-screens+4.15.4+001+delay-freeze-until-paint.patch @@ -0,0 +1,23 @@ +diff --git a/node_modules/react-native-screens/src/components/helpers/DelayedFreeze.tsx b/node_modules/react-native-screens/src/components/helpers/DelayedFreeze.tsx +index 443da63..72517fd 100644 +--- a/node_modules/react-native-screens/src/components/helpers/DelayedFreeze.tsx ++++ b/node_modules/react-native-screens/src/components/helpers/DelayedFreeze.tsx +@@ -12,12 +12,17 @@ function DelayedFreeze({ freeze, children }: FreezeWrapperProps) { + // flag used for determining whether freeze should be enabled + const [freezeState, setFreezeState] = React.useState(false); + ++ // We use requestAnimationFrame to ensure the current frame is fully painted ++ // before scheduling the freeze. This prevents freezing the screen while ++ // transitional UI states (e.g. opacity from useSingleExecution) are still visible. + React.useEffect(() => { ++ let rafID: number; + const id = setTimeout(() => { +- setFreezeState(freeze); ++ rafID = requestAnimationFrame(() => setFreezeState(freeze)); + }, 0); + return () => { + clearTimeout(id); ++ cancelAnimationFrame(rafID); + }; + }, [freeze]); + diff --git a/patches/react-navigation/@react-navigation+native-stack+7.3.14+003+freeze-screen-below-focused.patch b/patches/react-navigation/@react-navigation+native-stack+7.3.14+003+freeze-screen-below-focused.patch new file mode 100644 index 0000000000000..726fe73f8eccd --- /dev/null +++ b/patches/react-navigation/@react-navigation+native-stack+7.3.14+003+freeze-screen-below-focused.patch @@ -0,0 +1,36 @@ +diff --git a/node_modules/@react-navigation/native-stack/lib/module/views/NativeStackView.native.js b/node_modules/@react-navigation/native-stack/lib/module/views/NativeStackView.native.js +index 35dfc05..1758a24 100644 +--- a/node_modules/@react-navigation/native-stack/lib/module/views/NativeStackView.native.js ++++ b/node_modules/@react-navigation/native-stack/lib/module/views/NativeStackView.native.js +@@ -376,9 +376,9 @@ export function NativeStackView({ + const isModal = modalRouteKeys.includes(route.key); + const isPreloaded = preloadedDescriptors[route.key] !== undefined && descriptors[route.key] === undefined; + +- // On Fabric, when screen is frozen, animated and reanimated values are not updated +- // due to component being unmounted. To avoid this, we don't freeze the previous screen there +- const shouldFreeze = isFabric() ? !isPreloaded && !isFocused && !isBelowFocused : !isPreloaded && !isFocused; ++ // Freezing the screen below the focused one is safe on Fabric because ++ // DelayedFreeze defers it to the next macrotask, and transition animations run on the UI thread. ++ const shouldFreeze = !isPreloaded && !isFocused; + return /*#__PURE__*/_jsx(SceneView, { + index: index, + focused: isFocused, +diff --git a/node_modules/@react-navigation/native-stack/src/views/NativeStackView.native.tsx b/node_modules/@react-navigation/native-stack/src/views/NativeStackView.native.tsx +index ca15b1d..3191475 100644 +--- a/node_modules/@react-navigation/native-stack/src/views/NativeStackView.native.tsx ++++ b/node_modules/@react-navigation/native-stack/src/views/NativeStackView.native.tsx +@@ -542,11 +542,9 @@ export function NativeStackView({ + preloadedDescriptors[route.key] !== undefined && + descriptors[route.key] === undefined; + +- // On Fabric, when screen is frozen, animated and reanimated values are not updated +- // due to component being unmounted. To avoid this, we don't freeze the previous screen there +- const shouldFreeze = isFabric() +- ? !isPreloaded && !isFocused && !isBelowFocused +- : !isPreloaded && !isFocused; ++ // Freezing the screen below the focused one is safe on Fabric because ++ // DelayedFreeze defers it to the next macrotask, and transition animations run on the UI thread. ++ const shouldFreeze = !isPreloaded && !isFocused; + + return ( + { + if (!TooltipSense.isActive() || !isScreenBlurred) { + // eslint-disable-next-line react-hooks/set-state-in-effect + setFrozen(isScreenBlurred); + } + + // When a tooltip is active, defer freezing by one frame so it can dismiss first. + // Otherwise the frozen tree can't hide the tooltip portal rendered on . + const id = requestAnimationFrame(() => setFrozen(isScreenBlurred)); + return () => { + cancelAnimationFrame(id); + }; + }, [isScreenBlurred]); + + return {children}; +} + +export default ScreenFreezeWrapper; diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx index 730e269d507af..495ca5c25d390 100644 --- a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx +++ b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx @@ -13,6 +13,7 @@ import type { PlatformStackNavigatorProps, PlatformStackRouterOptions, } from '@libs/Navigation/PlatformStackNavigation/types'; +import ScreenFreezeWrapper from './ScreenFreezeWrapper'; function createPlatformStackNavigatorComponent( displayName: string, @@ -24,6 +25,7 @@ function createPlatformStackNavigatorComponent undefined); + const freezeNonTopScreens = options?.freezeNonTopScreens; function PlatformNavigator({ id, @@ -100,6 +102,25 @@ function createPlatformStackNavigatorComponent {descriptor.render()}, + }; + } + wrappedDescriptors = result; + } + const Content = useMemo( () => ( @@ -108,7 +129,7 @@ function createPlatformStackNavigatorComponent @@ -119,7 +140,7 @@ function createPlatformStackNavigatorComponent ), - [NavigationContent, customCodePropsWithCustomState, describe, descriptors, mappedState, navigation, props], + [NavigationContent, customCodePropsWithCustomState, describe, wrappedDescriptors, mappedState, navigation, props], ); // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/src/libs/Navigation/PlatformStackNavigation/types/NavigatorComponent.ts b/src/libs/Navigation/PlatformStackNavigation/types/NavigatorComponent.ts index 87d8141437a5d..eb0c95880c869 100644 --- a/src/libs/Navigation/PlatformStackNavigation/types/NavigatorComponent.ts +++ b/src/libs/Navigation/PlatformStackNavigation/types/NavigatorComponent.ts @@ -56,6 +56,7 @@ type CreatePlatformStackNavigatorComponentOptions; ExtraContent?: ExtraContent; NavigationContentWrapper?: NavigationContentWrapper; + freezeNonTopScreens?: boolean; }; export type {CustomCodeProps, CustomStateHookProps, CustomEffectsHookProps, CreatePlatformStackNavigatorComponentOptions, ExtraContentProps}; diff --git a/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent/index.tsx b/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent/index.tsx index b30a17fcdbc59..f884b9bc2837b 100644 --- a/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent/index.tsx +++ b/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent/index.tsx @@ -166,11 +166,12 @@ function AttachmentModalBaseContent({ return; } - if (onConfirm) { - onConfirm(Object.assign(files ?? {}, {source} as FileObject)); - } - onClose?.(); + + // Defer onConfirm to the next frame so the target screen has time to unfreeze and re-mount its refs (e.g. composerRef.clearWorklet) + requestAnimationFrame(() => { + onConfirm?.(Object.assign(files ?? {}, {source} as FileObject)); + }); }, [isConfirmButtonDisabled, onConfirm, onClose, files, source]); // Close the modal when the escape key is pressed diff --git a/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx b/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx index 4e95f50ca45d6..267cfc26ae480 100644 --- a/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx +++ b/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx @@ -142,7 +142,7 @@ function WorkspaceInviteMessageComponent({ } if ((backTo as string)?.endsWith('members')) { - Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.dismissModal()); + Navigation.dismissModal(); return; } diff --git a/src/setup/index.ts b/src/setup/index.ts index 6f8aec7bb1e8f..1d557e0553836 100644 --- a/src/setup/index.ts +++ b/src/setup/index.ts @@ -2,6 +2,7 @@ import toSortedPolyfill from 'array.prototype.tosorted'; import {I18nManager} from 'react-native'; import Config from 'react-native-config'; import Onyx from 'react-native-onyx'; +import {enableFreeze} from 'react-native-screens'; import intlPolyfill from '@libs/IntlPolyfill'; import {setDeviceID} from '@userActions/Device'; import initOnyxDerivedValues from '@userActions/OnyxDerived'; @@ -14,6 +15,10 @@ import telemetry from './telemetry'; const enableDevTools = Config?.USE_REDUX_DEVTOOLS ? Config.USE_REDUX_DEVTOOLS === 'true' : true; export default function () { + // Enable screen freezing on mobile to prevent unnecessary re-renders on screens that are not visible to the user. + // This is a no-op on web — for web, we use ScreenFreezeWrapper in SplitNavigator instead. + enableFreeze(true); + telemetry(); toSortedPolyfill.shim(); From 611e70ee9b5bd0b83366428d8445b998070089fb Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 16 Mar 2026 14:46:04 +0100 Subject: [PATCH 2/5] Fix DB 85184 --- .../ScreenFreezeWrapper.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeWrapper.tsx b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeWrapper.tsx index ca6e1e2ba6412..c4b3712446c1f 100644 --- a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeWrapper.tsx +++ b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeWrapper.tsx @@ -1,6 +1,7 @@ import React, {useLayoutEffect, useState} from 'react'; import {Freeze} from 'react-freeze'; import TooltipSense from '@components/Tooltip/TooltipSense'; +import {areAllModalsHidden} from '@userActions/Modal'; type ScreenFreezeWrapperProps = { /** Whether the screen is not currently visible to the user */ @@ -16,13 +17,22 @@ function ScreenFreezeWrapper({isScreenBlurred, children}: ScreenFreezeWrapperPro // Decouple the Suspense render task so it won't be interrupted by React's concurrent mode // and stuck in an infinite loop useLayoutEffect(() => { - if (!TooltipSense.isActive() || !isScreenBlurred) { + // When no modal or tooltip is active, freeze/unfreeze immediately. + if (!TooltipSense.isActive() && areAllModalsHidden()) { // eslint-disable-next-line react-hooks/set-state-in-effect setFrozen(isScreenBlurred); + return; } - // When a tooltip is active, defer freezing by one frame so it can dismiss first. - // Otherwise the frozen tree can't hide the tooltip portal rendered on . + // When unfreezing, always apply immediately so the screen is visible right away. + if (!isScreenBlurred) { + // eslint-disable-next-line react-hooks/set-state-in-effect + setFrozen(false); + return; + } + + // A modal or tooltip is still open. Defer freezing by one frame so it can dismiss + // before the tree is suspended. const id = requestAnimationFrame(() => setFrozen(isScreenBlurred)); return () => { cancelAnimationFrame(id); From 390daa4b6a247a76051f494cae6168dea1206005 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 16 Mar 2026 16:37:19 +0100 Subject: [PATCH 3/5] Fix DB 85170, 85185, 85250 --- patches/react-native-screens/details.md | 2 +- ...+4.15.4+001+delay-freeze-until-paint.patch | 33 +++++++++++++------ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/patches/react-native-screens/details.md b/patches/react-native-screens/details.md index e07de39d8108d..58353e4f7d10c 100644 --- a/patches/react-native-screens/details.md +++ b/patches/react-native-screens/details.md @@ -2,7 +2,7 @@ ### [react-native-screens+4.15.4+001+delay-freeze-until-paint.patch](react-native-screens+4.15.4+001+delay-freeze-until-paint.patch) -- Reason: Adds `requestAnimationFrame` to `DelayedFreeze` so the screen freeze is deferred until after the current frame is painted. Without this, screens can be frozen while transitional UI states are still visible, causing a visual flicker. +- Reason: Delays `DelayedFreeze` before freezing a screen. On iOS (Fabric), freezing a screen while an animation is still in flight (e.g. a popover closing or a swipe-back gesture finishing) blocks React rendering and can cause the app to become unresponsive. - Upstream PR/issue: N/A - E/App issue: https://github.com/Expensify/App/issues/33725 - PR introducing patch: https://github.com/Expensify/App/pull/82764 diff --git a/patches/react-native-screens/react-native-screens+4.15.4+001+delay-freeze-until-paint.patch b/patches/react-native-screens/react-native-screens+4.15.4+001+delay-freeze-until-paint.patch index 34b81d29b89fb..8afcd08548f92 100644 --- a/patches/react-native-screens/react-native-screens+4.15.4+001+delay-freeze-until-paint.patch +++ b/patches/react-native-screens/react-native-screens+4.15.4+001+delay-freeze-until-paint.patch @@ -2,22 +2,35 @@ diff --git a/node_modules/react-native-screens/src/components/helpers/DelayedFre index 443da63..72517fd 100644 --- a/node_modules/react-native-screens/src/components/helpers/DelayedFreeze.tsx +++ b/node_modules/react-native-screens/src/components/helpers/DelayedFreeze.tsx -@@ -12,12 +12,17 @@ function DelayedFreeze({ freeze, children }: FreezeWrapperProps) { +@@ -7,17 +7,27 @@ interface FreezeWrapperProps { + children: React.ReactNode; + } + ++// Delay freezing long enough for screen transition and modal/popover dismiss ++// animations to complete. On iOS, freezing a screen while an animation ++// is still in flight (e.g. a popover closing or a swipe-back gesture finishing) ++// blocks React rendering and can cause the app to become unresponsive. ++const FREEZE_DELAY_MS = 400; ++ + // This component allows one more render before freezing the screen. + // Allows activityState to reach the native side and useIsFocused to work correctly. + function DelayedFreeze({ freeze, children }: FreezeWrapperProps) { // flag used for determining whether freeze should be enabled const [freezeState, setFreezeState] = React.useState(false); - -+ // We use requestAnimationFrame to ensure the current frame is fully painted -+ // before scheduling the freeze. This prevents freezing the screen while -+ // transitional UI states (e.g. opacity from useSingleExecution) are still visible. - React.useEffect(() => { -+ let rafID: number; - const id = setTimeout(() => { + +- React.useEffect(() => { +- const id = setTimeout(() => { - setFreezeState(freeze); +- }, 0); ++ React.useEffect(() => { ++ let rafID: number; ++ // Wait for FREEZE_DELAY_MS, then schedule the state update after the next ++ // frame paint so transitional UI states are fully flushed before suspending. ++ const id = setTimeout(() => { + rafID = requestAnimationFrame(() => setFreezeState(freeze)); - }, 0); ++ }, FREEZE_DELAY_MS); return () => { clearTimeout(id); + cancelAnimationFrame(rafID); }; }, [freeze]); - From 2c92405cb7834474ccd0673019aea886357665a6 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 16 Mar 2026 17:55:58 +0100 Subject: [PATCH 4/5] Fix DB 85149 --- src/hooks/useKeyboardShortcut.ts | 5 +++ .../ScreenFreezeContext.ts | 23 ++++++++++ .../ScreenFreezeWrapper.tsx | 44 ++++++++++++------- 3 files changed, 57 insertions(+), 15 deletions(-) create mode 100644 src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeContext.ts diff --git a/src/hooks/useKeyboardShortcut.ts b/src/hooks/useKeyboardShortcut.ts index 2256481687b9b..d5fc599c258ab 100644 --- a/src/hooks/useKeyboardShortcut.ts +++ b/src/hooks/useKeyboardShortcut.ts @@ -2,6 +2,7 @@ import {useEffect} from 'react'; import type {GestureResponderEvent} from 'react-native'; import type {ValueOf} from 'type-fest'; import KeyboardShortcut from '@libs/KeyboardShortcut'; +import {useScreenFreezeContext} from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeContext'; import CONST from '@src/CONST'; type Shortcut = ValueOf; @@ -42,11 +43,14 @@ export default function useKeyboardShortcut(shortcut: Shortcut, callback: (e?: G shouldStopPropagation = false, } = config; + const {registerFreezeDefer} = useScreenFreezeContext(); + useEffect(() => { if (!isActive) { return () => {}; } + const unregisterFreezeDefer = registerFreezeDefer(); const unsubscribe = KeyboardShortcut.subscribe( shortcut.shortcutKey, callback, @@ -62,6 +66,7 @@ export default function useKeyboardShortcut(shortcut: Shortcut, callback: (e?: G return () => { unsubscribe(); + unregisterFreezeDefer(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [isActive, callback, captureOnInputs, excludedNodes, priority, shortcut.descriptionKey, shortcut.modifiers.join(), shortcut.shortcutKey, shouldBubble, shouldPreventDefault]); diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeContext.ts b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeContext.ts new file mode 100644 index 0000000000000..0598cb51f748d --- /dev/null +++ b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeContext.ts @@ -0,0 +1,23 @@ +import {createContext, useContext} from 'react'; + +type ScreenFreezeContextType = { + /** + * Register a freeze defer — signals that this component has cleanup work + * (e.g., keyboard shortcut unsubscription) that must run before the screen is frozen. + * When any defers are registered, ScreenFreezeWrapper delays freezing by one frame + * so cleanup can execute first. + * Returns an unregister function. + */ + registerFreezeDefer: () => () => void; +}; + +const ScreenFreezeContext = createContext({ + registerFreezeDefer: () => () => {}, +}); + +function useScreenFreezeContext() { + return useContext(ScreenFreezeContext); +} + +export default ScreenFreezeContext; +export {useScreenFreezeContext}; diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeWrapper.tsx b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeWrapper.tsx index c4b3712446c1f..f84cc104a8d87 100644 --- a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeWrapper.tsx +++ b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeWrapper.tsx @@ -1,7 +1,8 @@ -import React, {useLayoutEffect, useState} from 'react'; +import React, {useLayoutEffect, useRef, useState} from 'react'; import {Freeze} from 'react-freeze'; import TooltipSense from '@components/Tooltip/TooltipSense'; import {areAllModalsHidden} from '@userActions/Modal'; +import ScreenFreezeContext from './ScreenFreezeContext'; type ScreenFreezeWrapperProps = { /** Whether the screen is not currently visible to the user */ @@ -13,17 +14,20 @@ type ScreenFreezeWrapperProps = { function ScreenFreezeWrapper({isScreenBlurred, children}: ScreenFreezeWrapperProps) { const [frozen, setFrozen] = useState(false); + const freezeDeferCountRef = useRef(0); + + const registerFreezeDefer = () => { + freezeDeferCountRef.current++; + return () => { + freezeDeferCountRef.current--; + }; + }; + + const contextValue = {registerFreezeDefer}; // Decouple the Suspense render task so it won't be interrupted by React's concurrent mode // and stuck in an infinite loop useLayoutEffect(() => { - // When no modal or tooltip is active, freeze/unfreeze immediately. - if (!TooltipSense.isActive() && areAllModalsHidden()) { - // eslint-disable-next-line react-hooks/set-state-in-effect - setFrozen(isScreenBlurred); - return; - } - // When unfreezing, always apply immediately so the screen is visible right away. if (!isScreenBlurred) { // eslint-disable-next-line react-hooks/set-state-in-effect @@ -31,15 +35,25 @@ function ScreenFreezeWrapper({isScreenBlurred, children}: ScreenFreezeWrapperPro return; } - // A modal or tooltip is still open. Defer freezing by one frame so it can dismiss - // before the tree is suspended. - const id = requestAnimationFrame(() => setFrozen(isScreenBlurred)); - return () => { - cancelAnimationFrame(id); - }; + // When there are active freeze defers (e.g. keyboard shortcuts that need to unsubscribe), + // or when a modal/tooltip is still open, defer the freezing by one frame. + if (freezeDeferCountRef.current > 0 || TooltipSense.isActive() || !areAllModalsHidden()) { + const id = requestAnimationFrame(() => setFrozen(isScreenBlurred)); + return () => { + cancelAnimationFrame(id); + }; + } + + // No blockers or overlays — freeze immediately. + // eslint-disable-next-line react-hooks/set-state-in-effect + setFrozen(isScreenBlurred); }, [isScreenBlurred]); - return {children}; + return ( + + {children} + + ); } export default ScreenFreezeWrapper; From e5450de216bfda352b38776893e3553d0ecb7bb7 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 17 Mar 2026 10:36:00 +0100 Subject: [PATCH 5/5] Fix spell check --- .../ScreenFreezeContext.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeContext.ts b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeContext.ts index 0598cb51f748d..529febe6bda6c 100644 --- a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeContext.ts +++ b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeContext.ts @@ -3,7 +3,7 @@ import {createContext, useContext} from 'react'; type ScreenFreezeContextType = { /** * Register a freeze defer — signals that this component has cleanup work - * (e.g., keyboard shortcut unsubscription) that must run before the screen is frozen. + * (e.g., keyboard shortcut unsubscribing) that must run before the screen is frozen. * When any defers are registered, ScreenFreezeWrapper delays freezing by one frame * so cleanup can execute first. * Returns an unregister function.