@applicaster/quick-brick-core 16.0.0-alpha.2024822855 → 16.0.0-alpha.2105365027
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/App/ActionsProvider/LegacyActionsRegistryAdapter.tsx +16 -0
- package/App/ActionsProvider/__tests__/LegacyActionsRegistryAdapter.test.tsx +64 -0
- package/App/ModalProvider/ModalContent.tsx +3 -0
- package/App/ModalProvider/__tests__/ModalContent.test.tsx +39 -0
- package/App/ModalProvider/__tests__/index.test.tsx +99 -0
- package/App/ModalProvider/index.tsx +39 -14
- package/App/NavigationProvider/NavigationProvider.tsx +71 -10
- package/App/NavigationProvider/__tests__/navToolbox.test.ts +227 -0
- package/App/NavigationProvider/__tests__/navigationProvider.test.tsx +56 -0
- package/App/NavigationProvider/__tests__/utils.test.ts +31 -1
- package/App/NavigationProvider/navToolbox.ts +224 -0
- package/App/NavigationProvider/utils.ts +35 -0
- package/App/NetworkStatusProvider/NetworkStatusProvider.tsx +32 -4
- package/App/NetworkStatusProvider/ResumeVerificationController.ts +282 -0
- package/App/NetworkStatusProvider/__tests__/NetworkStatusProvider.test.tsx +152 -1
- package/App/NetworkStatusProvider/__tests__/ResumeVerificationController.test.ts +471 -0
- package/App/NetworkStatusProvider/__tests__/resumeVerification.test.ts +126 -0
- package/App/NetworkStatusProvider/__tests__/useResumeVerifiedNetInfo.test.ts +189 -0
- package/App/NetworkStatusProvider/resumeVerification.ts +54 -0
- package/App/NetworkStatusProvider/useResumeVerifiedNetInfo.ts +33 -0
- package/App/NotificationToastRenderer/NotificationToastRenderer.tv.tsx +6 -127
- package/App/NotificationToastRenderer/index.tsx +0 -2
- package/App/NotificationToastRenderer/useNotificationHeight.tsx +1 -0
- package/package.json +8 -8
|
@@ -66,6 +66,22 @@ function LegacyActionBridge({ plugin }: { plugin: LegacyActionPlugin }) {
|
|
|
66
66
|
};
|
|
67
67
|
}, [plugin.identifier]);
|
|
68
68
|
|
|
69
|
+
// Consumers resolve a snapshot of `value`; when the plugin publishes a new
|
|
70
|
+
// one (e.g. cast devices found), make them re-resolve so availability
|
|
71
|
+
// predicates are not read from a stale value. Registration already
|
|
72
|
+
// notifies, so the first render is skipped.
|
|
73
|
+
const isFirstValueRef = React.useRef(true);
|
|
74
|
+
|
|
75
|
+
React.useEffect(() => {
|
|
76
|
+
if (isFirstValueRef.current) {
|
|
77
|
+
isFirstValueRef.current = false;
|
|
78
|
+
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
uiActionsRegistry.notifyProviderChanged(plugin.identifier);
|
|
83
|
+
}, [value, plugin.identifier]);
|
|
84
|
+
|
|
69
85
|
return null;
|
|
70
86
|
}
|
|
71
87
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "@testing-library/react-native";
|
|
3
|
+
import { uiActionsRegistry } from "@applicaster/zapp-react-native-utils/uiActionsRegistrator";
|
|
4
|
+
import { LegacyActionsRegistryAdapter } from "../LegacyActionsRegistryAdapter";
|
|
5
|
+
|
|
6
|
+
const CastContext = React.createContext<any>(null);
|
|
7
|
+
|
|
8
|
+
const plugin = {
|
|
9
|
+
identifier: "cast-action-test",
|
|
10
|
+
module: { context: CastContext },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const renderWithValue = (value) => (
|
|
14
|
+
<CastContext.Provider value={value}>
|
|
15
|
+
<LegacyActionsRegistryAdapter actionPlugins={{ cast: plugin } as any} />
|
|
16
|
+
</CastContext.Provider>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
describe("LegacyActionsRegistryAdapter", () => {
|
|
20
|
+
it("makes consumers re-resolve when the plugin publishes a new value", () => {
|
|
21
|
+
const noDevices = { isActionAvailable: () => false };
|
|
22
|
+
const devicesFound = { isActionAvailable: () => true };
|
|
23
|
+
|
|
24
|
+
const { rerender, unmount } = render(renderWithValue(noDevices));
|
|
25
|
+
|
|
26
|
+
// what a consumer resolved at first paint
|
|
27
|
+
expect(
|
|
28
|
+
uiActionsRegistry
|
|
29
|
+
.getAction("cast-action-test")
|
|
30
|
+
?.isActionAvailable?.({} as any)
|
|
31
|
+
).toBe(false);
|
|
32
|
+
|
|
33
|
+
const listener = jest.fn();
|
|
34
|
+
const unsubscribe = uiActionsRegistry.subscribe(listener);
|
|
35
|
+
|
|
36
|
+
rerender(renderWithValue(devicesFound));
|
|
37
|
+
|
|
38
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
39
|
+
|
|
40
|
+
expect(
|
|
41
|
+
uiActionsRegistry
|
|
42
|
+
.getAction("cast-action-test")
|
|
43
|
+
?.isActionAvailable?.({} as any)
|
|
44
|
+
).toBe(true);
|
|
45
|
+
|
|
46
|
+
unsubscribe();
|
|
47
|
+
unmount();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("does not notify again when the value is unchanged", () => {
|
|
51
|
+
const stable = { isActionAvailable: () => true };
|
|
52
|
+
const { rerender, unmount } = render(renderWithValue(stable));
|
|
53
|
+
|
|
54
|
+
const listener = jest.fn();
|
|
55
|
+
const unsubscribe = uiActionsRegistry.subscribe(listener);
|
|
56
|
+
|
|
57
|
+
rerender(renderWithValue(stable));
|
|
58
|
+
|
|
59
|
+
expect(listener).not.toHaveBeenCalled();
|
|
60
|
+
|
|
61
|
+
unsubscribe();
|
|
62
|
+
unmount();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -30,6 +30,9 @@ export function ModalContent(props: Props) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
return (
|
|
33
|
+
// A presented screen fills the window, including the status bar and home
|
|
34
|
+
// indicator. The modal wraps it in its own SafeAreaProvider, so the screen
|
|
35
|
+
// reads the real insets itself and places its own chrome inside them.
|
|
33
36
|
<View style={styles.container}>
|
|
34
37
|
<ScreenResolver {...modalScreenProps} />
|
|
35
38
|
</View>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { StyleSheet } from "react-native";
|
|
3
|
+
import { render } from "@testing-library/react-native";
|
|
4
|
+
|
|
5
|
+
import { ModalContent } from "../ModalContent";
|
|
6
|
+
|
|
7
|
+
jest.mock("../ModalBottomSheet", () => ({ ModalBottomSheet: () => null }));
|
|
8
|
+
|
|
9
|
+
jest.mock(
|
|
10
|
+
"@applicaster/zapp-react-native-ui-components/Components/ScreenResolver",
|
|
11
|
+
() => ({ ScreenResolver: () => null })
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const flatStyle = (tree: any) => StyleSheet.flatten(tree.props.style);
|
|
15
|
+
|
|
16
|
+
describe("ModalContent", () => {
|
|
17
|
+
it("lets a presented screen fill the modal, leaving the safe area to the screen", () => {
|
|
18
|
+
const tree = render(
|
|
19
|
+
<ModalContent modalScreenProps={{ screenId: "river-1" }} />
|
|
20
|
+
).toJSON();
|
|
21
|
+
|
|
22
|
+
// The modal provides a SafeAreaProvider instead, so the presented screen
|
|
23
|
+
// reads the real insets itself rather than being padded from the outside.
|
|
24
|
+
expect(flatStyle(tree)).toEqual({ flex: 1 });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("leaves a bottom sheet full-bleed", () => {
|
|
28
|
+
const tree = render(
|
|
29
|
+
<ModalContent
|
|
30
|
+
modalScreenProps={{
|
|
31
|
+
screenData: { modalBottomSheetContentProps: { items: [] } },
|
|
32
|
+
}}
|
|
33
|
+
/>
|
|
34
|
+
).toJSON();
|
|
35
|
+
|
|
36
|
+
// Bottom sheets anchor to the screen edge and manage their own insets.
|
|
37
|
+
expect(tree).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "@testing-library/react-native";
|
|
3
|
+
|
|
4
|
+
import { modalStore } from "@applicaster/zapp-react-native-utils/modalState";
|
|
5
|
+
|
|
6
|
+
import { ModalProvider } from "../";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Records the ScreenDataContext value seen by the presented screen — the
|
|
10
|
+
* context useScreenContext()/useCurrentScreenData() read from.
|
|
11
|
+
*/
|
|
12
|
+
const mockSeenScreenData: any[] = [];
|
|
13
|
+
|
|
14
|
+
/** Records the `initialMetrics` each SafeAreaProvider the modal renders got. */
|
|
15
|
+
const mockSafeAreaProviderMetrics: any[] = [];
|
|
16
|
+
|
|
17
|
+
jest.mock(
|
|
18
|
+
"@applicaster/zapp-react-native-ui-components/Components/ScreenResolver",
|
|
19
|
+
() => {
|
|
20
|
+
const ReactLocal = require("react");
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
ScreenDataContext,
|
|
24
|
+
} = require("@applicaster/zapp-react-native-ui-components/Contexts/ScreenDataContext");
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
ScreenResolver: () => {
|
|
28
|
+
mockSeenScreenData.push(ReactLocal.useContext(ScreenDataContext));
|
|
29
|
+
|
|
30
|
+
return null;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// Needs the navigator + redux to build navbar state; irrelevant to this test.
|
|
37
|
+
jest.mock(
|
|
38
|
+
"@applicaster/zapp-react-native-ui-components/Contexts/ScreenContext",
|
|
39
|
+
() => ({
|
|
40
|
+
ScreenContextProvider: ({ children }: any) => children,
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Pulls in the whole Components barrel; only the ScreenResolver branch matters here.
|
|
45
|
+
jest.mock("../ModalBottomSheet", () => ({ ModalBottomSheet: () => null }));
|
|
46
|
+
|
|
47
|
+
jest.mock("react-native-safe-area-context", () => ({
|
|
48
|
+
useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }),
|
|
49
|
+
// A modal renders in its own native window, so it needs its own provider
|
|
50
|
+
// seeded with the window metrics; without them insets read as zero inside.
|
|
51
|
+
initialWindowMetrics: { insets: { top: 59, bottom: 34, left: 0, right: 0 } },
|
|
52
|
+
SafeAreaProvider: ({ children, initialMetrics }: any) => {
|
|
53
|
+
mockSafeAreaProviderMetrics.push(initialMetrics);
|
|
54
|
+
|
|
55
|
+
return children;
|
|
56
|
+
},
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
const river = { id: "river-parent-lock", type: "parent_lock" } as any;
|
|
60
|
+
|
|
61
|
+
const entry = { id: "entry-1", type: { value: "video" } } as any;
|
|
62
|
+
|
|
63
|
+
const seen = () => mockSeenScreenData[mockSeenScreenData.length - 1];
|
|
64
|
+
|
|
65
|
+
describe("ModalProvider", () => {
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
mockSeenScreenData.length = 0;
|
|
68
|
+
mockSafeAreaProviderMetrics.length = 0;
|
|
69
|
+
modalStore.getState().dismissModal();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("gives the presented screen its screen and entry through ScreenDataContext", () => {
|
|
73
|
+
modalStore.getState().openModal({ item: river, props: { entry } });
|
|
74
|
+
|
|
75
|
+
render(<ModalProvider />);
|
|
76
|
+
|
|
77
|
+
expect(seen()).toEqual({ screen: river, entry });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("gives the presented screen its screen when presented without an entry", () => {
|
|
81
|
+
modalStore.getState().openModal({ item: river });
|
|
82
|
+
|
|
83
|
+
render(<ModalProvider />);
|
|
84
|
+
|
|
85
|
+
expect(seen()).toMatchObject({ screen: river });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("seeds the presented screen's safe area with the window metrics", () => {
|
|
89
|
+
const { initialWindowMetrics } = require("react-native-safe-area-context");
|
|
90
|
+
|
|
91
|
+
modalStore.getState().openModal({ item: river });
|
|
92
|
+
|
|
93
|
+
render(<ModalProvider />);
|
|
94
|
+
|
|
95
|
+
expect(mockSafeAreaProviderMetrics).toEqual(
|
|
96
|
+
expect.arrayContaining([initialWindowMetrics])
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
initialWindowMetrics,
|
|
5
|
+
SafeAreaProvider,
|
|
6
|
+
} from "react-native-safe-area-context";
|
|
7
|
+
|
|
3
8
|
import { ModalPresenter } from "./ModalPresenter";
|
|
4
9
|
import { ModalContent } from "./ModalContent";
|
|
5
10
|
import { ModalChildrenWrapper } from "./ModalChildrenWrapper";
|
|
6
11
|
import { PathnameContext } from "@applicaster/zapp-react-native-ui-components/Contexts/PathnameContext";
|
|
12
|
+
import { ScreenDataContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ScreenDataContext";
|
|
7
13
|
import { ScreenContextProvider } from "@applicaster/zapp-react-native-ui-components/Contexts/ScreenContext";
|
|
8
14
|
import { ROUTE_TYPES } from "@applicaster/zapp-react-native-utils/navigationUtils/routeTypes";
|
|
9
15
|
import { useModalStoreState } from "@applicaster/zapp-react-native-utils/modalState";
|
|
@@ -14,6 +20,9 @@ export function ModalProvider() {
|
|
|
14
20
|
const [modalVisible, setModalVisible] = React.useState(false);
|
|
15
21
|
const [modalShown, setModalShown] = React.useState(false);
|
|
16
22
|
|
|
23
|
+
const screen = modalState?.screen;
|
|
24
|
+
const entry = modalState?.props?.entry as ZappEntry | undefined;
|
|
25
|
+
|
|
17
26
|
React.useEffect(() => {
|
|
18
27
|
setModalVisible(!!modalState);
|
|
19
28
|
|
|
@@ -22,13 +31,25 @@ export function ModalProvider() {
|
|
|
22
31
|
}
|
|
23
32
|
}, [modalState]);
|
|
24
33
|
|
|
25
|
-
|
|
34
|
+
/**
|
|
35
|
+
* The modal renders as a sibling of the stack navigator, so nothing upstream
|
|
36
|
+
* provides ScreenDataContext and the presented screen would otherwise see
|
|
37
|
+
* `null` from useScreenContext()/useCurrentScreenData(). Provide it here so a
|
|
38
|
+
* presented screen resolves its own screen and entry, the same way a pushed
|
|
39
|
+
* screen does through the navigator's scene.
|
|
40
|
+
*/
|
|
41
|
+
const screenDataContextValue = React.useMemo(
|
|
42
|
+
() => ({ screen, entry }),
|
|
43
|
+
[screen, entry]
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (!screen) {
|
|
26
47
|
return null;
|
|
27
48
|
}
|
|
28
49
|
|
|
29
50
|
const modalScreenProps = {
|
|
30
|
-
screenData:
|
|
31
|
-
screenId:
|
|
51
|
+
screenData: entry ? { ...entry, targetScreen: screen } : screen,
|
|
52
|
+
screenId: screen?.id,
|
|
32
53
|
screenType: "river",
|
|
33
54
|
...modalState.props,
|
|
34
55
|
};
|
|
@@ -39,17 +60,21 @@ export function ModalProvider() {
|
|
|
39
60
|
|
|
40
61
|
return (
|
|
41
62
|
<PathnameContext.Provider value={pathname}>
|
|
42
|
-
<
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
63
|
+
<ScreenDataContext.Provider value={screenDataContextValue}>
|
|
64
|
+
<ScreenContextProvider pathname={pathname}>
|
|
65
|
+
<ModalPresenter
|
|
66
|
+
visible={shouldShowModal}
|
|
67
|
+
{...modalState?.options}
|
|
68
|
+
statusBarTranslucent
|
|
69
|
+
>
|
|
70
|
+
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
|
|
71
|
+
<ModalChildrenWrapper>
|
|
72
|
+
<ModalContent modalScreenProps={modalScreenProps} />
|
|
73
|
+
</ModalChildrenWrapper>
|
|
74
|
+
</SafeAreaProvider>
|
|
75
|
+
</ModalPresenter>
|
|
76
|
+
</ScreenContextProvider>
|
|
77
|
+
</ScreenDataContext.Provider>
|
|
53
78
|
</PathnameContext.Provider>
|
|
54
79
|
);
|
|
55
80
|
}
|
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
useGetScreenOrientation,
|
|
14
14
|
} from "@applicaster/zapp-react-native-utils/appUtils/orientationHelper";
|
|
15
15
|
import { focusManager } from "@applicaster/zapp-react-native-utils/focusManager/FocusManager";
|
|
16
|
+
import { setXrayNavigator } from "@applicaster/zapp-react-native-utils/navigationUtils/xrayNavigator";
|
|
17
|
+
import { registerNavToolbox } from "./navToolbox";
|
|
16
18
|
|
|
17
19
|
import reducer, {
|
|
18
20
|
ACTIONS,
|
|
@@ -55,6 +57,7 @@ import {
|
|
|
55
57
|
getTargetScreen,
|
|
56
58
|
legacyScreenData,
|
|
57
59
|
openExternalUrl,
|
|
60
|
+
stripHookPrefix,
|
|
58
61
|
} from "./utils";
|
|
59
62
|
import {
|
|
60
63
|
debounce,
|
|
@@ -242,6 +245,10 @@ export function NavigationProvider({ children }: Props) {
|
|
|
242
245
|
playNextOverlayState.current = state;
|
|
243
246
|
};
|
|
244
247
|
|
|
248
|
+
// The play-next overlay owns remote focus while it is visible, so key
|
|
249
|
+
// handling (InteractionManager) needs to be able to ask for it.
|
|
250
|
+
const getPlayNextOverlay = () => playNextOverlayState.current ?? null;
|
|
251
|
+
|
|
245
252
|
const setNestedScreenContent = (entry: ZappEntry, screenId: string) => {
|
|
246
253
|
const screen = rivers[screenId];
|
|
247
254
|
const parent = findParent(context, navigator.currentRoute);
|
|
@@ -348,10 +355,7 @@ export function NavigationProvider({ children }: Props) {
|
|
|
348
355
|
state: firstRoute.state,
|
|
349
356
|
};
|
|
350
357
|
|
|
351
|
-
if (
|
|
352
|
-
stateForBack?.state?.entry &&
|
|
353
|
-
"home" in stateForBack?.state?.entry
|
|
354
|
-
) {
|
|
358
|
+
if (stateForBack?.state?.entry?.home) {
|
|
355
359
|
goHome();
|
|
356
360
|
} else {
|
|
357
361
|
dispatch(back(stateForBack));
|
|
@@ -414,6 +418,19 @@ export function NavigationProvider({ children }: Props) {
|
|
|
414
418
|
},
|
|
415
419
|
dispose
|
|
416
420
|
) => {
|
|
421
|
+
logger.debug({
|
|
422
|
+
message: `Hook screen presented: ${
|
|
423
|
+
hookPlugin.isModalHook() ? `(modal) ${route}` : route
|
|
424
|
+
}`,
|
|
425
|
+
data: {
|
|
426
|
+
route,
|
|
427
|
+
hookIdentifier: hookPlugin?.identifier,
|
|
428
|
+
hookScreenId: hookPlugin?.screen_id,
|
|
429
|
+
isModal: hookPlugin.isModalHook(),
|
|
430
|
+
lastHook: hookPlugin?.lastHook,
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
|
|
417
434
|
if (hookPlugin?.lastHook) {
|
|
418
435
|
dispose();
|
|
419
436
|
}
|
|
@@ -536,6 +553,17 @@ export function NavigationProvider({ children }: Props) {
|
|
|
536
553
|
currentStartUpHooks.current && setStartUpHooks(false);
|
|
537
554
|
}
|
|
538
555
|
|
|
556
|
+
logger.debug({
|
|
557
|
+
message: `Hooks complete, navigating to: ${targetRoute}`,
|
|
558
|
+
data: {
|
|
559
|
+
targetRoute,
|
|
560
|
+
hookIdentifier: hookPlugin?.identifier,
|
|
561
|
+
screenId: screen?.id,
|
|
562
|
+
screenName: screen?.name,
|
|
563
|
+
options,
|
|
564
|
+
},
|
|
565
|
+
});
|
|
566
|
+
|
|
539
567
|
dispatch(
|
|
540
568
|
navigationAction(targetRoute, { screen, entry: payload, options })
|
|
541
569
|
);
|
|
@@ -574,12 +602,28 @@ export function NavigationProvider({ children }: Props) {
|
|
|
574
602
|
) => {
|
|
575
603
|
const { entry, screen, targetRoute, externalUrl } = getNavigationTarget(
|
|
576
604
|
item,
|
|
577
|
-
action === ACTIONS.REPLACE ? "" : pathnameRef.current,
|
|
605
|
+
action === ACTIONS.REPLACE ? "" : stripHookPrefix(pathnameRef.current),
|
|
578
606
|
contentTypes,
|
|
579
607
|
layoutVersion,
|
|
580
608
|
rivers
|
|
581
609
|
);
|
|
582
610
|
|
|
611
|
+
logger.debug({
|
|
612
|
+
message: `Navigating: ${pathnameRef.current || "(none)"} -> ${
|
|
613
|
+
targetRoute || externalUrl || "(unresolved)"
|
|
614
|
+
}`,
|
|
615
|
+
data: {
|
|
616
|
+
action,
|
|
617
|
+
from: pathnameRef.current,
|
|
618
|
+
to: targetRoute,
|
|
619
|
+
externalUrl,
|
|
620
|
+
screenId: screen?.id,
|
|
621
|
+
screenName: screen?.name,
|
|
622
|
+
entryType: entry?.type?.value,
|
|
623
|
+
options,
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
|
|
583
627
|
if (externalUrl) {
|
|
584
628
|
const openURL = async (url) => {
|
|
585
629
|
const inflatedURL = await inflateUrl(url);
|
|
@@ -668,6 +712,15 @@ export function NavigationProvider({ children }: Props) {
|
|
|
668
712
|
|
|
669
713
|
hooksManager.handleHooks(legacyScreenData({ entry, screen }));
|
|
670
714
|
} else {
|
|
715
|
+
logger.debug({
|
|
716
|
+
message: `Navigating offline (hooks skipped) to: ${targetRoute}`,
|
|
717
|
+
data: {
|
|
718
|
+
targetRoute,
|
|
719
|
+
screenId: screen?.id,
|
|
720
|
+
screenName: screen?.name,
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
|
|
671
724
|
dispatch(navigationAction(targetRoute, { screen, entry, options }));
|
|
672
725
|
}
|
|
673
726
|
},
|
|
@@ -709,11 +762,11 @@ export function NavigationProvider({ children }: Props) {
|
|
|
709
762
|
const getStackForPathname = (
|
|
710
763
|
pathname: string
|
|
711
764
|
): NavigationScreenState | undefined => {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
765
|
+
// Topmost match: the active screen if a route is on the stack twice.
|
|
766
|
+
return R.findLast(
|
|
767
|
+
({ route }) => route === pathname,
|
|
768
|
+
state?.stack?.mainStack ?? []
|
|
769
|
+
);
|
|
717
770
|
};
|
|
718
771
|
|
|
719
772
|
const navigator: QuickBrickAppNavigator = React.useMemo(
|
|
@@ -727,6 +780,7 @@ export function NavigationProvider({ children }: Props) {
|
|
|
727
780
|
replace: replaceItem,
|
|
728
781
|
setNestedScreenContent,
|
|
729
782
|
setPlayNextOverlay,
|
|
783
|
+
getPlayNextOverlay,
|
|
730
784
|
goHome,
|
|
731
785
|
canGoBack,
|
|
732
786
|
goBack,
|
|
@@ -777,6 +831,13 @@ export function NavigationProvider({ children }: Props) {
|
|
|
777
831
|
navigator.state = state;
|
|
778
832
|
}
|
|
779
833
|
|
|
834
|
+
React.useLayoutEffect(() => {
|
|
835
|
+
setXrayNavigator(navigator);
|
|
836
|
+
registerNavToolbox();
|
|
837
|
+
|
|
838
|
+
return () => setXrayNavigator(undefined);
|
|
839
|
+
}, [navigator]);
|
|
840
|
+
|
|
780
841
|
React.useLayoutEffect(() => {
|
|
781
842
|
if (pathname === "/" && appLaunched) {
|
|
782
843
|
goHome(true);
|