@applicaster/zapp-react-native-ui-components 15.0.0-alpha.9102699023 → 15.0.0-alpha.9102777840
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/Components/GeneralContentScreen/GeneralContentScreen.tsx +3 -8
- package/Components/HandlePlayable/HandlePlayable.tsx +16 -29
- package/Components/PlayerContainer/PlayerContainer.tsx +1 -17
- package/Components/River/RefreshControl.tsx +9 -3
- package/Components/Screen/__tests__/Screen.test.tsx +1 -0
- package/Components/Screen/hooks.ts +73 -3
- package/Components/Screen/index.tsx +7 -1
- package/Components/ScreenResolver/index.tsx +14 -10
- package/Components/Transitioner/Scene.tsx +9 -15
- package/Components/Viewport/ViewportAware/__tests__/viewportAware.test.js +0 -2
- package/Contexts/ScreenContext/index.tsx +29 -1
- package/Contexts/ZappPipesContext/ZappPipesContextFactory.tsx +18 -7
- package/Decorators/ZappPipesDataConnector/ResolverSelector.tsx +25 -7
- package/Decorators/ZappPipesDataConnector/__tests__/ResolverSelector.test.tsx +212 -5
- package/Decorators/ZappPipesDataConnector/__tests__/UrlFeedResolver.test.tsx +39 -21
- package/Decorators/ZappPipesDataConnector/resolvers/UrlFeedResolver.tsx +18 -7
- package/package.json +5 -5
- package/Components/PlayerContainer/ErrorDisplay/ErrorDisplay.tsx +0 -57
- package/Components/PlayerContainer/ErrorDisplay/index.ts +0 -9
- package/Components/PlayerContainer/useRestrictMobilePlayback.tsx +0 -131
|
@@ -13,7 +13,7 @@ import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/he
|
|
|
13
13
|
import { ScreenTrackedViewPositionsContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ScreenTrackedViewPositionsContext";
|
|
14
14
|
import { useEventAlerts } from "./utils/useEventAlerts";
|
|
15
15
|
|
|
16
|
-
const {
|
|
16
|
+
const { log_debug } = createLogger({
|
|
17
17
|
category: "ScreenContainer",
|
|
18
18
|
subsystem: "General",
|
|
19
19
|
});
|
|
@@ -54,20 +54,15 @@ export const GeneralContentScreen = ({
|
|
|
54
54
|
useEffect(() => {
|
|
55
55
|
if (!riverActionProvidersReady) {
|
|
56
56
|
if (actionsInitialStateSetters.length > 0) {
|
|
57
|
-
log_info(
|
|
58
|
-
"ScreenContainer: starting to check river action providers to initialize",
|
|
59
|
-
{ actionsInitialStateSetters }
|
|
60
|
-
);
|
|
61
|
-
|
|
62
57
|
allSettled(actionsInitialStateSetters).finally(() => {
|
|
63
|
-
|
|
58
|
+
log_debug(
|
|
64
59
|
"ScreenContainer: action provider ready, completed. Starting to present screen"
|
|
65
60
|
);
|
|
66
61
|
|
|
67
62
|
setRiverActionProvidersReady(true);
|
|
68
63
|
});
|
|
69
64
|
} else {
|
|
70
|
-
|
|
65
|
+
log_debug(
|
|
71
66
|
"ScreenContainer: no action provider to check, completed. Starting to present screen"
|
|
72
67
|
);
|
|
73
68
|
|
|
@@ -5,8 +5,6 @@ import {
|
|
|
5
5
|
usePlugins,
|
|
6
6
|
} from "@applicaster/zapp-react-native-redux/hooks";
|
|
7
7
|
import {
|
|
8
|
-
useDimensions,
|
|
9
|
-
useIsTablet as isTablet,
|
|
10
8
|
useNavigation,
|
|
11
9
|
useRivers,
|
|
12
10
|
} from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
@@ -15,8 +13,8 @@ import { BufferAnimation } from "../PlayerContainer/BufferAnimation";
|
|
|
15
13
|
import { PlayerContainer } from "../PlayerContainer";
|
|
16
14
|
import { useModalSize } from "../VideoModal/hooks";
|
|
17
15
|
import { ViewStyle } from "react-native";
|
|
18
|
-
import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
|
|
19
16
|
import { findCastPlugin, getPlayer } from "./utils";
|
|
17
|
+
import { useWaitForValidOrientation } from "../Screen/hooks";
|
|
20
18
|
|
|
21
19
|
type Props = {
|
|
22
20
|
item: ZappEntry;
|
|
@@ -31,13 +29,6 @@ type PlayableComponent = {
|
|
|
31
29
|
Component: React.ComponentType<any>;
|
|
32
30
|
};
|
|
33
31
|
|
|
34
|
-
const dimensionsContext: "window" | "screen" = platformSelect({
|
|
35
|
-
android_tv: "window",
|
|
36
|
-
amazon: "window",
|
|
37
|
-
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
38
|
-
default: isTablet() ? "window" : "screen", // on tablet, window represents correct values, on phone it's not as the screen could be rotated
|
|
39
|
-
});
|
|
40
|
-
|
|
41
32
|
export function HandlePlayable({
|
|
42
33
|
item,
|
|
43
34
|
isModal,
|
|
@@ -97,27 +88,23 @@ export function HandlePlayable({
|
|
|
97
88
|
});
|
|
98
89
|
}, [casting]);
|
|
99
90
|
|
|
100
|
-
const { width: screenWidth, height: screenHeight } =
|
|
101
|
-
useDimensions(dimensionsContext);
|
|
102
|
-
|
|
103
91
|
const modalSize = useModalSize();
|
|
104
92
|
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
);
|
|
93
|
+
const isOrientationReady = useWaitForValidOrientation();
|
|
94
|
+
|
|
95
|
+
const style = React.useMemo(() => {
|
|
96
|
+
const isFullScreenReady =
|
|
97
|
+
mode === "PIP" || (mode === "FULLSCREEN" && isOrientationReady);
|
|
98
|
+
|
|
99
|
+
const getDimensionValue = (value: string | number) => {
|
|
100
|
+
return isModal ? value : isFullScreenReady ? "100%" : 0; // do not show player, until full screen mode is ready
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
width: getDimensionValue(modalSize.width),
|
|
105
|
+
height: getDimensionValue(modalSize.height),
|
|
106
|
+
} as ViewStyle;
|
|
107
|
+
}, [modalSize, isModal, mode, isOrientationReady]);
|
|
121
108
|
|
|
122
109
|
const Component = playable?.Component;
|
|
123
110
|
|
|
@@ -46,7 +46,6 @@ import {
|
|
|
46
46
|
PlayerContainerContextProvider,
|
|
47
47
|
} from "./PlayerContainerContext";
|
|
48
48
|
import { FocusableGroup } from "@applicaster/zapp-react-native-ui-components/Components/FocusableGroup";
|
|
49
|
-
import { ErrorDisplay } from "./ErrorDisplay";
|
|
50
49
|
import { PlayerFocusableWrapperView } from "./WappersView/PlayerFocusableWrapperView";
|
|
51
50
|
import { FocusableGroupMainContainerId } from "./index";
|
|
52
51
|
import { isPlayable } from "@applicaster/zapp-react-native-utils/navigationUtils/itemTypeMatchers";
|
|
@@ -61,7 +60,6 @@ import {
|
|
|
61
60
|
PlayerNativeSendCommand,
|
|
62
61
|
} from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand";
|
|
63
62
|
import { useAppData } from "@applicaster/zapp-react-native-redux";
|
|
64
|
-
import { useRestrictMobilePlayback } from "./useRestrictMobilePlayback";
|
|
65
63
|
|
|
66
64
|
type Props = {
|
|
67
65
|
Player: React.ComponentType<any>;
|
|
@@ -274,12 +272,6 @@ const PlayerContainerComponent = (props: Props) => {
|
|
|
274
272
|
);
|
|
275
273
|
}, [playerManager.isRegistered()]);
|
|
276
274
|
|
|
277
|
-
const { isRestricted } = useRestrictMobilePlayback({
|
|
278
|
-
player,
|
|
279
|
-
entry: item,
|
|
280
|
-
close,
|
|
281
|
-
});
|
|
282
|
-
|
|
283
275
|
const playEntry = (entry) => navigator.replaceTop(entry, { mode });
|
|
284
276
|
|
|
285
277
|
const onPlayNextPerformNextVideoPlay = React.useCallback(() => {
|
|
@@ -346,12 +338,6 @@ const PlayerContainerComponent = (props: Props) => {
|
|
|
346
338
|
playerContainerLogger.error(errorObj);
|
|
347
339
|
|
|
348
340
|
setState({ error: errorObj });
|
|
349
|
-
|
|
350
|
-
if (!isTvOS) {
|
|
351
|
-
setTimeout(() => {
|
|
352
|
-
close();
|
|
353
|
-
}, 800);
|
|
354
|
-
}
|
|
355
341
|
},
|
|
356
342
|
[close]
|
|
357
343
|
);
|
|
@@ -669,7 +655,7 @@ const PlayerContainerComponent = (props: Props) => {
|
|
|
669
655
|
<PlayerFocusableWrapperView
|
|
670
656
|
nextFocusDown={context.bottomFocusableId}
|
|
671
657
|
>
|
|
672
|
-
{!Player
|
|
658
|
+
{!Player ? null : (
|
|
673
659
|
<Player
|
|
674
660
|
source={{
|
|
675
661
|
uri,
|
|
@@ -702,8 +688,6 @@ const PlayerContainerComponent = (props: Props) => {
|
|
|
702
688
|
</Player>
|
|
703
689
|
)}
|
|
704
690
|
</PlayerFocusableWrapperView>
|
|
705
|
-
|
|
706
|
-
{state.error ? <ErrorDisplay error={state.error} /> : null}
|
|
707
691
|
</View>
|
|
708
692
|
{/* Components container */}
|
|
709
693
|
{isInlineTV && context.showComponentsContainer ? (
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
StyleSheet,
|
|
6
6
|
} from "react-native";
|
|
7
7
|
import * as R from "ramda";
|
|
8
|
+
import { path } from "@applicaster/zapp-react-native-utils/utils";
|
|
9
|
+
import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
|
|
8
10
|
import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
|
|
9
11
|
import { useLocalizedStrings } from "@applicaster/zapp-react-native-utils/localizationUtils";
|
|
10
12
|
import { useAnalytics } from "@applicaster/zapp-react-native-utils/analyticsUtils";
|
|
@@ -62,9 +64,13 @@ export const usePullToRefresh = (
|
|
|
62
64
|
|
|
63
65
|
const [refreshing, setRefreshing] = React.useState(false);
|
|
64
66
|
|
|
65
|
-
const feeds: string[] =
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
const feeds: string[] = React.useMemo(
|
|
68
|
+
() =>
|
|
69
|
+
(riverComponents || [])
|
|
70
|
+
.map((riverComponent) => path(["data", "source"], riverComponent))
|
|
71
|
+
.filter((feed) => !isNilOrEmpty(feed)),
|
|
72
|
+
[riverComponents]
|
|
73
|
+
);
|
|
68
74
|
|
|
69
75
|
const feedsLength = feeds.length;
|
|
70
76
|
|
|
@@ -11,6 +11,7 @@ const mockIsOrientationCompatible = jest.fn(() => true);
|
|
|
11
11
|
jest.mock("react-native-safe-area-context", () => ({
|
|
12
12
|
...jest.requireActual("react-native-safe-area-context"),
|
|
13
13
|
useSafeAreaInsets: () => ({ top: 44 }),
|
|
14
|
+
useSafeAreaFrame: () => ({ x: 0, y: 0, width: 375, height: 812 }),
|
|
14
15
|
}));
|
|
15
16
|
|
|
16
17
|
jest.mock("../../RouteManager", () => ({
|
|
@@ -5,15 +5,85 @@ import {
|
|
|
5
5
|
} from "@applicaster/zapp-react-native-utils/appUtils/orientationHelper";
|
|
6
6
|
import {
|
|
7
7
|
useCurrentScreenData,
|
|
8
|
-
useDimensions,
|
|
9
8
|
useRoute,
|
|
10
9
|
useIsTablet,
|
|
10
|
+
useIsScreenActive,
|
|
11
11
|
} from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
12
12
|
import { useMemo, useEffect, useState } from "react";
|
|
13
|
+
import { useSafeAreaFrame } from "react-native-safe-area-context";
|
|
14
|
+
|
|
15
|
+
type MemoizedSafeAreaFrameWithActiveStateOptions = {
|
|
16
|
+
updateForInactiveScreens?: boolean;
|
|
17
|
+
isActive: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Base hook that wraps useSafeAreaFrame with memoization and inactive screen filtering.
|
|
22
|
+
* Requires isActive to be passed explicitly - use useMemoizedSafeAreaFrame for automatic detection.
|
|
23
|
+
*
|
|
24
|
+
* @param options.updateForInactiveScreens - If false, frame won't update when screen is inactive (default: true)
|
|
25
|
+
* @param options.isActive - Whether the screen is currently active
|
|
26
|
+
* @returns The memoized safe area frame { x, y, width, height }
|
|
27
|
+
*/
|
|
28
|
+
export const useMemoizedSafeAreaFrameWithActiveState = (
|
|
29
|
+
options: MemoizedSafeAreaFrameWithActiveStateOptions
|
|
30
|
+
) => {
|
|
31
|
+
const { updateForInactiveScreens = true, isActive } = options;
|
|
32
|
+
const frame = useSafeAreaFrame();
|
|
33
|
+
|
|
34
|
+
const [memoFrame, setMemoFrame] = useState(frame);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const shouldUpdate = isActive || updateForInactiveScreens;
|
|
38
|
+
|
|
39
|
+
const dimensionsChanged =
|
|
40
|
+
frame.width !== memoFrame.width || frame.height !== memoFrame.height;
|
|
41
|
+
|
|
42
|
+
if (shouldUpdate && dimensionsChanged) {
|
|
43
|
+
setMemoFrame(frame);
|
|
44
|
+
}
|
|
45
|
+
}, [
|
|
46
|
+
frame.width,
|
|
47
|
+
frame.height,
|
|
48
|
+
isActive,
|
|
49
|
+
updateForInactiveScreens,
|
|
50
|
+
frame,
|
|
51
|
+
memoFrame.width,
|
|
52
|
+
memoFrame.height,
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
return memoFrame;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type MemoizedSafeAreaFrameOptions = {
|
|
59
|
+
updateForInactiveScreens?: boolean;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Hook that wraps useSafeAreaFrame with memoization and inactive screen filtering.
|
|
64
|
+
* Uses useIsScreenActive() internally to determine active state - use useMemoizedSafeAreaFrameWithActiveState
|
|
65
|
+
* if you need to pass isActive explicitly.
|
|
66
|
+
*
|
|
67
|
+
* @param options.updateForInactiveScreens - If false, frame won't update when screen is inactive (default: true)
|
|
68
|
+
* @returns The memoized safe area frame { x, y, width, height }
|
|
69
|
+
*/
|
|
70
|
+
export const useMemoizedSafeAreaFrame = (
|
|
71
|
+
options: MemoizedSafeAreaFrameOptions = {}
|
|
72
|
+
) => {
|
|
73
|
+
const { updateForInactiveScreens = true } = options;
|
|
74
|
+
const isActive = useIsScreenActive();
|
|
75
|
+
|
|
76
|
+
return useMemoizedSafeAreaFrameWithActiveState({
|
|
77
|
+
updateForInactiveScreens,
|
|
78
|
+
isActive,
|
|
79
|
+
});
|
|
80
|
+
};
|
|
13
81
|
|
|
14
82
|
export const useWaitForValidOrientation = () => {
|
|
15
|
-
|
|
16
|
-
|
|
83
|
+
// Use memoized safe area frame to synchronize with Scene's dimension source
|
|
84
|
+
// This prevents race conditions where the orientation check passes before
|
|
85
|
+
// Scene's memoFrame has updated to the new dimensions
|
|
86
|
+
const { width: screenWidth, height } = useMemoizedSafeAreaFrame({
|
|
17
87
|
updateForInactiveScreens: false,
|
|
18
88
|
});
|
|
19
89
|
|
|
@@ -92,7 +92,13 @@ export function Screen(_props: Props) {
|
|
|
92
92
|
const isActive = useIsScreenActive();
|
|
93
93
|
|
|
94
94
|
const ref = React.useRef(null);
|
|
95
|
-
const
|
|
95
|
+
const isOrientationReady = useWaitForValidOrientation();
|
|
96
|
+
|
|
97
|
+
// Playable screens handle their own orientation via the native player plugin,
|
|
98
|
+
// so we skip the orientation wait gate to avoid a deadlock where the screen
|
|
99
|
+
// waits for landscape but blocks rendering that would trigger the rotation.
|
|
100
|
+
const isPlayableRoute = pathname?.includes("/playable");
|
|
101
|
+
const isReady = isOrientationReady || isPlayableRoute;
|
|
96
102
|
|
|
97
103
|
React.useEffect(() => {
|
|
98
104
|
if (ref.current && isActive && isReady) {
|
|
@@ -57,7 +57,6 @@ export function ScreenResolverComponent(props: Props) {
|
|
|
57
57
|
const { hookPlugin } = screenData || {};
|
|
58
58
|
|
|
59
59
|
const plugins = usePlugins();
|
|
60
|
-
const rivers = useRivers();
|
|
61
60
|
|
|
62
61
|
const components = useAppSelector(selectComponents);
|
|
63
62
|
|
|
@@ -65,12 +64,6 @@ export function ScreenResolverComponent(props: Props) {
|
|
|
65
64
|
videoModalState: { mode },
|
|
66
65
|
} = useNavigation();
|
|
67
66
|
|
|
68
|
-
const [, setScreenContext] = ZappPipesScreenContext.useZappPipesContext();
|
|
69
|
-
|
|
70
|
-
React.useEffect(() => {
|
|
71
|
-
setScreenContext(rivers[screenId]);
|
|
72
|
-
}, [rivers, screenId, setScreenContext]);
|
|
73
|
-
|
|
74
67
|
const parentCallback = props.resultCallback;
|
|
75
68
|
|
|
76
69
|
const screenAction = useCallbackActions(
|
|
@@ -150,6 +143,17 @@ export function ScreenResolverComponent(props: Props) {
|
|
|
150
143
|
return null;
|
|
151
144
|
}
|
|
152
145
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
146
|
+
function withDefaultScreenContext(Component: React.ComponentType<any>) {
|
|
147
|
+
return function WithDefaultScreenContext(props: any) {
|
|
148
|
+
const screenId = props.screenId;
|
|
149
|
+
const rivers = useRivers();
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<ZappPipesScreenContext.Provider initialContextValue={rivers[screenId]}>
|
|
153
|
+
<Component {...props} />
|
|
154
|
+
</ZappPipesScreenContext.Provider>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const ScreenResolver = withDefaultScreenContext(ScreenResolverComponent);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from "react";
|
|
2
2
|
import { equals } from "ramda";
|
|
3
3
|
import { Animated, ViewProps, ViewStyle } from "react-native";
|
|
4
|
-
import { useSafeAreaFrame } from "react-native-safe-area-context";
|
|
5
4
|
|
|
6
5
|
import { useScreenOrientationHandler } from "@applicaster/zapp-react-native-ui-components/Components/Screen/orientationHandler";
|
|
6
|
+
import { useMemoizedSafeAreaFrameWithActiveState } from "@applicaster/zapp-react-native-ui-components/Components/Screen/hooks";
|
|
7
7
|
|
|
8
8
|
import { PathnameContext } from "../../Contexts/PathnameContext";
|
|
9
9
|
import { ScreenDataContext } from "../../Contexts/ScreenDataContext";
|
|
@@ -94,19 +94,13 @@ function SceneComponent({
|
|
|
94
94
|
isActive,
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
oldFrame.width === frame.width && oldFrame.height === frame.height
|
|
105
|
-
? oldFrame
|
|
106
|
-
: frame
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
}, [isActive, frame.width, frame.height]);
|
|
97
|
+
// Use shared memoized frame hook - synchronized with useWaitForValidOrientation
|
|
98
|
+
// to prevent race conditions during orientation changes
|
|
99
|
+
// Pass isActive from props since Scene knows its active state from Transitioner
|
|
100
|
+
const memoFrame = useMemoizedSafeAreaFrameWithActiveState({
|
|
101
|
+
updateForInactiveScreens: false,
|
|
102
|
+
isActive,
|
|
103
|
+
});
|
|
110
104
|
|
|
111
105
|
const isAnimating = animating && overlayStyle;
|
|
112
106
|
|
|
@@ -88,11 +88,29 @@ const createStore = () =>
|
|
|
88
88
|
}))
|
|
89
89
|
);
|
|
90
90
|
|
|
91
|
+
const createScreenComponentsStore = () =>
|
|
92
|
+
create(subscribeWithSelector<Record<string, unknown>>((_) => ({})));
|
|
93
|
+
|
|
91
94
|
type ScreenContextType = {
|
|
92
95
|
_navBarStore: ReturnType<typeof createStore>;
|
|
93
96
|
_stateStore: ReturnType<typeof createStateStore>;
|
|
94
97
|
navBar: NavBarState;
|
|
95
98
|
legacyFormatScreenData: LegacyNavigationScreenData | null;
|
|
99
|
+
/**
|
|
100
|
+
* Zustand store for component-level state within a screen.
|
|
101
|
+
*
|
|
102
|
+
* **Purpose:** Persists state across component mount/unmount cycles (e.g., during virtualization)
|
|
103
|
+
* and enables state sharing between components using the same key within the same screen.
|
|
104
|
+
*
|
|
105
|
+
* **Lifecycle:** Tied to the screen/route — recreated on each route change.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* // Used by useComponentScreenState hook:
|
|
109
|
+
* const store = useScreenContextV2()._componentStateStore;
|
|
110
|
+
* store.setState({ 'my-key': value });
|
|
111
|
+
* const value = store.getState()['my-key'];
|
|
112
|
+
*/
|
|
113
|
+
_componentStateStore: ReturnType<typeof createScreenComponentsStore>;
|
|
96
114
|
};
|
|
97
115
|
|
|
98
116
|
export const ScreenContext = createContext<ScreenContextType>({
|
|
@@ -107,6 +125,7 @@ export const ScreenContext = createContext<ScreenContextType>({
|
|
|
107
125
|
setSummary: (_subtitle) => void 0,
|
|
108
126
|
},
|
|
109
127
|
legacyFormatScreenData: {} as LegacyNavigationScreenData,
|
|
128
|
+
_componentStateStore: createScreenComponentsStore(),
|
|
110
129
|
});
|
|
111
130
|
|
|
112
131
|
export function ScreenContextProvider({
|
|
@@ -160,6 +179,14 @@ export function ScreenContextProvider({
|
|
|
160
179
|
return navBarState;
|
|
161
180
|
}, []);
|
|
162
181
|
|
|
182
|
+
// Component state store - recreated when pathname changes (route change).
|
|
183
|
+
// Unlike _navBarStore and _stateStore (cached via refs), this store
|
|
184
|
+
// resets only when pathname changes to provide a clean state for the new route.
|
|
185
|
+
const componentStateStore = useMemo(
|
|
186
|
+
() => createScreenComponentsStore(),
|
|
187
|
+
[pathname]
|
|
188
|
+
);
|
|
189
|
+
|
|
163
190
|
const screenNavBarState = getScreenNavBarState()(
|
|
164
191
|
useShallow((state) => ({
|
|
165
192
|
visible: state.visible,
|
|
@@ -212,8 +239,9 @@ export function ScreenContextProvider({
|
|
|
212
239
|
_stateStore: getScreenState(),
|
|
213
240
|
navBar: navBarState,
|
|
214
241
|
legacyFormatScreenData: routeScreenData,
|
|
242
|
+
_componentStateStore: componentStateStore,
|
|
215
243
|
}),
|
|
216
|
-
[navBarState, screenData, routeScreenData]
|
|
244
|
+
[navBarState, screenData, routeScreenData, componentStateStore]
|
|
217
245
|
)}
|
|
218
246
|
>
|
|
219
247
|
{children}
|
|
@@ -5,10 +5,10 @@ import React, {
|
|
|
5
5
|
useMemo,
|
|
6
6
|
useState,
|
|
7
7
|
} from "react";
|
|
8
|
-
import * as R from "ramda";
|
|
9
8
|
|
|
10
|
-
type ProviderProps = {
|
|
9
|
+
type ProviderProps<S> = {
|
|
11
10
|
children: React.ReactChild;
|
|
11
|
+
initialContextValue?: S;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
type ContextType<T> = {
|
|
@@ -27,21 +27,29 @@ export function createZappPipesContext<T, S = T>(
|
|
|
27
27
|
) {
|
|
28
28
|
const Context = createContext<ContextType<S>>(initialContext);
|
|
29
29
|
|
|
30
|
-
const
|
|
30
|
+
const defaultSelector = (c: any) => c;
|
|
31
|
+
const defaultPrepareContext = (n: any) => n;
|
|
32
|
+
const joinArgs = (args: any[]) => args.join("-");
|
|
33
|
+
|
|
34
|
+
const { selector = defaultSelector, prepareContext = defaultPrepareContext } =
|
|
35
|
+
options || {};
|
|
31
36
|
|
|
32
37
|
function useZappPipesContext(...hookArgs: any[]): [T, (T) => void] {
|
|
33
38
|
const { context, setContext } = useContext(Context);
|
|
39
|
+
const joinedArgs = joinArgs(hookArgs);
|
|
34
40
|
|
|
35
41
|
const contextValue = useMemo(
|
|
36
42
|
() => selector(context, ...hookArgs),
|
|
37
|
-
|
|
43
|
+
// eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
|
|
44
|
+
[context, joinedArgs]
|
|
38
45
|
);
|
|
39
46
|
|
|
40
47
|
const contextSetter = useCallback(
|
|
41
48
|
(newContext: T) => {
|
|
42
49
|
setContext(prepareContext(newContext, context, ...hookArgs));
|
|
43
50
|
},
|
|
44
|
-
|
|
51
|
+
// eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
|
|
52
|
+
[context, joinedArgs]
|
|
45
53
|
);
|
|
46
54
|
|
|
47
55
|
return useMemo(
|
|
@@ -50,8 +58,11 @@ export function createZappPipesContext<T, S = T>(
|
|
|
50
58
|
);
|
|
51
59
|
}
|
|
52
60
|
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
/** Provider accepts `initialContextValue` prop to set the initial context value */
|
|
62
|
+
function Provider({ children, initialContextValue }: ProviderProps<S>) {
|
|
63
|
+
const [context, setContext] = useState<S>(
|
|
64
|
+
initialContextValue ?? initialContext.context
|
|
65
|
+
);
|
|
55
66
|
|
|
56
67
|
return (
|
|
57
68
|
<Context.Provider value={{ context, setContext }}>
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// ResolverSelector.tsx
|
|
2
1
|
import React from "react";
|
|
3
2
|
import { ComponentDataSourceContext, ZappPipesDataProps } from "./types";
|
|
4
3
|
import { StaticFeedResolver } from "./resolvers/StaticFeedResolver";
|
|
@@ -12,14 +11,33 @@ type ResolverSelectorProps = ComponentDataSourceContext & {
|
|
|
12
11
|
export function ResolverSelector(props: ResolverSelectorProps) {
|
|
13
12
|
const { getStaticComponentFeed, component, feedUrl, children } = props;
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
const renderDefaultResolver = (
|
|
15
|
+
fallbackChildren: (dataProps: ZappPipesDataProps) => React.ReactNode
|
|
16
|
+
) => {
|
|
17
|
+
if (feedUrl || component?.data?.source) {
|
|
18
|
+
return <UrlFeedResolver {...props}>{fallbackChildren}</UrlFeedResolver>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return <NullFeedResolver>{fallbackChildren}</NullFeedResolver>;
|
|
22
|
+
};
|
|
23
|
+
|
|
16
24
|
if (getStaticComponentFeed) {
|
|
17
|
-
return
|
|
18
|
-
|
|
25
|
+
return (
|
|
26
|
+
<StaticFeedResolver {...props}>
|
|
27
|
+
{(zappPipesDataProps) => {
|
|
28
|
+
const { data, loading, error } = zappPipesDataProps.zappPipesData;
|
|
29
|
+
|
|
30
|
+
const shouldFallback = !loading && data === null && !error;
|
|
31
|
+
|
|
32
|
+
if (shouldFallback) {
|
|
33
|
+
return renderDefaultResolver(children);
|
|
34
|
+
}
|
|
19
35
|
|
|
20
|
-
|
|
21
|
-
|
|
36
|
+
return children(zappPipesDataProps);
|
|
37
|
+
}}
|
|
38
|
+
</StaticFeedResolver>
|
|
39
|
+
);
|
|
22
40
|
}
|
|
23
41
|
|
|
24
|
-
return
|
|
42
|
+
return renderDefaultResolver(children);
|
|
25
43
|
}
|
|
@@ -7,15 +7,30 @@ import { UrlFeedResolver } from "../resolvers/UrlFeedResolver";
|
|
|
7
7
|
import { NullFeedResolver } from "../resolvers/NullFeedResolver";
|
|
8
8
|
|
|
9
9
|
jest.mock("../resolvers/StaticFeedResolver", () => ({
|
|
10
|
-
StaticFeedResolver: jest.fn(() =>
|
|
10
|
+
StaticFeedResolver: jest.fn(({ children }) =>
|
|
11
|
+
children({
|
|
12
|
+
zappPipesData: { loading: true, data: null, error: null },
|
|
13
|
+
reloadData: jest.fn(),
|
|
14
|
+
})
|
|
15
|
+
),
|
|
11
16
|
}));
|
|
12
17
|
|
|
13
18
|
jest.mock("../resolvers/UrlFeedResolver", () => ({
|
|
14
|
-
UrlFeedResolver: jest.fn(() =>
|
|
19
|
+
UrlFeedResolver: jest.fn(({ children }) =>
|
|
20
|
+
children({
|
|
21
|
+
zappPipesData: { loading: true, data: null, error: null },
|
|
22
|
+
reloadData: jest.fn(),
|
|
23
|
+
})
|
|
24
|
+
),
|
|
15
25
|
}));
|
|
16
26
|
|
|
17
27
|
jest.mock("../resolvers/NullFeedResolver", () => ({
|
|
18
|
-
NullFeedResolver: jest.fn(() =>
|
|
28
|
+
NullFeedResolver: jest.fn(({ children }) =>
|
|
29
|
+
children({
|
|
30
|
+
zappPipesData: { loading: false, data: null, error: null },
|
|
31
|
+
reloadData: jest.fn(),
|
|
32
|
+
})
|
|
33
|
+
),
|
|
19
34
|
}));
|
|
20
35
|
|
|
21
36
|
const testPlugin = {
|
|
@@ -52,7 +67,7 @@ describe("ResolverSelector", () => {
|
|
|
52
67
|
expect.objectContaining({
|
|
53
68
|
getStaticComponentFeed: props.getStaticComponentFeed,
|
|
54
69
|
component: mockComponent,
|
|
55
|
-
children:
|
|
70
|
+
children: expect.any(Function),
|
|
56
71
|
}),
|
|
57
72
|
expect.anything()
|
|
58
73
|
);
|
|
@@ -151,7 +166,10 @@ describe("ResolverSelector", () => {
|
|
|
151
166
|
render(<ResolverSelector {...props} />);
|
|
152
167
|
|
|
153
168
|
expect(StaticFeedResolver).toHaveBeenCalledWith(
|
|
154
|
-
expect.objectContaining(
|
|
169
|
+
expect.objectContaining({
|
|
170
|
+
...props,
|
|
171
|
+
children: expect.any(Function),
|
|
172
|
+
}),
|
|
155
173
|
expect.anything()
|
|
156
174
|
);
|
|
157
175
|
});
|
|
@@ -202,4 +220,193 @@ describe("ResolverSelector", () => {
|
|
|
202
220
|
expect(UrlFeedResolver).not.toHaveBeenCalled();
|
|
203
221
|
expect(NullFeedResolver).not.toHaveBeenCalled();
|
|
204
222
|
});
|
|
223
|
+
|
|
224
|
+
describe("Fallback Logic", () => {
|
|
225
|
+
it("should fallback to UrlFeedResolver when StaticFeedResolver returns null and feedUrl is present", () => {
|
|
226
|
+
const props = {
|
|
227
|
+
getStaticComponentFeed: jest.fn(),
|
|
228
|
+
feedUrl: "https://example.com/feed",
|
|
229
|
+
component: mockComponent,
|
|
230
|
+
children: mockChildren,
|
|
231
|
+
riverId: "test-river",
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
(StaticFeedResolver as jest.Mock).mockImplementation(({ children }) =>
|
|
235
|
+
children({
|
|
236
|
+
zappPipesData: { loading: false, data: null, error: null },
|
|
237
|
+
reloadData: jest.fn(),
|
|
238
|
+
})
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
render(<ResolverSelector {...props} />);
|
|
242
|
+
|
|
243
|
+
expect(StaticFeedResolver).toHaveBeenCalled();
|
|
244
|
+
|
|
245
|
+
expect(UrlFeedResolver).toHaveBeenCalledWith(
|
|
246
|
+
expect.objectContaining({
|
|
247
|
+
feedUrl: props.feedUrl,
|
|
248
|
+
component: mockComponent,
|
|
249
|
+
children: mockChildren,
|
|
250
|
+
}),
|
|
251
|
+
expect.anything()
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("should fallback to UrlFeedResolver when StaticFeedResolver returns null and component.data.source is present", () => {
|
|
256
|
+
const componentWithSource = {
|
|
257
|
+
...mockComponent,
|
|
258
|
+
data: {
|
|
259
|
+
source: "data-source",
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const props = {
|
|
264
|
+
getStaticComponentFeed: jest.fn(),
|
|
265
|
+
component: componentWithSource,
|
|
266
|
+
children: mockChildren,
|
|
267
|
+
riverId: "test-river",
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
(StaticFeedResolver as jest.Mock).mockImplementation(({ children }) =>
|
|
271
|
+
children({
|
|
272
|
+
zappPipesData: { loading: false, data: null, error: null },
|
|
273
|
+
reloadData: jest.fn(),
|
|
274
|
+
})
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
render(<ResolverSelector {...props} />);
|
|
278
|
+
|
|
279
|
+
expect(StaticFeedResolver).toHaveBeenCalled();
|
|
280
|
+
|
|
281
|
+
expect(UrlFeedResolver).toHaveBeenCalledWith(
|
|
282
|
+
expect.objectContaining({
|
|
283
|
+
component: componentWithSource,
|
|
284
|
+
children: mockChildren,
|
|
285
|
+
}),
|
|
286
|
+
expect.anything()
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("should fallback to NullFeedResolver when StaticFeedResolver returns null and no feedUrl/source is present", () => {
|
|
291
|
+
const props = {
|
|
292
|
+
getStaticComponentFeed: jest.fn(),
|
|
293
|
+
component: mockComponent,
|
|
294
|
+
children: mockChildren,
|
|
295
|
+
riverId: "test-river",
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
(StaticFeedResolver as jest.Mock).mockImplementation(({ children }) =>
|
|
299
|
+
children({
|
|
300
|
+
zappPipesData: { loading: false, data: null, error: null },
|
|
301
|
+
reloadData: jest.fn(),
|
|
302
|
+
})
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
render(<ResolverSelector {...props} />);
|
|
306
|
+
|
|
307
|
+
expect(StaticFeedResolver).toHaveBeenCalled();
|
|
308
|
+
expect(UrlFeedResolver).not.toHaveBeenCalled();
|
|
309
|
+
|
|
310
|
+
expect(NullFeedResolver).toHaveBeenCalledWith(
|
|
311
|
+
expect.objectContaining({
|
|
312
|
+
children: mockChildren,
|
|
313
|
+
}),
|
|
314
|
+
expect.anything()
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("should NOT fallback and call children with static data when StaticFeedResolver returns data", () => {
|
|
319
|
+
const staticData = { entry: [{ id: "1" }] };
|
|
320
|
+
|
|
321
|
+
const props = {
|
|
322
|
+
getStaticComponentFeed: jest.fn(),
|
|
323
|
+
feedUrl: "https://example.com/feed",
|
|
324
|
+
component: mockComponent,
|
|
325
|
+
children: mockChildren,
|
|
326
|
+
riverId: "test-river",
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
(StaticFeedResolver as jest.Mock).mockImplementation(({ children }) =>
|
|
330
|
+
children({
|
|
331
|
+
zappPipesData: { loading: false, data: staticData, error: null },
|
|
332
|
+
reloadData: jest.fn(),
|
|
333
|
+
})
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
render(<ResolverSelector {...props} />);
|
|
337
|
+
|
|
338
|
+
expect(StaticFeedResolver).toHaveBeenCalled();
|
|
339
|
+
expect(UrlFeedResolver).not.toHaveBeenCalled();
|
|
340
|
+
|
|
341
|
+
expect(mockChildren).toHaveBeenCalledWith(
|
|
342
|
+
expect.objectContaining({
|
|
343
|
+
zappPipesData: expect.objectContaining({
|
|
344
|
+
data: staticData,
|
|
345
|
+
}),
|
|
346
|
+
})
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("should NOT fallback and call children with loading state when StaticFeedResolver is loading", () => {
|
|
351
|
+
const props = {
|
|
352
|
+
getStaticComponentFeed: jest.fn(),
|
|
353
|
+
feedUrl: "https://example.com/feed",
|
|
354
|
+
component: mockComponent,
|
|
355
|
+
children: mockChildren,
|
|
356
|
+
riverId: "test-river",
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
(StaticFeedResolver as jest.Mock).mockImplementation(({ children }) =>
|
|
360
|
+
children({
|
|
361
|
+
zappPipesData: { loading: true, data: null, error: null },
|
|
362
|
+
reloadData: jest.fn(),
|
|
363
|
+
})
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
render(<ResolverSelector {...props} />);
|
|
367
|
+
|
|
368
|
+
expect(StaticFeedResolver).toHaveBeenCalled();
|
|
369
|
+
expect(UrlFeedResolver).not.toHaveBeenCalled();
|
|
370
|
+
|
|
371
|
+
expect(mockChildren).toHaveBeenCalledWith(
|
|
372
|
+
expect.objectContaining({
|
|
373
|
+
zappPipesData: expect.objectContaining({
|
|
374
|
+
loading: true,
|
|
375
|
+
}),
|
|
376
|
+
})
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("should NOT fallback and call children with error when StaticFeedResolver returns error", () => {
|
|
381
|
+
const error = new Error("Static error");
|
|
382
|
+
|
|
383
|
+
const props = {
|
|
384
|
+
getStaticComponentFeed: jest.fn(),
|
|
385
|
+
feedUrl: "https://example.com/feed",
|
|
386
|
+
component: mockComponent,
|
|
387
|
+
children: mockChildren,
|
|
388
|
+
riverId: "test-river",
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
(StaticFeedResolver as jest.Mock).mockImplementation(({ children }) =>
|
|
392
|
+
children({
|
|
393
|
+
zappPipesData: { loading: false, data: null, error },
|
|
394
|
+
reloadData: jest.fn(),
|
|
395
|
+
})
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
render(<ResolverSelector {...props} />);
|
|
399
|
+
|
|
400
|
+
expect(StaticFeedResolver).toHaveBeenCalled();
|
|
401
|
+
expect(UrlFeedResolver).not.toHaveBeenCalled();
|
|
402
|
+
|
|
403
|
+
expect(mockChildren).toHaveBeenCalledWith(
|
|
404
|
+
expect.objectContaining({
|
|
405
|
+
zappPipesData: expect.objectContaining({
|
|
406
|
+
error,
|
|
407
|
+
}),
|
|
408
|
+
})
|
|
409
|
+
);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
205
412
|
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import * as useFeedLoaderModule from "@applicaster/zapp-react-native-utils/reactHooks/feed/useFeedLoader";
|
|
3
|
-
import * as useFeedRefreshModule from "@applicaster/zapp-react-native-utils/reactHooks/feed/useFeedRefresh";
|
|
4
3
|
import { favoritesListener } from "@applicaster/zapp-react-native-bridge/Favorites";
|
|
5
4
|
import { UrlFeedResolver } from "../resolvers/UrlFeedResolver";
|
|
6
5
|
import { renderWithProviders } from "@applicaster/zapp-react-native-utils/testUtils";
|
|
@@ -14,10 +13,6 @@ jest
|
|
|
14
13
|
|
|
15
14
|
jest.mock("@applicaster/zapp-react-native-utils/reactHooks/feed/useFeedLoader");
|
|
16
15
|
|
|
17
|
-
jest.mock(
|
|
18
|
-
"@applicaster/zapp-react-native-utils/reactHooks/feed/useFeedRefresh"
|
|
19
|
-
);
|
|
20
|
-
|
|
21
16
|
jest.mock("@applicaster/zapp-pipes-v2-client");
|
|
22
17
|
|
|
23
18
|
jest.mock("@applicaster/zapp-react-native-bridge/Favorites", () => ({
|
|
@@ -62,6 +57,9 @@ describe("UrlFeedResolver", () => {
|
|
|
62
57
|
component: { ...componentRequiredKeys } as any,
|
|
63
58
|
children: mockChildren,
|
|
64
59
|
riverId: "test-river",
|
|
60
|
+
screenContext: {
|
|
61
|
+
id: "screenId",
|
|
62
|
+
},
|
|
65
63
|
};
|
|
66
64
|
|
|
67
65
|
renderWithProviders(<UrlFeedResolver {...props} />);
|
|
@@ -82,6 +80,9 @@ describe("UrlFeedResolver", () => {
|
|
|
82
80
|
} as any,
|
|
83
81
|
children: mockChildren,
|
|
84
82
|
riverId: "test-river",
|
|
83
|
+
screenContext: {
|
|
84
|
+
id: "screenId",
|
|
85
|
+
},
|
|
85
86
|
};
|
|
86
87
|
|
|
87
88
|
renderWithProviders(<UrlFeedResolver {...props} />);
|
|
@@ -103,6 +104,9 @@ describe("UrlFeedResolver", () => {
|
|
|
103
104
|
} as any,
|
|
104
105
|
children: mockChildren,
|
|
105
106
|
riverId: "test-river",
|
|
107
|
+
screenContext: {
|
|
108
|
+
id: "screenId",
|
|
109
|
+
},
|
|
106
110
|
};
|
|
107
111
|
|
|
108
112
|
renderWithProviders(<UrlFeedResolver {...props} />);
|
|
@@ -132,6 +136,9 @@ describe("UrlFeedResolver", () => {
|
|
|
132
136
|
} as any,
|
|
133
137
|
children: mockChildren,
|
|
134
138
|
riverId: "test-river",
|
|
139
|
+
screenContext: {
|
|
140
|
+
id: "screenId",
|
|
141
|
+
},
|
|
135
142
|
};
|
|
136
143
|
|
|
137
144
|
renderWithProviders(<UrlFeedResolver {...props} />);
|
|
@@ -155,6 +162,9 @@ describe("UrlFeedResolver", () => {
|
|
|
155
162
|
children: mockChildren,
|
|
156
163
|
riverId: "test-river",
|
|
157
164
|
plugins: [],
|
|
165
|
+
screenContext: {
|
|
166
|
+
id: "screenId",
|
|
167
|
+
},
|
|
158
168
|
};
|
|
159
169
|
|
|
160
170
|
renderWithProviders(<UrlFeedResolver {...props} />);
|
|
@@ -173,6 +183,9 @@ describe("UrlFeedResolver", () => {
|
|
|
173
183
|
component: { ...componentRequiredKeys } as any,
|
|
174
184
|
children: mockChildren,
|
|
175
185
|
riverId: "test-river",
|
|
186
|
+
screenContext: {
|
|
187
|
+
id: "screenId",
|
|
188
|
+
},
|
|
176
189
|
};
|
|
177
190
|
|
|
178
191
|
renderWithProviders(<UrlFeedResolver {...props} />);
|
|
@@ -197,6 +210,9 @@ describe("UrlFeedResolver", () => {
|
|
|
197
210
|
},
|
|
198
211
|
},
|
|
199
212
|
] as any,
|
|
213
|
+
screenContext: {
|
|
214
|
+
id: "screenId",
|
|
215
|
+
},
|
|
200
216
|
};
|
|
201
217
|
|
|
202
218
|
renderWithProviders(<UrlFeedResolver {...props} />);
|
|
@@ -215,6 +231,9 @@ describe("UrlFeedResolver", () => {
|
|
|
215
231
|
} as any,
|
|
216
232
|
children: mockChildren,
|
|
217
233
|
riverId: "test-river",
|
|
234
|
+
screenContext: {
|
|
235
|
+
id: "screenId",
|
|
236
|
+
},
|
|
218
237
|
};
|
|
219
238
|
|
|
220
239
|
(useFeedLoaderModule.useFeedLoader as jest.Mock).mockReturnValue({
|
|
@@ -250,6 +269,9 @@ describe("UrlFeedResolver", () => {
|
|
|
250
269
|
children: mockChildren,
|
|
251
270
|
riverId: "test-river",
|
|
252
271
|
isLast: true,
|
|
272
|
+
screenContext: {
|
|
273
|
+
id: "screenId",
|
|
274
|
+
},
|
|
253
275
|
};
|
|
254
276
|
|
|
255
277
|
renderWithProviders(<UrlFeedResolver {...props1} />);
|
|
@@ -272,6 +294,9 @@ describe("UrlFeedResolver", () => {
|
|
|
272
294
|
children: mockChildren,
|
|
273
295
|
riverId: "test-river",
|
|
274
296
|
isLast: false,
|
|
297
|
+
screenContext: {
|
|
298
|
+
id: "screenId",
|
|
299
|
+
},
|
|
275
300
|
};
|
|
276
301
|
|
|
277
302
|
renderWithProviders(<UrlFeedResolver {...props2} />);
|
|
@@ -294,6 +319,9 @@ describe("UrlFeedResolver", () => {
|
|
|
294
319
|
children: mockChildren,
|
|
295
320
|
riverId: "test-river",
|
|
296
321
|
isLast: false,
|
|
322
|
+
screenContext: {
|
|
323
|
+
id: "screenId",
|
|
324
|
+
},
|
|
297
325
|
};
|
|
298
326
|
|
|
299
327
|
renderWithProviders(<UrlFeedResolver {...props3} />);
|
|
@@ -311,6 +339,9 @@ describe("UrlFeedResolver", () => {
|
|
|
311
339
|
component: { ...componentRequiredKeys } as any,
|
|
312
340
|
children: mockChildren,
|
|
313
341
|
riverId: "test-river",
|
|
342
|
+
screenContext: {
|
|
343
|
+
id: "screenId",
|
|
344
|
+
},
|
|
314
345
|
};
|
|
315
346
|
|
|
316
347
|
renderWithProviders(<UrlFeedResolver {...props} />);
|
|
@@ -327,22 +358,6 @@ describe("UrlFeedResolver", () => {
|
|
|
327
358
|
});
|
|
328
359
|
});
|
|
329
360
|
|
|
330
|
-
it("should apply feed refresh hook", () => {
|
|
331
|
-
const props = {
|
|
332
|
-
feedUrl: "https://example.com/feed",
|
|
333
|
-
component: { ...componentRequiredKeys } as any,
|
|
334
|
-
children: mockChildren,
|
|
335
|
-
riverId: "test-river",
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
renderWithProviders(<UrlFeedResolver {...props} />);
|
|
339
|
-
|
|
340
|
-
expect(useFeedRefreshModule.useFeedRefresh).toHaveBeenCalledWith({
|
|
341
|
-
reloadData: mockReloadData,
|
|
342
|
-
component: props.component,
|
|
343
|
-
});
|
|
344
|
-
});
|
|
345
|
-
|
|
346
361
|
it("should clean up listeners on unmount", () => {
|
|
347
362
|
const props = {
|
|
348
363
|
component: {
|
|
@@ -354,6 +369,9 @@ describe("UrlFeedResolver", () => {
|
|
|
354
369
|
} as any,
|
|
355
370
|
children: mockChildren,
|
|
356
371
|
riverId: "test-river",
|
|
372
|
+
screenContext: {
|
|
373
|
+
id: "screenId",
|
|
374
|
+
},
|
|
357
375
|
};
|
|
358
376
|
|
|
359
377
|
const { unmount } = renderWithProviders(<UrlFeedResolver {...props} />);
|
|
@@ -9,9 +9,9 @@ import {
|
|
|
9
9
|
getInflatedDataSourceUrl,
|
|
10
10
|
getSearchContext,
|
|
11
11
|
useFeedLoader,
|
|
12
|
-
useFeedRefresh,
|
|
13
12
|
useRoute,
|
|
14
13
|
} from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
14
|
+
import { refreshCoordinator } from "@applicaster/zapp-react-native-utils/refreshUtils/RefreshCoordinator";
|
|
15
15
|
|
|
16
16
|
import { ComponentDataSourceContext, ZappPipesDataProps } from "../types";
|
|
17
17
|
import { useScreenStateStore } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useScreenStateStore";
|
|
@@ -199,6 +199,8 @@ export function UrlFeedResolver({
|
|
|
199
199
|
const { pathname } = useRoute();
|
|
200
200
|
const screenStateStore = useScreenStateStore();
|
|
201
201
|
|
|
202
|
+
const screenId = (screenContext?.id || "unknown_screen_id") as string;
|
|
203
|
+
|
|
202
204
|
// Setup listeners for data source URL
|
|
203
205
|
useEffect(() => {
|
|
204
206
|
if (!reloadData) {
|
|
@@ -214,10 +216,19 @@ export function UrlFeedResolver({
|
|
|
214
216
|
url: dataSourceUrl,
|
|
215
217
|
pathname,
|
|
216
218
|
screenStateStore,
|
|
219
|
+
component,
|
|
220
|
+
screenId,
|
|
217
221
|
callback: reloadData,
|
|
218
222
|
});
|
|
219
223
|
}
|
|
220
|
-
}, [
|
|
224
|
+
}, [
|
|
225
|
+
dataSourceUrl,
|
|
226
|
+
reloadData,
|
|
227
|
+
pathname,
|
|
228
|
+
screenStateStore,
|
|
229
|
+
component,
|
|
230
|
+
screenId,
|
|
231
|
+
]);
|
|
221
232
|
|
|
222
233
|
// Setup favorites listener
|
|
223
234
|
useEffect(() => {
|
|
@@ -230,11 +241,11 @@ export function UrlFeedResolver({
|
|
|
230
241
|
}
|
|
231
242
|
}, [type, reloadData]);
|
|
232
243
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
});
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
const unregister = refreshCoordinator.register(component, screenId);
|
|
246
|
+
|
|
247
|
+
return () => unregister();
|
|
248
|
+
}, [component, screenId]);
|
|
238
249
|
|
|
239
250
|
const loadNextData = useMemo(
|
|
240
251
|
() => (!isLast && isVerticalListOrGrid(component) ? undefined : loadNext),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-ui-components",
|
|
3
|
-
"version": "15.0.0-alpha.
|
|
3
|
+
"version": "15.0.0-alpha.9102777840",
|
|
4
4
|
"description": "Applicaster Zapp React Native ui components for the Quick Brick App",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
},
|
|
29
29
|
"homepage": "https://github.com/applicaster/quickbrick#readme",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@applicaster/applicaster-types": "15.0.0-alpha.
|
|
32
|
-
"@applicaster/zapp-react-native-bridge": "15.0.0-alpha.
|
|
33
|
-
"@applicaster/zapp-react-native-redux": "15.0.0-alpha.
|
|
34
|
-
"@applicaster/zapp-react-native-utils": "15.0.0-alpha.
|
|
31
|
+
"@applicaster/applicaster-types": "15.0.0-alpha.9102777840",
|
|
32
|
+
"@applicaster/zapp-react-native-bridge": "15.0.0-alpha.9102777840",
|
|
33
|
+
"@applicaster/zapp-react-native-redux": "15.0.0-alpha.9102777840",
|
|
34
|
+
"@applicaster/zapp-react-native-utils": "15.0.0-alpha.9102777840",
|
|
35
35
|
"fast-json-stable-stringify": "^2.1.0",
|
|
36
36
|
"promise": "^8.3.0",
|
|
37
37
|
"url": "^0.11.0",
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import * as R from "ramda";
|
|
3
|
-
import { Text, TextStyle, View, ViewStyle } from "react-native";
|
|
4
|
-
|
|
5
|
-
import { getLocalizations } from "@applicaster/zapp-react-native-utils/localizationUtils";
|
|
6
|
-
import { getAppStylesColor } from "@applicaster/zapp-react-native-utils/stylesUtils";
|
|
7
|
-
import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
|
|
8
|
-
import { styleKeys } from "@applicaster/zapp-react-native-utils/styleKeysUtils";
|
|
9
|
-
|
|
10
|
-
type Props = {
|
|
11
|
-
styles: {};
|
|
12
|
-
error: {};
|
|
13
|
-
remoteConfigurations: { localizations: {} };
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const defaultAppStyles = {
|
|
17
|
-
loading_error_label: {
|
|
18
|
-
color: "#aaa",
|
|
19
|
-
},
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const textStyles = (appStyles = defaultAppStyles): TextStyle => ({
|
|
23
|
-
color: getAppStylesColor("loading_error_label", appStyles),
|
|
24
|
-
fontSize: 36,
|
|
25
|
-
textAlign: "center",
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const errorStyles = ({ backgroundColor }): ViewStyle => ({
|
|
29
|
-
flex: 1,
|
|
30
|
-
width: "100%",
|
|
31
|
-
height: "100%",
|
|
32
|
-
justifyContent: "center",
|
|
33
|
-
alignItems: "center",
|
|
34
|
-
position: "absolute",
|
|
35
|
-
zIndex: 100,
|
|
36
|
-
backgroundColor,
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
export function ErrorDisplayComponent({
|
|
40
|
-
styles,
|
|
41
|
-
remoteConfigurations: { localizations },
|
|
42
|
-
}: Props) {
|
|
43
|
-
const theme = useTheme();
|
|
44
|
-
const backgroundColor = theme?.app_background_color;
|
|
45
|
-
|
|
46
|
-
const { stream_error_message = "Cannot play stream" } = getLocalizations({
|
|
47
|
-
localizations,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const appStyles = R.prop(styleKeys.style_namespace, styles);
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<View style={errorStyles({ backgroundColor })}>
|
|
54
|
-
<Text style={textStyles(appStyles)}>{stream_error_message}</Text>
|
|
55
|
-
</View>
|
|
56
|
-
);
|
|
57
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import * as R from "ramda";
|
|
2
|
-
|
|
3
|
-
import { connectToStore } from "@applicaster/zapp-react-native-redux/utils/connectToStore";
|
|
4
|
-
|
|
5
|
-
import { ErrorDisplayComponent } from "./ErrorDisplay";
|
|
6
|
-
|
|
7
|
-
export const ErrorDisplay = R.compose(
|
|
8
|
-
connectToStore(R.pick(["remoteConfigurations"]))
|
|
9
|
-
)(ErrorDisplayComponent);
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { Player } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/player";
|
|
2
|
-
import NetInfo from "@react-native-community/netinfo";
|
|
3
|
-
|
|
4
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
-
import { showAlertDialog } from "@applicaster/zapp-react-native-utils/alertUtils";
|
|
6
|
-
import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
|
|
7
|
-
import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
|
|
8
|
-
import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils/playerManager";
|
|
9
|
-
import { usePlugins } from "@applicaster/zapp-react-native-redux/hooks";
|
|
10
|
-
import { getLocalizations } from "@applicaster/zapp-react-native-utils/localizationUtils";
|
|
11
|
-
import { log_info } from "./logger";
|
|
12
|
-
import { isTrue } from "@applicaster/zapp-react-native-utils/booleanUtils";
|
|
13
|
-
|
|
14
|
-
type RestrictMobilePlaybackProps = {
|
|
15
|
-
player?: Player;
|
|
16
|
-
entry?: ZappEntry;
|
|
17
|
-
close: () => void;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export const useRestrictMobilePlayback = ({
|
|
21
|
-
player,
|
|
22
|
-
entry,
|
|
23
|
-
close,
|
|
24
|
-
}: RestrictMobilePlaybackProps): { isRestricted: boolean } => {
|
|
25
|
-
const dialogVisibleRef = useRef<boolean>(false);
|
|
26
|
-
const theme = useTheme();
|
|
27
|
-
const plugins = usePlugins();
|
|
28
|
-
|
|
29
|
-
const restrictMobilePlugin = useMemo(
|
|
30
|
-
() =>
|
|
31
|
-
plugins.find((p) => p.identifier === "quick-brick-hook-restrict-mobile"),
|
|
32
|
-
[plugins]
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
const localizations = useMemo(
|
|
36
|
-
() => restrictMobilePlugin?.configuration?.localizations,
|
|
37
|
-
[restrictMobilePlugin]
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
const localize = useCallback(
|
|
41
|
-
(key: string) => {
|
|
42
|
-
const l = localizations && getLocalizations({ localizations });
|
|
43
|
-
|
|
44
|
-
return (l && l[key]) || "";
|
|
45
|
-
},
|
|
46
|
-
[localizations]
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
return () => {
|
|
51
|
-
if (isTV()) {
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
dialogVisibleRef.current = false;
|
|
56
|
-
};
|
|
57
|
-
}, []);
|
|
58
|
-
|
|
59
|
-
const isConnectionRestricted = useMemo(() => {
|
|
60
|
-
if (isTV()) {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Only restrict if the plugin exists
|
|
65
|
-
if (!restrictMobilePlugin) {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return player && isTrue(entry?.extensions?.connection_restricted);
|
|
70
|
-
}, [player, entry, restrictMobilePlugin]);
|
|
71
|
-
|
|
72
|
-
const [isRestricted, setIsRestricted] = useState<boolean>(
|
|
73
|
-
isConnectionRestricted
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
useEffect(() => {
|
|
77
|
-
if (!isConnectionRestricted) {
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const stopPlayer = () => {
|
|
82
|
-
log_info(
|
|
83
|
-
"Stopping player due to mobile restriction, connection_restricted: true"
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
player?.closeNativePlayer();
|
|
87
|
-
playerManager?.invokeHandler("close");
|
|
88
|
-
|
|
89
|
-
dialogVisibleRef.current = true;
|
|
90
|
-
|
|
91
|
-
showAlertDialog({
|
|
92
|
-
title:
|
|
93
|
-
localize("restrict_mobile_playback_error_title") ||
|
|
94
|
-
"Restricted Connection Type",
|
|
95
|
-
message:
|
|
96
|
-
localize("restrict_mobile_playback_error_message") ||
|
|
97
|
-
"This content can only be viewed over a Wi-Fi or LAN network.",
|
|
98
|
-
okButtonText: theme.ok_button || "OK",
|
|
99
|
-
completion: () => {
|
|
100
|
-
dialogVisibleRef.current = false;
|
|
101
|
-
|
|
102
|
-
close();
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
return NetInfo.addEventListener((state) => {
|
|
108
|
-
if (state.type === "cellular") {
|
|
109
|
-
setIsRestricted(true);
|
|
110
|
-
|
|
111
|
-
if (dialogVisibleRef.current) {
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
stopPlayer();
|
|
116
|
-
} else {
|
|
117
|
-
setIsRestricted(false);
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
}, [
|
|
121
|
-
close,
|
|
122
|
-
entry?.extensions?.connection_restricted,
|
|
123
|
-
localizations,
|
|
124
|
-
localize,
|
|
125
|
-
player,
|
|
126
|
-
theme.ok_button,
|
|
127
|
-
isConnectionRestricted,
|
|
128
|
-
]);
|
|
129
|
-
|
|
130
|
-
return { isRestricted };
|
|
131
|
-
};
|