@applicaster/zapp-react-native-ui-components 15.0.0-rc.99 → 15.1.0-alpha.1852552014

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 (137) hide show
  1. package/Components/BackgroundImage/BackgroundImage.tv.android.tsx +28 -0
  2. package/Components/BackgroundImage/BackgroundImage.tv.ios.tsx +24 -0
  3. package/Components/BackgroundImage/BackgroundImage.tv.web.tsx +42 -0
  4. package/Components/BackgroundImage/index.ts +8 -0
  5. package/Components/BaseFocusable/index.ios.ts +2 -12
  6. package/Components/Cell/FocusableWrapper.tsx +0 -3
  7. package/Components/Cell/TvOSCellComponent.tsx +0 -5
  8. package/Components/CellRendererResolver/index.ts +1 -1
  9. package/Components/ComponentResolver/__tests__/componentResolver.test.js +1 -1
  10. package/Components/Focusable/Focusable.tsx +2 -4
  11. package/Components/Focusable/FocusableTvOS.tsx +1 -18
  12. package/Components/Focusable/__tests__/__snapshots__/FocusableTvOS.test.tsx.snap +0 -1
  13. package/Components/FocusableGroup/FocusableTvOS.tsx +12 -37
  14. package/Components/GeneralContentScreen/GeneralContentScreen.tsx +39 -28
  15. package/Components/GeneralContentScreen/__tests__/GeneralContentScreen.test.tsx +104 -0
  16. package/Components/GeneralContentScreen/utils/__tests__/getScreenDataSource.test.ts +19 -0
  17. package/Components/GeneralContentScreen/utils/__tests__/useCurationAPI.test.js +1 -1
  18. package/Components/GeneralContentScreen/utils/getScreenDataSource.ts +9 -0
  19. package/Components/HandlePlayable/HandlePlayable.tsx +24 -42
  20. package/Components/HandlePlayable/utils.ts +31 -0
  21. package/Components/HookRenderer/HookRenderer.tsx +40 -10
  22. package/Components/HookRenderer/__tests__/HookRenderer.test.tsx +60 -0
  23. package/Components/Layout/FullWidthRow.tsx +38 -0
  24. package/Components/Layout/TV/ScreenContainer.tsx +11 -2
  25. package/Components/Layout/TV/__tests__/__snapshots__/index.test.tsx.snap +5 -1
  26. package/Components/Layout/TV/index.tsx +6 -6
  27. package/Components/Layout/TV/index.web.tsx +6 -6
  28. package/Components/LinkHandler/LinkHandler.tsx +2 -2
  29. package/Components/MasterCell/DefaultComponents/BorderContainerView/index.tsx +10 -4
  30. package/Components/MasterCell/DefaultComponents/Image/Image.android.tsx +1 -5
  31. package/Components/MasterCell/DefaultComponents/Image/Image.ios.tsx +3 -11
  32. package/Components/MasterCell/DefaultComponents/Image/Image.web.tsx +1 -9
  33. package/Components/MasterCell/DefaultComponents/Image/hooks/useImage.ts +14 -15
  34. package/Components/MasterCell/DefaultComponents/LiveImage/__tests__/prepareEntry.test.ts +352 -0
  35. package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +136 -0
  36. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +34 -16
  37. package/Components/MasterCell/DefaultComponents/SecondaryImage/hooks/__tests__/useGetImageDimensions.test.ts +6 -7
  38. package/Components/MasterCell/DefaultComponents/Text/index.tsx +2 -6
  39. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +2 -6
  40. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/getPluginIdentifier.test.ts +11 -233
  41. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +15 -19
  42. package/Components/MasterCell/dataAdapter.ts +15 -5
  43. package/Components/MasterCell/index.tsx +10 -1
  44. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +169 -132
  45. package/Components/MasterCell/utils/__tests__/resolveColorForProp.test.js +92 -0
  46. package/Components/MasterCell/utils/index.ts +89 -46
  47. package/Components/Navigator/StackNavigator.tsx +6 -0
  48. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +2 -2
  49. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +18 -17
  50. package/Components/OfflineHandler/__tests__/index.test.tsx +18 -27
  51. package/Components/PlayerContainer/PlayerContainer.tsx +20 -34
  52. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +286 -0
  53. package/Components/PreloaderWrapper/__tests__/index.test.tsx +26 -0
  54. package/Components/PreloaderWrapper/index.tsx +15 -0
  55. package/Components/River/ComponentsMap/ComponentsMap.tsx +3 -4
  56. package/Components/River/ComponentsMap/hooks/__tests__/useLoadingState.test.ts +1 -1
  57. package/Components/River/RefreshControl.tsx +9 -3
  58. package/Components/River/RiverItem.tsx +26 -20
  59. package/Components/River/TV/River.tsx +14 -31
  60. package/Components/River/TV/index.tsx +4 -8
  61. package/Components/River/TV/withTVEventHandler.tsx +36 -0
  62. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +0 -16
  63. package/Components/River/__tests__/componentsMap.test.js +0 -38
  64. package/Components/Screen/TV/hooks/__tests__/useAfterPaint.test.ts +60 -0
  65. package/Components/Screen/TV/hooks/index.ts +2 -0
  66. package/Components/Screen/TV/hooks/useAfterPaint.ts +23 -0
  67. package/Components/Screen/TV/index.web.tsx +18 -11
  68. package/Components/Screen/__tests__/Screen.test.tsx +43 -65
  69. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +44 -68
  70. package/Components/Screen/hooks.ts +76 -5
  71. package/Components/Screen/index.tsx +18 -7
  72. package/Components/Screen/orientationHandler.ts +3 -3
  73. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +46 -0
  74. package/Components/ScreenFeedLoader/__tests__/ScreenFeedLoader.test.tsx +94 -0
  75. package/Components/ScreenFeedLoader/index.ts +1 -0
  76. package/Components/ScreenResolver/__tests__/screenResolver.test.js +24 -0
  77. package/Components/ScreenResolver/hooks/index.ts +3 -0
  78. package/Components/ScreenResolver/hooks/useGetComponent.ts +15 -0
  79. package/Components/ScreenResolver/hooks/useScreenComponentResolver.tsx +90 -0
  80. package/Components/ScreenResolver/index.tsx +9 -115
  81. package/Components/ScreenResolver/utils/__tests__/getScreenTypeProps.test.ts +45 -0
  82. package/Components/ScreenResolver/utils/getScreenTypeProps.ts +43 -0
  83. package/Components/ScreenResolver/utils/index.ts +1 -0
  84. package/Components/ScreenResolver/withDefaultScreenContext.tsx +16 -0
  85. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +25 -0
  86. package/Components/ScreenResolverFeedProvider/__tests__/ScreenResolverFeedProvider.test.tsx +44 -0
  87. package/Components/ScreenResolverFeedProvider/index.ts +1 -0
  88. package/Components/ScreenRevealManager/Overlay.tsx +34 -0
  89. package/Components/ScreenRevealManager/ScreenRevealManager.ts +8 -40
  90. package/Components/ScreenRevealManager/__tests__/Overlay.test.tsx +88 -0
  91. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +69 -86
  92. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +10 -18
  93. package/Components/Tabs/TabContent.tsx +4 -7
  94. package/Components/Transitioner/Scene.tsx +9 -15
  95. package/Components/Transitioner/index.js +3 -3
  96. package/Components/VideoLive/LiveImageManager.ts +209 -53
  97. package/Components/VideoLive/PlayerLiveImageComponent.tsx +32 -32
  98. package/Components/VideoLive/__tests__/PlayerLiveImageComponent.test.tsx +2 -17
  99. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +5 -5
  100. package/Components/VideoModal/hooks/__tests__/useDelayedPlayerDetails.test.ts +7 -15
  101. package/Components/VideoModal/utils.ts +16 -13
  102. package/Components/Viewport/ViewportEvents/__tests__/viewportEvents.test.js +1 -1
  103. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +12 -5
  104. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  105. package/Components/ZappUIComponent/index.tsx +12 -6
  106. package/Components/index.js +1 -1
  107. package/Contexts/CachedDimensionsContext/index.ts +17 -2
  108. package/Contexts/ScreenContext/__tests__/index.test.tsx +57 -0
  109. package/Contexts/ScreenContext/index.tsx +64 -26
  110. package/Contexts/ScreenTrackedViewPositionsContext/__tests__/index.test.tsx +1 -1
  111. package/Contexts/ZappPipesContext/ZappPipesContextFactory.tsx +18 -7
  112. package/Decorators/Analytics/index.tsx +5 -6
  113. package/Decorators/ConfigurationWrapper/__tests__/__snapshots__/withConfigurationProvider.test.tsx.snap +0 -1
  114. package/Decorators/ConfigurationWrapper/const.ts +0 -1
  115. package/Decorators/ZappPipesDataConnector/ResolverSelector.tsx +25 -7
  116. package/Decorators/ZappPipesDataConnector/__tests__/ResolverSelector.test.tsx +212 -5
  117. package/Decorators/ZappPipesDataConnector/__tests__/zappPipesDataConnector.test.js +1 -1
  118. package/Decorators/ZappPipesDataConnector/index.tsx +2 -2
  119. package/Decorators/ZappPipesDataConnector/resolvers/StaticFeedResolver.tsx +1 -1
  120. package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
  121. package/Helpers/DataSourceHelper/index.js +19 -0
  122. package/Helpers/index.js +7 -40
  123. package/package.json +5 -5
  124. package/Components/Layout/TV/LayoutBackground.tsx +0 -31
  125. package/Components/MasterCell/DefaultComponents/Text/utils/__tests__/withAdjustedLineHeight.test.ts +0 -46
  126. package/Components/MasterCell/DefaultComponents/Text/utils/index.ts +0 -21
  127. package/Components/PlayerContainer/ErrorDisplay/ErrorDisplay.tsx +0 -57
  128. package/Components/PlayerContainer/ErrorDisplay/index.ts +0 -9
  129. package/Components/PlayerContainer/useRestrictMobilePlayback.tsx +0 -101
  130. package/Components/River/TV/utils/__tests__/toStringOrEmpty.test.ts +0 -30
  131. package/Components/River/TV/utils/index.ts +0 -4
  132. package/Components/River/TV/withFocusableGroupForContent.tsx +0 -71
  133. package/Components/Screen/utils.ts +0 -16
  134. package/Helpers/Analytics/index.js +0 -95
  135. package/Helpers/DataSourceHelper/__tests__/itemLimitForData.test.ts +0 -80
  136. package/Helpers/DataSourceHelper/index.ts +0 -19
  137. /package/Components/HookRenderer/{index.tsx → index.ts} +0 -0
@@ -14,6 +14,7 @@ import { tvPluginsWithCellRenderer } from "../../const";
14
14
  import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
15
15
  import type { BehaviorSubject } from "rxjs";
16
16
  import { useCallbackActions } from "@applicaster/zapp-react-native-utils/zappFrameworkUtils/HookCallback/useCallbackActions";
17
+ import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
17
18
 
18
19
  export type RiverItemType = {
19
20
  item: ZappUIComponent;
@@ -112,33 +113,38 @@ function RiverItemComponent(props: RiverItemType) {
112
113
  CellRenderer = undefined;
113
114
  }
114
115
 
115
- React.useEffect(() => {
116
- riverLogger.log({
117
- message: "mounting component",
118
- data: { item, feedUrl, Component, CellRenderer },
119
- jsOnly: true,
120
- });
116
+ const isComponentMissing = isNilOrEmpty(Component);
121
117
 
122
- if (!CellRenderer && !isGroup(item)) {
118
+ /**
119
+ * TODO: Move this plugin existence check further up the stack (before ComponentsMap).
120
+ * Filtering items at the list-rendering or data-processing level would prevent
121
+ * mounting RiverItem entirely for missing components.
122
+ */
123
+ React.useEffect(() => {
124
+ if (isComponentMissing) {
123
125
  riverLogger.warning({
124
- message: "Cell Renderer is null - will fallback to default cell",
125
- data: { item, CellRenderer },
126
+ message: `Component ${item.component_type} is null - skipping rendering`,
127
+ });
128
+
129
+ onLoadFinished(index);
130
+ } else {
131
+ riverLogger.log({
132
+ message: "mounting component",
133
+ data: { item, feedUrl, Component, CellRenderer },
126
134
  jsOnly: true,
127
135
  });
136
+
137
+ if (!CellRenderer && !isGroup(item)) {
138
+ riverLogger.warning({
139
+ message: "Cell Renderer is null - will fallback to default cell",
140
+ data: { item, CellRenderer },
141
+ jsOnly: true,
142
+ });
143
+ }
128
144
  }
129
145
  }, []);
130
146
 
131
- if (!readyToBeDisplayed) {
132
- return null;
133
- }
134
-
135
- if (Component === null || typeof Component === "undefined") {
136
- riverLogger.warning({
137
- message: `Component ${item.component_type} is null - skipping rendering`,
138
- });
139
-
140
- onLoadFinished(index);
141
-
147
+ if (!readyToBeDisplayed || isComponentMissing) {
142
148
  return null;
143
149
  }
144
150
 
@@ -2,8 +2,7 @@
2
2
 
3
3
  import * as React from "react";
4
4
  import { Text } from "react-native";
5
-
6
- import { mergeRight } from "@applicaster/zapp-react-native-utils/utils";
5
+ import * as R from "ramda";
7
6
 
8
7
  import { GeneralContentScreen } from "../../GeneralContentScreen";
9
8
  import { ScreenResolver } from "@applicaster/zapp-react-native-ui-components/Components/ScreenResolver";
@@ -14,8 +13,6 @@ import {
14
13
  } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
15
14
  import { useRivers } from "@applicaster/zapp-react-native-utils/reactHooks/state";
16
15
 
17
- import { toStringOrEmpty } from "./utils";
18
-
19
16
  type Props = {
20
17
  screenId: string;
21
18
  screenData: ZappRiver | ZappEntry;
@@ -27,7 +24,6 @@ type Props = {
27
24
  isInsideContainer?: boolean;
28
25
  extraAnchorPointYOffset: number;
29
26
  river?: ZappRiver | ZappEntry;
30
- groupId: string;
31
27
  };
32
28
 
33
29
  export const River = (props: Props) => {
@@ -39,7 +35,6 @@ export const River = (props: Props) => {
39
35
  componentsMapExtraProps,
40
36
  isInsideContainer,
41
37
  extraAnchorPointYOffset,
42
- groupId,
43
38
  } = props;
44
39
 
45
40
  const { title: screenTitle, summary: screenSummary } = useNavbarState();
@@ -56,41 +51,28 @@ export const River = (props: Props) => {
56
51
  [screenId]
57
52
  );
58
53
 
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
- ]);
54
+ const stringOrEmpty = (value: string | number | undefined): string =>
55
+ R.isNil(value) ? "" : String(value);
74
56
 
75
57
  React.useEffect(() => {
76
58
  if (!isInsideContainer) {
77
- setScreenTitle(toStringOrEmpty(screenData?.title));
78
- setScreenSummary(toStringOrEmpty(screenData?.summary));
59
+ setScreenTitle(stringOrEmpty(screenData?.title));
60
+ setScreenSummary(stringOrEmpty(screenData?.summary));
79
61
  }
80
62
  }, [screenData.id]);
81
63
 
82
64
  React.useEffect(() => {
83
65
  if (feedData && !isInsideContainer) {
84
66
  if (feedData.title && feedData.title !== screenTitle) {
85
- setScreenTitle(toStringOrEmpty(feedData.title));
67
+ setScreenTitle(stringOrEmpty(feedData.title));
86
68
  }
87
69
 
88
70
  if (feedData.summary && feedData.summary !== screenSummary) {
89
- setScreenSummary(toStringOrEmpty(feedData.summary));
71
+ setScreenSummary(stringOrEmpty(feedData.summary));
90
72
  }
91
73
  } else {
92
- setScreenTitle(toStringOrEmpty(screenData?.title));
93
- setScreenSummary(toStringOrEmpty(screenData?.summary));
74
+ setScreenTitle(stringOrEmpty(screenData?.title));
75
+ setScreenSummary(stringOrEmpty(screenData?.summary));
94
76
  }
95
77
  }, [feedData, screenData, screenTitle, screenSummary]);
96
78
 
@@ -104,13 +86,15 @@ export const River = (props: Props) => {
104
86
  }
105
87
 
106
88
  if (river.type !== "general_content") {
89
+ const extraData = { ...R.mergeRight(extraProps, screenResolverExtraProps) };
90
+
107
91
  return (
108
92
  <ScreenResolver
109
93
  screenType={river.type}
110
94
  screenId={screenId}
111
- screenData={screenResolverData.screenData}
112
- componentsMapExtraProps={screenResolverData.componentsMapExtraProps}
113
- {...screenResolverData.extraData}
95
+ screenData={R.merge(river, { groupId: extraData?.groupId })}
96
+ componentsMapExtraProps={componentsMapExtraProps}
97
+ {...extraData}
114
98
  />
115
99
  );
116
100
  }
@@ -122,7 +106,6 @@ export const River = (props: Props) => {
122
106
  isScreenWrappedInContainer={isInsideContainer}
123
107
  extraAnchorPointYOffset={extraAnchorPointYOffset}
124
108
  componentsMapExtraProps={componentsMapExtraProps}
125
- groupId={groupId}
126
109
  />
127
110
  );
128
111
  };
@@ -1,15 +1,11 @@
1
- import { compose, identity } from "@applicaster/zapp-react-native-utils/utils";
2
- import { isTvOSPlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
3
-
1
+ import { compose } from "ramda";
4
2
  import { River as RiverComponent } from "./River";
3
+ import { withTvEventHandler } from "./withTVEventHandler";
5
4
  import { withComponentsMapOffsetContext } from "../../../Contexts/ComponentsMapOffsetContext";
6
5
  import { withRiverDataLoader } from "./withRiverDataLoader";
7
- import { withFocusableGroupForContent } from "./withFocusableGroupForContent";
8
-
9
- const isTVOS = isTvOSPlatform();
10
6
 
11
7
  export const River = compose(
8
+ withTvEventHandler,
12
9
  withComponentsMapOffsetContext,
13
- withRiverDataLoader,
14
- isTVOS ? withFocusableGroupForContent : identity
10
+ withRiverDataLoader
15
11
  )(RiverComponent);
@@ -0,0 +1,36 @@
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
+ import { useIsStandaloneFullscreen } from "@applicaster/zapp-react-native-utils/reactHooks/screen";
7
+
8
+ export const withTvEventHandler = (Component) => {
9
+ return function WithTVEventHandler(props) {
10
+ const navigator = useNavigation();
11
+
12
+ const isStandaloneFullscreen = useIsStandaloneFullscreen();
13
+
14
+ const remoteHandler = (event) => {
15
+ const { eventType } = event;
16
+
17
+ const canGoBack = navigator.canGoBack();
18
+
19
+ if (eventType === "menu" && isStandaloneFullscreen) {
20
+ navigator.goHome();
21
+
22
+ return;
23
+ }
24
+
25
+ if (eventType === "menu" && canGoBack) {
26
+ navigator.goBack();
27
+ }
28
+ };
29
+
30
+ return (
31
+ <TVEventHandlerComponent tvEventHandler={remoteHandler}>
32
+ <Component {...props} />
33
+ </TVEventHandlerComponent>
34
+ );
35
+ };
36
+ };
@@ -10,11 +10,6 @@ exports[`componentsMap renders renders components map correctly 1`] = `
10
10
  >
11
11
  <View
12
12
  onLayout={[Function]}
13
- style={
14
- {
15
- "flex": 1,
16
- }
17
- }
18
13
  >
19
14
  <RCTScrollView
20
15
  ListFooterComponent={
@@ -155,7 +150,6 @@ exports[`componentsMap renders renders components map correctly 1`] = `
155
150
  }
156
151
  }
157
152
  stickyHeaderIndices={[]}
158
- testID="components-map-flat-list"
159
153
  viewabilityConfigCallbackPairs={[]}
160
154
  windowSize={12}
161
155
  >
@@ -167,11 +161,6 @@ exports[`componentsMap renders renders components map correctly 1`] = `
167
161
  >
168
162
  <View
169
163
  onLayout={[Function]}
170
- style={
171
- {
172
- "flex": 1,
173
- }
174
- }
175
164
  >
176
165
  <View />
177
166
  </View>
@@ -183,11 +172,6 @@ exports[`componentsMap renders renders components map correctly 1`] = `
183
172
  >
184
173
  <View
185
174
  onLayout={[Function]}
186
- style={
187
- {
188
- "flex": 1,
189
- }
190
- }
191
175
  />
192
176
  </View>
193
177
  <View
@@ -139,13 +139,7 @@ jest.mock(
139
139
  })
140
140
  );
141
141
 
142
- jest.mock("@applicaster/zapp-react-native-ui-components/events", () => ({
143
- ...jest.requireActual("@applicaster/zapp-react-native-ui-components/events"),
144
- emitScrollEndReached: jest.fn(),
145
- }));
146
-
147
142
  const { View } = require("react-native");
148
- const events = require("@applicaster/zapp-react-native-ui-components/events");
149
143
  const { ComponentsMap } = require("../ComponentsMap/ComponentsMap");
150
144
  const theme = require("./theme-mock.json");
151
145
 
@@ -196,36 +190,4 @@ describe("componentsMap", () => {
196
190
 
197
191
  expect(toJSON()).toMatchSnapshot();
198
192
  });
199
-
200
- it("calls emitScrollEndReached when onScroll was called and isScreenWrappedInContainer is false", () => {
201
- themeSpy = jest
202
- .spyOn(themeUtils, "useTheme")
203
- .mockImplementation(() => () => theme);
204
-
205
- events.emitScrollEndReached.mockClear();
206
-
207
- const { getByTestId } = render(
208
- <Provider store={store}>
209
- <ComponentsMap
210
- {...props}
211
- isScreenWrappedInContainer={false}
212
- feed={{ entry: [] }}
213
- />
214
- </Provider>
215
- );
216
-
217
- const flatList = getByTestId("components-map-flat-list");
218
-
219
- flatList.props.onScroll({
220
- nativeEvent: {
221
- contentOffset: { y: 0 },
222
- layoutMeasurement: { height: 100 },
223
- contentSize: { height: 200 },
224
- },
225
- });
226
-
227
- flatList.props.onEndReached();
228
-
229
- expect(events.emitScrollEndReached).toHaveBeenCalledTimes(1);
230
- });
231
193
  });
@@ -0,0 +1,60 @@
1
+ import { renderHook, act } from "@testing-library/react-native";
2
+
3
+ import { useAfterPaint } from "../useAfterPaint";
4
+
5
+ describe("useAfterPaint", () => {
6
+ let frameCallbacks: FrameRequestCallback[];
7
+ let originalRaf: typeof requestAnimationFrame;
8
+ let originalCancelRaf: typeof cancelAnimationFrame;
9
+
10
+ const flushFrame = () => {
11
+ const callbacks = frameCallbacks;
12
+ frameCallbacks = [];
13
+
14
+ act(() => {
15
+ callbacks.forEach((cb) => cb(0));
16
+ });
17
+ };
18
+
19
+ beforeEach(() => {
20
+ frameCallbacks = [];
21
+ originalRaf = global.requestAnimationFrame;
22
+ originalCancelRaf = global.cancelAnimationFrame;
23
+
24
+ global.requestAnimationFrame = jest.fn((cb: FrameRequestCallback) => {
25
+ frameCallbacks.push(cb);
26
+
27
+ return frameCallbacks.length;
28
+ }) as unknown as typeof requestAnimationFrame;
29
+
30
+ global.cancelAnimationFrame = jest.fn();
31
+ });
32
+
33
+ afterEach(() => {
34
+ global.requestAnimationFrame = originalRaf;
35
+ global.cancelAnimationFrame = originalCancelRaf;
36
+ });
37
+
38
+ it("returns false on the initial render", () => {
39
+ const { result } = renderHook(() => useAfterPaint());
40
+
41
+ expect(result.current).toBe(false);
42
+ });
43
+
44
+ it("stays false after only one animation frame (before paint completes)", () => {
45
+ const { result } = renderHook(() => useAfterPaint());
46
+
47
+ flushFrame();
48
+
49
+ expect(result.current).toBe(false);
50
+ });
51
+
52
+ it("returns true after two animation frames (a real post-paint boundary)", () => {
53
+ const { result } = renderHook(() => useAfterPaint());
54
+
55
+ flushFrame();
56
+ flushFrame();
57
+
58
+ expect(result.current).toBe(true);
59
+ });
60
+ });
@@ -1 +1,3 @@
1
1
  export { useInitialFocus } from "./useInitialFocus";
2
+
3
+ export { useAfterPaint } from "./useAfterPaint";
@@ -0,0 +1,23 @@
1
+ import * as React from "react";
2
+
3
+ export const useAfterPaint = (): boolean => {
4
+ const [painted, setPainted] = React.useState(false);
5
+
6
+ React.useEffect(() => {
7
+ let secondFrame: number | undefined;
8
+
9
+ const firstFrame = requestAnimationFrame(() => {
10
+ secondFrame = requestAnimationFrame(() => setPainted(true));
11
+ });
12
+
13
+ return () => {
14
+ cancelAnimationFrame(firstFrame);
15
+
16
+ if (secondFrame !== undefined) {
17
+ cancelAnimationFrame(secondFrame);
18
+ }
19
+ };
20
+ }, []);
21
+
22
+ return painted;
23
+ };
@@ -9,10 +9,9 @@ import {
9
9
  import {
10
10
  useIsScreenActive,
11
11
  useNavigation,
12
- useRivers,
13
12
  } from "@applicaster/zapp-react-native-utils/reactHooks";
14
13
  import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
15
- import { usePlugins } from "@applicaster/zapp-react-native-redux/hooks";
14
+ import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
16
15
  import {
17
16
  useNavbarState,
18
17
  useScreenBackgroundColor,
@@ -21,7 +20,7 @@ import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/he
21
20
 
22
21
  import { NavBarContainer } from "../../Layout/TV/NavBarContainer";
23
22
  import { ScreenResolver } from "../../ScreenResolver";
24
- import { useInitialFocus } from "./hooks";
23
+ import { useInitialFocus, useAfterPaint } from "./hooks";
25
24
  import { isPlayerPlugin } from "@applicaster/zapp-react-native-utils/pluginUtils";
26
25
  import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils";
27
26
  import { FreezeWithCallback } from "../../FreezeWithCallback";
@@ -103,8 +102,7 @@ export const Screen = ({ route, Components }: Props) => {
103
102
  }
104
103
 
105
104
  const navigator = useNavigation();
106
- const plugins = usePlugins();
107
- const rivers = useRivers();
105
+ const { plugins = [], rivers = [] } = usePickFromState(["plugins", "rivers"]);
108
106
 
109
107
  const pathAttributes = getPathAttributes({ pathname: route });
110
108
  const routeState = navigator.getStackForPathname(route);
@@ -157,6 +155,13 @@ export const Screen = ({ route, Components }: Props) => {
157
155
 
158
156
  const isScreenActive = useIsScreenActive();
159
157
 
158
+ // We need to render menu first and then proceed with screen content,
159
+ // otherwise screen will stay black until everything is loaded and screen
160
+ // rendering put huge load the CPU pushing rendering even further.
161
+ // With this approach, menu will be rendered immediately and screen content
162
+ // will be rendered after paint, which makes it more responsive and prevents black screen.
163
+ const isContentReady = useAfterPaint();
164
+
160
165
  return (
161
166
  <FreezeWithCallback freeze={!isScreenActive} onRelease={onRelease}>
162
167
  <View style={[styles.container, { backgroundColor }]}>
@@ -167,12 +172,14 @@ export const Screen = ({ route, Components }: Props) => {
167
172
  navigationProps={navigationProps}
168
173
  />
169
174
  </NavBarContainer>
170
- <ScreenResolver
171
- screenType={screenType}
172
- screenId={screenId}
173
- screenData={screenData}
174
- groupId={route}
175
- />
175
+ {isContentReady ? (
176
+ <ScreenResolver
177
+ screenType={screenType}
178
+ screenId={screenId}
179
+ screenData={screenData}
180
+ groupId={route}
181
+ />
182
+ ) : null}
176
183
  </View>
177
184
  </FreezeWithCallback>
178
185
  );
@@ -1,16 +1,20 @@
1
1
  import * as React from "react";
2
2
  import { View } from "react-native";
3
- import { renderWithProviders } from "@applicaster/zapp-react-native-utils/testUtils";
3
+ import { render } from "@testing-library/react-native";
4
4
 
5
5
  const Mocked_RouteManager = jest.fn((props) => (
6
6
  <View testID="routeManager" {...props} />
7
7
  ));
8
8
 
9
+ const mock_navBarVisibleFlag = true;
10
+
11
+ const mockIsNavBarVisible = jest.fn(() => mock_navBarVisibleFlag);
12
+
9
13
  const mockIsOrientationCompatible = jest.fn(() => true);
10
14
 
11
15
  jest.mock("react-native-safe-area-context", () => ({
12
- ...jest.requireActual("react-native-safe-area-context"),
13
16
  useSafeAreaInsets: () => ({ top: 44 }),
17
+ useSafeAreaFrame: () => ({ x: 0, y: 0, width: 375, height: 812 }),
14
18
  }));
15
19
 
16
20
  jest.mock("../../RouteManager", () => ({
@@ -32,14 +36,12 @@ jest.mock(
32
36
  );
33
37
 
34
38
  jest.mock("@applicaster/zapp-react-native-utils/analyticsUtils", () => ({
35
- ...jest.requireActual("@applicaster/zapp-react-native-utils/analyticsUtils"),
36
39
  useAnalytics: jest.fn(() => ({
37
40
  sendScreenEvent: jest.fn(),
38
41
  })),
39
42
  }));
40
43
 
41
44
  jest.mock("@applicaster/zapp-react-native-utils/theme", () => ({
42
- ...jest.requireActual("@applicaster/zapp-react-native-utils/theme"),
43
45
  useTheme: jest.fn(() => ({
44
46
  app_background_color: "blue",
45
47
  })),
@@ -76,44 +78,21 @@ jest.mock(
76
78
  })
77
79
  );
78
80
 
79
- jest.mock(
80
- "@applicaster/zapp-react-native-utils/reactHooks/navigation/useNavigation",
81
- () => ({
82
- ...jest.requireActual(
83
- "@applicaster/zapp-react-native-utils/reactHooks/navigation/useNavigation"
84
- ),
85
- useNavigation: jest.fn(() => ({
86
- canGoBack: () => false,
87
- currentRoute: "/river/testId",
88
- activeRiver: { id: "testId" },
89
- screenData: { id: "testId" },
90
- data: { screen: { id: "testId" } },
91
- })),
92
- })
93
- );
94
-
95
- jest.mock(
96
- "@applicaster/zapp-react-native-utils/reactHooks/navigation/useIsScreenActive",
97
- () => ({
98
- ...jest.requireActual(
99
- "@applicaster/zapp-react-native-utils/reactHooks/navigation/useIsScreenActive"
100
- ),
101
- useIsScreenActive: jest.fn().mockReturnValue(true),
102
- })
103
- );
104
-
105
- jest.mock(
106
- "@applicaster/zapp-react-native-utils/reactHooks/navigation/useRoute",
107
- () => ({
108
- ...jest.requireActual(
109
- "@applicaster/zapp-react-native-utils/reactHooks/navigation/useRoute"
110
- ),
111
- useRoute: jest.fn(() => ({
112
- pathname: "/river/testId",
113
- screenData: { id: "testId" },
114
- })),
115
- })
116
- );
81
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/navigation", () => ({
82
+ isNavBarVisible: mockIsNavBarVisible,
83
+ useIsScreenActive: jest.fn().mockReturnValue(true),
84
+ useNavigation: jest.fn(() => ({
85
+ canGoBack: () => false,
86
+ currentRoute: "/river/testId",
87
+ activeRiver: { id: "testId" },
88
+ screenData: { id: "testId" },
89
+ data: { screen: { id: "testId" } },
90
+ })),
91
+ useRoute: jest.fn(() => ({
92
+ pathname: "/river/testId",
93
+ screenData: { id: "testId" },
94
+ })),
95
+ }));
117
96
 
118
97
  jest.mock("@applicaster/zapp-react-native-utils/reactHooks", () => ({
119
98
  ...jest.requireActual("@applicaster/zapp-react-native-utils/reactHooks"),
@@ -134,6 +113,26 @@ jest.mock("@applicaster/zapp-react-native-utils/reactHooks", () => ({
134
113
  useIsTablet: jest.fn(() => false),
135
114
  }));
136
115
 
116
+ jest.mock("@applicaster/zapp-react-native-redux/hooks", () => {
117
+ const View = jest.requireActual("react-native").View;
118
+
119
+ return {
120
+ ...jest.requireActual("@applicaster/zapp-react-native-redux/hooks"),
121
+ usePickFromState: () => ({
122
+ plugins: [
123
+ {
124
+ name: "Offline Plugin",
125
+ identifier: "offline-experience",
126
+ type: "general",
127
+ module: {
128
+ OfflineFallbackScreen: ({ children }) => <View>{children}</View>, // eslint-disable-line
129
+ },
130
+ },
131
+ ],
132
+ }),
133
+ };
134
+ });
135
+
137
136
  const {
138
137
  allowedOrientationsForScreen,
139
138
  getOrientation,
@@ -153,19 +152,6 @@ const screenProps = {
153
152
 
154
153
  const { Screen } = require("..");
155
154
 
156
- const store = {
157
- plugins: [
158
- {
159
- name: "Offline Plugin",
160
- identifier: "offline-experience",
161
- type: "general",
162
- module: {
163
- OfflineFallbackScreen: ({ children }) => <View>{children}</View>, // eslint-disable-line
164
- },
165
- },
166
- ],
167
- };
168
-
169
155
  describe("<Screen Component />", () => {
170
156
  beforeEach(() => {
171
157
  allowedOrientationsForScreen.mockClear();
@@ -175,22 +161,14 @@ describe("<Screen Component />", () => {
175
161
 
176
162
  describe("when the navbar should show", () => {
177
163
  it("renders correctly", () => {
178
- const { toJSON } = renderWithProviders(
179
- <Screen {...screenProps} />,
180
- store
181
- );
182
-
164
+ const { toJSON } = render(<Screen {...screenProps} />);
183
165
  expect(toJSON()).toMatchSnapshot();
184
166
  });
185
167
  });
186
168
 
187
169
  describe("when the navbar should be hidden", () => {
188
170
  it("renders correctly", () => {
189
- const { toJSON } = renderWithProviders(
190
- <Screen {...screenProps} />,
191
- store
192
- );
193
-
171
+ const { toJSON } = render(<Screen {...screenProps} />);
194
172
  expect(toJSON()).toMatchSnapshot();
195
173
  });
196
174
  });