@applicaster/zapp-react-native-ui-components 16.0.0-alpha.9225550512 → 16.0.0-alpha.9506982263
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/AudioPlayer/tv/__tests__/audioPlayer.test.js +19 -0
- package/Components/AudioPlayer/tv/__tests__/index.test.tsx +120 -0
- package/Components/AudioPlayer/tv/index.tsx +5 -1
- package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
- package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
- package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
- package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
- package/Components/ModalComponent/BottomSheetModalContent.tsx +17 -10
- package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
- package/Components/PlayerContainer/PlayerContainer.tsx +11 -7
- package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
- package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +66 -13
- package/Components/VideoLive/LiveImageManager.ts +1 -1
- package/package.json +5 -5
|
@@ -1,15 +1,85 @@
|
|
|
1
|
-
import {
|
|
2
|
-
toFiniteNumberWithDefault,
|
|
3
|
-
toNumber,
|
|
4
|
-
} from "@applicaster/zapp-react-native-utils/numberUtils";
|
|
1
|
+
import { toFiniteNumberWithDefault } from "@applicaster/zapp-react-native-utils/numberUtils";
|
|
5
2
|
import { isNil } from "ramda";
|
|
3
|
+
import { coreLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
6
4
|
|
|
7
|
-
const
|
|
5
|
+
const imageResizingLogger = coreLogger
|
|
6
|
+
?.addSubsystem("components")
|
|
7
|
+
?.addSubsystem("ImageResizing");
|
|
8
8
|
|
|
9
|
+
const TEMPLATE_REGEX = /{{(width|height)}}/gi;
|
|
10
|
+
const UNRESOLVED_PLACEHOLDER_REGEX = /{{[^}]*}}?/;
|
|
11
|
+
const INVALID_PLACEHOLDER_EXTRACT_REGEX = /{{([^}]*)}}?/g;
|
|
12
|
+
const ALLOWED_PLACEHOLDERS = ["width", "height"];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Merges query-param string `paramsString` into `baseUrl`.
|
|
16
|
+
* Params from `paramsString` override any existing params with the same key.
|
|
17
|
+
*/
|
|
18
|
+
export const mergeQueryParams = (
|
|
19
|
+
baseUrl: string,
|
|
20
|
+
paramsString: string
|
|
21
|
+
): string => {
|
|
22
|
+
try {
|
|
23
|
+
const urlObj = new URL(baseUrl);
|
|
24
|
+
const newParams = new URLSearchParams(paramsString);
|
|
25
|
+
|
|
26
|
+
newParams.forEach((value, key) => {
|
|
27
|
+
urlObj.searchParams.set(key, value);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return urlObj.toString();
|
|
31
|
+
} catch {
|
|
32
|
+
// If URL parsing fails, return original
|
|
33
|
+
return baseUrl;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extracts unresolved placeholder names from a param pair and logs a warning.
|
|
39
|
+
*/
|
|
40
|
+
const logInvalidPlaceholders = (pair: string): void => {
|
|
41
|
+
const invalidNames: string[] = [];
|
|
42
|
+
let match: RegExpExecArray | null;
|
|
43
|
+
|
|
44
|
+
// Reset lastIndex before using the global regex
|
|
45
|
+
INVALID_PLACEHOLDER_EXTRACT_REGEX.lastIndex = 0;
|
|
46
|
+
|
|
47
|
+
while ((match = INVALID_PLACEHOLDER_EXTRACT_REGEX.exec(pair)) !== null) {
|
|
48
|
+
const name = match[1];
|
|
49
|
+
|
|
50
|
+
if (!ALLOWED_PLACEHOLDERS.includes(name?.toLowerCase())) {
|
|
51
|
+
invalidNames.push(name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (invalidNames.length > 0) {
|
|
56
|
+
imageResizingLogger?.warning({
|
|
57
|
+
message: `Image resizing: invalid placeholder(s) found: ${invalidNames.map((n) => `{{${n}}}`).join(", ")}. Allowed placeholders are: {{width}}, {{height}}. The param "${pair}" was ignored.`,
|
|
58
|
+
data: { pair, invalidNames, allowedPlaceholders: ALLOWED_PLACEHOLDERS },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parses a dimension mapping template string (query param format) and injects
|
|
65
|
+
* the resolved width/height values into the given URL.
|
|
66
|
+
*
|
|
67
|
+
* The template uses `{{width}}` and `{{height}}` placeholders.
|
|
68
|
+
* It is treated as query-param key=value pairs (e.g. "w={{width}}&h={{height}}"
|
|
69
|
+
* or "size={{width}}x{{height}}").
|
|
70
|
+
*
|
|
71
|
+
* Resolved params are **merged** into the original URL – existing params with
|
|
72
|
+
* the same key are replaced; new ones are appended.
|
|
73
|
+
*
|
|
74
|
+
* Invalid mappings (containing unresolved placeholders like `{{heit}}` or
|
|
75
|
+
* malformed `{{heit}`) are silently dropped and logged as warnings.
|
|
76
|
+
* A leading `?` in the template is stripped.
|
|
77
|
+
*/
|
|
9
78
|
export const withDimensions = (
|
|
10
79
|
url: Option<string>,
|
|
11
80
|
width: Option<number | string>,
|
|
12
|
-
height: Option<number | string
|
|
81
|
+
height: Option<number | string>,
|
|
82
|
+
dimensionMappingTemplate?: Option<string>
|
|
13
83
|
) => {
|
|
14
84
|
if (isNil(url)) {
|
|
15
85
|
return url;
|
|
@@ -22,23 +92,48 @@ export const withDimensions = (
|
|
|
22
92
|
const newWidth = Math.trunc(toFiniteNumberWithDefault(0, width));
|
|
23
93
|
const newHeight = Math.trunc(toFiniteNumberWithDefault(0, height));
|
|
24
94
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
95
|
+
if (newWidth <= 0 && newHeight <= 0) {
|
|
96
|
+
return url;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Strip leading `?` and whitespace from the template
|
|
100
|
+
const template = dimensionMappingTemplate?.trim()?.replace(/^\?/, "");
|
|
28
101
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
102
|
+
if (!template) {
|
|
103
|
+
return url;
|
|
104
|
+
}
|
|
33
105
|
|
|
34
|
-
|
|
35
|
-
|
|
106
|
+
// Replace {{width}} / {{height}} placeholders in the template
|
|
107
|
+
const resolvedTemplate = template.replace(TEMPLATE_REGEX, (_match, name) => {
|
|
108
|
+
if (name.toLowerCase() === "width") {
|
|
109
|
+
return newWidth > 0 ? String(newWidth) : _match;
|
|
110
|
+
}
|
|
36
111
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
112
|
+
if (name.toLowerCase() === "height") {
|
|
113
|
+
return newHeight > 0 ? String(newHeight) : _match;
|
|
40
114
|
}
|
|
41
115
|
|
|
42
|
-
return
|
|
116
|
+
return _match;
|
|
43
117
|
});
|
|
118
|
+
|
|
119
|
+
// Filter out param pairs that still contain unresolved / malformed placeholders
|
|
120
|
+
const cleanedParams = resolvedTemplate
|
|
121
|
+
.split("&")
|
|
122
|
+
.filter((pair) => {
|
|
123
|
+
if (UNRESOLVED_PLACEHOLDER_REGEX.test(pair)) {
|
|
124
|
+
logInvalidPlaceholders(pair);
|
|
125
|
+
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return true;
|
|
130
|
+
})
|
|
131
|
+
.join("&");
|
|
132
|
+
|
|
133
|
+
if (!cleanedParams) {
|
|
134
|
+
return url;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Parse the resolved template as query params and merge into URL
|
|
138
|
+
return mergeQueryParams(url, cleanedParams);
|
|
44
139
|
};
|
|
@@ -14,25 +14,36 @@ type Style = {
|
|
|
14
14
|
height: Option<number | string>;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
const withAppliedDimensions =
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
const withAppliedDimensions =
|
|
18
|
+
(style: Style, dimensionMappingTemplate: string) => (source: Source) => ({
|
|
19
|
+
...source,
|
|
20
|
+
uri: withDimensions(
|
|
21
|
+
source?.uri,
|
|
22
|
+
style?.width,
|
|
23
|
+
style?.height,
|
|
24
|
+
dimensionMappingTemplate
|
|
25
|
+
),
|
|
26
|
+
});
|
|
21
27
|
|
|
22
28
|
export const withDimensionsHOC = (Component) => {
|
|
23
29
|
return function WithDimensions(props: Record<string, unknown>) {
|
|
24
30
|
const theme = useTheme<BaseThemePropertiesMobile>();
|
|
25
31
|
|
|
26
|
-
const
|
|
27
|
-
theme.use_downscaling_images
|
|
32
|
+
const useImageResizing = toBooleanWithDefaultFalse(
|
|
33
|
+
theme.use_image_resizing ?? theme.use_downscaling_images
|
|
28
34
|
);
|
|
29
35
|
|
|
36
|
+
const dimensionMappingTemplate = theme.image_resizing_template;
|
|
37
|
+
|
|
30
38
|
return (
|
|
31
39
|
<Component
|
|
32
40
|
{...props}
|
|
33
41
|
withDimensions={
|
|
34
|
-
|
|
35
|
-
? withAppliedDimensions(
|
|
42
|
+
useImageResizing
|
|
43
|
+
? withAppliedDimensions(
|
|
44
|
+
(props?.style || {}) as Style,
|
|
45
|
+
dimensionMappingTemplate
|
|
46
|
+
)
|
|
36
47
|
: identity
|
|
37
48
|
}
|
|
38
49
|
/>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { PanGestureHandler } from "react-native-gesture-handler";
|
|
3
|
+
import { View } from "react-native";
|
|
4
|
+
import { useBottomSheet } from "@applicaster/zapp-react-native-utils/bottomSheet";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Turns its children into a drag zone for the enclosing DraggableBottomSheet:
|
|
8
|
+
* dragging here moves/dismisses the sheet (used for the modal header bar).
|
|
9
|
+
* Outside a sheet (no context) it renders children unchanged.
|
|
10
|
+
*/
|
|
11
|
+
export const BottomSheetDragArea = ({
|
|
12
|
+
children,
|
|
13
|
+
}: {
|
|
14
|
+
children: React.ReactElement;
|
|
15
|
+
}) => {
|
|
16
|
+
const sheet = useBottomSheet();
|
|
17
|
+
|
|
18
|
+
// Outside a sheet (no context) render children unchanged.
|
|
19
|
+
if (!sheet) return children;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<PanGestureHandler
|
|
23
|
+
simultaneousHandlers={[sheet.masterDrawerRef]}
|
|
24
|
+
shouldCancelWhenOutside={false}
|
|
25
|
+
onGestureEvent={sheet.onDragGestureEvent}
|
|
26
|
+
onHandlerStateChange={sheet.onDragStateChange}
|
|
27
|
+
>
|
|
28
|
+
{/* Old-API PanGestureHandler needs a host-component child; children may
|
|
29
|
+
be a function component (e.g. ModalHeader), so wrap in a View. */}
|
|
30
|
+
<View>{children}</View>
|
|
31
|
+
</PanGestureHandler>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
@@ -6,11 +6,11 @@ import { ItemProps } from "./Button/Item";
|
|
|
6
6
|
import { ItemLabelProps } from "./Button/ItemLabel";
|
|
7
7
|
import { getItemIconProps, getItemLabelProps, getItemProps } from "./utils";
|
|
8
8
|
import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
|
|
9
|
-
|
|
10
9
|
import type { PluginConfiguration } from "./";
|
|
11
10
|
|
|
12
11
|
import { ModalHeader as DefaultModalHeader } from "./Header";
|
|
13
12
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
13
|
+
import { BottomSheetDragArea } from "./BottomSheetDragArea";
|
|
14
14
|
|
|
15
15
|
type ModalComponentProps = {
|
|
16
16
|
items: any[];
|
|
@@ -55,8 +55,7 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
|
|
|
55
55
|
dismiss,
|
|
56
56
|
summary,
|
|
57
57
|
title,
|
|
58
|
-
headerComponent
|
|
59
|
-
|
|
58
|
+
headerComponent = DefaultModalHeader,
|
|
60
59
|
scrollViewContent = null,
|
|
61
60
|
|
|
62
61
|
buttonComponent: ButtonComponent = Button,
|
|
@@ -72,6 +71,8 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
|
|
|
72
71
|
const paddingTop = Number(theme.modal_bottom_sheet_padding_top);
|
|
73
72
|
const paddingBottom = Number(theme.modal_bottom_sheet_padding_bottom);
|
|
74
73
|
|
|
74
|
+
const Header = headerComponent;
|
|
75
|
+
|
|
75
76
|
useEffect(() => {
|
|
76
77
|
if (currentRoute !== route.current) {
|
|
77
78
|
props.dismiss();
|
|
@@ -107,13 +108,20 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
|
|
|
107
108
|
paddingTop: paddingTop,
|
|
108
109
|
}}
|
|
109
110
|
>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
111
|
+
{Header ? (
|
|
112
|
+
<BottomSheetDragArea>
|
|
113
|
+
<Header
|
|
114
|
+
dismiss={dismiss}
|
|
115
|
+
configuration={theme}
|
|
116
|
+
summary={summary}
|
|
117
|
+
title={title}
|
|
118
|
+
/>
|
|
119
|
+
</BottomSheetDragArea>
|
|
120
|
+
) : null}
|
|
116
121
|
<ScrollView
|
|
122
|
+
overScrollMode="never"
|
|
123
|
+
bounces={false}
|
|
124
|
+
showsVerticalScrollIndicator={false}
|
|
117
125
|
contentContainerStyle={{
|
|
118
126
|
paddingBottom: paddingBottom + bottomInset,
|
|
119
127
|
}}
|
|
@@ -123,7 +131,6 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
|
|
|
123
131
|
scrollViewContent.renderItem(item, index)
|
|
124
132
|
)
|
|
125
133
|
: null}
|
|
126
|
-
|
|
127
134
|
{!scrollViewContent &&
|
|
128
135
|
items.map((item, index) =>
|
|
129
136
|
item ? (
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text } from "react-native";
|
|
3
|
+
import { render, screen } from "@testing-library/react-native";
|
|
4
|
+
import { BottomSheetDragArea } from "../BottomSheetDragArea";
|
|
5
|
+
import { BottomSheetContext } from "@applicaster/zapp-react-native-utils/bottomSheet";
|
|
6
|
+
|
|
7
|
+
// Reveal PanGestureHandler and capture its props.
|
|
8
|
+
jest.mock("react-native-gesture-handler", () => {
|
|
9
|
+
const { View } = require("react-native");
|
|
10
|
+
const actual = jest.requireActual("react-native-gesture-handler");
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
__esModule: true,
|
|
14
|
+
...actual,
|
|
15
|
+
PanGestureHandler: ({
|
|
16
|
+
children,
|
|
17
|
+
onGestureEvent,
|
|
18
|
+
onHandlerStateChange,
|
|
19
|
+
simultaneousHandlers,
|
|
20
|
+
}: any) => (
|
|
21
|
+
<View
|
|
22
|
+
testID="drag-pan"
|
|
23
|
+
onGestureEvent={onGestureEvent}
|
|
24
|
+
onHandlerStateChange={onHandlerStateChange}
|
|
25
|
+
simultaneousHandlers={simultaneousHandlers}
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</View>
|
|
29
|
+
),
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const makeValue = () => ({
|
|
34
|
+
scrollRef: React.createRef<any>(),
|
|
35
|
+
masterDrawerRef: React.createRef<any>(),
|
|
36
|
+
drawerRef: React.createRef<any>(),
|
|
37
|
+
onScroll: jest.fn(),
|
|
38
|
+
setBodyDragEnabled: jest.fn(),
|
|
39
|
+
onDragGestureEvent: jest.fn(),
|
|
40
|
+
onDragStateChange: jest.fn(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("BottomSheetDragArea", () => {
|
|
44
|
+
it("wraps children in a sheet-wired PanGestureHandler when inside a sheet", () => {
|
|
45
|
+
const value = makeValue();
|
|
46
|
+
|
|
47
|
+
render(
|
|
48
|
+
<BottomSheetContext.Provider value={value}>
|
|
49
|
+
<BottomSheetDragArea>
|
|
50
|
+
<Text testID="child">hi</Text>
|
|
51
|
+
</BottomSheetDragArea>
|
|
52
|
+
</BottomSheetContext.Provider>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const pan = screen.getByTestId("drag-pan");
|
|
56
|
+
expect(pan.props.onGestureEvent).toBe(value.onDragGestureEvent);
|
|
57
|
+
expect(pan.props.onHandlerStateChange).toBe(value.onDragStateChange);
|
|
58
|
+
|
|
59
|
+
expect(pan.props.simultaneousHandlers).toEqual([value.masterDrawerRef]);
|
|
60
|
+
|
|
61
|
+
expect(screen.getByTestId("child")).toBeTruthy();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("renders children bare when used outside a sheet", () => {
|
|
65
|
+
render(
|
|
66
|
+
<BottomSheetDragArea>
|
|
67
|
+
<Text testID="child">hi</Text>
|
|
68
|
+
</BottomSheetDragArea>
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(screen.queryByTestId("drag-pan")).toBeNull();
|
|
72
|
+
expect(screen.getByTestId("child")).toBeTruthy();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
platformSelect,
|
|
10
10
|
isAndroidTVPlatform,
|
|
11
11
|
isTvOSPlatform,
|
|
12
|
+
isApplePlatform,
|
|
12
13
|
} from "@applicaster/zapp-react-native-utils/reactUtils";
|
|
13
14
|
import {
|
|
14
15
|
isAudioItem,
|
|
@@ -60,6 +61,7 @@ import {
|
|
|
60
61
|
PlayerNativeSendCommand,
|
|
61
62
|
} from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand";
|
|
62
63
|
import { useAppData } from "@applicaster/zapp-react-native-redux";
|
|
64
|
+
import { PLAYER_EVENTS } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/const";
|
|
63
65
|
|
|
64
66
|
type Props = {
|
|
65
67
|
Player: React.ComponentType<any>;
|
|
@@ -188,12 +190,10 @@ const renderApplePlayer = ({
|
|
|
188
190
|
return null;
|
|
189
191
|
}
|
|
190
192
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
+
// Title/Subtitle are not resolved here: `ProgramInfo` reads them from the
|
|
194
|
+
// player manager's content channel via `usePlayerContent`.
|
|
193
195
|
return (
|
|
194
196
|
<ProgramInfo
|
|
195
|
-
title={title}
|
|
196
|
-
subtitle={summary}
|
|
197
197
|
plugin_configuration={pluginConfiguration}
|
|
198
198
|
videoStyle={videoStyle}
|
|
199
199
|
/>
|
|
@@ -282,12 +282,16 @@ const PlayerContainerComponent = (props: Props) => {
|
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
log_debug(
|
|
285
|
-
`onPlayNextPerformNextVideoPlay: delay completed, replaceTop to item: ${playNextOverlayState.entry.title}, id: ${playNextOverlayState.entry.id}`
|
|
285
|
+
`onPlayNextPerformNextVideoPlay: delay completed, replaceTop to item: ${playNextOverlayState.entry.title}, id: ${playNextOverlayState.entry.id}, outgoing item id: ${item?.id}`
|
|
286
286
|
);
|
|
287
287
|
|
|
288
|
+
playNextOverlayState.hidePlayNext();
|
|
289
|
+
|
|
290
|
+
playerEvent(PLAYER_EVENTS.PlayNextTriggered, { id: item?.id });
|
|
291
|
+
|
|
288
292
|
setIsLoadingNextVideo(true);
|
|
289
293
|
playEntry(playNextOverlayState.entry);
|
|
290
|
-
}, [playNextOverlayState]);
|
|
294
|
+
}, [playNextOverlayState, item?.id]);
|
|
291
295
|
|
|
292
296
|
const playNextData: Nullable<PlayNextData> = React.useMemo(() => {
|
|
293
297
|
if (!playNextOverlayState) {
|
|
@@ -495,7 +499,7 @@ const PlayerContainerComponent = (props: Props) => {
|
|
|
495
499
|
const isAudioContent = isAudioItem(item);
|
|
496
500
|
|
|
497
501
|
useEffect(() => {
|
|
498
|
-
if (!isAudioContent) {
|
|
502
|
+
if (!isAudioContent && isApplePlatform()) {
|
|
499
503
|
sendQuickBrickEvent(
|
|
500
504
|
QUICK_BRICK_EVENTS.IDLE_TIMER_DISABLED as QuickBrickEvent,
|
|
501
505
|
{
|
|
@@ -7,12 +7,11 @@ import * as React from "react";
|
|
|
7
7
|
import { Text, TextStyle, View, ViewStyle } from "react-native";
|
|
8
8
|
import { QBImage as Image } from "@applicaster/zapp-react-native-ui-components/Components/Image";
|
|
9
9
|
|
|
10
|
+
import { usePlayerContent } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayerContent";
|
|
10
11
|
import * as programStyles from "./styles";
|
|
11
12
|
|
|
12
13
|
type Props = {
|
|
13
14
|
logo?: string;
|
|
14
|
-
title: string;
|
|
15
|
-
subtitle: string;
|
|
16
15
|
isAudioContent: boolean;
|
|
17
16
|
styles: {};
|
|
18
17
|
plugin_configuration: {
|
|
@@ -28,13 +27,13 @@ const defaultColor = "#EFEFEF";
|
|
|
28
27
|
|
|
29
28
|
function ProgramInfoComponent({
|
|
30
29
|
logo,
|
|
31
|
-
title = "",
|
|
32
|
-
subtitle = "",
|
|
33
30
|
isAudioContent,
|
|
34
31
|
styles,
|
|
35
32
|
plugin_configuration = {},
|
|
36
33
|
videoStyle = {},
|
|
37
34
|
}: Props) {
|
|
35
|
+
const { title = "", summary: subtitle = "" } = usePlayerContent();
|
|
36
|
+
|
|
38
37
|
const platformKey = styleKeys.style_namespace;
|
|
39
38
|
|
|
40
39
|
const colorTitle =
|
|
@@ -17,16 +17,37 @@ jest.mock(
|
|
|
17
17
|
);
|
|
18
18
|
|
|
19
19
|
jest.mock("@applicaster/zapp-react-native-utils/reactUtils", () => ({
|
|
20
|
+
// Defaults to `false` for the hardware-back-handling tests below, which
|
|
21
|
+
// key off `isTV`. The ProgramInfo/content-context describe block below
|
|
22
|
+
// overrides this per test (`isTV()` is also what PlayerContainer's
|
|
23
|
+
// `playerContentValue` uses to pick TV vs mobile data keys, decoupled
|
|
24
|
+
// from `isTvOSPlatform` below).
|
|
20
25
|
isTV: jest.fn(() => false),
|
|
21
26
|
isAndroidTVPlatform: jest.fn(() => false),
|
|
22
|
-
isTvOSPlatform
|
|
27
|
+
// `isTvOSPlatform` backs a module-level constant in PlayerContainer.tsx that
|
|
28
|
+
// is read once at import time, so it must already be `true` here in order
|
|
29
|
+
// to exercise `renderApplePlayer`'s tvOS-only ProgramInfo branch in this
|
|
30
|
+
// test file. It does not affect the hardware-back-handling tests below,
|
|
31
|
+
// which key off `isTV` instead.
|
|
32
|
+
isTvOSPlatform: jest.fn(() => true),
|
|
33
|
+
isApplePlatform: jest.fn(() => true),
|
|
23
34
|
platformSelect: jest.fn(({ native }) => native),
|
|
24
35
|
}));
|
|
25
36
|
|
|
26
|
-
jest.mock("@applicaster/zapp-react-native-utils/playerUtils", () =>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
jest.mock("@applicaster/zapp-react-native-utils/playerUtils", () => {
|
|
38
|
+
// Keep the real data-key resolver (createConfigValueGetter,
|
|
39
|
+
// resolvePlayerContentText, VIDEO_PLAYER_CONTENT_DATA_KEYS) so the tvOS
|
|
40
|
+
// ProgramInfo data-key tests exercise actual resolution logic, not a stub.
|
|
41
|
+
const actual = jest.requireActual(
|
|
42
|
+
"@applicaster/zapp-react-native-utils/playerUtils"
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
...actual,
|
|
47
|
+
isAudioItem: jest.fn(() => false),
|
|
48
|
+
isInlineTV: jest.fn(() => false),
|
|
49
|
+
};
|
|
50
|
+
});
|
|
30
51
|
|
|
31
52
|
jest.mock(
|
|
32
53
|
"@applicaster/zapp-react-native-tvos-ui-components/Components/TVEventHandlerComponent",
|
|
@@ -75,6 +96,11 @@ jest.mock("@applicaster/zapp-react-native-bridge/QuickBrick", () => ({
|
|
|
75
96
|
sendQuickBrickEvent: jest.fn(),
|
|
76
97
|
}));
|
|
77
98
|
|
|
99
|
+
// Stubbed so PlayerContainer can render without ProgramInfo's own
|
|
100
|
+
// redux/style plumbing. It no longer needs to capture Title/Subtitle: those
|
|
101
|
+
// come from the player manager's content channel, and the key-set selection
|
|
102
|
+
// they depend on is covered directly in
|
|
103
|
+
// `playerManager/__tests__/playerContent.test.ts`.
|
|
78
104
|
jest.mock("../ProgramInfo", () => ({
|
|
79
105
|
ProgramInfo: () => null,
|
|
80
106
|
}));
|
|
@@ -142,14 +168,33 @@ jest.mock(
|
|
|
142
168
|
jest.mock("../PlayerContainerContext", () => {
|
|
143
169
|
const ReactActual = require("react");
|
|
144
170
|
|
|
171
|
+
const PlayerContainerContext = ReactActual.createContext({
|
|
172
|
+
isLanguageOverlayVisible: false,
|
|
173
|
+
bottomFocusableId: "bottom",
|
|
174
|
+
showComponentsContainer: false,
|
|
175
|
+
refs: null,
|
|
176
|
+
});
|
|
177
|
+
|
|
145
178
|
return {
|
|
146
|
-
PlayerContainerContext
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
})
|
|
152
|
-
PlayerContainerContextProvider: ({ children }) =>
|
|
179
|
+
PlayerContainerContext,
|
|
180
|
+
// Mirrors just enough of the real provider for these tests: renders an
|
|
181
|
+
// actual `PlayerContainerContext.Provider` so the real
|
|
182
|
+
// `PlayerContainerContext.Consumer` inside PlayerContainer.tsx sees the
|
|
183
|
+
// container fields a real provider would give it, instead of a bare
|
|
184
|
+
// `({ children }) => children` passthrough.
|
|
185
|
+
PlayerContainerContextProvider: ({ children }: any) =>
|
|
186
|
+
ReactActual.createElement(
|
|
187
|
+
PlayerContainerContext.Provider,
|
|
188
|
+
{
|
|
189
|
+
value: {
|
|
190
|
+
isLanguageOverlayVisible: false,
|
|
191
|
+
bottomFocusableId: "bottom",
|
|
192
|
+
showComponentsContainer: false,
|
|
193
|
+
refs: null,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
children
|
|
197
|
+
),
|
|
153
198
|
};
|
|
154
199
|
});
|
|
155
200
|
|
|
@@ -200,7 +245,14 @@ const { __player: mockPlayer } = jest.requireMock(
|
|
|
200
245
|
"@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer"
|
|
201
246
|
);
|
|
202
247
|
|
|
203
|
-
const
|
|
248
|
+
const { isTV: mockIsTV } = jest.requireMock(
|
|
249
|
+
"@applicaster/zapp-react-native-utils/reactUtils"
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// Renders children so that `renderApplePlayer`'s returned <ProgramInfo>
|
|
253
|
+
// element (passed as this component's `children` prop) actually mounts —
|
|
254
|
+
// otherwise React never invokes the ProgramInfo function component at all.
|
|
255
|
+
const Player = React.forwardRef((props: any, _ref) => props.children ?? null);
|
|
204
256
|
|
|
205
257
|
const item = {
|
|
206
258
|
id: "entry-1",
|
|
@@ -234,6 +286,7 @@ const pressHardwareBack = () => {
|
|
|
234
286
|
describe("PlayerContainer hardware back handling", () => {
|
|
235
287
|
beforeEach(() => {
|
|
236
288
|
jest.clearAllMocks();
|
|
289
|
+
mockIsTV.mockReturnValue(false);
|
|
237
290
|
mockBackHandlerRef.current = null;
|
|
238
291
|
mockPlayer.getPluginConfiguration.mockReturnValue({});
|
|
239
292
|
mockPlayer.isPaused.mockReturnValue(false);
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "@applicaster/zapp-react-native-utils/appUtils/playerManager";
|
|
6
6
|
import { setUserCellPlayerMutedPreference } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/userCellPlayerMutedPreference";
|
|
7
7
|
import { playerFactory } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerFactory";
|
|
8
|
-
import { PlayerRole } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/
|
|
8
|
+
import { PlayerRole } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/const";
|
|
9
9
|
import { loggerLiveImageManager } from "./loggerHelper";
|
|
10
10
|
import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
|
|
11
11
|
import {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-ui-components",
|
|
3
|
-
"version": "16.0.0-alpha.
|
|
3
|
+
"version": "16.0.0-alpha.9506982263",
|
|
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": "16.0.0-alpha.
|
|
32
|
-
"@applicaster/zapp-react-native-bridge": "16.0.0-alpha.
|
|
33
|
-
"@applicaster/zapp-react-native-redux": "16.0.0-alpha.
|
|
34
|
-
"@applicaster/zapp-react-native-utils": "16.0.0-alpha.
|
|
31
|
+
"@applicaster/applicaster-types": "16.0.0-alpha.9506982263",
|
|
32
|
+
"@applicaster/zapp-react-native-bridge": "16.0.0-alpha.9506982263",
|
|
33
|
+
"@applicaster/zapp-react-native-redux": "16.0.0-alpha.9506982263",
|
|
34
|
+
"@applicaster/zapp-react-native-utils": "16.0.0-alpha.9506982263",
|
|
35
35
|
"fast-json-stable-stringify": "^2.1.0",
|
|
36
36
|
"promise": "^8.3.0",
|
|
37
37
|
"url": "^0.11.0",
|