@applicaster/zapp-react-native-ui-components 15.1.0-rc.9 → 16.0.0-alpha.1740103407

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 (156) hide show
  1. package/Components/BackgroundImage/BackgroundImage.tsx +42 -0
  2. package/Components/BackgroundImage/BackgroundImage.tv.android.tsx +28 -0
  3. package/Components/BackgroundImage/BackgroundImage.tv.ios.tsx +24 -0
  4. package/Components/BackgroundImage/index.ts +1 -0
  5. package/Components/BaseFocusable/index.ios.ts +12 -2
  6. package/Components/Cell/FocusableWrapper.tsx +3 -0
  7. package/Components/Cell/TvOSCellComponent.tsx +6 -3
  8. package/Components/Focusable/Focusable.tsx +4 -2
  9. package/Components/Focusable/FocusableTvOS.tsx +18 -1
  10. package/Components/Focusable/__tests__/__snapshots__/FocusableTvOS.test.tsx.snap +1 -0
  11. package/Components/FocusableGroup/FocusableTvOS.tsx +41 -8
  12. package/Components/GeneralContentScreen/utils/__tests__/useCurationAPI.test.js +1 -1
  13. package/Components/HandlePlayable/HandlePlayable.tsx +13 -8
  14. package/Components/Layout/FullWidthRow.tsx +38 -0
  15. package/Components/Layout/TV/NavBarContainer.tsx +1 -10
  16. package/Components/Layout/TV/ScreenContainer.tsx +2 -6
  17. package/Components/Layout/TV/__tests__/__snapshots__/NavBarContainer.test.tsx.snap +7 -12
  18. package/Components/Layout/TV/__tests__/__snapshots__/ScreenContainer.test.tsx.snap +7 -12
  19. package/Components/Layout/TV/__tests__/__snapshots__/index.test.tsx.snap +0 -1
  20. package/Components/Layout/TV/index.tsx +5 -7
  21. package/Components/Layout/TV/index.web.tsx +5 -7
  22. package/Components/LinkHandler/LinkHandler.tsx +2 -2
  23. package/Components/MasterCell/CONFIG_BUILDER_TO_REACT_COMPONENT.md +144 -0
  24. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +16 -5
  25. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/model.test.ts +80 -0
  26. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +187 -0
  27. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/selectors.test.ts +45 -0
  28. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/style.test.ts +49 -0
  29. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +165 -0
  30. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +405 -0
  31. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/index.ts +1 -0
  32. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/model.ts +47 -0
  33. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +170 -0
  34. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/selectors.ts +26 -0
  35. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/style.ts +29 -0
  36. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/types.ts +37 -0
  37. package/Components/MasterCell/DefaultComponents/BorderContainerView/index.tsx +4 -10
  38. package/Components/MasterCell/DefaultComponents/Button.tsx +0 -15
  39. package/Components/MasterCell/DefaultComponents/ButtonContainerView/components/HorizontalSeparator.tsx +8 -0
  40. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tsx +15 -0
  41. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tv.android.tsx +58 -0
  42. package/Components/MasterCell/DefaultComponents/{tv/ButtonContainerView/index.tsx → ButtonContainerView/index.tv.tsx} +3 -11
  43. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.web.ts +1 -0
  44. package/Components/MasterCell/DefaultComponents/ButtonContainerView/types.ts +40 -0
  45. package/Components/MasterCell/DefaultComponents/DataProvider/index.tsx +163 -0
  46. package/Components/MasterCell/DefaultComponents/FocusableView/index.android.tsx +2 -23
  47. package/Components/MasterCell/DefaultComponents/FocusableView/index.tsx +4 -22
  48. package/Components/MasterCell/DefaultComponents/Image/Image.android.tsx +8 -2
  49. package/Components/MasterCell/DefaultComponents/Image/Image.ios.tsx +11 -3
  50. package/Components/MasterCell/DefaultComponents/Image/Image.web.tsx +9 -1
  51. package/Components/MasterCell/DefaultComponents/Image/hooks/useImage.ts +15 -14
  52. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +1 -2
  53. package/Components/MasterCell/DefaultComponents/PressableView.tsx +34 -0
  54. package/Components/MasterCell/DefaultComponents/SecondaryImage/hooks/__tests__/useGetImageDimensions.test.ts +7 -6
  55. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +11 -0
  56. package/Components/MasterCell/DefaultComponents/__tests__/DataProvider.test.tsx +141 -0
  57. package/Components/MasterCell/DefaultComponents/index.ts +9 -3
  58. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +135 -0
  59. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Asset.ts +33 -0
  60. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +22 -0
  61. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +125 -0
  62. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +16 -0
  63. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabel.ts +67 -0
  64. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +37 -0
  65. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +393 -0
  66. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/builders.test.ts +141 -0
  67. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +343 -0
  68. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/helpers.ts +105 -0
  69. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +122 -0
  70. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +118 -0
  71. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +238 -0
  72. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/Asset.ts +4 -18
  73. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/Button.ts +24 -73
  74. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/TextLabelsContainer.ts +37 -18
  75. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/TvActionButton.tsx +27 -0
  76. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/index.test.ts +89 -0
  77. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/renderedTree.test.tsx +231 -0
  78. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +47 -48
  79. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/getPluginIdentifier.test.ts +115 -29
  80. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +101 -144
  81. package/Components/MasterCell/MappingFunctions/index.js +3 -2
  82. package/Components/MasterCell/README.md +4 -0
  83. package/Components/MasterCell/__tests__/__snapshots__/dataAdapter.test.js.snap +24 -0
  84. package/Components/MasterCell/__tests__/configInflater.test.js +1 -0
  85. package/Components/MasterCell/__tests__/elementMapper.test.js +46 -0
  86. package/Components/MasterCell/dataAdapter.ts +4 -1
  87. package/Components/MasterCell/elementMapper.tsx +52 -7
  88. package/Components/MasterCell/utils/__tests__/cloneChildrenWithIds.test.tsx +43 -0
  89. package/Components/MasterCell/utils/__tests__/useFilterChildren.test.tsx +80 -0
  90. package/Components/MasterCell/utils/index.ts +85 -15
  91. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +2 -2
  92. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +17 -18
  93. package/Components/OfflineHandler/__tests__/index.test.tsx +27 -18
  94. package/Components/PlayerContainer/PlayerContainer.tsx +8 -5
  95. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +284 -0
  96. package/Components/River/ComponentsMap/ComponentsMap.tsx +188 -118
  97. package/Components/River/ComponentsMap/hooks/__tests__/useLoadingState.test.ts +1 -1
  98. package/Components/River/RefreshControl.tsx +19 -88
  99. package/Components/River/River.tsx +9 -82
  100. package/Components/River/TV/River.tsx +31 -14
  101. package/Components/River/TV/index.tsx +8 -4
  102. package/Components/River/TV/utils/__tests__/toStringOrEmpty.test.ts +30 -0
  103. package/Components/River/TV/utils/index.ts +4 -0
  104. package/Components/River/TV/withFocusableGroupForContent.tsx +71 -0
  105. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +2 -22
  106. package/Components/River/__tests__/componentsMap.test.js +38 -0
  107. package/Components/River/hooks/__tests__/usePullToRefresh.test.ts +132 -0
  108. package/Components/River/hooks/index.ts +1 -0
  109. package/Components/River/hooks/usePullToRefresh.ts +51 -0
  110. package/Components/Screen/TV/index.web.tsx +4 -2
  111. package/Components/Screen/__tests__/Screen.test.tsx +65 -42
  112. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +68 -44
  113. package/Components/Screen/hooks.ts +2 -3
  114. package/Components/Screen/index.tsx +2 -3
  115. package/Components/Screen/orientationHandler.ts +3 -3
  116. package/Components/ScreenResolver/index.tsx +9 -5
  117. package/Components/ScreenRevealManager/Overlay.tsx +34 -0
  118. package/Components/ScreenRevealManager/ScreenRevealManager.ts +40 -8
  119. package/Components/ScreenRevealManager/__tests__/Overlay.test.tsx +88 -0
  120. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +86 -69
  121. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +9 -20
  122. package/Components/Tabs/TabContent.tsx +7 -4
  123. package/Components/TopCutoffOverlay/__tests__/TopCutoffOverlay.test.tsx +201 -0
  124. package/Components/TopCutoffOverlay/hooks/__tests__/useMarginTop.test.ts +130 -0
  125. package/Components/TopCutoffOverlay/hooks/index.ts +1 -0
  126. package/Components/TopCutoffOverlay/hooks/useMarginTop.ts +59 -0
  127. package/Components/TopCutoffOverlay/index.tsx +55 -0
  128. package/Components/Transitioner/index.js +3 -3
  129. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +5 -5
  130. package/Components/VideoModal/hooks/__tests__/useDelayedPlayerDetails.test.ts +15 -7
  131. package/Components/VideoModal/utils.ts +12 -10
  132. package/Components/Viewport/ViewportAware/__tests__/viewportAware.test.js +0 -2
  133. package/Components/Viewport/ViewportAware/index.tsx +16 -7
  134. package/Components/Viewport/ViewportEvents/__tests__/viewportEvents.test.js +1 -1
  135. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +4 -6
  136. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  137. package/Components/default-cell-renderer/viewTrees/mobile/index.ts +0 -3
  138. package/Contexts/ScreenContext/index.tsx +25 -18
  139. package/Contexts/ScreenTrackedViewPositionsContext/__tests__/index.test.tsx +1 -1
  140. package/Decorators/Analytics/index.tsx +6 -5
  141. package/Decorators/ConfigurationWrapper/__tests__/__snapshots__/withConfigurationProvider.test.tsx.snap +1 -0
  142. package/Decorators/ConfigurationWrapper/const.ts +1 -0
  143. package/Decorators/ZappPipesDataConnector/__tests__/UrlFeedResolver.test.tsx +39 -21
  144. package/Decorators/ZappPipesDataConnector/__tests__/zappPipesDataConnector.test.js +1 -1
  145. package/Decorators/ZappPipesDataConnector/index.tsx +2 -2
  146. package/Decorators/ZappPipesDataConnector/resolvers/StaticFeedResolver.tsx +1 -1
  147. package/Decorators/ZappPipesDataConnector/resolvers/UrlFeedResolver.tsx +18 -7
  148. package/Helpers/DataSourceHelper/__tests__/itemLimitForData.test.ts +80 -0
  149. package/Helpers/DataSourceHelper/index.ts +19 -0
  150. package/Helpers/index.js +7 -1
  151. package/package.json +5 -5
  152. package/Components/Layout/TV/LayoutBackground.tsx +0 -28
  153. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/index.android.tsx +0 -135
  154. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/types.ts +0 -25
  155. package/Components/River/TV/withTVEventHandler.tsx +0 -36
  156. package/Helpers/DataSourceHelper/index.js +0 -19
@@ -0,0 +1,284 @@
1
+ import * as React from "react";
2
+ import { render } from "@testing-library/react-native";
3
+
4
+ jest.mock(
5
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager",
6
+ () => ({
7
+ playerManager: {
8
+ invokeHandler: jest.fn(),
9
+ getPluginConfiguration: jest.fn(() => ({})),
10
+ isRegistered: jest.fn(() => true),
11
+ on: jest.fn().mockReturnThis(),
12
+ removeHandler: jest.fn().mockReturnThis(),
13
+ getActivePlayer: jest.fn(() => null),
14
+ closeNativePlayer: jest.fn(),
15
+ },
16
+ })
17
+ );
18
+
19
+ jest.mock("@applicaster/zapp-react-native-utils/reactUtils", () => ({
20
+ isTV: jest.fn(() => false),
21
+ isAndroidTVPlatform: jest.fn(() => false),
22
+ isTvOSPlatform: jest.fn(() => false),
23
+ platformSelect: jest.fn(({ native }) => native),
24
+ }));
25
+
26
+ jest.mock("@applicaster/zapp-react-native-utils/playerUtils", () => ({
27
+ isAudioItem: jest.fn(() => false),
28
+ isInlineTV: jest.fn(() => false),
29
+ }));
30
+
31
+ jest.mock(
32
+ "@applicaster/zapp-react-native-tvos-ui-components/Components/TVEventHandlerComponent",
33
+ () => ({
34
+ TVEventHandlerComponent: ({ children }) => children,
35
+ })
36
+ );
37
+
38
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/utils", () => ({
39
+ usePrevious: jest.fn(),
40
+ }));
41
+
42
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks", () => {
43
+ const navigator = {
44
+ closeVideoModal: jest.fn(),
45
+ minimiseVideoModal: jest.fn(),
46
+ maximiseVideoModal: jest.fn(),
47
+ fullscreenVideoModal: jest.fn(),
48
+ isVideoModalDocked: jest.fn(() => false),
49
+ goBack: jest.fn(),
50
+ replaceTop: jest.fn(),
51
+ push: jest.fn(),
52
+ setPlayNextOverlay: jest.fn(),
53
+ currentRoute: "/playable/entry-1",
54
+ };
55
+
56
+ const backHandlerRef: { current: (() => boolean) | null } = {
57
+ current: null,
58
+ };
59
+
60
+ return {
61
+ useBackHandler: jest.fn((cb) => {
62
+ backHandlerRef.current = cb;
63
+ }),
64
+ useNavigation: jest.fn(() => navigator),
65
+ __navigator: navigator,
66
+ __backHandlerRef: backHandlerRef,
67
+ };
68
+ });
69
+
70
+ jest.mock("@applicaster/zapp-react-native-bridge/QuickBrick", () => ({
71
+ QUICK_BRICK_EVENTS: {
72
+ IDLE_TIMER_DISABLED: "IDLE_TIMER_DISABLED",
73
+ MOVE_APP_TO_BACKGROUND: "MOVE_APP_TO_BACKGROUND",
74
+ },
75
+ sendQuickBrickEvent: jest.fn(),
76
+ }));
77
+
78
+ jest.mock("../ProgramInfo", () => ({
79
+ ProgramInfo: () => null,
80
+ }));
81
+
82
+ jest.mock("../../AudioPlayer", () => ({
83
+ AudioPlayer: () => null,
84
+ }));
85
+
86
+ jest.mock("../logger", () => ({
87
+ log_debug: jest.fn(),
88
+ log_info: jest.fn(),
89
+ log_warning: jest.fn(),
90
+ playerContainerLogger: { error: jest.fn() },
91
+ }));
92
+
93
+ jest.mock(
94
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer",
95
+ () => {
96
+ const player = {
97
+ addListener: jest.fn(),
98
+ removeListener: jest.fn(),
99
+ isPaused: jest.fn(() => false),
100
+ isAd: jest.fn(() => false),
101
+ seekTo: jest.fn(),
102
+ getPluginConfiguration: jest.fn(() => ({})),
103
+ getOverlayObservable: jest.fn(() => ({ getPlayNextEntry: () => null })),
104
+ };
105
+
106
+ return {
107
+ usePlayer: jest.fn(() => player),
108
+ __player: player,
109
+ };
110
+ }
111
+ );
112
+
113
+ jest.mock(
114
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayNextOverlay",
115
+ () => ({
116
+ usePlayNextOverlay: jest.fn(() => null),
117
+ })
118
+ );
119
+
120
+ jest.mock(
121
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand",
122
+ () => ({
123
+ PlayerNativeCommandTypes: { clearPlayerData: "clearPlayerData" },
124
+ PlayerNativeSendCommand: jest.fn(),
125
+ })
126
+ );
127
+
128
+ jest.mock(
129
+ "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext",
130
+ () => ({
131
+ useSetNavbarState: jest.fn(() => ({ setVisible: jest.fn() })),
132
+ })
133
+ );
134
+
135
+ jest.mock(
136
+ "@applicaster/zapp-react-native-utils/reactHooks/screen/useTargetScreenData",
137
+ () => ({
138
+ useTargetScreenData: jest.fn(() => ({ id: "screen-id", styles: {} })),
139
+ })
140
+ );
141
+
142
+ jest.mock("../PlayerContainerContext", () => {
143
+ const ReactActual = require("react");
144
+
145
+ return {
146
+ PlayerContainerContext: ReactActual.createContext({
147
+ isLanguageOverlayVisible: false,
148
+ bottomFocusableId: "bottom",
149
+ showComponentsContainer: false,
150
+ refs: null,
151
+ }),
152
+ PlayerContainerContextProvider: ({ children }) => children,
153
+ };
154
+ });
155
+
156
+ jest.mock(
157
+ "@applicaster/zapp-react-native-ui-components/Components/FocusableGroup",
158
+ () => ({
159
+ FocusableGroup: ({ children }) => children,
160
+ })
161
+ );
162
+
163
+ jest.mock("../WappersView/PlayerFocusableWrapperView", () => ({
164
+ PlayerFocusableWrapperView: ({ children }) => children,
165
+ }));
166
+
167
+ jest.mock("../WappersView/ComponentFocusableWrapperView", () => ({
168
+ ComponentFocusableWrapperView: () => null,
169
+ }));
170
+
171
+ jest.mock("../index", () => ({
172
+ FocusableGroupMainContainerId: "player-container-general",
173
+ }));
174
+
175
+ jest.mock(
176
+ "@applicaster/zapp-react-native-utils/navigationUtils/itemTypeMatchers",
177
+ () => ({
178
+ isPlayable: jest.fn(() => true),
179
+ })
180
+ );
181
+
182
+ jest.mock("../../GeneralContentScreen", () => ({
183
+ GeneralContentScreen: () => null,
184
+ }));
185
+
186
+ jest.mock("@applicaster/zapp-react-native-redux", () => ({
187
+ useAppData: jest.fn(() => ({ isTabletPortrait: false })),
188
+ }));
189
+
190
+ import { PlayerContainer, VideoModalMode } from "../PlayerContainer";
191
+
192
+ const { __navigator: mockNavigator, __backHandlerRef: mockBackHandlerRef } =
193
+ jest.requireMock("@applicaster/zapp-react-native-utils/reactHooks");
194
+
195
+ const { playerManager: mockPlayerManager } = jest.requireMock(
196
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager"
197
+ );
198
+
199
+ const { __player: mockPlayer } = jest.requireMock(
200
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer"
201
+ );
202
+
203
+ const Player = React.forwardRef(() => null);
204
+
205
+ const item = {
206
+ id: "entry-1",
207
+ title: "Test entry",
208
+ summary: "",
209
+ content: { src: "https://example.com/video.m3u8" },
210
+ extensions: {},
211
+ } as any;
212
+
213
+ const renderPlayerContainer = (props = {}) =>
214
+ render(
215
+ <PlayerContainer
216
+ Player={Player}
217
+ item={item}
218
+ style={{}}
219
+ loadPipesData={jest.fn()}
220
+ isModal={true}
221
+ mode={VideoModalMode.MAXIMIZED}
222
+ {...props}
223
+ />
224
+ );
225
+
226
+ const pressHardwareBack = () => {
227
+ if (!mockBackHandlerRef.current) {
228
+ throw new Error("back handler was not registered via useBackHandler");
229
+ }
230
+
231
+ return mockBackHandlerRef.current();
232
+ };
233
+
234
+ describe("PlayerContainer hardware back handling", () => {
235
+ beforeEach(() => {
236
+ jest.clearAllMocks();
237
+ mockBackHandlerRef.current = null;
238
+ mockPlayer.getPluginConfiguration.mockReturnValue({});
239
+ mockPlayer.isPaused.mockReturnValue(false);
240
+ mockPlayer.isAd.mockReturnValue(false);
241
+
242
+ mockPlayer.getOverlayObservable.mockReturnValue({
243
+ getPlayNextEntry: () => null,
244
+ });
245
+
246
+ mockPlayerManager.getPluginConfiguration.mockReturnValue({});
247
+ mockPlayerManager.on.mockReturnThis();
248
+ mockPlayerManager.removeHandler.mockReturnThis();
249
+ });
250
+
251
+ it("minimises the video modal on back press when mini player is enabled", () => {
252
+ renderPlayerContainer();
253
+
254
+ const handled = pressHardwareBack();
255
+
256
+ expect(mockNavigator.minimiseVideoModal).toHaveBeenCalled();
257
+ expect(mockNavigator.closeVideoModal).not.toHaveBeenCalled();
258
+ expect(handled).toBe(true);
259
+ });
260
+
261
+ it("closes the video modal on back press when mini player is disabled", () => {
262
+ mockPlayer.getPluginConfiguration.mockReturnValue({
263
+ disable_mini_player_when_inline: true,
264
+ });
265
+
266
+ renderPlayerContainer();
267
+
268
+ const handled = pressHardwareBack();
269
+
270
+ expect(mockNavigator.closeVideoModal).toHaveBeenCalled();
271
+ expect(mockNavigator.minimiseVideoModal).not.toHaveBeenCalled();
272
+ expect(handled).toBe(true);
273
+ });
274
+
275
+ it("does not handle back press when the player is already minimised", () => {
276
+ renderPlayerContainer({ mode: VideoModalMode.MINIMIZED });
277
+
278
+ const handled = pressHardwareBack();
279
+
280
+ expect(handled).toBe(false);
281
+ expect(mockNavigator.minimiseVideoModal).not.toHaveBeenCalled();
282
+ expect(mockNavigator.closeVideoModal).not.toHaveBeenCalled();
283
+ });
284
+ });
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import * as R from "ramda";
3
- import { FlatList, StyleSheet, View } from "react-native";
3
+ import { FlatList, ScrollView, ScrollViewProps, View } 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";
@@ -36,20 +36,16 @@ type Props = {
36
36
  groupId?: string;
37
37
  isScreenWrappedInContainer?: boolean;
38
38
  riverComponents: ZappUIComponent[];
39
- scrollViewExtraProps?: {};
39
+ scrollViewExtraProps?: Partial<ScrollViewProps>;
40
40
  riverId?: string;
41
- getStaticComponentFeed: any;
42
- pullToRefreshPipesV1RefreshingStateUpdater: () => boolean;
43
- refreshingPipesV1?: boolean;
41
+ getStaticComponentFeed: (args: {
42
+ component: ZappUIComponent;
43
+ index: number;
44
+ }) => ZappFeed | Promise<ZappFeed>;
44
45
  stickyHeaderIndices?: number[];
46
+ useScrollView?: boolean;
45
47
  };
46
48
 
47
- const styles = StyleSheet.create({
48
- container: {
49
- flex: 1,
50
- },
51
- });
52
-
53
49
  const getNearestValue = (value, optionA, optionB) =>
54
50
  Math.abs(value - optionA) < Math.abs(value - optionB) ? optionA : optionB;
55
51
 
@@ -65,16 +61,15 @@ function ComponentsMapComponent(props: Props) {
65
61
  groupId,
66
62
  riverId,
67
63
  getStaticComponentFeed,
68
- // Method added to keep pipes v1 logic up to date with the pullToRefresh state.
69
- // TODO: Remove when pipes v1 is deprecated.
70
- pullToRefreshPipesV1RefreshingStateUpdater,
71
- refreshingPipesV1,
72
64
  stickyHeaderIndices,
65
+ useScrollView,
73
66
  } = props;
74
67
 
75
68
  const flatListRef = React.useRef<FlatList | null>(null);
69
+ const scrollViewRef = React.useRef<ScrollView | null>(null);
76
70
  const flatListWrapperRef = React.useRef<View | null>(null);
77
71
  const hasUserScrolledRef = React.useRef(false);
72
+ const scrollEndReachedRef = React.useRef(false);
78
73
  const screenConfig = useScreenConfiguration(riverId);
79
74
  const screenData = useScreenData(riverId);
80
75
  const pullToRefreshEnabled = screenData?.rules?.pull_to_refresh_enabled;
@@ -93,7 +88,7 @@ function ComponentsMapComponent(props: Props) {
93
88
 
94
89
  const onLoadDone = React.useCallback(() => {
95
90
  logTimestamp(riverId?.toString());
96
- }, [logTimestamp]);
91
+ }, [logTimestamp, riverId]);
97
92
 
98
93
  const { loadingState, onLoadFinished, onLoadFailed, shouldShowLoadingError } =
99
94
  useLoadingState(riverComponents.length, onLoadDone);
@@ -122,7 +117,17 @@ function ComponentsMapComponent(props: Props) {
122
117
  </ScreenLoadingMeasurementsListItemWrapper>
123
118
  );
124
119
  },
125
- [feed, getStaticComponentFeed, onLoadFailed, onLoadFinished]
120
+ [
121
+ feed,
122
+ getStaticComponentFeed,
123
+ groupId,
124
+ isScreenWrappedInContainer,
125
+ loadingState,
126
+ onLoadFailed,
127
+ onLoadFinished,
128
+ riverComponents.length,
129
+ riverId,
130
+ ]
126
131
  );
127
132
 
128
133
  const screenStyle = React.useMemo(
@@ -144,7 +149,13 @@ function ComponentsMapComponent(props: Props) {
144
149
  R.prop("screen_padding_right")(theme)
145
150
  ),
146
151
  }),
147
- [riverId]
152
+ [
153
+ screenConfig.paddingTop,
154
+ screenConfig.paddingBottom,
155
+ screenConfig.paddingLeft,
156
+ screenConfig.paddingRight,
157
+ theme,
158
+ ]
148
159
  );
149
160
 
150
161
  const handleOnLayout = React.useCallback(
@@ -163,16 +174,8 @@ function ComponentsMapComponent(props: Props) {
163
174
  usePipesCacheReset(riverId, riverComponents);
164
175
 
165
176
  const refreshControl = React.useMemo(
166
- () =>
167
- pullToRefreshEnabled ? (
168
- <RefreshControl
169
- pullToRefreshPipesV1RefreshingStateUpdater={
170
- pullToRefreshPipesV1RefreshingStateUpdater
171
- }
172
- refreshingPipesV1={refreshingPipesV1}
173
- />
174
- ) : null,
175
- []
177
+ () => (pullToRefreshEnabled ? <RefreshControl /> : null),
178
+ [pullToRefreshEnabled]
176
179
  );
177
180
 
178
181
  const navBarStore = useScreenContextV2()._navBarStore;
@@ -187,55 +190,50 @@ function ComponentsMapComponent(props: Props) {
187
190
  canMomentum.current = true;
188
191
  }, []);
189
192
 
190
- /* onMomentumScrollEnd: Handling snap to expanded/collapsed state ( for navbar ) */
191
- const _onMomentumScrollEnd = React.useCallback(({ nativeEvent }) => {
192
- if (!canMomentum.current) return;
193
- canMomentum.current = false;
193
+ /* Snap the navbar to its expanded/collapsed state based on current offset. */
194
+ const snapToHeaderState = React.useCallback(
195
+ (rawOffsetY: number) => {
196
+ const { height: headerHeight, scrollState } = navBarStore.getState();
194
197
 
195
- const { height: headerHeight, scrollState } = navBarStore.getState();
198
+ // Already fully expanded or fully collapsed — nothing to snap.
199
+ if (scrollState === 0 || scrollState === -headerHeight) return;
200
+ if (!flatListRef.current) return;
196
201
 
197
- const offsetY = Math.max(0, nativeEvent.contentOffset.y);
202
+ const offsetY = Math.max(0, rawOffsetY);
198
203
 
199
- if (
200
- !(scrollState === 0 || scrollState === -headerHeight) &&
201
- flatListRef.current
202
- ) {
203
- const offset =
204
+ const snapsToCollapsed =
204
205
  getNearestValue(Math.round(scrollState), -headerHeight, 0) ===
205
- -headerHeight
206
- ? Math.round(offsetY + scrollState + headerHeight)
207
- : Math.round(offsetY + scrollState);
208
-
209
- flatListRef.current.scrollToOffset({
210
- animated: true,
211
- offset,
212
- });
213
- }
214
- }, []);
206
+ -headerHeight;
215
207
 
216
- /* onScrollEnd: Handling snap to expanded/collapsed state ( for navbar ) */
217
- const _onScrollEndDrag = React.useCallback(({ nativeEvent }) => {
218
- const { height: headerHeight, scrollState } = navBarStore.getState();
208
+ const offset = snapsToCollapsed
209
+ ? Math.round(offsetY + scrollState + headerHeight)
210
+ : Math.round(offsetY + scrollState);
219
211
 
220
- const offsetY = Math.max(0, nativeEvent.contentOffset.y);
212
+ flatListRef.current.scrollToOffset({ animated: true, offset });
213
+ },
214
+ [navBarStore]
215
+ );
221
216
 
222
- if (
223
- !(scrollState === 0 || scrollState === -headerHeight) &&
224
- flatListRef.current &&
225
- nativeEvent.velocity.y === 0
226
- ) {
227
- const offset =
228
- getNearestValue(Math.round(scrollState), -headerHeight, 0) ===
229
- -headerHeight
230
- ? Math.round(offsetY + scrollState + headerHeight)
231
- : Math.round(offsetY + scrollState);
232
-
233
- flatListRef.current.scrollToOffset({
234
- animated: true,
235
- offset,
236
- });
237
- }
238
- }, []);
217
+ /* onMomentumScrollEnd: Handling snap to expanded/collapsed state ( for navbar ) */
218
+ const _onMomentumScrollEnd = React.useCallback(
219
+ ({ nativeEvent }) => {
220
+ if (!canMomentum.current) return;
221
+ canMomentum.current = false;
222
+ snapToHeaderState(nativeEvent.contentOffset.y);
223
+ },
224
+ [snapToHeaderState]
225
+ );
226
+
227
+ /* onScrollEndDrag: Handling snap to expanded/collapsed state ( for navbar ) */
228
+ const _onScrollEndDrag = React.useCallback(
229
+ ({ nativeEvent }) => {
230
+ // Only snap on drag-end if there's no momentum to follow;
231
+ // otherwise _onMomentumScrollEnd handles it.
232
+ if (nativeEvent.velocity?.y !== 0) return;
233
+ snapToHeaderState(nativeEvent.contentOffset.y);
234
+ },
235
+ [snapToHeaderState]
236
+ );
239
237
 
240
238
  const onScroll = React.useCallback((event) => {
241
239
  hasUserScrolledRef.current = true;
@@ -259,8 +257,42 @@ function ComponentsMapComponent(props: Props) {
259
257
  }
260
258
  }, []);
261
259
 
260
+ const onScrollWithEndReached = React.useCallback(
261
+ (event) => {
262
+ onScroll(event);
263
+
264
+ if (!hasUserScrolledRef.current) return;
265
+ if (isScreenWrappedInContainer) return;
266
+
267
+ const {
268
+ nativeEvent: {
269
+ contentOffset: { y },
270
+ layoutMeasurement: { height },
271
+ contentSize: { height: contentHeight },
272
+ },
273
+ } = event;
274
+
275
+ const distanceFromEnd = contentHeight - y - height;
276
+
277
+ if (distanceFromEnd <= height * 0.5) {
278
+ if (!scrollEndReachedRef.current) {
279
+ scrollEndReachedRef.current = true;
280
+ emitScrollEndReached();
281
+ }
282
+ } else {
283
+ scrollEndReachedRef.current = false;
284
+ }
285
+ },
286
+ [onScroll, isScreenWrappedInContainer]
287
+ );
288
+
262
289
  const contentContainerStyle = React.useMemo(
263
- () => (isScreenWrappedInContainer ? {} : screenStyle),
290
+ () =>
291
+ isScreenWrappedInContainer
292
+ ? {
293
+ flexGrow: 1,
294
+ }
295
+ : screenStyle,
264
296
  [isScreenWrappedInContainer, screenStyle]
265
297
  );
266
298
 
@@ -271,60 +303,98 @@ function ComponentsMapComponent(props: Props) {
271
303
  // TODO: support both "isScreenWrappedInContainer" and hasScreenPicker().
272
304
  // The Screen Picker in Mobile is completly different than the TV
273
305
  // so the various offsets / margins in TV do not apply here.
306
+ // Fix for WebView rerender crashes on Android API 28+
307
+ // https://github.com/react-native-webview/react-native-webview/issues/1915#issuecomment-964035468
308
+ const sharedScrollProps = {
309
+ overScrollMode: isAndroid ? ("never" as const) : ("auto" as const),
310
+ scrollIndicatorInsets,
311
+ onLayout: handleOnLayout,
312
+ contentContainerStyle,
313
+ refreshControl,
314
+ onScrollBeginDrag,
315
+ onMomentumScrollEnd: _onMomentumScrollEnd,
316
+ onScrollEndDrag: _onScrollEndDrag,
317
+ scrollEventThrottle: 16,
318
+ ...scrollViewExtraProps,
319
+ };
320
+
321
+ const listContent = useScrollView ? (
322
+ <ScrollView
323
+ testID="components-map-scroll-view"
324
+ // ViewportTracker clones this child and calls its `ref` as a function
325
+ // (childrenRef.current.ref(node)), so a callback ref is required here
326
+ // — an object ref would throw "ref is not a function".
327
+ ref={(ref) => {
328
+ scrollViewRef.current = ref;
329
+ }}
330
+ onScroll={onScrollWithEndReached}
331
+ {...sharedScrollProps}
332
+ >
333
+ {riverComponents.map((item, index) => (
334
+ <React.Fragment key={keyExtractor(item)}>
335
+ {renderRiverItem({ item, index })}
336
+ </React.Fragment>
337
+ ))}
338
+ <RiverFooter
339
+ flatListHeight={flatListHeight}
340
+ loadingState={loadingState}
341
+ />
342
+ </ScrollView>
343
+ ) : (
344
+ <FlatList
345
+ testID="components-map-flat-list"
346
+ // ViewportTracker clones this child and calls its `ref` as a function
347
+ // (childrenRef.current.ref(node)), so a callback ref is required here
348
+ // — an object ref would throw "ref is not a function".
349
+ ref={(ref) => {
350
+ flatListRef.current = ref;
351
+ }}
352
+ extraData={feed}
353
+ stickyHeaderIndices={stickyHeaderIndices}
354
+ removeClippedSubviews={isAndroid}
355
+ initialNumToRender={3}
356
+ maxToRenderPerBatch={10}
357
+ windowSize={12}
358
+ keyExtractor={keyExtractor}
359
+ renderItem={renderRiverItem}
360
+ data={riverComponents}
361
+ ListFooterComponent={
362
+ <RiverFooter
363
+ flatListHeight={flatListHeight}
364
+ loadingState={loadingState}
365
+ />
366
+ }
367
+ onScroll={onScroll}
368
+ onEndReached={
369
+ /* TODO: end-reached detection is inconsistent between the two paths:
370
+ the ScrollView branch uses manual pixel-offset math (onScrollWithEndReached)
371
+ with a scrollEndReachedRef debounce, while this FlatList relies on the
372
+ native onEndReached. Consider unifying the threshold/debounce behavior. */
373
+ /* When wrapped in a parent ScrollView (e.g. tabs),
374
+ this FlatList doesn't scroll so onEndReached can fire repeatedly;
375
+ skip it here and let the parent ScrollView emit scroll-end instead. */
376
+ isScreenWrappedInContainer
377
+ ? undefined
378
+ : () => {
379
+ if (!hasUserScrolledRef.current) return;
380
+ emitScrollEndReached();
381
+ }
382
+ }
383
+ {...sharedScrollProps}
384
+ />
385
+ );
386
+
274
387
  return (
275
- <View style={styles.container} ref={flatListWrapperRef}>
388
+ // Wrapper View exists only to expose a ref (via ComponentsMapRefContext)
389
+ // for measuring the list's position/height; it intentionally has no style.
390
+ <View ref={flatListWrapperRef}>
276
391
  <ComponentsMapHeightContext.Provider value={flatListHeight}>
277
392
  <ComponentsMapRefContext.Provider value={flatListWrapperRef}>
278
393
  <ScreenLoadingMeasurements
279
394
  riverId={riverId}
280
395
  numberOfComponents={riverComponents.length}
281
396
  >
282
- <ViewportTracker>
283
- <FlatList
284
- ref={(ref) => {
285
- flatListRef.current = ref;
286
- }}
287
- // Fix for WebView rerender crashes on Android API 28+
288
- // https://github.com/react-native-webview/react-native-webview/issues/1915#issuecomment-964035468
289
- overScrollMode={isAndroid ? "never" : "auto"}
290
- scrollIndicatorInsets={scrollIndicatorInsets}
291
- extraData={feed}
292
- stickyHeaderIndices={stickyHeaderIndices}
293
- removeClippedSubviews={isAndroid}
294
- onLayout={handleOnLayout}
295
- initialNumToRender={3}
296
- maxToRenderPerBatch={10}
297
- windowSize={12}
298
- keyExtractor={keyExtractor}
299
- renderItem={renderRiverItem}
300
- data={riverComponents}
301
- contentContainerStyle={contentContainerStyle}
302
- ListFooterComponent={
303
- <RiverFooter
304
- flatListHeight={flatListHeight}
305
- loadingState={loadingState}
306
- />
307
- }
308
- refreshControl={refreshControl}
309
- onScrollBeginDrag={onScrollBeginDrag}
310
- onScroll={onScroll}
311
- onMomentumScrollEnd={_onMomentumScrollEnd}
312
- onScrollEndDrag={_onScrollEndDrag}
313
- scrollEventThrottle={16}
314
- {...scrollViewExtraProps}
315
- /* When wrapped in a parent ScrollView (e.g. tabs),
316
- this FlatList doesn't scroll so onEndReached can fire repeatedly;
317
- skip it here and let the parent ScrollView emit scroll-end instead. */
318
- onEndReached={
319
- isScreenWrappedInContainer
320
- ? undefined
321
- : () => {
322
- if (!hasUserScrolledRef.current) return;
323
- emitScrollEndReached();
324
- }
325
- }
326
- />
327
- </ViewportTracker>
397
+ <ViewportTracker>{listContent}</ViewportTracker>
328
398
  </ScreenLoadingMeasurements>
329
399
  </ComponentsMapRefContext.Provider>
330
400
  </ComponentsMapHeightContext.Provider>
@@ -1,4 +1,4 @@
1
- import { renderHook, act } from "@testing-library/react-hooks";
1
+ import { renderHook, act } from "@testing-library/react-native";
2
2
  import { BehaviorSubject } from "rxjs";
3
3
  import { useLoadingState } from "../useLoadingState";
4
4