@applicaster/zapp-react-native-ui-components 15.0.0-alpha.4413958104 → 15.0.0-alpha.4429053208

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/BaseFocusable/index.ios.ts +12 -2
  2. package/Components/Cell/FocusableWrapper.tsx +3 -0
  3. package/Components/Cell/TvOSCellComponent.tsx +5 -0
  4. package/Components/Focusable/Focusable.tsx +4 -2
  5. package/Components/Focusable/FocusableTvOS.tsx +18 -1
  6. package/Components/Focusable/__tests__/__snapshots__/FocusableTvOS.test.tsx.snap +1 -0
  7. package/Components/FocusableGroup/FocusableTvOS.tsx +55 -1
  8. package/Components/FocusableGroup/hooks/__tests__/useIsFocusEnabled.test.ts +113 -0
  9. package/Components/FocusableGroup/hooks/index.ts +1 -0
  10. package/Components/FocusableGroup/hooks/useIsFocusEnabled.ts +68 -0
  11. package/Components/GeneralContentScreen/utils/useCurationAPI.ts +19 -3
  12. package/Components/MasterCell/DefaultComponents/Image/Image.android.tsx +5 -1
  13. package/Components/MasterCell/DefaultComponents/Image/Image.ios.tsx +11 -3
  14. package/Components/MasterCell/DefaultComponents/Image/Image.web.tsx +9 -1
  15. package/Components/MasterCell/DefaultComponents/Image/hooks/useImage.ts +15 -14
  16. package/Components/PlayerContainer/PlayerContainer.tsx +48 -38
  17. package/Components/PlayerContainer/useRestrictMobilePlayback.tsx +101 -0
  18. package/Components/River/TV/River.tsx +31 -14
  19. package/Components/River/TV/index.tsx +8 -4
  20. package/Components/River/TV/utils/__tests__/toStringOrEmpty.test.ts +30 -0
  21. package/Components/River/TV/utils/index.ts +4 -0
  22. package/Components/River/TV/withFocusableGroupForContent.tsx +71 -0
  23. package/Contexts/AboveTabsScreenContext/index.tsx +33 -0
  24. package/Decorators/ConfigurationWrapper/__tests__/__snapshots__/withConfigurationProvider.test.tsx.snap +1 -0
  25. package/Decorators/ConfigurationWrapper/const.ts +1 -0
  26. package/Decorators/ZappPipesDataConnector/__tests__/zappPipesDataConnector.test.js +1 -1
  27. package/Decorators/ZappPipesDataConnector/resolvers/StaticFeedResolver.tsx +1 -1
  28. package/Helpers/DataSourceHelper/index.ts +10 -1
  29. package/package.json +5 -5
  30. package/Components/River/TV/withTVEventHandler.tsx +0 -27
@@ -22,6 +22,7 @@ type Props = {
22
22
  onFocus?: FocusManager.FocusEventCB;
23
23
  onBlur?: FocusManager.FocusEventCB;
24
24
  selected?: boolean;
25
+ skipFocusManagerRegistration?: boolean;
25
26
  };
26
27
 
27
28
  export class BaseFocusable<
@@ -61,10 +62,14 @@ export class BaseFocusable<
61
62
  }
62
63
 
63
64
  componentDidMount() {
64
- const { id } = this.props;
65
+ const { id, skipFocusManagerRegistration } = this.props;
65
66
  const component = this;
66
67
  this.node = this.ref.current;
67
68
 
69
+ if (skipFocusManagerRegistration) {
70
+ return;
71
+ }
72
+
68
73
  focusManager.register({
69
74
  id,
70
75
  component: component,
@@ -118,7 +123,12 @@ export class BaseFocusable<
118
123
 
119
124
  componentWillUnmount() {
120
125
  this._isMounted = false;
121
- const { id } = this.props;
126
+ const { id, skipFocusManagerRegistration } = this.props;
127
+
128
+ if (skipFocusManagerRegistration) {
129
+ return;
130
+ }
131
+
122
132
  focusManager.unregister(id, { group: this.isGroup || false });
123
133
  }
124
134
 
@@ -10,6 +10,7 @@ type Props = {
10
10
  children: (focused: boolean) => React.ReactNode;
11
11
  onFocus: (arg1: any, index?: number) => void;
12
12
  onBlur: Callback;
13
+ skipFocusManagerRegistration?: boolean;
13
14
  };
14
15
 
15
16
  export const FocusableWrapper = ({
@@ -20,6 +21,7 @@ export const FocusableWrapper = ({
20
21
  applyWrapper,
21
22
  onFocus,
22
23
  onBlur,
24
+ skipFocusManagerRegistration,
23
25
  }: Props) => {
24
26
  if (applyWrapper) {
25
27
  return (
@@ -34,6 +36,7 @@ export const FocusableWrapper = ({
34
36
  // @ts-ignore
35
37
  offsetUpdater={noop}
36
38
  isFocusable
39
+ skipFocusManagerRegistration={skipFocusManagerRegistration}
37
40
  >
38
41
  {(focused) => children(focused)}
39
42
  </Focusable>
@@ -80,6 +80,7 @@ type Props = {
80
80
  componentsMapOffset: number;
81
81
  applyFocusableWrapper: boolean;
82
82
  hasFocusableInside: boolean;
83
+ skipFocusManagerRegistration?: boolean;
83
84
  };
84
85
 
85
86
  type State = {
@@ -266,6 +267,7 @@ class TvOSCell extends React.Component<Props, State> {
266
267
  behavior,
267
268
  applyFocusableWrapper,
268
269
  hasFocusableInside,
270
+ skipFocusManagerRegistration,
269
271
  } = this.props;
270
272
 
271
273
  const { id } = item;
@@ -291,6 +293,7 @@ class TvOSCell extends React.Component<Props, State> {
291
293
  onFocus={handleFocus}
292
294
  onBlur={onBlur || this.onBlur}
293
295
  applyWrapper={applyFocusableWrapper}
296
+ skipFocusManagerRegistration={skipFocusManagerRegistration}
294
297
  >
295
298
  {(focused) => (
296
299
  <CellWithFocusable
@@ -305,6 +308,7 @@ class TvOSCell extends React.Component<Props, State> {
305
308
  focused={focused || this.props.focused}
306
309
  behavior={behavior}
307
310
  isFocusable={isFocusable}
311
+ skipFocusManagerRegistration={skipFocusManagerRegistration}
308
312
  />
309
313
  )}
310
314
  </FocusableWrapper>
@@ -327,6 +331,7 @@ class TvOSCell extends React.Component<Props, State> {
327
331
  offsetUpdater={offsetUpdater}
328
332
  style={baseCellStyles}
329
333
  isFocusable={isFocusable}
334
+ skipFocusManagerRegistration={skipFocusManagerRegistration}
330
335
  >
331
336
  {(focused) => (
332
337
  <FocusableCell
@@ -8,6 +8,8 @@ import { withFocusableContext } from "../../Contexts/FocusableGroupContext/withF
8
8
  import { StyleSheet, ViewStyle } from "react-native";
9
9
  import { AccessibilityManager } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager";
10
10
 
11
+ import { isSearchInputId } from "@applicaster/zapp-react-native-utils/searchUtils";
12
+
11
13
  type Props = {
12
14
  initialFocus?: boolean;
13
15
  id: string;
@@ -106,7 +108,7 @@ class Focusable extends BaseFocusable<Props> {
106
108
  onMouseEnter() {
107
109
  const { id } = this.props;
108
110
 
109
- if (id !== "search_input_group_id") {
111
+ if (!isSearchInputId(id)) {
110
112
  this.mouse = true;
111
113
  this.props?.handleFocus?.({ mouse: true });
112
114
 
@@ -120,7 +122,7 @@ class Focusable extends BaseFocusable<Props> {
120
122
  onMouseLeave() {
121
123
  const { id } = this.props;
122
124
 
123
- if (id !== "search_input_group_id") {
125
+ if (!isSearchInputId(id)) {
124
126
  this.mouse = false;
125
127
  this.blur(null);
126
128
  }
@@ -10,8 +10,12 @@ import {
10
10
  forceFocusableFocus,
11
11
  } from "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios";
12
12
  import { findNodeHandle, ViewStyle } from "react-native";
13
+ import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
13
14
 
14
- function noop() {}
15
+ import {
16
+ emitDidFocused,
17
+ emitNativeRegistered,
18
+ } from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios";
15
19
 
16
20
  type Props = {
17
21
  id: string;
@@ -39,6 +43,7 @@ type Props = {
39
43
  hasReceivedFocus: () => void;
40
44
  offsetUpdater: (arg1: string, arg2: number) => number;
41
45
  style: ViewStyle;
46
+ skipFocusManagerRegistration?: boolean;
42
47
  };
43
48
 
44
49
  export class Focusable extends BaseFocusable<Props> {
@@ -53,6 +58,7 @@ export class Focusable extends BaseFocusable<Props> {
53
58
  this.nextFocusableReactTags = {};
54
59
  this.preferredFocus = this.preferredFocus.bind(this);
55
60
  this.measureView = this.measureView.bind(this);
61
+ this.onRegistered = this.onRegistered.bind(this);
56
62
  }
57
63
 
58
64
  /**
@@ -84,6 +90,9 @@ export class Focusable extends BaseFocusable<Props> {
84
90
  });
85
91
  }
86
92
 
93
+ const id: string = nativeEvent.itemID;
94
+ emitDidFocused(id);
95
+
87
96
  onFocus(nativeEvent);
88
97
  }
89
98
 
@@ -169,6 +178,13 @@ export class Focusable extends BaseFocusable<Props> {
169
178
  });
170
179
  }
171
180
 
181
+ onRegistered({ nativeEvent }) {
182
+ const groupId = nativeEvent?.groupId;
183
+ const id = nativeEvent?.itemId;
184
+
185
+ emitNativeRegistered({ id, groupId, isGroup: false });
186
+ }
187
+
172
188
  render() {
173
189
  const {
174
190
  children,
@@ -203,6 +219,7 @@ export class Focusable extends BaseFocusable<Props> {
203
219
  focusable={isFocusable}
204
220
  {...this.nextFocusableReactTags}
205
221
  {...otherProps}
222
+ onRegistered={this.onRegistered}
206
223
  >
207
224
  {typeof children === "function" ? children(focused) : children}
208
225
  </FocusableItemNative>
@@ -5,6 +5,7 @@ exports[`FocusableTvOS should render correctly 1`] = `
5
5
  groupId={null}
6
6
  itemId={null}
7
7
  onLayout={[Function]}
8
+ onRegistered={[Function]}
8
9
  onViewBlur={[Function]}
9
10
  onViewFocus={[Function]}
10
11
  onViewPress={[Function]}
@@ -1,7 +1,15 @@
1
1
  import * as React from "react";
2
+ import { compose } from "@applicaster/zapp-react-native-utils/utils";
2
3
  import { FocusableGroupNative } from "@applicaster/zapp-react-native-ui-components/Components/NativeFocusables";
3
4
  import { BaseFocusable } from "@applicaster/zapp-react-native-ui-components/Components/BaseFocusable";
4
5
  import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
6
+ import { LayoutContext } from "@applicaster/zapp-react-native-tvos-app/Context/LayoutContext";
7
+ import { useRoute } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useRoute";
8
+ import { isScreenPlayable } from "@applicaster/zapp-react-native-utils/navigationUtils/itemTypes";
9
+ import { emitNativeRegistered } from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios";
10
+ import { withAboveTabsScreenContextConsumer } from "@applicaster/zapp-react-native-ui-components/Contexts/AboveTabsScreenContext";
11
+
12
+ import { useIsFocusEnabled } from "./hooks";
5
13
 
6
14
  const { log_verbose } = createLogger({
7
15
  subsystem: "General",
@@ -33,7 +41,16 @@ type Props = {
33
41
  screenData: { screenId: string; parentScreenId: string };
34
42
  };
35
43
 
36
- export class FocusableGroup extends BaseFocusable<Props> {
44
+ class FocusableGroupComponent extends BaseFocusable<Props> {
45
+ public readonly isGroup: boolean = true;
46
+
47
+ onRegistered = ({ nativeEvent }) => {
48
+ const groupId = nativeEvent?.groupId;
49
+ const id = nativeEvent?.itemId;
50
+
51
+ emitNativeRegistered({ id, groupId, isGroup: true });
52
+ };
53
+
37
54
  render() {
38
55
  const {
39
56
  children,
@@ -66,9 +83,46 @@ export class FocusableGroup extends BaseFocusable<Props> {
66
83
  onGroupBlur={onGroupBlur}
67
84
  style={style}
68
85
  {...otherProps}
86
+ onRegistered={this.onRegistered}
69
87
  >
70
88
  {children}
71
89
  </FocusableGroupNative>
72
90
  );
73
91
  }
74
92
  }
93
+
94
+ export const withFocusDisabledHOC = (Component) => {
95
+ return function WithFocusDisabledHOC(props) {
96
+ // @ts-ignore
97
+ const { screenFocusBlocked } = React.useContext(LayoutContext.ReactContext);
98
+
99
+ const { pathname } = useRoute();
100
+
101
+ const isPlayerPresented = isScreenPlayable(pathname);
102
+
103
+ const blockScreenFocus = isPlayerPresented === false && screenFocusBlocked;
104
+
105
+ return (
106
+ <Component
107
+ {...props}
108
+ isFocusDisabled={blockScreenFocus || props.isFocusDisabled}
109
+ />
110
+ );
111
+ };
112
+ };
113
+
114
+ const withAboveTabsScreenHOC = (Component) => {
115
+ return function WithAboveTabsScreenHOC(props) {
116
+ const { aboveTabsScreen } = props;
117
+
118
+ const isFocusEnabled = useIsFocusEnabled(aboveTabsScreen);
119
+
120
+ return <Component {...props} isFocusDisabled={!isFocusEnabled} />;
121
+ };
122
+ };
123
+
124
+ export const FocusableGroup = compose(
125
+ withAboveTabsScreenContextConsumer,
126
+ withAboveTabsScreenHOC,
127
+ withFocusDisabledHOC
128
+ )(FocusableGroupComponent);
@@ -0,0 +1,113 @@
1
+ import { act, renderHook } from "@testing-library/react-native";
2
+
3
+ import { useIsFocusEnabled } from "../useIsFocusEnabled";
4
+
5
+ // ----------------- MOCKS -----------------
6
+ jest.mock(
7
+ "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios",
8
+ () => ({
9
+ focusManager: {
10
+ isFocusOnMenu: jest.fn(),
11
+ isFocusOnTabsScreen: jest.fn(),
12
+ },
13
+ })
14
+ );
15
+
16
+ jest.mock(
17
+ "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios",
18
+ () => {
19
+ const { Subject } = require("rxjs");
20
+
21
+ return {
22
+ willFocused$: new Subject<void>(),
23
+ didFocused$: new Subject<void>(),
24
+ TabsScreenScreenSelectorContainerRegistry: {
25
+ observable$: new Subject<string>(),
26
+ },
27
+ };
28
+ }
29
+ );
30
+
31
+ import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios";
32
+ import {
33
+ willFocused$,
34
+ didFocused$,
35
+ TabsScreenScreenSelectorContainerRegistry,
36
+ } from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios";
37
+
38
+ // ----------------- TESTS -----------------
39
+ describe("useIsFocusEnabled", () => {
40
+ beforeEach(() => {
41
+ jest.clearAllMocks();
42
+ });
43
+
44
+ it("returns true by default", () => {
45
+ const { result } = renderHook(() => useIsFocusEnabled(true));
46
+
47
+ expect(result.current).toBe(true);
48
+ });
49
+
50
+ it("disables focus when focus moves to menu", () => {
51
+ (focusManager.isFocusOnMenu as jest.Mock).mockReturnValue(true);
52
+
53
+ const { result } = renderHook(() => useIsFocusEnabled(true));
54
+
55
+ act(() => {
56
+ willFocused$.next();
57
+ didFocused$.next();
58
+ });
59
+
60
+ expect(result.current).toBe(false);
61
+ });
62
+
63
+ it("re-enables focus when focus lands on tabs screen", () => {
64
+ (focusManager.isFocusOnMenu as jest.Mock).mockReturnValue(true);
65
+ (focusManager.isFocusOnTabsScreen as jest.Mock).mockReturnValue(true);
66
+
67
+ const { result } = renderHook(() => useIsFocusEnabled(true));
68
+
69
+ // Disable focus first
70
+ act(() => {
71
+ willFocused$.next();
72
+ didFocused$.next();
73
+ });
74
+
75
+ expect(result.current).toBe(false);
76
+
77
+ // Enable focus again
78
+ act(() => {
79
+ didFocused$.next();
80
+ TabsScreenScreenSelectorContainerRegistry.observable$.next("tabs-id");
81
+ });
82
+
83
+ expect(result.current).toBe(true);
84
+ });
85
+
86
+ it("does nothing when not above tabs screen", () => {
87
+ const { result } = renderHook(() => useIsFocusEnabled(false));
88
+
89
+ act(() => {
90
+ willFocused$.next();
91
+ didFocused$.next();
92
+ TabsScreenScreenSelectorContainerRegistry.observable$.next("id");
93
+ });
94
+
95
+ expect(result.current).toBe(true);
96
+ });
97
+
98
+ it("cleans up subscriptions on unmount", () => {
99
+ (focusManager.isFocusOnMenu as jest.Mock).mockReturnValue(true);
100
+
101
+ const { unmount, result } = renderHook(() => useIsFocusEnabled(true));
102
+
103
+ unmount();
104
+
105
+ act(() => {
106
+ willFocused$.next();
107
+ didFocused$.next();
108
+ });
109
+
110
+ // State should not change after unmount
111
+ expect(result.current).toBe(true);
112
+ });
113
+ });
@@ -0,0 +1 @@
1
+ export { useIsFocusEnabled } from "./useIsFocusEnabled";
@@ -0,0 +1,68 @@
1
+ import * as React from "react";
2
+ import { switchMap, first, filter, repeat } from "rxjs/operators";
3
+ import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios";
4
+
5
+ import {
6
+ willFocused$,
7
+ didFocused$,
8
+ TabsScreenScreenSelectorContainerRegistry,
9
+ } from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios";
10
+
11
+ export const useIsFocusEnabled = (isAboveTabsScreen: boolean): boolean => {
12
+ const [isFocusEnabled, setIsFocusedEnabled] = React.useState(true);
13
+
14
+ const enableFocus = React.useCallback(() => {
15
+ setIsFocusedEnabled(true);
16
+ }, []);
17
+
18
+ const disableFocus = React.useCallback(() => {
19
+ setIsFocusedEnabled(false);
20
+ }, []);
21
+
22
+ React.useEffect(() => {
23
+ const subscription = didFocused$
24
+ .pipe(
25
+ filter(() => isAboveTabsScreen && !isFocusEnabled),
26
+
27
+ switchMap(() => TabsScreenScreenSelectorContainerRegistry.observable$),
28
+ filter((id) => id && focusManager.isFocusOnTabsScreen(id)),
29
+
30
+ // run only once, then re-subscribe on didFocused$ again
31
+ first(),
32
+ repeat()
33
+ )
34
+ .subscribe(() => {
35
+ enableFocus();
36
+ });
37
+
38
+ return () => {
39
+ subscription.unsubscribe();
40
+ };
41
+ }, [enableFocus, isFocusEnabled, isAboveTabsScreen]);
42
+
43
+ React.useEffect(() => {
44
+ const subscription = willFocused$
45
+ .pipe(
46
+ filter(() => isAboveTabsScreen && isFocusEnabled),
47
+
48
+ // start waiting onFocus event
49
+ switchMap(() => didFocused$),
50
+
51
+ // focus is landed on top-menu
52
+ filter(() => focusManager.isFocusOnMenu()),
53
+
54
+ // run only once, then re-subscribe on willFocused$ again
55
+ first(),
56
+ repeat()
57
+ )
58
+ .subscribe(() => {
59
+ disableFocus();
60
+ });
61
+
62
+ return () => {
63
+ subscription.unsubscribe();
64
+ };
65
+ }, [disableFocus, isFocusEnabled, isAboveTabsScreen]);
66
+
67
+ return isFocusEnabled;
68
+ };
@@ -1,4 +1,4 @@
1
- import { all, equals, path, prop, isEmpty, pluck, values } from "ramda";
1
+ import { all, equals, isEmpty, path, pluck, prop, values } from "ramda";
2
2
 
3
3
  import { useEffect, useMemo } from "react";
4
4
 
@@ -24,6 +24,7 @@ import {
24
24
 
25
25
  import { produce } from "immer";
26
26
  import { useLoadPipesDataDispatch } from "@applicaster/zapp-react-native-utils/reactHooks";
27
+
27
28
  // types reference
28
29
 
29
30
  declare interface CurationEntry {
@@ -35,6 +36,8 @@ type Feeds = Record<string, ZappPipesData>;
35
36
 
36
37
  type LayoutPresets = PresetsMapping["presets_mappings"];
37
38
 
39
+ const TABS_SCREEN_TYPE = "tabs_screen";
40
+ const QB_TABS_SCREEN_TYPE = "quick-brick-tabs";
38
41
  const SMART_COMPONENT_TYPE = "quick-brick-smart-component";
39
42
  const SOURCE_PATH = ["data", "source"];
40
43
  const MAPPING_PATH = ["data", "mapping"];
@@ -53,7 +56,10 @@ export const getTransformedPreset = (
53
56
  const presetComponent = layoutPresets?.[preset?.preset_name];
54
57
 
55
58
  if (!presetComponent) {
56
- logger.log_error("Preset missing or wrong data format", { entry: preset });
59
+ logger.log_error(
60
+ `Preset "${preset?.preset_name}" missing or wrong data format`,
61
+ { entry: preset }
62
+ );
57
63
 
58
64
  return;
59
65
  }
@@ -131,10 +137,20 @@ export const useCurationAPI = (
131
137
  );
132
138
 
133
139
  const { pathname } = useRoute();
134
- const [entryContext] = ZappPipesEntryContext.useZappPipesContext(pathname);
135
140
  const [searchContext] = ZappPipesSearchContext.useZappPipesContext();
136
141
  const [screenContext] = ZappPipesScreenContext.useZappPipesContext();
137
142
 
143
+ const screenContextType = screenContext?.type;
144
+
145
+ const isNestedScreen =
146
+ screenContextType === TABS_SCREEN_TYPE ||
147
+ screenContextType === QB_TABS_SCREEN_TYPE;
148
+
149
+ const [entryContext] = ZappPipesEntryContext.useZappPipesContext(
150
+ pathname,
151
+ isNestedScreen
152
+ );
153
+
138
154
  const urlsMap = useMemo<{ [key: string]: string }>(() => {
139
155
  const map = {};
140
156
 
@@ -32,6 +32,7 @@ export default function Image({
32
32
  placeholderImage,
33
33
  entry,
34
34
  withDimensions,
35
+ source: sourceProp,
35
36
  ...otherProps
36
37
  }: Props) {
37
38
  const [showDefault, setShowDefault] = React.useState(false);
@@ -48,7 +49,10 @@ export default function Image({
48
49
  entry,
49
50
  showDefault,
50
51
  placeholderImage: placeholderImage || "",
51
- otherProps,
52
+ otherProps: {
53
+ source: sourceProp,
54
+ state: otherProps.state,
55
+ },
52
56
  });
53
57
 
54
58
  const onError = React.useCallback(() => {
@@ -1,8 +1,8 @@
1
1
  import * as React from "react";
2
2
  import { Image as RnImage, ImageStyle } from "react-native";
3
- import { equals, omit } from "ramda";
4
3
 
5
4
  import { useImageSource } from "./hooks";
5
+ import { equals } from "@applicaster/zapp-react-native-utils/utils";
6
6
 
7
7
  type Source = {
8
8
  uri: string;
@@ -25,11 +25,19 @@ function Image({
25
25
  placeholderImage,
26
26
  entry,
27
27
  withDimensions,
28
+ source: sourceProp,
28
29
  ...otherProps
29
30
  }: Props) {
30
31
  const [error, setErrorState] = React.useState(null);
31
32
 
32
- const source = useImageSource({ uri, entry, otherProps });
33
+ const source = useImageSource({
34
+ uri,
35
+ entry,
36
+ otherProps: {
37
+ source: sourceProp,
38
+ state: otherProps.state,
39
+ },
40
+ });
33
41
 
34
42
  React.useEffect(() => {
35
43
  // reset error state on URI change as the error is referencing previous uri
@@ -49,7 +57,7 @@ function Image({
49
57
  onError={React.useCallback(() => setErrorState(true), [])}
50
58
  // as we have defaults as "" for placeholder image, we need to pass undefined to source to not throw warnings
51
59
  source={_source?.uri ? _source : undefined}
52
- {...omit(["source"], otherProps)}
60
+ {...otherProps}
53
61
  />
54
62
  );
55
63
  }
@@ -23,9 +23,17 @@ function Image({
23
23
  placeholderImage,
24
24
  entry,
25
25
  withDimensions,
26
+ source: sourceProp,
26
27
  ...otherProps
27
28
  }: Props) {
28
- const source = useImageSource({ uri, entry, otherProps });
29
+ const source = useImageSource({
30
+ uri,
31
+ entry,
32
+ otherProps: {
33
+ source: sourceProp,
34
+ state: otherProps.state,
35
+ },
36
+ });
29
37
 
30
38
  const updatedSource = source ? withDimensions(source) : { uri: "" };
31
39
 
@@ -1,19 +1,21 @@
1
1
  import * as React from "react";
2
- import { path } from "ramda";
3
2
 
4
3
  import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
5
4
  import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
6
5
  import { extractAsset } from "./utils";
7
6
 
8
7
  type Return = { uri: string } | null;
8
+ type Source = { context?: string; uri?: string } | null | undefined;
9
9
 
10
- const getSourceContext = path(["source", "context"]);
11
- const getSourceUri = path(["source", "uri"]);
12
- const getState = path(["state"]);
10
+ const getSourceContext = (source: Source) => source?.context;
11
+ const getSourceUri = (source: Source) => source?.uri;
13
12
 
14
- export const useImageSource = ({ uri, entry, otherProps }): Return => {
15
- const uriContext = getSourceContext(otherProps);
16
- const uriState = getState(otherProps);
13
+ export const useImageSource = ({
14
+ uri,
15
+ entry,
16
+ otherProps: { source, state: uriState },
17
+ }): Return => {
18
+ const uriContext = getSourceContext(source);
17
19
 
18
20
  const action = useActions(uriContext);
19
21
 
@@ -38,7 +40,7 @@ export const useImageSource = ({ uri, entry, otherProps }): Return => {
38
40
  return { uri };
39
41
  }
40
42
 
41
- const uriFromSource = getSourceUri(otherProps);
43
+ const uriFromSource = getSourceUri(source);
42
44
 
43
45
  if (uriFromSource) {
44
46
  return { uri: uriFromSource };
@@ -47,7 +49,7 @@ export const useImageSource = ({ uri, entry, otherProps }): Return => {
47
49
  return null;
48
50
  };
49
51
 
50
- const getSource = (uri, showDefault, placeholderImage, otherProps) => {
52
+ const getSource = (uri, showDefault, placeholderImage, source) => {
51
53
  const placeholderName = placeholderImage || "";
52
54
 
53
55
  const defaultPath = {
@@ -60,7 +62,7 @@ const getSource = (uri, showDefault, placeholderImage, otherProps) => {
60
62
  return { uri };
61
63
  }
62
64
 
63
- const uriFromSource = getSourceUri(otherProps);
65
+ const uriFromSource = getSourceUri(source);
64
66
 
65
67
  if (uriFromSource) {
66
68
  return { uri: uriFromSource };
@@ -74,10 +76,9 @@ export const useImageSourceWithDefault = ({
74
76
  entry,
75
77
  showDefault,
76
78
  placeholderImage,
77
- otherProps,
79
+ otherProps: { state: uriState, source },
78
80
  }): Return => {
79
- const uriContext = getSourceContext(otherProps);
80
- const uriState = getState(otherProps);
81
+ const uriContext = getSourceContext(source);
81
82
 
82
83
  const action = useActions(uriContext);
83
84
 
@@ -98,5 +99,5 @@ export const useImageSourceWithDefault = ({
98
99
  return extractAsset(!isTV(), entryStateLocal.asset, uriState);
99
100
  }
100
101
 
101
- return getSource(uri, showDefault, placeholderImage, otherProps);
102
+ return getSource(uri, showDefault, placeholderImage, source);
102
103
  };
@@ -61,6 +61,7 @@ import {
61
61
  PlayerNativeSendCommand,
62
62
  } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand";
63
63
  import { useAppData } from "@applicaster/zapp-react-native-redux";
64
+ import { useRestrictMobilePlayback } from "./useRestrictMobilePlayback";
64
65
 
65
66
  type Props = {
66
67
  Player: React.ComponentType<any>;
@@ -264,7 +265,21 @@ const PlayerContainerComponent = (props: Props) => {
264
265
 
265
266
  showNavBar(true);
266
267
  navigator.goBack();
267
- }, [isModal, navigator.goBack, state.playerId, showNavBar]);
268
+ }, [isModal, state.playerId, showNavBar, navigator]);
269
+
270
+ const pluginConfiguration = React.useMemo(() => {
271
+ return (
272
+ playerManager.getPluginConfiguration() ||
273
+ R.prop("__plugin_configuration", Player)
274
+ );
275
+ }, [playerManager.isRegistered()]);
276
+
277
+ const { isRestricted } = useRestrictMobilePlayback({
278
+ player,
279
+ entry: item,
280
+ pluginConfiguration,
281
+ close,
282
+ });
268
283
 
269
284
  const playEntry = (entry) => navigator.replaceTop(entry, { mode });
270
285
 
@@ -456,13 +471,6 @@ const PlayerContainerComponent = (props: Props) => {
456
471
  }
457
472
  }, []);
458
473
 
459
- const pluginConfiguration = React.useMemo(() => {
460
- return (
461
- playerManager.getPluginConfiguration() ||
462
- R.prop("__plugin_configuration", Player)
463
- );
464
- }, [playerManager.isRegistered()]);
465
-
466
474
  const disableMiniPlayer = React.useMemo(() => {
467
475
  return pluginConfiguration?.disable_mini_player_when_inline;
468
476
  }, [pluginConfiguration]);
@@ -662,36 +670,38 @@ const PlayerContainerComponent = (props: Props) => {
662
670
  <PlayerFocusableWrapperView
663
671
  nextFocusDown={context.bottomFocusableId}
664
672
  >
665
- <Player
666
- source={{
667
- uri,
668
- entry: item,
669
- }}
670
- focused={isInlineTV ? true : undefined}
671
- autoplay={true}
672
- controls={false}
673
- disableCastAction={disableCastAction}
674
- docked={navigator.isVideoModalDocked()}
675
- entry={item}
676
- fullscreen={mode === VideoModalMode.FULLSCREEN}
677
- inline={inline}
678
- isModal={isModal}
679
- isTabletPortrait={isTabletPortrait}
680
- muted={false}
681
- playableItem={item}
682
- playerEvent={playerEvent}
683
- playerId={state.playerId}
684
- pluginConfiguration={pluginConfiguration}
685
- ref={playerRef}
686
- toggleFullscreen={toggleFullscreen}
687
- style={videoStyle}
688
- playNextData={playNextData}
689
- setNextVideoPreloadThresholdPercentage={
690
- setNextVideoPreloadThresholdPercentage
691
- }
692
- >
693
- {renderApplePlayer(applePlayerProps)}
694
- </Player>
673
+ {isRestricted ? null : (
674
+ <Player
675
+ source={{
676
+ uri,
677
+ entry: item,
678
+ }}
679
+ focused={isInlineTV ? true : undefined}
680
+ autoplay={true}
681
+ controls={false}
682
+ disableCastAction={disableCastAction}
683
+ docked={navigator.isVideoModalDocked()}
684
+ entry={item}
685
+ fullscreen={mode === VideoModalMode.FULLSCREEN}
686
+ inline={inline}
687
+ isModal={isModal}
688
+ isTabletPortrait={isTabletPortrait}
689
+ muted={false}
690
+ playableItem={item}
691
+ playerEvent={playerEvent}
692
+ playerId={state.playerId}
693
+ pluginConfiguration={pluginConfiguration}
694
+ ref={playerRef}
695
+ toggleFullscreen={toggleFullscreen}
696
+ style={videoStyle}
697
+ playNextData={playNextData}
698
+ setNextVideoPreloadThresholdPercentage={
699
+ setNextVideoPreloadThresholdPercentage
700
+ }
701
+ >
702
+ {renderApplePlayer(applePlayerProps)}
703
+ </Player>
704
+ )}
695
705
  </PlayerFocusableWrapperView>
696
706
 
697
707
  {state.error ? <ErrorDisplay error={state.error} /> : null}
@@ -0,0 +1,101 @@
1
+ import { Player } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/player";
2
+ import NetInfo from "@react-native-community/netinfo";
3
+
4
+ import { useEffect, useMemo, useRef, useState } from "react";
5
+ import { showAlertDialog } from "@applicaster/zapp-react-native-utils/alertUtils";
6
+ import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
7
+ import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
8
+ import { log_info } from "./logger";
9
+
10
+ type RestrictMobilePlaybackProps = {
11
+ player?: Player;
12
+ entry?: ZappEntry;
13
+ pluginConfiguration?: Record<string, string>;
14
+ close: () => void;
15
+ };
16
+
17
+ export const useRestrictMobilePlayback = ({
18
+ player,
19
+ entry,
20
+ pluginConfiguration,
21
+ close,
22
+ }: RestrictMobilePlaybackProps): { isRestricted: boolean } => {
23
+ const dialogVisibleRef = useRef<boolean>(false);
24
+ const theme = useTheme();
25
+
26
+ useEffect(() => {
27
+ return () => {
28
+ if (isTV()) {
29
+ return;
30
+ }
31
+
32
+ dialogVisibleRef.current = false;
33
+ };
34
+ }, []);
35
+
36
+ const isConnectionRestricted = useMemo(() => {
37
+ if (isTV()) {
38
+ return false;
39
+ }
40
+
41
+ return player && entry?.extensions?.connection_restricted;
42
+ }, [player, entry]);
43
+
44
+ const [isRestricted, setIsRestricted] = useState<boolean>(
45
+ isConnectionRestricted
46
+ );
47
+
48
+ useEffect(() => {
49
+ if (!isConnectionRestricted) {
50
+ return;
51
+ }
52
+
53
+ const stopPlayer = () => {
54
+ log_info(
55
+ "Stopping player due to mobile restriction, connection_restricted: true"
56
+ );
57
+
58
+ player?.close();
59
+
60
+ dialogVisibleRef.current = true;
61
+
62
+ showAlertDialog({
63
+ title:
64
+ pluginConfiguration?.mobile_connection_restricted_alert_title ||
65
+ "Restricted Connection Type",
66
+ message:
67
+ pluginConfiguration?.mobile_connection_restricted_alert_message ||
68
+ "This content can only be viewed over a Wi-Fi or LAN network.",
69
+ okButtonText: theme.ok_button || "OK",
70
+ completion: () => {
71
+ dialogVisibleRef.current = false;
72
+
73
+ close();
74
+ },
75
+ });
76
+ };
77
+
78
+ return NetInfo.addEventListener((state) => {
79
+ if (state.type === "cellular") {
80
+ setIsRestricted(true);
81
+
82
+ if (dialogVisibleRef.current) {
83
+ return;
84
+ }
85
+
86
+ stopPlayer();
87
+ } else {
88
+ setIsRestricted(false);
89
+ }
90
+ });
91
+ }, [
92
+ close,
93
+ entry?.extensions?.connection_restricted,
94
+ pluginConfiguration,
95
+ player,
96
+ theme.ok_button,
97
+ isConnectionRestricted,
98
+ ]);
99
+
100
+ return { isRestricted };
101
+ };
@@ -2,7 +2,8 @@
2
2
 
3
3
  import * as React from "react";
4
4
  import { Text } from "react-native";
5
- import * as R from "ramda";
5
+
6
+ import { mergeRight } from "@applicaster/zapp-react-native-utils/utils";
6
7
 
7
8
  import { GeneralContentScreen } from "../../GeneralContentScreen";
8
9
  import { ScreenResolver } from "@applicaster/zapp-react-native-ui-components/Components/ScreenResolver";
@@ -13,6 +14,8 @@ import {
13
14
  } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
14
15
  import { useRivers } from "@applicaster/zapp-react-native-utils/reactHooks/state";
15
16
 
17
+ import { toStringOrEmpty } from "./utils";
18
+
16
19
  type Props = {
17
20
  screenId: string;
18
21
  screenData: ZappRiver | ZappEntry;
@@ -24,6 +27,7 @@ type Props = {
24
27
  isInsideContainer?: boolean;
25
28
  extraAnchorPointYOffset: number;
26
29
  river?: ZappRiver | ZappEntry;
30
+ groupId: string;
27
31
  };
28
32
 
29
33
  export const River = (props: Props) => {
@@ -35,6 +39,7 @@ export const River = (props: Props) => {
35
39
  componentsMapExtraProps,
36
40
  isInsideContainer,
37
41
  extraAnchorPointYOffset,
42
+ groupId,
38
43
  } = props;
39
44
 
40
45
  const { title: screenTitle, summary: screenSummary } = useNavbarState();
@@ -51,28 +56,41 @@ export const River = (props: Props) => {
51
56
  [screenId]
52
57
  );
53
58
 
54
- const stringOrEmpty = (value: string | number | undefined): string =>
55
- R.isNil(value) ? "" : String(value);
59
+ const screenResolverData = React.useMemo(() => {
60
+ const extraData = mergeRight(extraProps, screenResolverExtraProps);
61
+
62
+ return {
63
+ extraData,
64
+ screenData: mergeRight(river, { groupId: extraData?.groupId }),
65
+ componentsMapExtraProps: mergeRight(componentsMapExtraProps, { groupId }),
66
+ };
67
+ }, [
68
+ extraProps,
69
+ screenResolverExtraProps,
70
+ river,
71
+ componentsMapExtraProps,
72
+ groupId,
73
+ ]);
56
74
 
57
75
  React.useEffect(() => {
58
76
  if (!isInsideContainer) {
59
- setScreenTitle(stringOrEmpty(screenData?.title));
60
- setScreenSummary(stringOrEmpty(screenData?.summary));
77
+ setScreenTitle(toStringOrEmpty(screenData?.title));
78
+ setScreenSummary(toStringOrEmpty(screenData?.summary));
61
79
  }
62
80
  }, [screenData.id]);
63
81
 
64
82
  React.useEffect(() => {
65
83
  if (feedData && !isInsideContainer) {
66
84
  if (feedData.title && feedData.title !== screenTitle) {
67
- setScreenTitle(stringOrEmpty(feedData.title));
85
+ setScreenTitle(toStringOrEmpty(feedData.title));
68
86
  }
69
87
 
70
88
  if (feedData.summary && feedData.summary !== screenSummary) {
71
- setScreenSummary(stringOrEmpty(feedData.summary));
89
+ setScreenSummary(toStringOrEmpty(feedData.summary));
72
90
  }
73
91
  } else {
74
- setScreenTitle(stringOrEmpty(screenData?.title));
75
- setScreenSummary(stringOrEmpty(screenData?.summary));
92
+ setScreenTitle(toStringOrEmpty(screenData?.title));
93
+ setScreenSummary(toStringOrEmpty(screenData?.summary));
76
94
  }
77
95
  }, [feedData, screenData, screenTitle, screenSummary]);
78
96
 
@@ -86,15 +104,13 @@ export const River = (props: Props) => {
86
104
  }
87
105
 
88
106
  if (river.type !== "general_content") {
89
- const extraData = { ...R.mergeRight(extraProps, screenResolverExtraProps) };
90
-
91
107
  return (
92
108
  <ScreenResolver
93
109
  screenType={river.type}
94
110
  screenId={screenId}
95
- screenData={R.merge(river, { groupId: extraData?.groupId })}
96
- componentsMapExtraProps={componentsMapExtraProps}
97
- {...extraData}
111
+ screenData={screenResolverData.screenData}
112
+ componentsMapExtraProps={screenResolverData.componentsMapExtraProps}
113
+ {...screenResolverData.extraData}
98
114
  />
99
115
  );
100
116
  }
@@ -106,6 +122,7 @@ export const River = (props: Props) => {
106
122
  isScreenWrappedInContainer={isInsideContainer}
107
123
  extraAnchorPointYOffset={extraAnchorPointYOffset}
108
124
  componentsMapExtraProps={componentsMapExtraProps}
125
+ groupId={groupId}
109
126
  />
110
127
  );
111
128
  };
@@ -1,11 +1,15 @@
1
- import { compose } from "ramda";
1
+ import { compose, identity } from "@applicaster/zapp-react-native-utils/utils";
2
+ import { isTvOSPlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
3
+
2
4
  import { River as RiverComponent } from "./River";
3
- import { withTvEventHandler } from "./withTVEventHandler";
4
5
  import { withComponentsMapOffsetContext } from "../../../Contexts/ComponentsMapOffsetContext";
5
6
  import { withRiverDataLoader } from "./withRiverDataLoader";
7
+ import { withFocusableGroupForContent } from "./withFocusableGroupForContent";
8
+
9
+ const isTVOS = isTvOSPlatform();
6
10
 
7
11
  export const River = compose(
8
- withTvEventHandler,
9
12
  withComponentsMapOffsetContext,
10
- withRiverDataLoader
13
+ withRiverDataLoader,
14
+ isTVOS ? withFocusableGroupForContent : identity
11
15
  )(RiverComponent);
@@ -0,0 +1,30 @@
1
+ import { toStringOrEmpty } from "..";
2
+
3
+ describe("toStringOrEmpty", () => {
4
+ test("returns empty string for undefined", () => {
5
+ expect(toStringOrEmpty(undefined)).toBe("");
6
+ });
7
+
8
+ test("returns empty string for null", () => {
9
+ expect(toStringOrEmpty(null)).toBe("");
10
+ });
11
+
12
+ test("converts number to string", () => {
13
+ expect(toStringOrEmpty(0)).toBe("0");
14
+ expect(toStringOrEmpty(123)).toBe("123");
15
+ expect(toStringOrEmpty(-42)).toBe("-42");
16
+ });
17
+
18
+ test("returns string as is", () => {
19
+ expect(toStringOrEmpty("hello")).toBe("hello");
20
+ expect(toStringOrEmpty("")).toBe("");
21
+ });
22
+
23
+ test("works with numeric strings", () => {
24
+ expect(toStringOrEmpty("123")).toBe("123");
25
+ });
26
+
27
+ test("does not throw on falsy values like 0", () => {
28
+ expect(toStringOrEmpty(0)).toBe("0");
29
+ });
30
+ });
@@ -0,0 +1,4 @@
1
+ import { isNil } from "@applicaster/zapp-react-native-utils/utils";
2
+
3
+ export const toStringOrEmpty = (value: unknown): string =>
4
+ isNil(value) ? "" : String(value);
@@ -0,0 +1,71 @@
1
+ import * as React from "react";
2
+ import { View, StyleSheet } from "react-native";
3
+
4
+ import { FocusableGroup } from "@applicaster/zapp-react-native-ui-components/Components/FocusableGroup";
5
+ import { riverFocusManager } from "@applicaster/zapp-react-native-utils/appUtils/RiverFocusManager";
6
+
7
+ import { topMenuLayoutChange$ } from "@applicaster/zapp-react-native-tvos-app/Layout/topMenu";
8
+
9
+ const styles = StyleSheet.create({
10
+ flexOne: {
11
+ flex: 1,
12
+ },
13
+ });
14
+
15
+ export const withFocusableGroupForContent = (Component) => {
16
+ return function WithFocusableGroupForContent(props) {
17
+ const { screenId, isInsideContainer } = props;
18
+
19
+ const [topMenuHeight, setTopMenuHeight] = React.useState(0);
20
+
21
+ React.useEffect(() => {
22
+ const subscription = topMenuLayoutChange$.subscribe((layout) => {
23
+ setTopMenuHeight(layout.height);
24
+ });
25
+
26
+ return () => {
27
+ subscription.unsubscribe();
28
+ };
29
+ }, []);
30
+
31
+ const focusableId = React.useMemo(
32
+ () =>
33
+ riverFocusManager.screenFocusableGroupId({
34
+ screenId,
35
+ isInsideContainer,
36
+ }),
37
+ [screenId, isInsideContainer]
38
+ );
39
+
40
+ if (isInsideContainer) {
41
+ return <Component {...props} />;
42
+ }
43
+
44
+ return (
45
+ <FocusableGroup
46
+ key={focusableId}
47
+ id={focusableId}
48
+ // The top menu is rendered in its own FocusableGroup, anchored at the top of the screen.
49
+ // When the "content" FocusableGroup starts at y = 0 as well, the two groups visually overlap.
50
+ // On TvOS platform this overlap can confuse the focus engine, because the focusable bounds of
51
+ // the top-menu group and the content group intersect, leading to erratic navigation between
52
+ // the menu and the content (e.g. unexpected jumps or focus getting "stuck").
53
+ //
54
+ // To avoid this, we shift the entire content FocusableGroup down by the dynamic top menu
55
+ // height (marginTop: topMenuHeight). This separates the focus regions of the two groups in
56
+ // focus space, so they no longer intersect.
57
+ //
58
+ // The inner <View> below then applies the inverse margin (marginTop: -topMenuHeight) so that
59
+ // the actual visual position of the content on screen does not change; only the focusable
60
+ // bounds of the outer group are offset.
61
+ style={[styles.flexOne, { marginTop: topMenuHeight }]}
62
+ // this group does not have parent
63
+ groupId={undefined}
64
+ >
65
+ <View style={[styles.flexOne, { marginTop: -1 * topMenuHeight }]}>
66
+ <Component {...props} groupId={focusableId} />
67
+ </View>
68
+ </FocusableGroup>
69
+ );
70
+ };
71
+ };
@@ -0,0 +1,33 @@
1
+ import React, { useMemo } from "react";
2
+ import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils";
3
+
4
+ export const AboveTabsScreenContext = React.createContext({
5
+ aboveTabsScreen: false,
6
+ });
7
+
8
+ export const withAboveTabsScreenContext = (Component) =>
9
+ function AbovetabsScreenContextProviderWrapper(props) {
10
+ const aboveTabsScreen = toBooleanWithDefaultFalse(
11
+ props?.item?.above_tabs_screen
12
+ );
13
+
14
+ const contextValue = useMemo(
15
+ () => ({
16
+ aboveTabsScreen,
17
+ }),
18
+ [aboveTabsScreen]
19
+ );
20
+
21
+ return (
22
+ <AboveTabsScreenContext.Provider value={contextValue}>
23
+ <Component {...props} />
24
+ </AboveTabsScreenContext.Provider>
25
+ );
26
+ };
27
+
28
+ export const withAboveTabsScreenContextConsumer = (Component) =>
29
+ function AboveTabsScreenContextConsumerWrapper(props) {
30
+ const { aboveTabsScreen } = React.useContext(AboveTabsScreenContext);
31
+
32
+ return <Component {...props} aboveTabsScreen={aboveTabsScreen} />;
33
+ };
@@ -88,6 +88,7 @@ exports[`withConfigurationProvider correctly passes all the configuration keys c
88
88
  tab_cell_padding_right={10}
89
89
  tab_cell_padding_top={14}
90
90
  tablet_theme={false}
91
+ tabs_screen_background_color="transparent"
91
92
  target={false}
92
93
  target_screen_switch={false}
93
94
  text_label_active_font_color="rgba(239, 239, 239, 0.5)"
@@ -208,4 +208,5 @@ export const keysMap: Record<string, Function> = {
208
208
  tab_bar_item_margin_right: castOrDefaultValue(Number, 0),
209
209
  tab_bar_item_margin_bottom: castOrDefaultValue(Number, 0),
210
210
  tab_bar_item_margin_left: castOrDefaultValue(Number, 0),
211
+ tabs_screen_background_color: castOrDefaultValue(R.identity, "transparent"),
211
212
  };
@@ -3,7 +3,7 @@ import { renderWithProviders } from "@applicaster/zapp-react-native-utils/testUt
3
3
 
4
4
  import * as zappPipesRedux from "@applicaster/zapp-react-native-redux/ZappPipes";
5
5
  import configureStore from "redux-mock-store";
6
- import thunk from "redux-thunk";
6
+ import { thunk } from "redux-thunk";
7
7
 
8
8
  import { zappPipesDataConnector } from "../index";
9
9
 
@@ -76,7 +76,7 @@ export function StaticFeedResolver({
76
76
 
77
77
  const zappPipesDataProps = useMemo(
78
78
  () => ({
79
- zappPipesData: { url, loading, data, error },
79
+ zappPipesData: { url, loading, data, error }, // todo: add applyItemLimit
80
80
  reloadData,
81
81
  loadNextData: undefined, // Static resolver doesn't support pagination
82
82
  }),
@@ -1,6 +1,15 @@
1
1
  import { toPositiveNumberWithDefault } from "@applicaster/zapp-react-native-utils/numberUtils";
2
2
 
3
- export function itemLimitForData(entry, component) {
3
+ export function itemLimitForData(
4
+ entry: ZappEntry[],
5
+ component: {
6
+ rules?: { item_limit?: number | string };
7
+ }
8
+ ) {
9
+ if (!component?.rules?.item_limit) {
10
+ return entry;
11
+ }
12
+
4
13
  const itemLimit = toPositiveNumberWithDefault(
5
14
  Infinity,
6
15
  component?.rules?.item_limit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-ui-components",
3
- "version": "15.0.0-alpha.4413958104",
3
+ "version": "15.0.0-alpha.4429053208",
4
4
  "description": "Applicaster Zapp React Native ui components for the Quick Brick App",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -28,10 +28,10 @@
28
28
  },
29
29
  "homepage": "https://github.com/applicaster/quickbrick#readme",
30
30
  "dependencies": {
31
- "@applicaster/applicaster-types": "15.0.0-alpha.4413958104",
32
- "@applicaster/zapp-react-native-bridge": "15.0.0-alpha.4413958104",
33
- "@applicaster/zapp-react-native-redux": "15.0.0-alpha.4413958104",
34
- "@applicaster/zapp-react-native-utils": "15.0.0-alpha.4413958104",
31
+ "@applicaster/applicaster-types": "15.0.0-alpha.4429053208",
32
+ "@applicaster/zapp-react-native-bridge": "15.0.0-alpha.4429053208",
33
+ "@applicaster/zapp-react-native-redux": "15.0.0-alpha.4429053208",
34
+ "@applicaster/zapp-react-native-utils": "15.0.0-alpha.4429053208",
35
35
  "fast-json-stable-stringify": "^2.1.0",
36
36
  "promise": "^8.3.0",
37
37
  "url": "^0.11.0",
@@ -1,27 +0,0 @@
1
- /* eslint max-len: off */
2
-
3
- import React from "react";
4
- import { TVEventHandlerComponent } from "@applicaster/zapp-react-native-tvos-ui-components/Components/TVEventHandlerComponent";
5
- import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
6
-
7
- export const withTvEventHandler = (Component) => {
8
- return function WithTVEventHandler(props) {
9
- const navigator = useNavigation();
10
-
11
- const remoteHandler = (event) => {
12
- const { eventType } = event;
13
-
14
- const canGoBack = navigator.canGoBack();
15
-
16
- if (eventType === "menu" && canGoBack) {
17
- navigator.goBack();
18
- }
19
- };
20
-
21
- return (
22
- <TVEventHandlerComponent tvEventHandler={remoteHandler}>
23
- <Component {...props} />
24
- </TVEventHandlerComponent>
25
- );
26
- };
27
- };