@applicaster/zapp-react-native-ui-components 15.0.0-rc.98 → 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 (138) 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 +15 -0
  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 +1 -15
  63. package/Components/Screen/TV/hooks/__tests__/useAfterPaint.test.ts +60 -0
  64. package/Components/Screen/TV/hooks/index.ts +2 -0
  65. package/Components/Screen/TV/hooks/useAfterPaint.ts +23 -0
  66. package/Components/Screen/TV/index.web.tsx +18 -11
  67. package/Components/Screen/__tests__/Screen.test.tsx +43 -65
  68. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +44 -68
  69. package/Components/Screen/hooks.ts +76 -5
  70. package/Components/Screen/index.tsx +18 -7
  71. package/Components/Screen/orientationHandler.ts +3 -3
  72. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +46 -0
  73. package/Components/ScreenFeedLoader/__tests__/ScreenFeedLoader.test.tsx +94 -0
  74. package/Components/ScreenFeedLoader/index.ts +1 -0
  75. package/Components/ScreenResolver/__tests__/screenResolver.test.js +24 -0
  76. package/Components/ScreenResolver/hooks/index.ts +3 -0
  77. package/Components/ScreenResolver/hooks/useGetComponent.ts +15 -0
  78. package/Components/ScreenResolver/hooks/useScreenComponentResolver.tsx +90 -0
  79. package/Components/ScreenResolver/index.tsx +9 -115
  80. package/Components/ScreenResolver/utils/__tests__/getScreenTypeProps.test.ts +45 -0
  81. package/Components/ScreenResolver/utils/getScreenTypeProps.ts +43 -0
  82. package/Components/ScreenResolver/utils/index.ts +1 -0
  83. package/Components/ScreenResolver/withDefaultScreenContext.tsx +16 -0
  84. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +25 -0
  85. package/Components/ScreenResolverFeedProvider/__tests__/ScreenResolverFeedProvider.test.tsx +44 -0
  86. package/Components/ScreenResolverFeedProvider/index.ts +1 -0
  87. package/Components/ScreenRevealManager/Overlay.tsx +34 -0
  88. package/Components/ScreenRevealManager/ScreenRevealManager.ts +8 -40
  89. package/Components/ScreenRevealManager/__tests__/Overlay.test.tsx +88 -0
  90. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +69 -86
  91. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +10 -18
  92. package/Components/Tabs/TabContent.tsx +4 -7
  93. package/Components/Transitioner/Scene.tsx +9 -15
  94. package/Components/Transitioner/index.js +3 -3
  95. package/Components/VideoLive/LiveImageManager.ts +209 -53
  96. package/Components/VideoLive/PlayerLiveImageComponent.tsx +32 -32
  97. package/Components/VideoLive/__tests__/PlayerLiveImageComponent.test.tsx +2 -17
  98. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +5 -5
  99. package/Components/VideoModal/hooks/__tests__/useDelayedPlayerDetails.test.ts +7 -15
  100. package/Components/VideoModal/utils.ts +16 -13
  101. package/Components/Viewport/ViewportEvents/__tests__/viewportEvents.test.js +1 -1
  102. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +12 -5
  103. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  104. package/Components/ZappUIComponent/index.tsx +12 -6
  105. package/Components/index.js +1 -1
  106. package/Contexts/CachedDimensionsContext/index.ts +17 -2
  107. package/Contexts/ScreenContext/__tests__/index.test.tsx +57 -0
  108. package/Contexts/ScreenContext/index.tsx +64 -26
  109. package/Contexts/ScreenTrackedViewPositionsContext/__tests__/index.test.tsx +1 -1
  110. package/Contexts/ZappPipesContext/ZappPipesContextFactory.tsx +18 -7
  111. package/Decorators/Analytics/index.tsx +5 -6
  112. package/Decorators/ConfigurationWrapper/__tests__/__snapshots__/withConfigurationProvider.test.tsx.snap +0 -1
  113. package/Decorators/ConfigurationWrapper/const.ts +0 -1
  114. package/Decorators/ZappPipesDataConnector/ResolverSelector.tsx +25 -7
  115. package/Decorators/ZappPipesDataConnector/__tests__/ResolverSelector.test.tsx +212 -5
  116. package/Decorators/ZappPipesDataConnector/__tests__/zappPipesDataConnector.test.js +1 -1
  117. package/Decorators/ZappPipesDataConnector/index.tsx +2 -2
  118. package/Decorators/ZappPipesDataConnector/resolvers/StaticFeedResolver.tsx +1 -1
  119. package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
  120. package/Helpers/DataSourceHelper/index.js +19 -0
  121. package/Helpers/index.js +7 -40
  122. package/events/index.ts +2 -0
  123. package/events/scrollEndReached.ts +15 -0
  124. package/package.json +5 -5
  125. package/Components/Layout/TV/LayoutBackground.tsx +0 -31
  126. package/Components/MasterCell/DefaultComponents/Text/utils/__tests__/withAdjustedLineHeight.test.ts +0 -46
  127. package/Components/MasterCell/DefaultComponents/Text/utils/index.ts +0 -21
  128. package/Components/PlayerContainer/ErrorDisplay/ErrorDisplay.tsx +0 -57
  129. package/Components/PlayerContainer/ErrorDisplay/index.ts +0 -9
  130. package/Components/PlayerContainer/useRestrictMobilePlayback.tsx +0 -101
  131. package/Components/River/TV/utils/__tests__/toStringOrEmpty.test.ts +0 -30
  132. package/Components/River/TV/utils/index.ts +0 -4
  133. package/Components/River/TV/withFocusableGroupForContent.tsx +0 -71
  134. package/Components/Screen/utils.ts +0 -16
  135. package/Helpers/Analytics/index.js +0 -95
  136. package/Helpers/DataSourceHelper/__tests__/itemLimitForData.test.ts +0 -80
  137. package/Helpers/DataSourceHelper/index.ts +0 -19
  138. /package/Components/HookRenderer/{index.tsx → index.ts} +0 -0
@@ -6,11 +6,16 @@ import { getPathAttributes } from "@applicaster/zapp-react-native-utils/navigati
6
6
  import { FocusableGroup } from "@applicaster/zapp-react-native-ui-components/Components/FocusableGroup";
7
7
  import { ScreenDataContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ScreenDataContext";
8
8
  import { PathnameContext } from "@applicaster/zapp-react-native-ui-components/Contexts/PathnameContext";
9
+ import { StyleSheet } from "react-native";
9
10
 
10
11
  import { Screen } from "../Screen/TV/index.web";
11
12
  import { isLast } from "@applicaster/zapp-react-native-utils/arrayUtils";
12
13
  import { ScreenContextProvider } from "../../Contexts/ScreenContext";
13
14
 
15
+ const styles = StyleSheet.create({
16
+ container: { flex: 1 },
17
+ });
18
+
14
19
  type Components = {
15
20
  NavBar: React.ComponentType<any>;
16
21
  Background: React.ComponentType<any>;
@@ -41,6 +46,7 @@ export const StackNavigator = ({ Components }: Props) => {
41
46
  preferredFocus={isLast(index, mainStack.length)}
42
47
  excludeFromFocusSearching
43
48
  key={routeId}
49
+ style={styles.container}
44
50
  >
45
51
  <ScreenDataContext.Provider value={state}>
46
52
  <PathnameContext.Provider value={route}>
@@ -8,7 +8,7 @@ import {
8
8
  TouchableWithoutFeedback,
9
9
  } from "react-native";
10
10
 
11
- import { usePlugins } from "@applicaster/zapp-react-native-redux";
11
+ import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
12
12
  import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
13
13
  import { textTransform } from "@applicaster/zapp-react-native-utils/cellUtils";
14
14
  import { useNotificationHeight } from "../utils";
@@ -46,7 +46,7 @@ export const NotificationView = ({
46
46
  online = true,
47
47
  dismiss,
48
48
  }: Props) => {
49
- const plugins = usePlugins();
49
+ const { plugins } = usePickFromState(["plugins"]);
50
50
  const { statusHeight, notificationHeight } = useNotificationHeight();
51
51
 
52
52
  const offlinePlugin = R.find(
@@ -8,24 +8,25 @@ import {
8
8
  offlinePhrase,
9
9
  } from "../NotificationView";
10
10
 
11
- jest.mock("@applicaster/zapp-react-native-redux", () => ({
12
- ...jest.requireActual("@applicaster/zapp-react-native-redux"),
13
- usePlugins: () => [
14
- {
15
- name: "offline experience",
16
- identifier: "offline-experience",
17
- type: "general",
18
- module: {
19
- useOfflineExperienceConfiguration: () => ({
20
- configurationFields: {},
21
- localizations: {
22
- offline_toast_message: "No internet connection",
23
- online_toast_message: "You are back online",
24
- },
25
- }),
11
+ jest.mock("@applicaster/zapp-react-native-redux/hooks", () => ({
12
+ usePickFromState: () => ({
13
+ plugins: [
14
+ {
15
+ name: "offline experience",
16
+ identifier: "offline-experience",
17
+ type: "general",
18
+ module: {
19
+ useOfflineExperienceConfiguration: () => ({
20
+ configurationFields: {},
21
+ localizations: {
22
+ offline_toast_message: "No internet connection",
23
+ online_toast_message: "You are back online",
24
+ },
25
+ }),
26
+ },
26
27
  },
27
- },
28
- ],
28
+ ],
29
+ }),
29
30
  }));
30
31
 
31
32
  jest.mock("react-native-safe-area-context", () => ({
@@ -8,45 +8,36 @@ const mockPreviousValue = null;
8
8
 
9
9
  jest.mock("@applicaster/zapp-react-native-utils/reactHooks/connection", () => {
10
10
  return {
11
- ...jest.requireActual(
12
- "@applicaster/zapp-react-native-utils/reactHooks/connection"
13
- ),
14
11
  useConnectionInfo: jest.fn(() => mockConnectionStatus),
15
12
  };
16
13
  });
17
14
 
18
15
  jest.mock("@applicaster/zapp-react-native-utils/reactHooks/utils", () => {
19
16
  return {
20
- ...jest.requireActual(
21
- "@applicaster/zapp-react-native-utils/reactHooks/utils"
22
- ),
23
17
  usePrevious: jest.fn(() => mockPreviousValue),
24
18
  };
25
19
  });
26
20
 
27
- const mock_storeObj = {
28
- plugins: [
29
- {
30
- name: "offline experience",
31
- identifier: "offline-experience",
32
- type: "general",
33
- module: {
34
- useOfflineExperienceConfiguration: () => ({
35
- configurationFields: {},
36
- localizations: {
37
- offline_toast_message: "No internet connection",
38
- online_toast_message: "You are back online",
39
- },
40
- }),
41
- },
42
- },
43
- ],
44
- };
45
-
46
21
  jest.mock("@applicaster/zapp-react-native-redux/hooks", () => ({
47
22
  ...jest.requireActual("@applicaster/zapp-react-native-redux/hooks"),
48
- usePlugins: () => mock_storeObj.plugins,
49
- usePickFromState: () => ({}),
23
+ usePickFromState: () => ({
24
+ plugins: [
25
+ {
26
+ name: "offline experience",
27
+ identifier: "offline-experience",
28
+ type: "general",
29
+ module: {
30
+ useOfflineExperienceConfiguration: () => ({
31
+ configurationFields: {},
32
+ localizations: {
33
+ offline_toast_message: "No internet connection",
34
+ online_toast_message: "You are back online",
35
+ },
36
+ }),
37
+ },
38
+ },
39
+ ],
40
+ }),
50
41
  }));
51
42
 
52
43
  jest.mock("react-native-safe-area-context", () => ({
@@ -17,7 +17,7 @@ import {
17
17
 
18
18
  import { TVEventHandlerComponent } from "@applicaster/zapp-react-native-tvos-ui-components/Components/TVEventHandlerComponent";
19
19
  import { usePrevious } from "@applicaster/zapp-react-native-utils/reactHooks/utils";
20
-
20
+ import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
21
21
  import {
22
22
  useBackHandler,
23
23
  useNavigation,
@@ -46,7 +46,6 @@ import {
46
46
  PlayerContainerContextProvider,
47
47
  } from "./PlayerContainerContext";
48
48
  import { FocusableGroup } from "@applicaster/zapp-react-native-ui-components/Components/FocusableGroup";
49
- import { ErrorDisplay } from "./ErrorDisplay";
50
49
  import { PlayerFocusableWrapperView } from "./WappersView/PlayerFocusableWrapperView";
51
50
  import { FocusableGroupMainContainerId } from "./index";
52
51
  import { isPlayable } from "@applicaster/zapp-react-native-utils/navigationUtils/itemTypeMatchers";
@@ -60,8 +59,6 @@ import {
60
59
  PlayerNativeCommandTypes,
61
60
  PlayerNativeSendCommand,
62
61
  } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand";
63
- import { useAppData } from "@applicaster/zapp-react-native-redux";
64
- import { useRestrictMobilePlayback } from "./useRestrictMobilePlayback";
65
62
 
66
63
  type Props = {
67
64
  Player: React.ComponentType<any>;
@@ -111,7 +108,7 @@ const withBorderHack = () => {
111
108
  /* @HACK: see GH#7269 */
112
109
  return {
113
110
  borderWidth: 1,
114
- borderColor: "transparent",
111
+ borderColor: "black",
115
112
  };
116
113
  }
117
114
 
@@ -240,7 +237,7 @@ const PlayerContainerComponent = (props: Props) => {
240
237
  const [isLoadingNextVideo, setIsLoadingNextVideo] = React.useState(false);
241
238
 
242
239
  const navigator = useNavigation();
243
- const { isTabletPortrait } = useAppData();
240
+ const { appData } = usePickFromState(["appData"]);
244
241
  const prevItemId = usePrevious(item?.id);
245
242
  const screenData = useTargetScreenData(item);
246
243
  const { setVisible: showNavBar } = useSetNavbarState();
@@ -267,21 +264,12 @@ const PlayerContainerComponent = (props: Props) => {
267
264
  navigator.goBack();
268
265
  }, [isModal, state.playerId, showNavBar, navigator]);
269
266
 
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
- });
267
+ const pluginConfiguration = React.useMemo(
268
+ () => player?.getPluginConfiguration(),
269
+ [player]
270
+ );
283
271
 
284
- const playEntry = (entry) => navigator.replaceTop(entry, { mode });
272
+ const playEntry = (entry: ZappEntry) => navigator.replaceTop(entry, { mode });
285
273
 
286
274
  const onPlayNextPerformNextVideoPlay = React.useCallback(() => {
287
275
  if (!playNextOverlayState.entry) {
@@ -347,12 +335,6 @@ const PlayerContainerComponent = (props: Props) => {
347
335
  playerContainerLogger.error(errorObj);
348
336
 
349
337
  setState({ error: errorObj });
350
-
351
- if (!isTvOS) {
352
- setTimeout(() => {
353
- close();
354
- }, 800);
355
- }
356
338
  },
357
339
  [close]
358
340
  );
@@ -361,11 +343,15 @@ const PlayerContainerComponent = (props: Props) => {
361
343
  setIsLoadingNextVideo(false);
362
344
 
363
345
  const resumeTime = Number(item?.extensions?.resumeTime);
346
+ const activePlayer = playerManager.getActivePlayer();
364
347
 
365
- // Сhecking that the player itself knows where to start playing, and there is no need to call seekTo after returning from the Сhromecast
348
+ // Сhecking that the player itself knows where to start playing, and there is no need to call seekTo after returning from the Сhromecast.
349
+ // Live streams and a missing/zero resume position are skipped: seeking to 0 on
350
+ // a live stream pulls playback off the live edge into a buffer-starvation seek loop.
366
351
  if (
367
- !playerManager.getActivePlayer()?.hasResumePosition() &&
368
- !isNaN(resumeTime)
352
+ !activePlayer?.hasResumePosition() &&
353
+ resumeTime > 0 &&
354
+ !activePlayer?.isLive()
369
355
  ) {
370
356
  player?.seekTo(resumeTime);
371
357
  }
@@ -483,6 +469,8 @@ const PlayerContainerComponent = (props: Props) => {
483
469
  if (isModal && mode === VideoModalMode.MAXIMIZED) {
484
470
  if (disableMiniPlayer) {
485
471
  navigator.closeVideoModal();
472
+ } else {
473
+ navigator.minimiseVideoModal();
486
474
  }
487
475
  }
488
476
 
@@ -670,7 +658,7 @@ const PlayerContainerComponent = (props: Props) => {
670
658
  <PlayerFocusableWrapperView
671
659
  nextFocusDown={context.bottomFocusableId}
672
660
  >
673
- {isRestricted ? null : (
661
+ {Player ? (
674
662
  <Player
675
663
  source={{
676
664
  uri,
@@ -685,7 +673,7 @@ const PlayerContainerComponent = (props: Props) => {
685
673
  fullscreen={mode === VideoModalMode.FULLSCREEN}
686
674
  inline={inline}
687
675
  isModal={isModal}
688
- isTabletPortrait={isTabletPortrait}
676
+ isTabletPortrait={appData.isTabletPortrait}
689
677
  muted={false}
690
678
  playableItem={item}
691
679
  playerEvent={playerEvent}
@@ -701,10 +689,8 @@ const PlayerContainerComponent = (props: Props) => {
701
689
  >
702
690
  {renderApplePlayer(applePlayerProps)}
703
691
  </Player>
704
- )}
692
+ ) : null}
705
693
  </PlayerFocusableWrapperView>
706
-
707
- {state.error ? <ErrorDisplay error={state.error} /> : null}
708
694
  </View>
709
695
  {/* Components container */}
710
696
  {isInlineTV && context.showComponentsContainer ? (
@@ -0,0 +1,286 @@
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/hooks", () => ({
187
+ usePickFromState: jest.fn(() => ({
188
+ appData: { isTabletPortrait: false },
189
+ })),
190
+ }));
191
+
192
+ import { PlayerContainer, VideoModalMode } from "../PlayerContainer";
193
+
194
+ const { __navigator: mockNavigator, __backHandlerRef: mockBackHandlerRef } =
195
+ jest.requireMock("@applicaster/zapp-react-native-utils/reactHooks");
196
+
197
+ const { playerManager: mockPlayerManager } = jest.requireMock(
198
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager"
199
+ );
200
+
201
+ const { __player: mockPlayer } = jest.requireMock(
202
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer"
203
+ );
204
+
205
+ const Player = React.forwardRef(() => null);
206
+
207
+ const item = {
208
+ id: "entry-1",
209
+ title: "Test entry",
210
+ summary: "",
211
+ content: { src: "https://example.com/video.m3u8" },
212
+ extensions: {},
213
+ } as any;
214
+
215
+ const renderPlayerContainer = (props = {}) =>
216
+ render(
217
+ <PlayerContainer
218
+ Player={Player}
219
+ item={item}
220
+ style={{}}
221
+ loadPipesData={jest.fn()}
222
+ isModal={true}
223
+ mode={VideoModalMode.MAXIMIZED}
224
+ {...props}
225
+ />
226
+ );
227
+
228
+ const pressHardwareBack = () => {
229
+ if (!mockBackHandlerRef.current) {
230
+ throw new Error("back handler was not registered via useBackHandler");
231
+ }
232
+
233
+ return mockBackHandlerRef.current();
234
+ };
235
+
236
+ describe("PlayerContainer hardware back handling", () => {
237
+ beforeEach(() => {
238
+ jest.clearAllMocks();
239
+ mockBackHandlerRef.current = null;
240
+ mockPlayer.getPluginConfiguration.mockReturnValue({});
241
+ mockPlayer.isPaused.mockReturnValue(false);
242
+ mockPlayer.isAd.mockReturnValue(false);
243
+
244
+ mockPlayer.getOverlayObservable.mockReturnValue({
245
+ getPlayNextEntry: () => null,
246
+ });
247
+
248
+ mockPlayerManager.getPluginConfiguration.mockReturnValue({});
249
+ mockPlayerManager.on.mockReturnThis();
250
+ mockPlayerManager.removeHandler.mockReturnThis();
251
+ });
252
+
253
+ it("minimises the video modal on back press when mini player is enabled", () => {
254
+ renderPlayerContainer();
255
+
256
+ const handled = pressHardwareBack();
257
+
258
+ expect(mockNavigator.minimiseVideoModal).toHaveBeenCalled();
259
+ expect(mockNavigator.closeVideoModal).not.toHaveBeenCalled();
260
+ expect(handled).toBe(true);
261
+ });
262
+
263
+ it("closes the video modal on back press when mini player is disabled", () => {
264
+ mockPlayer.getPluginConfiguration.mockReturnValue({
265
+ disable_mini_player_when_inline: true,
266
+ });
267
+
268
+ renderPlayerContainer();
269
+
270
+ const handled = pressHardwareBack();
271
+
272
+ expect(mockNavigator.closeVideoModal).toHaveBeenCalled();
273
+ expect(mockNavigator.minimiseVideoModal).not.toHaveBeenCalled();
274
+ expect(handled).toBe(true);
275
+ });
276
+
277
+ it("does not handle back press when the player is already minimised", () => {
278
+ renderPlayerContainer({ mode: VideoModalMode.MINIMIZED });
279
+
280
+ const handled = pressHardwareBack();
281
+
282
+ expect(handled).toBe(false);
283
+ expect(mockNavigator.minimiseVideoModal).not.toHaveBeenCalled();
284
+ expect(mockNavigator.closeVideoModal).not.toHaveBeenCalled();
285
+ });
286
+ });
@@ -0,0 +1,26 @@
1
+ import React from "react";
2
+ import { Text } from "react-native";
3
+ import { render } from "@testing-library/react-native";
4
+ import { PreloaderWrapper } from "..";
5
+
6
+ describe("PreloaderWrapper", () => {
7
+ it("renders children when preloader is hidden", () => {
8
+ const { getByText } = render(
9
+ <PreloaderWrapper showPreloader={false}>
10
+ <Text>content</Text>
11
+ </PreloaderWrapper>
12
+ );
13
+
14
+ expect(getByText("content")).toBeDefined();
15
+ });
16
+
17
+ it("renders null when preloader is shown", () => {
18
+ const { queryByText } = render(
19
+ <PreloaderWrapper showPreloader>
20
+ <Text>content</Text>
21
+ </PreloaderWrapper>
22
+ );
23
+
24
+ expect(queryByText("content")).toBeNull();
25
+ });
26
+ });
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+
3
+ type PreloaderWrapperProps = {
4
+ showPreloader?: boolean;
5
+ children?: React.ReactNode;
6
+ };
7
+
8
+ export const PreloaderWrapper: React.FC<PreloaderWrapperProps> = ({
9
+ showPreloader = false,
10
+ children,
11
+ }) => {
12
+ return !showPreloader ? children : null;
13
+ };
14
+
15
+ export default PreloaderWrapper;
@@ -23,6 +23,7 @@ import { isLast } from "@applicaster/zapp-react-native-utils/arrayUtils";
23
23
  import { withComponentsMapProvider } from "@applicaster/zapp-react-native-ui-components/Decorators/ComponentsMapWrapper";
24
24
  import { useScreenContextV2 } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
25
25
  import { useShallow } from "zustand/react/shallow";
26
+ import { emitScrollEndReached } from "@applicaster/zapp-react-native-ui-components/events";
26
27
 
27
28
  import { isAndroidPlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
28
29
  import { ComponentsMapHeightContext } from "./ContextProviders/ComponentsMapHeightContext";
@@ -73,6 +74,7 @@ function ComponentsMapComponent(props: Props) {
73
74
 
74
75
  const flatListRef = React.useRef<FlatList | null>(null);
75
76
  const flatListWrapperRef = React.useRef<View | null>(null);
77
+ const hasUserScrolledRef = React.useRef(false);
76
78
  const screenConfig = useScreenConfiguration(riverId);
77
79
  const screenData = useScreenData(riverId);
78
80
  const pullToRefreshEnabled = screenData?.rules?.pull_to_refresh_enabled;
@@ -236,6 +238,8 @@ function ComponentsMapComponent(props: Props) {
236
238
  }, []);
237
239
 
238
240
  const onScroll = React.useCallback((event) => {
241
+ hasUserScrolledRef.current = true;
242
+
239
243
  const {
240
244
  nativeEvent: {
241
245
  contentOffset: { y },
@@ -308,6 +312,17 @@ function ComponentsMapComponent(props: Props) {
308
312
  onScrollEndDrag={_onScrollEndDrag}
309
313
  scrollEventThrottle={16}
310
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
+ }
311
326
  />
312
327
  </ViewportTracker>
313
328
  </ScreenLoadingMeasurements>
@@ -1,4 +1,4 @@
1
- import { renderHook, act } from "@testing-library/react-native";
1
+ import { renderHook, act } from "@testing-library/react-hooks";
2
2
  import { BehaviorSubject } from "rxjs";
3
3
  import { useLoadingState } from "../useLoadingState";
4
4