@applicaster/zapp-react-native-ui-components 13.0.0-rc.99 → 14.0.0-alpha.1054425138

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.
Files changed (30) hide show
  1. package/Components/AudioPlayer/AudioPlayer.tsx +0 -11
  2. package/Components/AudioPlayer/AudioPlayerLayout.tsx +6 -7
  3. package/Components/AudioPlayer/helpers.tsx +0 -2
  4. package/Components/GeneralContentScreen/GeneralContentScreen.tsx +3 -0
  5. package/Components/GeneralContentScreen/utils/useCurationAPI.ts +4 -2
  6. package/Components/GeneralContentScreen/utils/useEventAlerts.ts +30 -0
  7. package/Components/MasterCell/utils/behaviorProvider.ts +65 -10
  8. package/Components/MasterCell/utils/index.ts +13 -3
  9. package/Components/ModalComponent/BottomSheetModalContent.tsx +32 -46
  10. package/Components/ModalComponent/Button/index.tsx +25 -29
  11. package/Components/ModalComponent/Header/index.tsx +9 -8
  12. package/Components/PlayerContainer/PlayerContainer.tsx +4 -4
  13. package/Components/River/ComponentsMap/ComponentsMap.tsx +11 -3
  14. package/Components/River/RiverItem.tsx +6 -2
  15. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +7 -1
  16. package/Components/RouteManager/TestId.tsx +1 -5
  17. package/Components/RouteManager/__tests__/__snapshots__/routeManager.test.js.snap +0 -1
  18. package/Components/RouteManager/__tests__/testId.test.js +0 -4
  19. package/Components/Screen/TV/__tests__/index.web.test.tsx +26 -0
  20. package/Components/Screen/__tests__/Screen.test.tsx +22 -14
  21. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +2 -2
  22. package/Components/VideoModal/ModalAnimation/AnimationComponent.tsx +1 -2
  23. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +1 -0
  24. package/Components/VideoModal/ModalAnimation/utils.ts +3 -9
  25. package/Components/VideoModal/PlayerWrapper.tsx +9 -19
  26. package/Decorators/RiverFeedLoader/index.tsx +8 -2
  27. package/Decorators/RiverFeedLoader/utils/index.ts +7 -2
  28. package/Decorators/ZappPipesDataConnector/index.tsx +21 -2
  29. package/package.json +5 -6
  30. package/Decorators/Navigator/__tests__/react-router-native-mock.js +0 -11
@@ -22,8 +22,6 @@ type Props = {
22
22
  audio_player_title_color?: string;
23
23
  audio_player_summary_color?: string;
24
24
  audio_player_rtl?: boolean;
25
- magic_background?: boolean;
26
- audio_player_background_image_query?: string;
27
25
  audio_player_background_image_default_color?: string;
28
26
  start_time?: string;
29
27
  end_time?: string;
@@ -34,8 +32,6 @@ type Props = {
34
32
  audio_player_title_color?: string;
35
33
  audio_player_summary_color?: string;
36
34
  audio_player_rtl?: string;
37
- magic_background?: string;
38
- audio_player_background_image_query?: string;
39
35
  audio_player_background_image_default_color?: string;
40
36
  audio_player_background_image?: string;
41
37
  audio_player_artwork_aspect_ratio?: string;
@@ -76,11 +72,6 @@ export function AudioPlayer(props: Props) {
76
72
  const artworkAspectRatio = getProp("audio_player_artwork_aspect_ratio");
77
73
  const channelIcon = getProp("audio_player_channel_icon");
78
74
  const rtlFlag = getProp("audio_player_rtl");
79
- const magicBackground = getProp("magic_background");
80
-
81
- const audioPlayerBackgroundImageQuery = getProp(
82
- "audio_player_background_image_query"
83
- );
84
75
 
85
76
  const audioPlayerBackgroundImageDefaultColor = getProp(
86
77
  "audio_player_background_image_default_color"
@@ -162,8 +153,6 @@ export function AudioPlayer(props: Props) {
162
153
  runTimeFontSize,
163
154
  artworkAspectRatio,
164
155
  channelIcon,
165
- magicBackground,
166
- audioPlayerBackgroundImageQuery,
167
156
  audioPlayerBackgroundImageDefaultColor,
168
157
  };
169
158
  }, [getProp]);
@@ -32,7 +32,7 @@ export function AudioPlayerLayout({ artwork, config, children, style }: Props) {
32
32
 
33
33
  const { isRTL, backgroundColor, backgroundImage } = config;
34
34
 
35
- const backgroundImg = { uri: backgroundImage };
35
+ const backgroundImageSource = { uri: backgroundImage };
36
36
 
37
37
  const backgroundColorStyle = backgroundImage
38
38
  ? "transparent"
@@ -103,9 +103,8 @@ export function AudioPlayerLayout({ artwork, config, children, style }: Props) {
103
103
  justifyContent: "center",
104
104
  },
105
105
  android_tv: {
106
- position: "absolute",
107
- width: 1920,
108
- height: 1080,
106
+ width: "100%",
107
+ height: "100%",
109
108
  alignItems: "center",
110
109
  justifyContent: "center",
111
110
  },
@@ -146,9 +145,9 @@ export function AudioPlayerLayout({ artwork, config, children, style }: Props) {
146
145
  },
147
146
  });
148
147
 
149
- const audioPlayerLayoutTV = backgroundImg?.uri ? (
148
+ const audioPlayerLayoutTV = backgroundImageSource?.uri ? (
150
149
  <ImageBackground
151
- source={backgroundImg}
150
+ source={backgroundImageSource}
152
151
  style={backgroundImgStyles}
153
152
  resizeMode="cover"
154
153
  >
@@ -178,7 +177,7 @@ export function AudioPlayerLayout({ artwork, config, children, style }: Props) {
178
177
  ]}
179
178
  >
180
179
  <ImageBackground
181
- source={backgroundImg}
180
+ source={backgroundImageSource}
182
181
  style={backgroundImgStyles}
183
182
  resizeMode="cover"
184
183
  >
@@ -4,8 +4,6 @@ const defaults = {
4
4
  audio_player_background_color: "black",
5
5
  audio_player_artwork_aspect_ratio: "1:1",
6
6
  audio_player_rtl: false,
7
- magic_background: false,
8
- audio_player_background_image_query: "",
9
7
  audio_player_background_image_default_color: "",
10
8
  };
11
9
 
@@ -11,6 +11,7 @@ import { allSettled } from "promise";
11
11
  import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
12
12
  import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
13
13
  import { ScreenTrackedViewPositionsContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ScreenTrackedViewPositionsContext";
14
+ import { useEventAlerts } from "./utils/useEventAlerts";
14
15
 
15
16
  const { log_info } = createLogger({
16
17
  category: "ScreenContainer",
@@ -103,6 +104,8 @@ export const GeneralContentScreen = ({
103
104
  [typeof cellTapAction === "function" ? cellTapAction : onCellTapAction]
104
105
  );
105
106
 
107
+ useEventAlerts(screenData);
108
+
106
109
  if (!isReady || isNilOrEmpty(components || uiComponents)) return null;
107
110
 
108
111
  return (
@@ -3,8 +3,10 @@ import { all, equals, path, prop, isEmpty, pluck, values } from "ramda";
3
3
  import { useEffect, useMemo } from "react";
4
4
  import { useDispatch } from "react-redux";
5
5
 
6
- import { useLayoutPresets } from "@applicaster/zapp-react-native-redux/hooks/useLayoutPresets";
7
- import { useZappPipesFeeds } from "@applicaster/zapp-react-native-redux/hooks/useZappPipesFeeds";
6
+ import {
7
+ useLayoutPresets,
8
+ useZappPipesFeeds,
9
+ } from "@applicaster/zapp-react-native-redux/hooks";
8
10
  import { loadPipesData } from "@applicaster/zapp-react-native-redux/ZappPipes";
9
11
  import { isEmptyOrNil } from "@applicaster/zapp-react-native-utils/cellUtils";
10
12
  import { Categories } from "./logger";
@@ -0,0 +1,30 @@
1
+ import React from "react";
2
+ import { showAlertDialog } from "@applicaster/zapp-react-native-utils/alertUtils";
3
+ import { TOGGLE_FLAG_MAX_ITEMS_REACHED_EVENT } from "@applicaster/zapp-react-native-utils/actionsExecutor/consts";
4
+ import { useLocalizedStrings } from "@applicaster/zapp-react-native-utils/localizationUtils";
5
+ import { useIsScreenActive } from "@applicaster/zapp-react-native-utils/reactHooks";
6
+ import { useSubscriberFor } from "@applicaster/zapp-react-native-utils/reactHooks/useSubscriberFor";
7
+
8
+ export const useEventAlerts = (screenData: ZappRiver) => {
9
+ const localizations = useLocalizedStrings({
10
+ localizations: screenData?.localizations || {},
11
+ });
12
+
13
+ const isActive = useIsScreenActive();
14
+
15
+ const onMaxTagsReached = React.useCallback(() => {
16
+ // We can't skip subscribe hook call, so we have to check.
17
+ if (!isActive || !localizations?.msg_maximum_selection_reached_message) {
18
+ return;
19
+ }
20
+
21
+ showAlertDialog({
22
+ title: "",
23
+ message: localizations.msg_maximum_selection_reached_message,
24
+ okButtonText:
25
+ localizations.msg_maximum_selection_reached_message_ok_button || "OK",
26
+ });
27
+ }, [localizations, isActive]);
28
+
29
+ useSubscriberFor(TOGGLE_FLAG_MAX_ITEMS_REACHED_EVENT, onMaxTagsReached);
30
+ };
@@ -7,22 +7,48 @@ import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerM
7
7
  import { BehaviorSubject } from "rxjs";
8
8
  import { masterCellLogger } from "../logger";
9
9
  import get from "lodash/get";
10
+ import { ScreenMultiSelectProvider } from "@applicaster/zapp-react-native-bridge/ZappStorage/ScreenStateMultiSelectProvider";
11
+ import { ScreenSingleValueProvider } from "@applicaster/zapp-react-native-bridge/ZappStorage/ScreenSingleValueProvider";
12
+ import { useRoute } from "@applicaster/zapp-react-native-utils/reactHooks";
10
13
 
11
- const parseContextKey = (key: string): string | null => {
12
- if (!key?.startsWith("@{ctx/")) return null;
14
+ const parseContextKey = (
15
+ key: string,
16
+ context: string = "ctx"
17
+ ): string | null => {
18
+ if (!key?.startsWith(`@{${context}/`)) return null;
13
19
 
14
- return key.substring("@{ctx/".length, key.length - 1);
20
+ return key.substring(`@{${context}/`.length, key.length - 1);
15
21
  };
16
22
 
17
23
  const getDataSourceProvider = (
18
- behavior: Behavior
24
+ behavior: Behavior,
25
+ screenRoute: string
19
26
  ): BehaviorSubject<string[] | string> | null => {
20
27
  if (!behavior) return null;
21
28
 
22
29
  const selection = String(behavior.current_selection);
30
+ const screenKey = parseContextKey(selection, "screen");
31
+
32
+ if (screenKey) {
33
+ if (behavior.select_mode === "multi") {
34
+ return ScreenMultiSelectProvider.getProvider(
35
+ screenKey,
36
+ screenRoute
37
+ ).getObservable();
38
+ }
39
+
40
+ if (behavior.select_mode === "single") {
41
+ return ScreenSingleValueProvider.getProvider(
42
+ screenKey,
43
+ screenRoute
44
+ ).getObservable();
45
+ }
46
+ }
47
+
23
48
  const contextKey = parseContextKey(selection);
24
49
 
25
50
  if (contextKey) {
51
+ // TODO: Add storage scope to behavior
26
52
  if (behavior.select_mode === "multi") {
27
53
  return StorageMultiSelectProvider.getProvider(contextKey).getObservable();
28
54
  }
@@ -41,6 +67,7 @@ const getDataSourceProvider = (
41
67
 
42
68
  export const useBehaviorUpdate = (behavior: Behavior) => {
43
69
  const [lastUpdate, setLastUpdate] = React.useState<number | null>(null);
70
+ const screenRoute = useRoute()?.pathname || "";
44
71
  const player = usePlayer();
45
72
 
46
73
  const triggerUpdate = () => setLastUpdate(Date.now());
@@ -48,7 +75,7 @@ export const useBehaviorUpdate = (behavior: Behavior) => {
48
75
  useEffect(() => {
49
76
  if (!behavior) return;
50
77
 
51
- const dataSource = getDataSourceProvider(behavior);
78
+ const dataSource = getDataSourceProvider(behavior, screenRoute);
52
79
 
53
80
  if (dataSource) {
54
81
  const subscription = dataSource.subscribe(triggerUpdate);
@@ -72,10 +99,15 @@ export const useBehaviorUpdate = (behavior: Behavior) => {
72
99
 
73
100
  // We cant use async in this function (its inside render),
74
101
  // so we rely on useBehaviorUpdate to update current value and trigger re-render
75
- export const isCellSelected = (
76
- item: ZappEntry,
77
- behavior?: Behavior
78
- ): boolean => {
102
+ export const isCellSelected = ({
103
+ item,
104
+ screenRoute,
105
+ behavior,
106
+ }: {
107
+ item: ZappEntry;
108
+ screenRoute: string;
109
+ behavior?: Behavior;
110
+ }): boolean => {
79
111
  if (!behavior) return false;
80
112
 
81
113
  const id = behavior.selector ? get(item, behavior.selector) : item.id;
@@ -99,7 +131,30 @@ export const isCellSelected = (
99
131
  }
100
132
 
101
133
  const selection = String(behavior.current_selection);
102
- const contextKey = parseContextKey(selection);
134
+
135
+ const screenKey = parseContextKey(selection, "screen");
136
+
137
+ if (screenKey) {
138
+ if (behavior.select_mode === "single") {
139
+ const selectedItem = ScreenSingleValueProvider.getProvider(
140
+ screenKey,
141
+ screenRoute
142
+ ).getValue();
143
+
144
+ return selectedItem === String(id);
145
+ }
146
+
147
+ if (behavior.select_mode === "multi") {
148
+ const selectedItems = ScreenMultiSelectProvider.getProvider(
149
+ screenKey,
150
+ screenRoute
151
+ ).getSelectedItems();
152
+
153
+ return selectedItems?.includes(String(id));
154
+ }
155
+ }
156
+
157
+ const contextKey = parseContextKey(selection, "ctx");
103
158
 
104
159
  if (contextKey) {
105
160
  if (behavior.select_mode === "single") {
@@ -8,6 +8,7 @@ import { masterCellLogger } from "../logger";
8
8
  import { getCellState } from "../../Cell/utils";
9
9
  import { getColorFromData } from "@applicaster/zapp-react-native-utils/cellUtils";
10
10
  import { isCellSelected, useBehaviorUpdate } from "./behaviorProvider";
11
+ import { useRoute } from "@applicaster/zapp-react-native-utils/reactHooks";
11
12
 
12
13
  const hasElementSpecificViewType = (viewType) => (element) => {
13
14
  if (R.isNil(element)) {
@@ -190,8 +191,16 @@ export const getFocusedButtonId = (focusable) => {
190
191
  });
191
192
  };
192
193
 
193
- export const isSelected = (item: ZappEntry, behavior?: Behavior) => {
194
- return isCellSelected(item, behavior);
194
+ export const isSelected = ({
195
+ item,
196
+ screenRoute,
197
+ behavior,
198
+ }: {
199
+ item: ZappEntry;
200
+ screenRoute: string;
201
+ behavior?: Behavior;
202
+ }) => {
203
+ return isCellSelected({ item, screenRoute, behavior });
195
204
  };
196
205
 
197
206
  export const useCellState = ({
@@ -204,9 +213,10 @@ export const useCellState = ({
204
213
  focused: boolean;
205
214
  }): CellState => {
206
215
  const lastUpdate = useBehaviorUpdate(behavior);
216
+ const router = useRoute();
207
217
 
208
218
  const _isSelected = useMemo(
209
- () => isSelected(item, behavior),
219
+ () => isSelected({ item, screenRoute: router?.pathname, behavior }),
210
220
  [behavior, item, lastUpdate]
211
221
  );
212
222
 
@@ -1,11 +1,5 @@
1
- import React, {
2
- useCallback,
3
- useEffect,
4
- useMemo,
5
- useRef,
6
- useState,
7
- } from "react";
8
- import { View, LayoutChangeEvent, ScrollView } from "react-native";
1
+ import React, { useCallback, useEffect, useMemo, useRef } from "react";
2
+ import { View, ScrollView } from "react-native";
9
3
  import { Button } from "./Button";
10
4
  import { ItemIconProps } from "./Button/ItemIcon";
11
5
  import { ItemProps } from "./Button/Item";
@@ -15,7 +9,8 @@ import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
15
9
 
16
10
  import type { PluginConfiguration } from "./";
17
11
 
18
- import { ModalHeader } from "./Header";
12
+ import { ModalHeader as DefaultModalHeader } from "./Header";
13
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
19
14
 
20
15
  type ModalComponentProps = {
21
16
  items: any[];
@@ -27,6 +22,7 @@ type ModalComponentProps = {
27
22
  title?: string;
28
23
  maxHeight?: number;
29
24
  dismiss: () => void;
25
+ headerComponent?: React.ComponentType;
30
26
  buttonComponent?: React.ComponentType;
31
27
  iconProps?: ItemIconProps | ((theme: PluginConfiguration) => ItemIconProps);
32
28
  itemProps?:
@@ -48,33 +44,25 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
48
44
  const {
49
45
  items,
50
46
  currentRoute,
51
- maxHeight,
52
47
  current_selection = null,
53
48
  onPress,
54
49
  dismiss,
55
50
  summary,
56
51
  title,
52
+ headerComponent: ModalHeader = DefaultModalHeader,
57
53
  buttonComponent: ButtonComponent = Button,
58
- getSelectedItemIcon = (theme) =>
59
- theme.modal_bottom_sheet_item_selected_icon,
54
+ getSelectedItemIcon = ({
55
+ modal_bottom_sheet_item_selected_icon,
56
+ }: BaseThemePropertiesMobile) => modal_bottom_sheet_item_selected_icon,
60
57
  getDefaultItemIcon = () => null,
61
58
  iconPlacement,
62
59
  } = props;
63
60
 
64
- const [headerHeight, setHeaderHeight] = useState(0);
65
61
  const route = useRef(currentRoute);
66
62
  const theme = useTheme<BaseThemePropertiesMobile>();
67
63
  const paddingTop = Number(theme.modal_bottom_sheet_padding_top);
68
64
  const paddingBottom = Number(theme.modal_bottom_sheet_padding_bottom);
69
65
 
70
- const maxContentHeight = maxHeight
71
- ? maxHeight - headerHeight - paddingTop
72
- : undefined;
73
-
74
- const onHeaderLayout = useCallback((event: LayoutChangeEvent) => {
75
- setHeaderHeight(event.nativeEvent.layout.height);
76
- }, []);
77
-
78
66
  useEffect(() => {
79
67
  if (currentRoute !== route.current) {
80
68
  props.dismiss();
@@ -101,46 +89,44 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
101
89
  [onPress, dismiss]
102
90
  );
103
91
 
92
+ const bottomInset = useSafeAreaInsets().bottom;
93
+
104
94
  return (
105
95
  <View
106
96
  style={{
107
- maxWidth: props.width,
108
- paddingTop,
97
+ maxHeight: props.maxHeight,
98
+ paddingTop: paddingTop,
109
99
  }}
110
100
  >
111
101
  <ModalHeader
112
- width={props.width}
113
102
  dismiss={dismiss}
114
103
  configuration={theme}
115
- onLayout={onHeaderLayout}
116
104
  summary={summary}
117
105
  title={title}
118
106
  />
119
107
  <ScrollView
120
- bounces={false}
121
- style={{ maxHeight: maxContentHeight }}
122
108
  contentContainerStyle={{
123
- paddingBottom,
124
- paddingTop,
109
+ paddingBottom: paddingBottom + bottomInset,
125
110
  }}
126
111
  >
127
- {items.map((item, index) => (
128
- <ButtonComponent
129
- key={index}
130
- width={props.width}
131
- configuration={theme}
132
- selectedItem={current_selection}
133
- item={item}
134
- onPress={handlePress}
135
- label={theme[item?.label] ?? item?.label}
136
- iconBaseProps={iconBaseProps}
137
- itemBaseProps={itemBaseProps}
138
- labelBaseProps={labelBaseProps}
139
- selectedItemIcon={getSelectedItemIcon(theme)}
140
- defaultItemIcon={getDefaultItemIcon(theme)}
141
- iconPlacement={iconPlacement}
142
- />
143
- ))}
112
+ {items.map((item, index) =>
113
+ item ? (
114
+ <ButtonComponent
115
+ key={index}
116
+ configuration={theme}
117
+ selectedItem={current_selection}
118
+ item={item}
119
+ onPress={handlePress}
120
+ label={theme[item?.label] ?? item?.label}
121
+ iconBaseProps={iconBaseProps}
122
+ itemBaseProps={itemBaseProps}
123
+ labelBaseProps={labelBaseProps}
124
+ selectedItemIcon={getSelectedItemIcon(theme)}
125
+ defaultItemIcon={getDefaultItemIcon(theme)}
126
+ iconPlacement={iconPlacement}
127
+ />
128
+ ) : null
129
+ )}
144
130
  </ScrollView>
145
131
  </View>
146
132
  );
@@ -7,7 +7,6 @@ import { defaultSelectedAsset } from "./assets";
7
7
 
8
8
  type ButtonProps = {
9
9
  configuration: any;
10
- width: number;
11
10
  selectedItem?: any;
12
11
  item: any; // Adjust type as needed
13
12
  onPress: (item: any) => void; // Adjust type as needed
@@ -33,7 +32,6 @@ export function Button({
33
32
  item,
34
33
  onPress,
35
34
  configuration,
36
- width,
37
35
  iconBaseProps,
38
36
  itemBaseProps: itemProps,
39
37
  labelBaseProps,
@@ -65,34 +63,32 @@ export function Button({
65
63
  if (disabled) return null;
66
64
 
67
65
  return (
68
- <View style={{ maxWidth: width }}>
69
- <TouchableOpacity
70
- activeOpacity={1}
71
- onPress={() => onPress(item)}
72
- onPressIn={() => setFocused(true)}
73
- onPressOut={() => setFocused(false)}
74
- >
75
- <Item {...itemProps} focused={focused} selected={selected}>
76
- <View style={styles.label_icon_container}>
77
- {iconPlacement === "left" && renderItemIcon}
66
+ <TouchableOpacity
67
+ activeOpacity={1}
68
+ onPress={() => onPress(item)}
69
+ onPressIn={() => setFocused(true)}
70
+ onPressOut={() => setFocused(false)}
71
+ >
72
+ <Item {...itemProps} focused={focused} selected={selected}>
73
+ <View style={styles.label_icon_container}>
74
+ {iconPlacement === "left" && renderItemIcon}
78
75
 
79
- {label ? (
80
- <ItemLabel
81
- {...labelBaseProps}
82
- label={label ?? null}
83
- focused={focused}
84
- selected={selected}
85
- />
86
- ) : null}
87
- </View>
76
+ {label ? (
77
+ <ItemLabel
78
+ {...labelBaseProps}
79
+ label={label ?? null}
80
+ focused={focused}
81
+ selected={selected}
82
+ />
83
+ ) : null}
84
+ </View>
88
85
 
89
- {selected ? (
90
- <ItemIcon {...iconBaseProps} asset={selectedItemIconPropsAssets} />
91
- ) : (
92
- iconPlacement === "right" && renderItemIcon
93
- )}
94
- </Item>
95
- </TouchableOpacity>
96
- </View>
86
+ {selected ? (
87
+ <ItemIcon {...iconBaseProps} asset={selectedItemIconPropsAssets} />
88
+ ) : (
89
+ iconPlacement === "right" && renderItemIcon
90
+ )}
91
+ </Item>
92
+ </TouchableOpacity>
97
93
  );
98
94
  }
@@ -8,13 +8,18 @@ import { PluginConfiguration } from "../index";
8
8
  type Props = {
9
9
  dismiss: () => void;
10
10
  configuration: PluginConfiguration;
11
- width: number;
12
11
  onLayout?: (event: LayoutChangeEvent) => void;
13
12
  summary?: string;
14
13
  title?: string;
15
14
  };
16
15
 
17
16
  const styles = StyleSheet.create({
17
+ noFlex: {
18
+ flex: 0,
19
+ },
20
+ flex: {
21
+ flex: 1,
22
+ },
18
23
  container: {
19
24
  flexDirection: "row",
20
25
  alignItems: "center",
@@ -22,7 +27,7 @@ const styles = StyleSheet.create({
22
27
  });
23
28
 
24
29
  export function ModalHeader(props: Props) {
25
- const { configuration, dismiss, width, onLayout, summary, title } = props;
30
+ const { configuration, dismiss, onLayout, summary, title } = props;
26
31
 
27
32
  const closeButtonProps = useMemo<CloseButtonProps>(
28
33
  () => ({
@@ -55,15 +60,11 @@ export function ModalHeader(props: Props) {
55
60
 
56
61
  return (
57
62
  <View onLayout={onLayout} style={styles.container}>
58
- <View
59
- style={{
60
- width: width - buttonsContainerWidth,
61
- }}
62
- >
63
+ <View style={styles.flex}>
63
64
  {title ? <Title {...titleProps} /> : null}
64
65
  {summary ? <Title {...summaryProps} /> : null}
65
66
  </View>
66
- <View style={{ width: buttonsContainerWidth }}>
67
+ <View style={[styles.noFlex, { width: buttonsContainerWidth }]}>
67
68
  <CloseButton {...closeButtonProps} />
68
69
  </View>
69
70
  </View>
@@ -11,6 +11,7 @@ import {
11
11
  platformSelect,
12
12
  isAndroidTVPlatform,
13
13
  } from "@applicaster/zapp-react-native-utils/reactUtils";
14
+ import { isAudioItem } from "@applicaster/zapp-react-native-utils/playerUtils";
14
15
 
15
16
  import { TVEventHandlerComponent } from "@applicaster/zapp-react-native-tvos-ui-components/Components/TVEventHandlerComponent";
16
17
  import { usePrevious } from "@applicaster/zapp-react-native-utils/reactHooks/utils";
@@ -493,9 +494,7 @@ const PlayerContainerComponent = (props: Props) => {
493
494
  // TODO: Skip hook call on TV platform
494
495
  useBackHandler(backHandler);
495
496
 
496
- const isAudioContent =
497
- typeof item?.content?.type === "string" &&
498
- item?.content?.type.includes("audio");
497
+ const isAudioContent = isAudioItem(item);
499
498
 
500
499
  useEffect(() => {
501
500
  if (!isAudioContent) {
@@ -597,7 +596,7 @@ const PlayerContainerComponent = (props: Props) => {
597
596
  );
598
597
  }
599
598
 
600
- if (screen_background_color) {
599
+ if (screen_background_color && mode !== VideoModalMode.FULLSCREEN) {
601
600
  updatedStyles.playerScreen.backgroundColor = screen_background_color;
602
601
  }
603
602
 
@@ -615,6 +614,7 @@ const PlayerContainerComponent = (props: Props) => {
615
614
  tv_component_container_height,
616
615
  screen_background_color,
617
616
  isInlineTV,
617
+ mode,
618
618
  ]);
619
619
 
620
620
  const applePlayerProps = {
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import * as R from "ramda";
3
- import { View, StyleSheet, Platform, FlatList } from "react-native";
3
+ import { View, StyleSheet, FlatList } from "react-native";
4
4
  import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
5
5
  import { RiverItem } from "../RiverItem";
6
6
  import { RiverFooter } from "../RiverFooter";
@@ -24,6 +24,10 @@ import { withComponentsMapProvider } from "@applicaster/zapp-react-native-ui-com
24
24
  import { useScreenContextV2 } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
25
25
  import { useShallow } from "zustand/react/shallow";
26
26
 
27
+ import { isAndroidPlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
28
+
29
+ const isAndroid = isAndroidPlatform();
30
+
27
31
  type Props = {
28
32
  feed: ZappFeed;
29
33
  groupId?: string;
@@ -273,12 +277,12 @@ function ComponentsMapComponent(props: Props) {
273
277
  }}
274
278
  // Fix for WebView rerender crashes on Android API 28+
275
279
  // https://github.com/react-native-webview/react-native-webview/issues/1915#issuecomment-964035468
276
- overScrollMode={Platform.OS === "android" ? "never" : "auto"}
280
+ overScrollMode={isAndroid ? "never" : "auto"}
277
281
  scrollIndicatorInsets={scrollIndicatorInsets}
278
282
  extraData={feed}
279
283
  stickyHeaderIndices={stickyHeaderIndices}
280
284
  onLayout={handleOnLayout}
281
- removeClippedSubviews
285
+ removeClippedSubviews={isAndroid}
282
286
  initialNumToRender={3}
283
287
  maxToRenderPerBatch={10}
284
288
  windowSize={12}
@@ -299,6 +303,10 @@ function ComponentsMapComponent(props: Props) {
299
303
  onMomentumScrollEnd={_onMomentumScrollEnd}
300
304
  onScrollEndDrag={_onScrollEndDrag}
301
305
  scrollEventThrottle={16}
306
+ maintainVisibleContentPosition={{
307
+ minIndexForVisible: 0,
308
+ autoscrollToTopThreshold: 10,
309
+ }}
302
310
  {...scrollViewExtraProps}
303
311
  />
304
312
  </ViewportTracker>
@@ -48,16 +48,20 @@ const useLoadingState = (
48
48
  loadingState: RiverItemType["loadingState"]
49
49
  ) => {
50
50
  const [readyToBeDisplayed, setReadyToBeDisplayed] = React.useState(
51
- loadingState.getValue().index + 1 === currentIndex
51
+ loadingState.getValue().index + 1 >= currentIndex
52
52
  );
53
53
 
54
54
  useEffect(() => {
55
55
  const subscription = loadingState.subscribe(({ index }) => {
56
- if (index + 1 === currentIndex) {
56
+ if (index + 1 >= currentIndex) {
57
57
  setReadyToBeDisplayed(true);
58
58
  }
59
59
  });
60
60
 
61
+ if (loadingState.getValue().index + 1 >= currentIndex) {
62
+ setReadyToBeDisplayed(true);
63
+ }
64
+
61
65
  return () => {
62
66
  subscription.unsubscribe();
63
67
  };
@@ -135,6 +135,12 @@ exports[`componentsMap renders renders components map correctly 1`] = `
135
135
  getItemCount={[Function]}
136
136
  initialNumToRender={3}
137
137
  keyExtractor={[Function]}
138
+ maintainVisibleContentPosition={
139
+ {
140
+ "autoscrollToTopThreshold": 10,
141
+ "minIndexForVisible": 0,
142
+ }
143
+ }
138
144
  maxToRenderPerBatch={10}
139
145
  onContentSizeChange={[Function]}
140
146
  onLayout={[Function]}
@@ -145,7 +151,7 @@ exports[`componentsMap renders renders components map correctly 1`] = `
145
151
  onScrollEndDrag={[Function]}
146
152
  overScrollMode="auto"
147
153
  refreshControl={null}
148
- removeClippedSubviews={true}
154
+ removeClippedSubviews={false}
149
155
  renderItem={[Function]}
150
156
  scrollEventThrottle={16}
151
157
  scrollIndicatorInsets={
@@ -12,11 +12,7 @@ const styles = StyleSheet.create({
12
12
 
13
13
  export function TestId({ screenId, children }: Props) {
14
14
  return (
15
- <View
16
- testID={screenId}
17
- accessibilityLabel={screenId}
18
- style={styles.container}
19
- >
15
+ <View testID={screenId} style={styles.container}>
20
16
  {children}
21
17
  </View>
22
18
  );
@@ -2,7 +2,6 @@
2
2
 
3
3
  exports[`<RouteManager /> renders correctly 1`] = `
4
4
  <View
5
- accessibilityLabel="A1234"
6
5
  style={
7
6
  {
8
7
  "flex": 1,
@@ -24,9 +24,5 @@ describe("TestId", () => {
24
24
  );
25
25
 
26
26
  expect(wrapper.getByTestId(screenId)).toBeTruthy();
27
-
28
- expect(wrapper.getByTestId(screenId).props.accessibilityLabel).toEqual(
29
- screenId
30
- );
31
27
  });
32
28
  });
@@ -0,0 +1,26 @@
1
+ const {
2
+ useScreenBackgroundColor,
3
+ } = require("@applicaster/zapp-react-native-utils/reactHooks/screen");
4
+
5
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/screen", () => ({
6
+ useScreenBackgroundColor: jest.fn(),
7
+ useNavbarState: jest.fn().mockReturnValue({ visible: true }),
8
+ }));
9
+
10
+ describe("TV Screen Component", () => {
11
+ it("uses the useScreenBackgroundColor hook with the correct screen ID", () => {
12
+ useScreenBackgroundColor.mockReturnValue("#FF0000");
13
+
14
+ expect(useScreenBackgroundColor("test-screen-id")).toBe("#FF0000");
15
+
16
+ // Verify the hook was called with the correct screen ID
17
+ expect(useScreenBackgroundColor).toHaveBeenCalledWith("test-screen-id");
18
+ });
19
+
20
+ it("returns 'transparent' when the screen background color is not defined", () => {
21
+ useScreenBackgroundColor.mockReturnValue("transparent");
22
+
23
+ // Verify the hook returns 'transparent'
24
+ expect(useScreenBackgroundColor("test-screen-id")).toBe("transparent");
25
+ });
26
+ });
@@ -68,21 +68,21 @@ jest.mock(
68
68
  })
69
69
  );
70
70
 
71
- jest.mock(
72
- "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenData",
73
- () => ({
74
- useScreenData: jest.fn(() => ({
75
- id: "testId",
76
- navigations: [{ id: "testId", category: "nav_bar" }],
77
- })),
78
- })
79
- );
80
-
81
71
  jest.mock("@applicaster/zapp-react-native-utils/reactHooks/navigation", () => ({
82
72
  isNavBarVisible: mockIsNavBarVisible,
83
- useRoute: jest.fn(() => ({
84
- pathname: "/river/testId",
85
- screenData: { id: "testId" },
73
+ useIsScreenActive: jest.fn().mockReturnValue(true),
74
+ }));
75
+
76
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks", () => ({
77
+ useCurrentScreenData: jest.fn(() => ({
78
+ id: "testId",
79
+ })),
80
+ useNavbarState: jest.fn(() => ({
81
+ title: "Test Title",
82
+ })),
83
+ useScreenData: jest.fn(() => ({
84
+ id: "testId",
85
+ navigations: [{ id: "testId", category: "nav_bar" }],
86
86
  })),
87
87
  useNavigation: jest.fn(() => ({
88
88
  canGoBack: () => false,
@@ -91,7 +91,15 @@ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/navigation", () => ({
91
91
  screenData: { id: "testId" },
92
92
  data: { screen: { id: "testId" } },
93
93
  })),
94
- useIsScreenActive: jest.fn().mockReturnValue(true),
94
+ useRoute: jest.fn(() => ({
95
+ pathname: "/river/testId",
96
+ screenData: { id: "testId" },
97
+ })),
98
+ useDimensions: jest.fn(() => ({
99
+ width: 1920,
100
+ height: 1080,
101
+ })),
102
+ useIsTablet: jest.fn(() => false),
95
103
  }));
96
104
 
97
105
  jest.mock("@applicaster/zapp-react-native-redux/hooks/usePickFromState", () => {
@@ -16,7 +16,7 @@ exports[`<Screen Component /> when the navbar should be hidden renders correctly
16
16
  pathname="/river/testId"
17
17
  selected="testId"
18
18
  testID="navBar"
19
- title=""
19
+ title="Test Title"
20
20
  />
21
21
  <View>
22
22
  <View
@@ -48,7 +48,7 @@ exports[`<Screen Component /> when the navbar should show renders correctly 1`]
48
48
  pathname="/river/testId"
49
49
  selected="testId"
50
50
  testID="navBar"
51
- title=""
51
+ title="Test Title"
52
52
  />
53
53
  <View>
54
54
  <View
@@ -94,7 +94,7 @@ export const AnimationView = ({
94
94
  const isAudioItem = React.useMemo(
95
95
  () =>
96
96
  videoModalItem?.content?.type?.includes?.("audio") ||
97
- videoModalItem?.type?.value === "audio",
97
+ videoModalItem?.type?.value?.includes?.("audio"),
98
98
  [videoModalItem]
99
99
  );
100
100
 
@@ -107,7 +107,6 @@ export const AnimationView = ({
107
107
  progressBarHeight,
108
108
  isTablet,
109
109
  isTabletLandscape,
110
- inlineAudioPlayer,
111
110
  tabletLandscapePlayerTopPosition,
112
111
  });
113
112
 
@@ -17,6 +17,7 @@ export enum PlayerAnimationStateEnum {
17
17
  maximize = "maximize",
18
18
  drag_player = "drag_player",
19
19
  drag_scroll = "drag_scroll",
20
+ appear_as_docked = "appear_as_docked",
20
21
  }
21
22
 
22
23
  export type PlayerAnimationStateT = number | PlayerAnimationStateEnum | null;
@@ -309,7 +309,6 @@ export const getMoveUpValue = ({
309
309
  progressBarHeight,
310
310
  isTablet,
311
311
  isTabletLandscape,
312
- inlineAudioPlayer,
313
312
  tabletLandscapePlayerTopPosition,
314
313
  }) => {
315
314
  if (additionalData.saveArea) {
@@ -322,17 +321,12 @@ export const getMoveUpValue = ({
322
321
  }
323
322
 
324
323
  if (isTablet) {
325
- if (
326
- isTabletLandscape &&
327
- (!isAudioItem || (isAudioItem && inlineAudioPlayer))
328
- ) {
324
+ if (isTabletLandscape) {
329
325
  return -tabletLandscapePlayerTopPosition + progressBarHeight;
326
+ } else {
327
+ return -130;
330
328
  }
331
-
332
- // for audioPlayer(tablet/isTabletPortrait) in docked mode
333
- return -130;
334
329
  }
335
330
 
336
- // for audioPlayer(mobile) in docked mode
337
331
  return -50 + progressBarHeight;
338
332
  };
@@ -74,20 +74,6 @@ const getScreenAspectRatio = () => {
74
74
  return longEdge / shortEdge;
75
75
  };
76
76
 
77
- const safeAreaStyles = (
78
- configuration: Configuration,
79
- isTablet: boolean
80
- ): ViewStyle => {
81
- const tablet_landscape_player_container_background_color =
82
- configuration?.tablet_landscape_player_container_background_color;
83
-
84
- return {
85
- backgroundColor: isTablet
86
- ? tablet_landscape_player_container_background_color
87
- : "transparent",
88
- };
89
- };
90
-
91
77
  const getEdges = (isTablet: boolean, isInlineModal: boolean) => {
92
78
  if (isTablet) {
93
79
  return ["top"];
@@ -156,6 +142,7 @@ const PlayerWrapperComponent = (props: Props) => {
156
142
  children,
157
143
  isTabletPortrait,
158
144
  configuration,
145
+ fullscreen,
159
146
  pip,
160
147
  } = props;
161
148
 
@@ -211,14 +198,17 @@ const PlayerWrapperComponent = (props: Props) => {
211
198
  [containerDimensions, playerDimensionsHack]
212
199
  );
213
200
 
201
+ const wrapperViewStyle: ViewStyle = {
202
+ backgroundColor:
203
+ isTablet && !fullscreen
204
+ ? configuration?.tablet_landscape_player_container_background_color
205
+ : "transparent",
206
+ };
207
+
214
208
  return (
215
209
  <WrapperView
216
210
  edges={getEdges(isTablet, isInlineModal) as readonly Edge[]}
217
- style={[
218
- safeAreaStyles(configuration, isTablet),
219
- style,
220
- playerDimensionsHack,
221
- ]}
211
+ style={[wrapperViewStyle, style, playerDimensionsHack]}
222
212
  >
223
213
  <AnimationComponent
224
214
  animationType={ComponentAnimationType.moveUpComponent}
@@ -13,6 +13,7 @@ import {
13
13
  loadDatasources,
14
14
  usePipesContexts,
15
15
  } from "./utils";
16
+ import { useScreenResolvers } from "../ZappPipesDataConnector";
16
17
 
17
18
  type RiverProps = {
18
19
  dispatch: DispatchProp;
@@ -25,7 +26,7 @@ export function WithRiverFeedLoader(Component: ZappComponent) {
25
26
  return function WrappedWithRiverFeedLoader(props: RiverProps) {
26
27
  const { river } = props;
27
28
  const { screenData, pathname } = useRoute();
28
-
29
+ const resolvers = useScreenResolvers(pathname);
29
30
  const pipesContexts = usePipesContexts(river.id, pathname);
30
31
 
31
32
  const componentsToLoad = ignoreComponentsWithClearCacheFlag(
@@ -49,7 +50,12 @@ export function WithRiverFeedLoader(Component: ZappComponent) {
49
50
  item?.filter((item2) => item2 !== undefined)
50
51
  );
51
52
 
52
- loadDatasources(nonEmptyDataSources, river?.id, props.dispatch);
53
+ loadDatasources(
54
+ nonEmptyDataSources,
55
+ river?.id,
56
+ props.dispatch,
57
+ resolvers
58
+ );
53
59
  }, []);
54
60
 
55
61
  return <Component {...props} />;
@@ -12,11 +12,16 @@ export { getDatasourceUrl } from "./getDatasourceUrl";
12
12
 
13
13
  export const DATASOURCE_CHUNKS = 10;
14
14
 
15
- export async function loadDatasources(urls: string[][], riverId, dispatch) {
15
+ export async function loadDatasources(
16
+ urls: string[][],
17
+ riverId,
18
+ dispatch,
19
+ resolvers
20
+ ) {
16
21
  return reducePromises<string, void>(
17
22
  mapPromises<string, void>((url) => {
18
23
  if (url) {
19
- return dispatch(loadPipesData(url, { riverId }));
24
+ return dispatch(loadPipesData(url, { riverId, resolvers }));
20
25
  }
21
26
  }),
22
27
  undefined,
@@ -19,7 +19,11 @@ import { ZappPipesSearchContext } from "@applicaster/zapp-react-native-ui-compon
19
19
  import { useScreenContext } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
20
20
 
21
21
  import { isVerticalListOrGrid } from "./utils";
22
- import { subscribeForUrlContextKeyChanges } from "@applicaster/zapp-pipes-v2-client";
22
+ import {
23
+ subscribeForUrlContextKeyChanges,
24
+ subscribeForUrlScreenKeyChanges,
25
+ } from "@applicaster/zapp-pipes-v2-client";
26
+ import { ScreenStateResolver } from "@applicaster/zapp-react-native-utils/appUtils/contextKeysManager/contextResolver";
23
27
 
24
28
  type Props = {
25
29
  component: ZappUIComponent;
@@ -200,11 +204,15 @@ const useGetStaticFeed = (getter, component, index) => {
200
204
  return { url, loading, data, error };
201
205
  };
202
206
 
207
+ export const useScreenResolvers = (screenRoute: string) => {
208
+ return { screen: new ScreenStateResolver(screenRoute) };
209
+ };
210
+
203
211
  export function zappPipesDataConnector(
204
212
  Component: React.FC<any> | React.ComponentClass<any>
205
213
  ) {
206
214
  return function WrappedWithZappPipesData(props: Props) {
207
- const { screenData } = useRoute();
215
+ const { screenData, pathname } = useRoute();
208
216
  const { plugins } = usePickFromState(["plugins"]);
209
217
 
210
218
  const screenContextData = useScreenContext();
@@ -286,6 +294,17 @@ export function zappPipesDataConnector(
286
294
  componentIndex
287
295
  );
288
296
 
297
+ useEffect(() => {
298
+ if (!(dataSourceUrl?.includes("pipesv2://") && reloadData)) {
299
+ return subscribeForUrlScreenKeyChanges(
300
+ dataSourceUrl,
301
+ pathname,
302
+ {},
303
+ reloadData
304
+ );
305
+ }
306
+ }, [dataSourceUrl, reloadData]);
307
+
289
308
  useEffect(() => {
290
309
  if (dataSourceUrl?.includes("pipesv2://") && reloadData) {
291
310
  const addListener = getListenerFromPlugin(dataSourceUrl, plugins);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-ui-components",
3
- "version": "13.0.0-rc.99",
3
+ "version": "14.0.0-alpha.1054425138",
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",
@@ -31,12 +31,11 @@
31
31
  "redux-mock-store": "^1.5.3"
32
32
  },
33
33
  "dependencies": {
34
- "@applicaster/applicaster-types": "13.0.0-rc.99",
35
- "@applicaster/zapp-react-native-bridge": "13.0.0-rc.99",
36
- "@applicaster/zapp-react-native-redux": "13.0.0-rc.99",
37
- "@applicaster/zapp-react-native-utils": "13.0.0-rc.99",
34
+ "@applicaster/applicaster-types": "14.0.0-alpha.1054425138",
35
+ "@applicaster/zapp-react-native-bridge": "14.0.0-alpha.1054425138",
36
+ "@applicaster/zapp-react-native-redux": "14.0.0-alpha.1054425138",
37
+ "@applicaster/zapp-react-native-utils": "14.0.0-alpha.1054425138",
38
38
  "promise": "^8.3.0",
39
- "react-router-native": "^5.1.2",
40
39
  "url": "^0.11.0",
41
40
  "uuid": "^3.3.2"
42
41
  },
@@ -1,11 +0,0 @@
1
- import React from "react";
2
-
3
- const reactRouter = jest.genMockFromModule("react-router-native");
4
-
5
- function withRouter(Component) {
6
- return (props) => <Component {...props} />; // eslint-disable-line react/display-name
7
- }
8
-
9
- reactRouter.withRouter = withRouter;
10
-
11
- module.exports = reactRouter;