@applicaster/zapp-react-native-utils 16.0.0-rc.4 → 16.0.0-rc.40
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/actionsExecutor/ActionExecutor.ts +8 -10
- package/actionsExecutor/ActionExecutorContext.tsx +43 -326
- package/actionsExecutor/actions/appRestart.ts +24 -0
- package/actionsExecutor/actions/confirmDialog.ts +53 -0
- package/actionsExecutor/actions/index.ts +30 -0
- package/actionsExecutor/actions/localStorageRemove.ts +27 -0
- package/actionsExecutor/actions/localStorageSet.ts +28 -0
- package/actionsExecutor/actions/localStorageToggleFlag.ts +28 -0
- package/actionsExecutor/actions/navigateToScreen.ts +123 -0
- package/actionsExecutor/actions/refreshComponent.ts +79 -0
- package/actionsExecutor/actions/screenSetVariable.ts +35 -0
- package/actionsExecutor/actions/screenToggleFlag.ts +38 -0
- package/actionsExecutor/actions/sendCloudEvent.ts +88 -0
- package/actionsExecutor/actions/sessionStorageRemove.ts +19 -0
- package/actionsExecutor/actions/sessionStorageSet.ts +20 -0
- package/actionsExecutor/actions/sessionStorageToggleFlag.ts +15 -0
- package/actionsExecutor/actions/showToast.ts +31 -0
- package/actionsExecutor/actions/switchLayout.ts +40 -0
- package/actionsExecutor/types.ts +59 -0
- package/analyticsUtils/AnalyticsEvents/sendMenuClickEvent.ts +2 -2
- package/analyticsUtils/PlayerAnalyticsManager.ts +11 -1
- package/analyticsUtils/__tests__/analyticsMapper.test.ts +35 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testACP_events.json +1 -1
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testBlockUnlistedParams_rules.json +1 -1
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_events.json +4 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_rules.json +6 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEventRegex_events.json +3 -9
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_events.json +18 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_rules.json +16 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_events.json +4 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_rules.json +6 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_events.json +4 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_rules.json +6 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_events.json +4 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_rules.json +6 -0
- package/analyticsUtils/__tests__/fixtures/index.js +20 -0
- package/analyticsUtils/analyticsMapper.ts +4 -1
- package/analyticsUtils/playerAnalyticsTracker.ts +26 -3
- package/appUtils/HooksManager/index.ts +3 -8
- package/appUtils/contextKeysManager/__tests__/getKey/failure.test.ts +1 -1
- package/appUtils/contextKeysManager/__tests__/removeKey/failure.test.ts +1 -1
- package/appUtils/contextKeysManager/__tests__/setKey/failure/invalidKey.test.ts +4 -4
- package/appUtils/contextKeysManager/index.ts +1 -1
- package/appUtils/contextKeysManager/utils/index.ts +38 -25
- package/appUtils/playerManager/__tests__/playerFactory.test.ts +150 -0
- package/appUtils/playerManager/playerFactory.ts +17 -34
- package/cellUtils/index.ts +3 -5
- package/colorUtils/__tests__/isTransparentColor.test.ts +76 -0
- package/colorUtils/__tests__/isValidColor.test.ts +70 -0
- package/colorUtils/index.ts +50 -0
- package/manifestUtils/_internals/index.js +6 -0
- package/manifestUtils/defaultManifestConfigurations/generalContent.js +35 -0
- package/manifestUtils/defaultManifestConfigurations/player.js +15 -15
- package/manifestUtils/fieldUtils/__tests__/fieldUtils.test.js +84 -0
- package/manifestUtils/fieldUtils/index.js +125 -0
- package/manifestUtils/keys.js +9 -0
- package/manifestUtils/mobileAction/button/index.js +16 -0
- package/manifestUtils/mobileAction/container/index.js +3 -1
- package/manifestUtils/mobileAction/groups/defaults.js +3 -1
- package/manifestUtils/platformIsTV.js +1 -0
- package/package.json +2 -2
- package/pipesUtils/__tests__/buildUrlWithQuery.test.ts +33 -0
- package/pipesUtils/__tests__/withPipesEndpoint.test.tsx +56 -0
- package/pipesUtils/index.ts +1 -0
- package/pipesUtils/withPipesEndpoint.tsx +54 -0
- package/playerUtils/index.ts +24 -2
- package/reactHooks/actions/index.ts +51 -1
- package/reactHooks/cell-click/index.ts +18 -10
- package/reactHooks/feed/__mocks__/useMarkPipesDataStale.ts +4 -0
- package/reactHooks/feed/index.ts +5 -1
- package/reactHooks/feed/useBatchLoading.ts +9 -1
- package/reactHooks/feed/{usePipesCacheReset.ts → useMarkPipesDataStale.ts} +12 -4
- package/reactHooks/navigation/__mocks__/index.ts +4 -0
- package/reactHooks/navigation/__tests__/useIsScreenActive.test.ts +42 -0
- package/reactHooks/navigation/index.ts +1 -1
- package/reactHooks/navigation/useIsScreenActive.ts +29 -8
- package/reactHooks/screen/__tests__/useIsStandaloneFullscreen.test.ts +114 -0
- package/reactHooks/screen/index.ts +2 -0
- package/reactHooks/screen/useIsStandaloneFullscreen.ts +12 -0
- package/reactHooks/state/useComponentScreenState.ts +1 -1
- package/reactHooks/utils/index.ts +3 -2
- package/reactHooks/videoModal/hooks/useVideoModalScreenData.tsx +22 -4
- package/riverComponetsMeasurementProvider/index.tsx +12 -8
- package/uiActionsRegistrator/index.ts +203 -0
- package/reactHooks/feed/__mocks__/usePipesCacheReset.ts +0 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render } from "@testing-library/react-native";
|
|
3
|
+
import { Text } from "react-native";
|
|
4
|
+
|
|
5
|
+
const mockedUseBuildPipesUrl = jest.fn();
|
|
6
|
+
|
|
7
|
+
jest.mock("@applicaster/zapp-react-native-utils/reactHooks/feed", () => ({
|
|
8
|
+
useBuildPipesUrl: (args) => mockedUseBuildPipesUrl(args),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const { withPipesEndpoint } = require("../withPipesEndpoint");
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line react/display-name
|
|
14
|
+
const Wrapped = React.forwardRef((props: any, _ref) => (
|
|
15
|
+
<Text testID="wrapped">{`${props.uri}|${JSON.stringify(props.headers)}`}</Text>
|
|
16
|
+
));
|
|
17
|
+
|
|
18
|
+
describe("withPipesEndpoint", () => {
|
|
19
|
+
afterEach(() => jest.clearAllMocks());
|
|
20
|
+
|
|
21
|
+
it("renders nothing while the endpoint context is resolving", () => {
|
|
22
|
+
mockedUseBuildPipesUrl.mockReturnValue({ requestParams: null });
|
|
23
|
+
|
|
24
|
+
const Decorated = withPipesEndpoint(Wrapped);
|
|
25
|
+
const { queryByTestId } = render(<Decorated uri="https://foo.com" />);
|
|
26
|
+
|
|
27
|
+
expect(queryByTestId("wrapped")).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("passes the uri through unchanged when there are no params", () => {
|
|
31
|
+
mockedUseBuildPipesUrl.mockReturnValue({ requestParams: {} });
|
|
32
|
+
|
|
33
|
+
const Decorated = withPipesEndpoint(Wrapped);
|
|
34
|
+
const { getByTestId } = render(<Decorated uri="https://foo.com/path" />);
|
|
35
|
+
|
|
36
|
+
expect(getByTestId("wrapped").props.children).toBe(
|
|
37
|
+
"https://foo.com/path|{}"
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("merges resolved params into the uri and forwards headers", () => {
|
|
42
|
+
mockedUseBuildPipesUrl.mockReturnValue({
|
|
43
|
+
requestParams: {
|
|
44
|
+
params: { token: "abc" },
|
|
45
|
+
headers: { Authorization: "Bearer xyz" },
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const Decorated = withPipesEndpoint(Wrapped);
|
|
50
|
+
const { getByTestId } = render(<Decorated uri="https://foo.com/path" />);
|
|
51
|
+
|
|
52
|
+
expect(getByTestId("wrapped").props.children).toBe(
|
|
53
|
+
'https://foo.com/path?token=abc|{"Authorization":"Bearer xyz"}'
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { withPipesEndpoint, buildUrlWithQuery } from "./withPipesEndpoint";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React, { forwardRef, RefObject } from "react";
|
|
2
|
+
import URL from "url";
|
|
3
|
+
|
|
4
|
+
import { useBuildPipesUrl } from "@applicaster/zapp-react-native-utils/reactHooks/feed";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Merges the query params resolved from a pipes endpoint context into the url.
|
|
8
|
+
* Returns the url unchanged when there is nothing to merge.
|
|
9
|
+
*/
|
|
10
|
+
export function buildUrlWithQuery(url: string, requestParams): string {
|
|
11
|
+
if (!url || !requestParams?.params) {
|
|
12
|
+
return url;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const parsedURL = URL.parse(url, true);
|
|
16
|
+
|
|
17
|
+
parsedURL.query = { ...parsedURL.query, ...requestParams.params };
|
|
18
|
+
parsedURL.search = null;
|
|
19
|
+
|
|
20
|
+
return URL.format(parsedURL);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Props = {
|
|
24
|
+
uri: string;
|
|
25
|
+
} & Record<string, unknown>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* HOC that resolves the wrapped component's `uri` against its matching Zapp
|
|
29
|
+
* Pipes endpoint. Endpoint context keys are added as query params (merged into
|
|
30
|
+
* the uri) and as request `headers`. Rendering is deferred until the context
|
|
31
|
+
* has been resolved so the component never loads without its required headers.
|
|
32
|
+
*/
|
|
33
|
+
export function withPipesEndpoint(Component) {
|
|
34
|
+
function WithPipesEndpoint(props: Props, ref: RefObject<unknown>) {
|
|
35
|
+
const { requestParams } = useBuildPipesUrl({ url: props.uri });
|
|
36
|
+
|
|
37
|
+
if (requestParams !== null) {
|
|
38
|
+
const urlWithQuery = buildUrlWithQuery(props.uri, requestParams);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Component
|
|
42
|
+
ref={ref}
|
|
43
|
+
{...props}
|
|
44
|
+
uri={urlWithQuery}
|
|
45
|
+
headers={requestParams.headers || {}}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return forwardRef(WithPipesEndpoint);
|
|
54
|
+
}
|
package/playerUtils/index.ts
CHANGED
|
@@ -9,6 +9,16 @@ import { Dimensions } from "react-native";
|
|
|
9
9
|
|
|
10
10
|
export { getPlayerActionButtons } from "./getPlayerActionButtons";
|
|
11
11
|
|
|
12
|
+
export const HLS_MIME_TYPES = [
|
|
13
|
+
"application/x-mpegURL",
|
|
14
|
+
"application/vnd.apple.mpegurl",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export const STREAMING_VIDEO_MIME_TYPES = [
|
|
18
|
+
...HLS_MIME_TYPES,
|
|
19
|
+
"application/dash+xml",
|
|
20
|
+
];
|
|
21
|
+
|
|
12
22
|
/**
|
|
13
23
|
* Gets duration value from player manager, and from extensions
|
|
14
24
|
* then checks whether the value from either is a not a valid number
|
|
@@ -27,7 +37,7 @@ export function isLiveLegacy(content) {
|
|
|
27
37
|
]);
|
|
28
38
|
|
|
29
39
|
const durationFromExt = R.path(["extensions", "duration"], content);
|
|
30
|
-
const durationFromMgr = playerManager.getDuration();
|
|
40
|
+
const durationFromMgr = playerManager.getInstanceController()?.getDuration();
|
|
31
41
|
const duration = Math.floor(durationFromExt || durationFromMgr);
|
|
32
42
|
|
|
33
43
|
const isLive = R.anyPass([isNotaValidNumber, R.lte(R.__, 0)])(duration);
|
|
@@ -71,7 +81,9 @@ function isLiveByManager(): boolean {
|
|
|
71
81
|
return true;
|
|
72
82
|
}
|
|
73
83
|
|
|
74
|
-
const durationFromPlayerManager = playerManager
|
|
84
|
+
const durationFromPlayerManager = playerManager
|
|
85
|
+
.getInstanceController()
|
|
86
|
+
?.getDuration();
|
|
75
87
|
|
|
76
88
|
return isLiveByDuration(durationFromPlayerManager);
|
|
77
89
|
}
|
|
@@ -80,6 +92,16 @@ export function isLive(entry: ZappEntry): boolean {
|
|
|
80
92
|
return isEntryLive(entry) || isLiveByManager();
|
|
81
93
|
}
|
|
82
94
|
|
|
95
|
+
export const isVideoItem = (item: Option<ZappEntry>) => {
|
|
96
|
+
const contentType = item?.content?.type;
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
isString(contentType) &&
|
|
100
|
+
(contentType.includes("video") ||
|
|
101
|
+
STREAMING_VIDEO_MIME_TYPES.includes(contentType))
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
83
105
|
export const isAudioItem = (item: Option<ZappEntry>) => {
|
|
84
106
|
if (
|
|
85
107
|
isString(item?.content?.type) &&
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/// <reference types="@applicaster/applicaster-types" />
|
|
2
2
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
3
|
-
import { Context, useContext, useState } from "react";
|
|
3
|
+
import { Context, useCallback, useContext, useEffect, useState } from "react";
|
|
4
4
|
import { ActionsContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ActionsContext";
|
|
5
5
|
|
|
6
|
+
import {
|
|
7
|
+
observeEntryState,
|
|
8
|
+
RegisteredActionValue,
|
|
9
|
+
} from "../../uiActionsRegistrator";
|
|
6
10
|
import { reactHooksLogger } from "../logger";
|
|
7
11
|
|
|
8
12
|
const logger = reactHooksLogger.addSubsystem("actions");
|
|
@@ -46,3 +50,49 @@ export function useActions<T = unknown>(plugId: string): any {
|
|
|
46
50
|
|
|
47
51
|
return context;
|
|
48
52
|
}
|
|
53
|
+
|
|
54
|
+
export type UseEntryActionState = {
|
|
55
|
+
state: CellActionEntryState | undefined;
|
|
56
|
+
invokeAction: (options?: InvokeArgsOptions) => void;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Subscribes a component to a single action's state for a given `entry`.
|
|
61
|
+
*
|
|
62
|
+
* It observes state changes reactively via `observeEntryState` (which is backed
|
|
63
|
+
* by the action's `addListener`), seeds the initial value synchronously from
|
|
64
|
+
* `initialEntryState`, and returns a memoized `invokeAction` that forwards
|
|
65
|
+
* optimistic `updateState` updates back into local state.
|
|
66
|
+
*
|
|
67
|
+
* This is the single, unified way to drive an action button - it works the same
|
|
68
|
+
* for registry actions, legacy context-provider actions and entry actions.
|
|
69
|
+
*/
|
|
70
|
+
export function useEntryActionState(
|
|
71
|
+
action: RegisteredActionValue | undefined,
|
|
72
|
+
entry: ZappEntry | ZappFeed
|
|
73
|
+
): UseEntryActionState {
|
|
74
|
+
const [state, setState] = useState<CellActionEntryState | undefined>(() =>
|
|
75
|
+
action?.initialEntryState?.(entry)
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const entryId = (entry as ZappEntry)?.id;
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!action) return undefined;
|
|
82
|
+
|
|
83
|
+
const subscription = observeEntryState(action, entry).subscribe(setState);
|
|
84
|
+
|
|
85
|
+
return () => subscription.unsubscribe();
|
|
86
|
+
// eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
|
|
87
|
+
}, [action, entryId]);
|
|
88
|
+
|
|
89
|
+
const invokeAction = useCallback(
|
|
90
|
+
(options: InvokeArgsOptions = {}) => {
|
|
91
|
+
action?.invokeAction?.(entry, { updateState: setState, ...options });
|
|
92
|
+
},
|
|
93
|
+
// eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
|
|
94
|
+
[action, entryId]
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return { state, invokeAction };
|
|
98
|
+
}
|
|
@@ -4,6 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import * as R from "ramda";
|
|
5
5
|
import { handleActionSchemeUrl } from "@applicaster/quick-brick-core/App/DeepLinking/URLSchemeHandler/SchemeHandlerHooks/useUrlSchemeHandler";
|
|
6
6
|
import { CellTapContext } from "@applicaster/zapp-react-native-ui-components/Contexts/CellTapContext";
|
|
7
|
+
import { useUIComponentContext } from "@applicaster/zapp-react-native-ui-components/Contexts/UIComponentContext";
|
|
7
8
|
import {
|
|
8
9
|
useNavigation,
|
|
9
10
|
useProfilerLogging,
|
|
@@ -28,7 +29,6 @@ import { useScreenStateStore } from "../navigation/useScreenStateStore";
|
|
|
28
29
|
type Props = {
|
|
29
30
|
item?: ZappEntry;
|
|
30
31
|
index?: number;
|
|
31
|
-
component?: ZappUIComponent;
|
|
32
32
|
zappPipesData?: ZappPipesData;
|
|
33
33
|
};
|
|
34
34
|
|
|
@@ -37,24 +37,25 @@ type onPressReturnFn =
|
|
|
37
37
|
| (() => void);
|
|
38
38
|
|
|
39
39
|
export const useCellClick = ({
|
|
40
|
-
component,
|
|
41
40
|
zappPipesData,
|
|
42
41
|
item,
|
|
43
|
-
}: Props): onPressReturnFn => {
|
|
42
|
+
}: Props = {}): onPressReturnFn => {
|
|
44
43
|
const { push, currentRoute } = useNavigation();
|
|
45
44
|
const { pathname } = useRoute();
|
|
46
45
|
const screenStateStore = useScreenStateStore();
|
|
47
46
|
|
|
48
47
|
const onCellTap: Option<Function> = React.useContext(CellTapContext);
|
|
49
48
|
const actionExecutor = React.useContext(ActionExecutorContext);
|
|
49
|
+
const component = useUIComponentContext();
|
|
50
50
|
const screenData = useCurrentScreenData();
|
|
51
51
|
const screenState = useScreenContext()?.options;
|
|
52
|
+
const entry = useScreenContext()?.entry;
|
|
52
53
|
|
|
53
54
|
const cellSelectable = toBooleanWithDefaultTrue(
|
|
54
55
|
component?.rules?.component_cells_selectable
|
|
55
56
|
);
|
|
56
57
|
|
|
57
|
-
const [
|
|
58
|
+
const [_entryContext, setEntryContext] =
|
|
58
59
|
ZappPipesEntryContext.useZappPipesContext(pathname);
|
|
59
60
|
|
|
60
61
|
const logTimestamp = useProfilerLogging();
|
|
@@ -89,7 +90,8 @@ export const useCellClick = ({
|
|
|
89
90
|
screenState,
|
|
90
91
|
screenRoute: pathname,
|
|
91
92
|
screenStateStore,
|
|
92
|
-
entryContext,
|
|
93
|
+
entryContext: selectedItem,
|
|
94
|
+
screenEntry: entry,
|
|
93
95
|
});
|
|
94
96
|
}
|
|
95
97
|
|
|
@@ -117,14 +119,20 @@ export const useCellClick = ({
|
|
|
117
119
|
}
|
|
118
120
|
},
|
|
119
121
|
[
|
|
120
|
-
|
|
121
|
-
currentRoute,
|
|
122
|
+
item,
|
|
122
123
|
setEntryContext,
|
|
123
|
-
pathname,
|
|
124
|
-
push,
|
|
125
124
|
sendAnalyticsOnPress,
|
|
125
|
+
logTimestamp,
|
|
126
|
+
pathname,
|
|
127
|
+
component,
|
|
128
|
+
onCellTap,
|
|
129
|
+
entry,
|
|
130
|
+
actionExecutor,
|
|
126
131
|
screenData,
|
|
127
132
|
screenState,
|
|
133
|
+
screenStateStore,
|
|
134
|
+
currentRoute,
|
|
135
|
+
push,
|
|
128
136
|
]
|
|
129
137
|
);
|
|
130
138
|
|
|
@@ -138,5 +146,5 @@ export const useCellClick = ({
|
|
|
138
146
|
onPressRef.current = onPress;
|
|
139
147
|
}
|
|
140
148
|
|
|
141
|
-
return React.useCallback(onPressRef.current, []);
|
|
149
|
+
return React.useCallback(onPressRef.current, [item]);
|
|
142
150
|
};
|
package/reactHooks/feed/index.ts
CHANGED
|
@@ -6,7 +6,11 @@ export { useEntryScreenId } from "./useEntryScreenId";
|
|
|
6
6
|
|
|
7
7
|
export { useBuildPipesUrl } from "./useBuildPipesUrl";
|
|
8
8
|
|
|
9
|
-
export {
|
|
9
|
+
export {
|
|
10
|
+
useMarkPipesDataStale,
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
12
|
+
usePipesCacheReset,
|
|
13
|
+
} from "./useMarkPipesDataStale";
|
|
10
14
|
|
|
11
15
|
export { useBatchLoading } from "./useBatchLoading";
|
|
12
16
|
|
|
@@ -159,9 +159,17 @@ export const useBatchLoading = (
|
|
|
159
159
|
options.riverId,
|
|
160
160
|
]);
|
|
161
161
|
|
|
162
|
+
// Initial preload only. Batch loading warms the first batch of component
|
|
163
|
+
// feeds and signals readiness; the feed set is frozen at mount on purpose.
|
|
164
|
+
// Per-component loads and reloads (including stale revalidation) are owned by
|
|
165
|
+
// useFeedLoader in ZappPipesDataConnector. Re-running on `feeds`/input changes
|
|
166
|
+
// would re-fire on every store update and cause redundant dispatch storms, so
|
|
167
|
+
// this intentionally runs once on mount.
|
|
168
|
+
/* eslint-disable @wogns3623/better-exhaustive-deps/exhaustive-deps */
|
|
162
169
|
React.useEffect(() => {
|
|
163
170
|
runBatchLoading();
|
|
164
|
-
}, [
|
|
171
|
+
}, []);
|
|
172
|
+
/* eslint-enable @wogns3623/better-exhaustive-deps/exhaustive-deps */
|
|
165
173
|
|
|
166
174
|
React.useEffect(() => {
|
|
167
175
|
// check if all feeds are ready and set hasEverBeenReady to true
|
|
@@ -2,17 +2,19 @@ import React from "react";
|
|
|
2
2
|
|
|
3
3
|
import { getDatasourceUrl } from "@applicaster/zapp-react-native-ui-components/Decorators/RiverFeedLoader/utils/getDatasourceUrl";
|
|
4
4
|
import { usePipesContexts } from "@applicaster/zapp-react-native-ui-components/Decorators/RiverFeedLoader/utils/usePipesContexts";
|
|
5
|
-
import {
|
|
5
|
+
import { markPipesDataStale } from "@applicaster/zapp-react-native-redux/ZappPipes";
|
|
6
6
|
|
|
7
7
|
import { useRoute } from "../navigation";
|
|
8
8
|
import { useAppDispatch } from "@applicaster/zapp-react-native-redux";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* Mark a river's `clear_cache_on_reload` feeds as stale when the screen is
|
|
12
|
+
* unmounted. The cached data is kept (so a screen sharing the same feed does
|
|
13
|
+
* not lose it mid-mount) and revalidated on next access.
|
|
12
14
|
* @param {string} riverId screen id
|
|
13
15
|
* @param {Array} riverComponents list of UI components
|
|
14
16
|
*/
|
|
15
|
-
export const
|
|
17
|
+
export const useMarkPipesDataStale = (riverId, riverComponents) => {
|
|
16
18
|
const dispatch = useAppDispatch();
|
|
17
19
|
const { screenData, pathname } = useRoute();
|
|
18
20
|
const pipesContexts = usePipesContexts(riverId, pathname);
|
|
@@ -35,10 +37,16 @@ export const usePipesCacheReset = (riverId, riverComponents) => {
|
|
|
35
37
|
);
|
|
36
38
|
|
|
37
39
|
if (url) {
|
|
38
|
-
dispatch(
|
|
40
|
+
dispatch(markPipesDataStale(url, { riverId }));
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
});
|
|
42
44
|
};
|
|
43
45
|
}, []);
|
|
44
46
|
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @deprecated Renamed to `useMarkPipesDataStale`. Kept as an alias for backward
|
|
50
|
+
* compatibility with external consumers.
|
|
51
|
+
*/
|
|
52
|
+
export const usePipesCacheReset = useMarkPipesDataStale;
|
|
@@ -38,3 +38,7 @@ export const useNavigationPluginData = jest.fn().mockReturnValue(mockMenu);
|
|
|
38
38
|
export const useIsNavBarVisible = jest.fn().mockReturnValue(true);
|
|
39
39
|
|
|
40
40
|
export const useIsScreenActive = jest.fn().mockReturnValue(true);
|
|
41
|
+
|
|
42
|
+
export const isScreenActiveForRoute = jest.fn(
|
|
43
|
+
(route, currentRoute) => route === currentRoute
|
|
44
|
+
);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isScreenActiveForRoute } from "../useIsScreenActive";
|
|
2
|
+
|
|
3
|
+
describe("isScreenActiveForRoute", () => {
|
|
4
|
+
const route = "/river/home/river/tabs";
|
|
5
|
+
const currentRoute = "/river/home/river/tabs";
|
|
6
|
+
|
|
7
|
+
it("returns false when route does not match", () => {
|
|
8
|
+
expect(
|
|
9
|
+
isScreenActiveForRoute(route, "/river/home", { visible: false })
|
|
10
|
+
).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("keeps underlying screen active when video modal is docked", () => {
|
|
14
|
+
expect(
|
|
15
|
+
isScreenActiveForRoute(route, currentRoute, {
|
|
16
|
+
visible: true,
|
|
17
|
+
mode: "MINIMIZED",
|
|
18
|
+
})
|
|
19
|
+
).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it.each(["FULLSCREEN", "MAXIMIZED", "PIP"])(
|
|
23
|
+
"deactivates underlying screen when video modal is visible",
|
|
24
|
+
(mode) => {
|
|
25
|
+
expect(
|
|
26
|
+
isScreenActiveForRoute(route, currentRoute, {
|
|
27
|
+
visible: true,
|
|
28
|
+
mode,
|
|
29
|
+
})
|
|
30
|
+
).toBe(false);
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
it("keeps video-modal route active when video modal is open", () => {
|
|
35
|
+
expect(
|
|
36
|
+
isScreenActiveForRoute("video-modal/123", currentRoute, {
|
|
37
|
+
visible: true,
|
|
38
|
+
mode: "FULLSCREEN",
|
|
39
|
+
})
|
|
40
|
+
).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -32,7 +32,7 @@ export { useNavigationType } from "./useNavigationType";
|
|
|
32
32
|
|
|
33
33
|
export { useGetBottomTabBarHeight } from "./useGetBottomTabBarHeight";
|
|
34
34
|
|
|
35
|
-
export { useIsScreenActive } from "./useIsScreenActive";
|
|
35
|
+
export { useIsScreenActive, isScreenActiveForRoute } from "./useIsScreenActive";
|
|
36
36
|
|
|
37
37
|
export { useProfilerLogging } from "./useProfilerLogging";
|
|
38
38
|
|
|
@@ -2,20 +2,41 @@ import { ROUTE_TYPES } from "@applicaster/zapp-react-native-utils/navigationUtil
|
|
|
2
2
|
import { useNavigation } from "./useNavigation";
|
|
3
3
|
import { usePathname } from "./usePathname";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
type VideoModalState = {
|
|
6
|
+
visible?: boolean;
|
|
7
|
+
mode?: string;
|
|
8
|
+
} | null;
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Whether a navigation route should be treated as the active/focused screen.
|
|
12
|
+
* Docked (MINIMIZED) video modal does not deactivate the underlying screen —
|
|
13
|
+
* only FULLSCREEN / MAXIMIZED / PIP do.
|
|
14
|
+
*/
|
|
15
|
+
export function isScreenActiveForRoute(
|
|
16
|
+
route: string,
|
|
17
|
+
currentRoute: string,
|
|
18
|
+
videoModalState: VideoModalState
|
|
19
|
+
) {
|
|
20
|
+
if (videoModalState?.visible) {
|
|
21
|
+
if (route.includes(ROUTE_TYPES.VIDEO_MODAL)) {
|
|
12
22
|
return true;
|
|
13
23
|
}
|
|
14
24
|
|
|
15
|
-
if (
|
|
25
|
+
if (
|
|
26
|
+
["FULLSCREEN", "MAXIMIZED", "PIP"].includes(videoModalState.mode ?? "")
|
|
27
|
+
) {
|
|
16
28
|
return false;
|
|
17
29
|
}
|
|
30
|
+
// MINIMIZED (docked): fall through — underlying screen stays active
|
|
18
31
|
}
|
|
19
32
|
|
|
20
|
-
return
|
|
33
|
+
return route === currentRoute;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// If current screen is active/focused (visible to the user)
|
|
37
|
+
export const useIsScreenActive = () => {
|
|
38
|
+
const pathname = usePathname();
|
|
39
|
+
const { currentRoute, videoModalState } = useNavigation();
|
|
40
|
+
|
|
41
|
+
return isScreenActiveForRoute(pathname, currentRoute, videoModalState);
|
|
21
42
|
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react-native";
|
|
2
|
+
import { useIsStandaloneFullscreen } from "../useIsStandaloneFullscreen";
|
|
3
|
+
|
|
4
|
+
import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
5
|
+
import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils";
|
|
6
|
+
|
|
7
|
+
jest.mock("@applicaster/zapp-react-native-utils/reactHooks");
|
|
8
|
+
jest.mock("@applicaster/zapp-react-native-utils/booleanUtils");
|
|
9
|
+
|
|
10
|
+
const mockUseNavigation = useNavigation as jest.Mock;
|
|
11
|
+
const mockToBoolean = toBooleanWithDefaultFalse as jest.Mock;
|
|
12
|
+
|
|
13
|
+
describe("useIsStandaloneFullscreen", () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
jest.clearAllMocks();
|
|
16
|
+
|
|
17
|
+
mockUseNavigation.mockReturnValue({
|
|
18
|
+
canGoBack: jest.fn().mockReturnValue(false),
|
|
19
|
+
screenData: {
|
|
20
|
+
general: { allow_screen_plugin_presentation: true },
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
mockToBoolean.mockReturnValue(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("returns true when cannot go back and screen plugin presentation is allowed", () => {
|
|
28
|
+
mockToBoolean.mockReturnValue(true);
|
|
29
|
+
|
|
30
|
+
const { result } = renderHook(() => useIsStandaloneFullscreen());
|
|
31
|
+
|
|
32
|
+
expect(mockToBoolean).toHaveBeenCalledWith(true);
|
|
33
|
+
expect(result.current).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns false when can go back even if screen plugin presentation is allowed", () => {
|
|
37
|
+
mockUseNavigation.mockReturnValue({
|
|
38
|
+
canGoBack: jest.fn().mockReturnValue(true),
|
|
39
|
+
screenData: {
|
|
40
|
+
general: { allow_screen_plugin_presentation: true },
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
mockToBoolean.mockReturnValue(false);
|
|
45
|
+
|
|
46
|
+
const { result } = renderHook(() => useIsStandaloneFullscreen());
|
|
47
|
+
|
|
48
|
+
expect(mockToBoolean).toHaveBeenCalledWith(false);
|
|
49
|
+
expect(result.current).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("returns false when cannot go back but screen plugin presentation is not allowed", () => {
|
|
53
|
+
mockUseNavigation.mockReturnValue({
|
|
54
|
+
canGoBack: jest.fn().mockReturnValue(false),
|
|
55
|
+
screenData: {
|
|
56
|
+
general: { allow_screen_plugin_presentation: false },
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
mockToBoolean.mockReturnValue(false);
|
|
61
|
+
|
|
62
|
+
const { result } = renderHook(() => useIsStandaloneFullscreen());
|
|
63
|
+
|
|
64
|
+
expect(mockToBoolean).toHaveBeenCalledWith(false);
|
|
65
|
+
expect(result.current).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("returns false when screen plugin presentation flag is undefined", () => {
|
|
69
|
+
mockUseNavigation.mockReturnValue({
|
|
70
|
+
canGoBack: jest.fn().mockReturnValue(false),
|
|
71
|
+
screenData: { general: {} },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
mockToBoolean.mockReturnValue(false);
|
|
75
|
+
|
|
76
|
+
const { result } = renderHook(() => useIsStandaloneFullscreen());
|
|
77
|
+
|
|
78
|
+
expect(mockToBoolean).toHaveBeenCalledWith(undefined);
|
|
79
|
+
expect(result.current).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("returns false when navigator is undefined", () => {
|
|
83
|
+
mockUseNavigation.mockReturnValue(undefined);
|
|
84
|
+
mockToBoolean.mockReturnValue(false);
|
|
85
|
+
|
|
86
|
+
const { result } = renderHook(() => useIsStandaloneFullscreen());
|
|
87
|
+
|
|
88
|
+
expect(mockToBoolean).toHaveBeenCalledWith(undefined);
|
|
89
|
+
expect(result.current).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("returns false when screenData is undefined", () => {
|
|
93
|
+
mockUseNavigation.mockReturnValue({
|
|
94
|
+
canGoBack: jest.fn().mockReturnValue(false),
|
|
95
|
+
screenData: undefined,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
mockToBoolean.mockReturnValue(false);
|
|
99
|
+
|
|
100
|
+
const { result } = renderHook(() => useIsStandaloneFullscreen());
|
|
101
|
+
|
|
102
|
+
expect(mockToBoolean).toHaveBeenCalledWith(undefined);
|
|
103
|
+
expect(result.current).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("passes the combined condition through toBooleanWithDefaultFalse", () => {
|
|
107
|
+
mockToBoolean.mockImplementation((val) => Boolean(val));
|
|
108
|
+
|
|
109
|
+
const { result } = renderHook(() => useIsStandaloneFullscreen());
|
|
110
|
+
|
|
111
|
+
expect(mockToBoolean).toHaveBeenCalledWith(true);
|
|
112
|
+
expect(result.current).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -16,3 +16,5 @@ export { useScreenBackgroundColor } from "./useScreenBackgroundColor";
|
|
|
16
16
|
export { useCurrentScreenIsHook } from "./useCurrentScreenIsHook";
|
|
17
17
|
|
|
18
18
|
export { useCurrentScreenIsStartupHook } from "./useCurrentScreenIsStartupHook";
|
|
19
|
+
|
|
20
|
+
export { useIsStandaloneFullscreen } from "./useIsStandaloneFullscreen";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
2
|
+
import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils";
|
|
3
|
+
|
|
4
|
+
export const useIsStandaloneFullscreen = (): boolean => {
|
|
5
|
+
const navigator = useNavigation();
|
|
6
|
+
|
|
7
|
+
return toBooleanWithDefaultFalse(
|
|
8
|
+
!navigator?.canGoBack() &&
|
|
9
|
+
// @ts-ignore
|
|
10
|
+
navigator?.screenData?.general?.allow_screen_plugin_presentation
|
|
11
|
+
);
|
|
12
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* This hook returns a previous value that was passed to it.
|
|
@@ -40,6 +40,7 @@ export const shouldDispatchData = (
|
|
|
40
40
|
) => {
|
|
41
41
|
const currentFeedHasData = feed?.data;
|
|
42
42
|
const isLocalFeed = checkIsLocalFeed(url);
|
|
43
|
+
const isFeedStale = feed?.stale;
|
|
43
44
|
|
|
44
|
-
return !currentFeedHasData || clearCache || isLocalFeed;
|
|
45
|
+
return !currentFeedHasData || clearCache || isFeedStale || isLocalFeed;
|
|
45
46
|
};
|