@applicaster/zapp-react-native-ui-components 15.0.0-alpha.5170277721 → 15.0.0-alpha.5219062121

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 (112) hide show
  1. package/Components/BaseFocusable/index.ios.ts +12 -2
  2. package/Components/Cell/Cell.tsx +14 -3
  3. package/Components/Cell/CellWithFocusable.tsx +9 -0
  4. package/Components/Cell/FocusableWrapper.tsx +3 -0
  5. package/Components/Cell/TvOSCellComponent.tsx +26 -5
  6. package/Components/Focusable/Focusable.tsx +4 -2
  7. package/Components/Focusable/FocusableTvOS.tsx +18 -1
  8. package/Components/Focusable/__tests__/__snapshots__/FocusableTvOS.test.tsx.snap +1 -0
  9. package/Components/FocusableGroup/FocusableTvOS.tsx +32 -1
  10. package/Components/GeneralContentScreen/utils/__tests__/useCurationAPI.test.js +1 -1
  11. package/Components/GeneralContentScreen/utils/useCurationAPI.ts +22 -6
  12. package/Components/HandlePlayable/HandlePlayable.tsx +10 -7
  13. package/Components/HandlePlayable/utils.ts +31 -0
  14. package/Components/Layout/TV/LayoutBackground.tsx +5 -2
  15. package/Components/Layout/TV/ScreenContainer.tsx +2 -6
  16. package/Components/Layout/TV/index.tsx +3 -4
  17. package/Components/Layout/TV/index.web.tsx +3 -4
  18. package/Components/LinkHandler/LinkHandler.tsx +2 -2
  19. package/Components/MasterCell/DefaultComponents/BorderContainerView/__tests__/index.test.tsx +16 -1
  20. package/Components/MasterCell/DefaultComponents/BorderContainerView/index.tsx +30 -2
  21. package/Components/MasterCell/DefaultComponents/Image/Image.android.tsx +5 -1
  22. package/Components/MasterCell/DefaultComponents/Image/Image.ios.tsx +11 -3
  23. package/Components/MasterCell/DefaultComponents/Image/Image.web.tsx +9 -1
  24. package/Components/MasterCell/DefaultComponents/Image/hooks/useImage.ts +15 -14
  25. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +10 -6
  26. package/Components/MasterCell/DefaultComponents/SecondaryImage/Image.tsx +40 -39
  27. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/Image.test.tsx +95 -0
  28. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/__snapshots__/Image.test.tsx.snap +86 -0
  29. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/index.test.ts +141 -0
  30. package/Components/MasterCell/DefaultComponents/SecondaryImage/hooks/__tests__/useGetImageDimensions.test.ts +7 -6
  31. package/Components/MasterCell/DefaultComponents/SecondaryImage/index.ts +1 -1
  32. package/Components/MasterCell/DefaultComponents/Text/index.tsx +8 -8
  33. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +6 -2
  34. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/getPluginIdentifier.test.ts +233 -11
  35. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +19 -15
  36. package/Components/MasterCell/hoc/__tests__/withAsyncRender.test.tsx +219 -0
  37. package/Components/MasterCell/hoc/withAsyncRender.tsx +9 -7
  38. package/Components/MasterCell/index.tsx +2 -0
  39. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +82 -3
  40. package/Components/MasterCell/utils/index.ts +61 -31
  41. package/Components/MeasurmentsPortal/MeasurementsPortal.tsx +102 -87
  42. package/Components/MeasurmentsPortal/__tests__/MeasurementsPortal.test.tsx +355 -0
  43. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +17 -9
  44. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +16 -8
  45. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +2 -2
  46. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +17 -18
  47. package/Components/OfflineHandler/NotificationView/utils.ts +34 -0
  48. package/Components/OfflineHandler/__tests__/index.test.tsx +27 -18
  49. package/Components/PlayerContainer/PlayerContainer.tsx +50 -55
  50. package/Components/PlayerContainer/useRestrictMobilePlayback.tsx +131 -0
  51. package/Components/PlayerImageBackground/index.tsx +3 -22
  52. package/Components/River/ComponentsMap/ComponentsMap.tsx +16 -0
  53. package/Components/River/ComponentsMap/hooks/__tests__/useLoadingState.test.ts +1 -1
  54. package/Components/River/TV/River.tsx +31 -14
  55. package/Components/River/TV/index.tsx +8 -4
  56. package/Components/River/TV/utils/__tests__/toStringOrEmpty.test.ts +30 -0
  57. package/Components/River/TV/utils/index.ts +4 -0
  58. package/Components/River/TV/withFocusableGroupForContent.tsx +71 -0
  59. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +2 -0
  60. package/Components/River/__tests__/componentsMap.test.js +38 -0
  61. package/Components/Screen/TV/index.web.tsx +4 -2
  62. package/Components/Screen/__tests__/Screen.test.tsx +65 -42
  63. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +68 -44
  64. package/Components/Screen/hooks.ts +2 -3
  65. package/Components/Screen/index.tsx +2 -3
  66. package/Components/Screen/navigationHandler.ts +49 -24
  67. package/Components/Screen/orientationHandler.ts +10 -13
  68. package/Components/ScreenResolver/index.tsx +13 -7
  69. package/Components/ScreenRevealManager/ScreenRevealManager.ts +40 -8
  70. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +86 -69
  71. package/Components/Tabs/TV/Tabs.tsx +20 -3
  72. package/Components/Tabs/TabContent.tsx +7 -4
  73. package/Components/Transitioner/Scene.tsx +15 -2
  74. package/Components/Transitioner/index.js +3 -3
  75. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +118 -171
  76. package/Components/VideoModal/ModalAnimation/index.ts +2 -13
  77. package/Components/VideoModal/ModalAnimation/utils.ts +1 -327
  78. package/Components/VideoModal/PlayerWrapper.tsx +14 -52
  79. package/Components/VideoModal/VideoModal.tsx +1 -5
  80. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +1 -0
  81. package/Components/VideoModal/hooks/__tests__/useDelayedPlayerDetails.test.ts +15 -7
  82. package/Components/VideoModal/hooks/useModalSize.ts +5 -1
  83. package/Components/VideoModal/playerWrapperStyle.ts +70 -0
  84. package/Components/VideoModal/playerWrapperUtils.ts +16 -12
  85. package/Components/VideoModal/utils.ts +19 -9
  86. package/Components/Viewport/ViewportAware/index.tsx +16 -7
  87. package/Components/Viewport/ViewportEvents/__tests__/viewportEvents.test.js +1 -1
  88. package/Contexts/ScreenContext/index.tsx +25 -18
  89. package/Contexts/ScreenTrackedViewPositionsContext/__tests__/index.test.tsx +1 -1
  90. package/Contexts/ZappHookModalContext/index.tsx +37 -61
  91. package/Contexts/index.ts +0 -2
  92. package/Decorators/Analytics/index.tsx +6 -5
  93. package/Decorators/ConfigurationWrapper/__tests__/__snapshots__/withConfigurationProvider.test.tsx.snap +1 -0
  94. package/Decorators/ConfigurationWrapper/const.ts +1 -0
  95. package/Decorators/ZappPipesDataConnector/__tests__/zappPipesDataConnector.test.js +1 -1
  96. package/Decorators/ZappPipesDataConnector/index.tsx +2 -2
  97. package/Decorators/ZappPipesDataConnector/resolvers/StaticFeedResolver.tsx +1 -1
  98. package/Helpers/DataSourceHelper/__tests__/itemLimitForData.test.ts +80 -0
  99. package/Helpers/DataSourceHelper/index.ts +19 -0
  100. package/events/index.ts +3 -0
  101. package/events/scrollEndReached.ts +15 -0
  102. package/index.d.ts +7 -0
  103. package/package.json +6 -5
  104. package/Components/River/TV/withTVEventHandler.tsx +0 -27
  105. package/Components/VideoModal/ModalAnimation/AnimatedPlayerModalWrapper.tsx +0 -60
  106. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.tsx +0 -417
  107. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.web.tsx +0 -294
  108. package/Components/VideoModal/ModalAnimation/AnimatedVideoPlayerComponent.tsx +0 -176
  109. package/Components/VideoModal/ModalAnimation/AnimatedVideoPlayerComponent.web.tsx +0 -93
  110. package/Components/VideoModal/ModalAnimation/AnimationComponent.tsx +0 -500
  111. package/Components/VideoModal/ModalAnimation/__tests__/getMoveUpValue.test.ts +0 -108
  112. package/Helpers/DataSourceHelper/index.js +0 -19
@@ -8,25 +8,24 @@ import {
8
8
  offlinePhrase,
9
9
  } from "../NotificationView";
10
10
 
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
- },
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
+ }),
27
26
  },
28
- ],
29
- }),
27
+ },
28
+ ],
30
29
  }));
31
30
 
32
31
  jest.mock("react-native-safe-area-context", () => ({
@@ -0,0 +1,34 @@
1
+ import * as React from "react";
2
+ import { sessionStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/SessionStorage";
3
+
4
+ export const NETWORK_STATUS_LOCALIZATIONS_KEY = "network_status_localizations";
5
+
6
+ export const THEME_STORAGE_NAMESPACE = "quick-brick-theme";
7
+
8
+ export function useNetworkStatusLocalizations() {
9
+ const [storedLocalizations, setStoredLocalizations] = React.useState<Record<
10
+ string,
11
+ string
12
+ > | null>(null);
13
+
14
+ React.useEffect(() => {
15
+ async function loadStoredLocalizations() {
16
+ try {
17
+ const stored = await sessionStorage.getItem(
18
+ NETWORK_STATUS_LOCALIZATIONS_KEY,
19
+ THEME_STORAGE_NAMESPACE
20
+ );
21
+
22
+ if (stored) {
23
+ setStoredLocalizations(stored);
24
+ }
25
+ } catch (error) {
26
+ console.error("Error loading network status localizations", error);
27
+ }
28
+ }
29
+
30
+ loadStoredLocalizations();
31
+ }, []);
32
+
33
+ return storedLocalizations;
34
+ }
@@ -8,36 +8,45 @@ 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
+ ),
11
14
  useConnectionInfo: jest.fn(() => mockConnectionStatus),
12
15
  };
13
16
  });
14
17
 
15
18
  jest.mock("@applicaster/zapp-react-native-utils/reactHooks/utils", () => {
16
19
  return {
20
+ ...jest.requireActual(
21
+ "@applicaster/zapp-react-native-utils/reactHooks/utils"
22
+ ),
17
23
  usePrevious: jest.fn(() => mockPreviousValue),
18
24
  };
19
25
  });
20
26
 
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
+
21
46
  jest.mock("@applicaster/zapp-react-native-redux/hooks", () => ({
22
47
  ...jest.requireActual("@applicaster/zapp-react-native-redux/hooks"),
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
- }),
48
+ usePlugins: () => mock_storeObj.plugins,
49
+ usePickFromState: () => ({}),
41
50
  }));
42
51
 
43
52
  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
- import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
20
+
21
21
  import {
22
22
  useBackHandler,
23
23
  useNavigation,
@@ -56,15 +56,12 @@ import { toNumber } from "@applicaster/zapp-react-native-utils/numberUtils";
56
56
  import { usePlayNextOverlay } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayNextOverlay";
57
57
  import { PlayNextState } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/OverlayObserver/OverlaysObserver";
58
58
 
59
- import {
60
- PlayerAnimationStateEnum,
61
- useModalAnimationContext,
62
- } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/ModalAnimation";
63
-
64
59
  import {
65
60
  PlayerNativeCommandTypes,
66
61
  PlayerNativeSendCommand,
67
62
  } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand";
63
+ import { useAppData } from "@applicaster/zapp-react-native-redux";
64
+ import { useRestrictMobilePlayback } from "./useRestrictMobilePlayback";
68
65
 
69
66
  type Props = {
70
67
  Player: React.ComponentType<any>;
@@ -243,14 +240,11 @@ const PlayerContainerComponent = (props: Props) => {
243
240
  const [isLoadingNextVideo, setIsLoadingNextVideo] = React.useState(false);
244
241
 
245
242
  const navigator = useNavigation();
246
- const { appData } = usePickFromState(["appData"]);
243
+ const { isTabletPortrait } = useAppData();
247
244
  const prevItemId = usePrevious(item?.id);
248
245
  const screenData = useTargetScreenData(item);
249
246
  const { setVisible: showNavBar } = useSetNavbarState();
250
247
 
251
- const { isActiveGesture, startComponentsAnimation, setPlayerAnimationState } =
252
- useModalAnimationContext();
253
-
254
248
  const playerEvent = (event, ...args) => {
255
249
  playerManager.invokeHandler(event, ...args);
256
250
  };
@@ -271,7 +265,20 @@ const PlayerContainerComponent = (props: Props) => {
271
265
 
272
266
  showNavBar(true);
273
267
  navigator.goBack();
274
- }, [isModal, navigator.goBack, state.playerId, showNavBar]);
268
+ }, [isModal, state.playerId, showNavBar, navigator]);
269
+
270
+ const pluginConfiguration = React.useMemo(() => {
271
+ return (
272
+ playerManager.getPluginConfiguration() ||
273
+ R.prop("__plugin_configuration", Player)
274
+ );
275
+ }, [playerManager.isRegistered()]);
276
+
277
+ const { isRestricted } = useRestrictMobilePlayback({
278
+ player,
279
+ entry: item,
280
+ close,
281
+ });
275
282
 
276
283
  const playEntry = (entry) => navigator.replaceTop(entry, { mode });
277
284
 
@@ -463,13 +470,6 @@ const PlayerContainerComponent = (props: Props) => {
463
470
  }
464
471
  }, []);
465
472
 
466
- const pluginConfiguration = React.useMemo(() => {
467
- return (
468
- playerManager.getPluginConfiguration() ||
469
- R.prop("__plugin_configuration", Player)
470
- );
471
- }, [playerManager.isRegistered()]);
472
-
473
473
  const disableMiniPlayer = React.useMemo(() => {
474
474
  return pluginConfiguration?.disable_mini_player_when_inline;
475
475
  }, [pluginConfiguration]);
@@ -482,8 +482,6 @@ const PlayerContainerComponent = (props: Props) => {
482
482
  if (isModal && mode === VideoModalMode.MAXIMIZED) {
483
483
  if (disableMiniPlayer) {
484
484
  navigator.closeVideoModal();
485
- } else {
486
- setPlayerAnimationState(PlayerAnimationStateEnum.minimize);
487
485
  }
488
486
  }
489
487
 
@@ -671,41 +669,38 @@ const PlayerContainerComponent = (props: Props) => {
671
669
  <PlayerFocusableWrapperView
672
670
  nextFocusDown={context.bottomFocusableId}
673
671
  >
674
- <Player
675
- source={{
676
- uri,
677
- entry: item,
678
- }}
679
- focused={isInlineTV ? true : undefined}
680
- autoplay={true}
681
- controls={false}
682
- disableCastAction={disableCastAction}
683
- docked={
684
- navigator.isVideoModalDocked() &&
685
- !startComponentsAnimation &&
686
- !isActiveGesture
687
- }
688
- entry={item}
689
- fullscreen={mode === VideoModalMode.FULLSCREEN}
690
- inline={inline}
691
- isModal={isModal}
692
- isTabletPortrait={appData.isTabletPortrait}
693
- muted={false}
694
- playableItem={item}
695
- playerEvent={playerEvent}
696
- playerId={state.playerId}
697
- pluginConfiguration={pluginConfiguration}
698
- ref={playerRef}
699
- toggleFullscreen={toggleFullscreen}
700
- style={videoStyle}
701
- playNextData={playNextData}
702
- setNextVideoPreloadThresholdPercentage={
703
- setNextVideoPreloadThresholdPercentage
704
- }
705
- startComponentsAnimation={startComponentsAnimation}
706
- >
707
- {renderApplePlayer(applePlayerProps)}
708
- </Player>
672
+ {!Player || isRestricted ? null : (
673
+ <Player
674
+ source={{
675
+ uri,
676
+ entry: item,
677
+ }}
678
+ focused={isInlineTV ? true : undefined}
679
+ autoplay={true}
680
+ controls={false}
681
+ disableCastAction={disableCastAction}
682
+ docked={navigator.isVideoModalDocked()}
683
+ entry={item}
684
+ fullscreen={mode === VideoModalMode.FULLSCREEN}
685
+ inline={inline}
686
+ isModal={isModal}
687
+ isTabletPortrait={isTabletPortrait}
688
+ muted={false}
689
+ playableItem={item}
690
+ playerEvent={playerEvent}
691
+ playerId={state.playerId}
692
+ pluginConfiguration={pluginConfiguration}
693
+ ref={playerRef}
694
+ toggleFullscreen={toggleFullscreen}
695
+ style={videoStyle}
696
+ playNextData={playNextData}
697
+ setNextVideoPreloadThresholdPercentage={
698
+ setNextVideoPreloadThresholdPercentage
699
+ }
700
+ >
701
+ {renderApplePlayer(applePlayerProps)}
702
+ </Player>
703
+ )}
709
704
  </PlayerFocusableWrapperView>
710
705
 
711
706
  {state.error ? <ErrorDisplay error={state.error} /> : null}
@@ -0,0 +1,131 @@
1
+ import { Player } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/player";
2
+ import NetInfo from "@react-native-community/netinfo";
3
+
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
+ import { showAlertDialog } from "@applicaster/zapp-react-native-utils/alertUtils";
6
+ import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
7
+ import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
8
+ import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils/playerManager";
9
+ import { usePlugins } from "@applicaster/zapp-react-native-redux/hooks";
10
+ import { getLocalizations } from "@applicaster/zapp-react-native-utils/localizationUtils";
11
+ import { log_info } from "./logger";
12
+ import { isTrue } from "@applicaster/zapp-react-native-utils/booleanUtils";
13
+
14
+ type RestrictMobilePlaybackProps = {
15
+ player?: Player;
16
+ entry?: ZappEntry;
17
+ close: () => void;
18
+ };
19
+
20
+ export const useRestrictMobilePlayback = ({
21
+ player,
22
+ entry,
23
+ close,
24
+ }: RestrictMobilePlaybackProps): { isRestricted: boolean } => {
25
+ const dialogVisibleRef = useRef<boolean>(false);
26
+ const theme = useTheme();
27
+ const plugins = usePlugins();
28
+
29
+ const restrictMobilePlugin = useMemo(
30
+ () =>
31
+ plugins.find((p) => p.identifier === "quick-brick-hook-restrict-mobile"),
32
+ [plugins]
33
+ );
34
+
35
+ const localizations = useMemo(
36
+ () => restrictMobilePlugin?.configuration?.localizations,
37
+ [restrictMobilePlugin]
38
+ );
39
+
40
+ const localize = useCallback(
41
+ (key: string) => {
42
+ const l = localizations && getLocalizations({ localizations });
43
+
44
+ return (l && l[key]) || "";
45
+ },
46
+ [localizations]
47
+ );
48
+
49
+ useEffect(() => {
50
+ return () => {
51
+ if (isTV()) {
52
+ return;
53
+ }
54
+
55
+ dialogVisibleRef.current = false;
56
+ };
57
+ }, []);
58
+
59
+ const isConnectionRestricted = useMemo(() => {
60
+ if (isTV()) {
61
+ return false;
62
+ }
63
+
64
+ // Only restrict if the plugin exists
65
+ if (!restrictMobilePlugin) {
66
+ return false;
67
+ }
68
+
69
+ return player && isTrue(entry?.extensions?.connection_restricted);
70
+ }, [player, entry, restrictMobilePlugin]);
71
+
72
+ const [isRestricted, setIsRestricted] = useState<boolean>(
73
+ isConnectionRestricted
74
+ );
75
+
76
+ useEffect(() => {
77
+ if (!isConnectionRestricted) {
78
+ return;
79
+ }
80
+
81
+ const stopPlayer = () => {
82
+ log_info(
83
+ "Stopping player due to mobile restriction, connection_restricted: true"
84
+ );
85
+
86
+ player?.closeNativePlayer();
87
+ playerManager?.invokeHandler("close");
88
+
89
+ dialogVisibleRef.current = true;
90
+
91
+ showAlertDialog({
92
+ title:
93
+ localize("restrict_mobile_playback_error_title") ||
94
+ "Restricted Connection Type",
95
+ message:
96
+ localize("restrict_mobile_playback_error_message") ||
97
+ "This content can only be viewed over a Wi-Fi or LAN network.",
98
+ okButtonText: theme.ok_button || "OK",
99
+ completion: () => {
100
+ dialogVisibleRef.current = false;
101
+
102
+ close();
103
+ },
104
+ });
105
+ };
106
+
107
+ return NetInfo.addEventListener((state) => {
108
+ if (state.type === "cellular") {
109
+ setIsRestricted(true);
110
+
111
+ if (dialogVisibleRef.current) {
112
+ return;
113
+ }
114
+
115
+ stopPlayer();
116
+ } else {
117
+ setIsRestricted(false);
118
+ }
119
+ });
120
+ }, [
121
+ close,
122
+ entry?.extensions?.connection_restricted,
123
+ localizations,
124
+ localize,
125
+ player,
126
+ theme.ok_button,
127
+ isConnectionRestricted,
128
+ ]);
129
+
130
+ return { isRestricted };
131
+ };
@@ -2,12 +2,6 @@ import React, { PropsWithChildren } from "react";
2
2
  import { ImageBackground, View } from "react-native";
3
3
 
4
4
  import { imageSrcFromMediaItem } from "@applicaster/zapp-react-native-utils/configurationUtils";
5
- import {
6
- AnimationComponent,
7
- ComponentAnimationType,
8
- useModalAnimationContext,
9
- PlayerAnimationStateEnum,
10
- } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/ModalAnimation";
11
5
 
12
6
  type Props = PropsWithChildren<{
13
7
  entry: ZappEntry;
@@ -25,30 +19,17 @@ const PlayerImageBackgroundComponent = ({
25
19
  style,
26
20
  imageStyle,
27
21
  imageKey,
28
- defaultImageDimensions,
29
22
  }: Props) => {
30
23
  const source = React.useMemo(
31
24
  () => ({ uri: imageSrcFromMediaItem(entry, [imageKey]) }),
32
25
  [imageKey, entry]
33
26
  );
34
27
 
35
- const { playerAnimationState } = useModalAnimationContext();
36
-
37
28
  if (!source) return <>{children}</>;
38
29
 
39
30
  return (
40
- <View
41
- style={
42
- playerAnimationState === PlayerAnimationStateEnum.maximize
43
- ? defaultImageDimensions
44
- : style
45
- }
46
- >
47
- <AnimationComponent
48
- style={style}
49
- animationType={ComponentAnimationType.player}
50
- additionalData={defaultImageDimensions}
51
- >
31
+ <View style={style}>
32
+ <View style={style}>
52
33
  <ImageBackground
53
34
  resizeMode="cover"
54
35
  style={imageSize}
@@ -57,7 +38,7 @@ const PlayerImageBackgroundComponent = ({
57
38
  >
58
39
  {children}
59
40
  </ImageBackground>
60
- </AnimationComponent>
41
+ </View>
61
42
  </View>
62
43
  );
63
44
  };
@@ -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 },
@@ -277,6 +281,7 @@ function ComponentsMapComponent(props: Props) {
277
281
  >
278
282
  <ViewportTracker>
279
283
  <FlatList
284
+ testID="components-map-flat-list"
280
285
  ref={(ref) => {
281
286
  flatListRef.current = ref;
282
287
  }}
@@ -308,6 +313,17 @@ function ComponentsMapComponent(props: Props) {
308
313
  onScrollEndDrag={_onScrollEndDrag}
309
314
  scrollEventThrottle={16}
310
315
  {...scrollViewExtraProps}
316
+ onEndReached={
317
+ /* When wrapped in a parent ScrollView (e.g. tabs),
318
+ this FlatList doesn't scroll so onEndReached can fire repeatedly;
319
+ skip it here and let the parent ScrollView emit scroll-end instead. */
320
+ isScreenWrappedInContainer
321
+ ? undefined
322
+ : () => {
323
+ if (!hasUserScrolledRef.current) return;
324
+ emitScrollEndReached();
325
+ }
326
+ }
311
327
  />
312
328
  </ViewportTracker>
313
329
  </ScreenLoadingMeasurements>
@@ -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
 
@@ -2,7 +2,8 @@
2
2
 
3
3
  import * as React from "react";
4
4
  import { Text } from "react-native";
5
- import * as R from "ramda";
5
+
6
+ import { mergeRight } from "@applicaster/zapp-react-native-utils/utils";
6
7
 
7
8
  import { GeneralContentScreen } from "../../GeneralContentScreen";
8
9
  import { ScreenResolver } from "@applicaster/zapp-react-native-ui-components/Components/ScreenResolver";
@@ -13,6 +14,8 @@ import {
13
14
  } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
14
15
  import { useRivers } from "@applicaster/zapp-react-native-utils/reactHooks/state";
15
16
 
17
+ import { toStringOrEmpty } from "./utils";
18
+
16
19
  type Props = {
17
20
  screenId: string;
18
21
  screenData: ZappRiver | ZappEntry;
@@ -24,6 +27,7 @@ type Props = {
24
27
  isInsideContainer?: boolean;
25
28
  extraAnchorPointYOffset: number;
26
29
  river?: ZappRiver | ZappEntry;
30
+ groupId: string;
27
31
  };
28
32
 
29
33
  export const River = (props: Props) => {
@@ -35,6 +39,7 @@ export const River = (props: Props) => {
35
39
  componentsMapExtraProps,
36
40
  isInsideContainer,
37
41
  extraAnchorPointYOffset,
42
+ groupId,
38
43
  } = props;
39
44
 
40
45
  const { title: screenTitle, summary: screenSummary } = useNavbarState();
@@ -51,28 +56,41 @@ export const River = (props: Props) => {
51
56
  [screenId]
52
57
  );
53
58
 
54
- const stringOrEmpty = (value: string | number | undefined): string =>
55
- R.isNil(value) ? "" : String(value);
59
+ const screenResolverData = React.useMemo(() => {
60
+ const extraData = mergeRight(extraProps, screenResolverExtraProps);
61
+
62
+ return {
63
+ extraData,
64
+ screenData: mergeRight(river, { groupId: extraData?.groupId }),
65
+ componentsMapExtraProps: mergeRight(componentsMapExtraProps, { groupId }),
66
+ };
67
+ }, [
68
+ extraProps,
69
+ screenResolverExtraProps,
70
+ river,
71
+ componentsMapExtraProps,
72
+ groupId,
73
+ ]);
56
74
 
57
75
  React.useEffect(() => {
58
76
  if (!isInsideContainer) {
59
- setScreenTitle(stringOrEmpty(screenData?.title));
60
- setScreenSummary(stringOrEmpty(screenData?.summary));
77
+ setScreenTitle(toStringOrEmpty(screenData?.title));
78
+ setScreenSummary(toStringOrEmpty(screenData?.summary));
61
79
  }
62
80
  }, [screenData.id]);
63
81
 
64
82
  React.useEffect(() => {
65
83
  if (feedData && !isInsideContainer) {
66
84
  if (feedData.title && feedData.title !== screenTitle) {
67
- setScreenTitle(stringOrEmpty(feedData.title));
85
+ setScreenTitle(toStringOrEmpty(feedData.title));
68
86
  }
69
87
 
70
88
  if (feedData.summary && feedData.summary !== screenSummary) {
71
- setScreenSummary(stringOrEmpty(feedData.summary));
89
+ setScreenSummary(toStringOrEmpty(feedData.summary));
72
90
  }
73
91
  } else {
74
- setScreenTitle(stringOrEmpty(screenData?.title));
75
- setScreenSummary(stringOrEmpty(screenData?.summary));
92
+ setScreenTitle(toStringOrEmpty(screenData?.title));
93
+ setScreenSummary(toStringOrEmpty(screenData?.summary));
76
94
  }
77
95
  }, [feedData, screenData, screenTitle, screenSummary]);
78
96
 
@@ -86,15 +104,13 @@ export const River = (props: Props) => {
86
104
  }
87
105
 
88
106
  if (river.type !== "general_content") {
89
- const extraData = { ...R.mergeRight(extraProps, screenResolverExtraProps) };
90
-
91
107
  return (
92
108
  <ScreenResolver
93
109
  screenType={river.type}
94
110
  screenId={screenId}
95
- screenData={R.merge(river, { groupId: extraData?.groupId })}
96
- componentsMapExtraProps={componentsMapExtraProps}
97
- {...extraData}
111
+ screenData={screenResolverData.screenData}
112
+ componentsMapExtraProps={screenResolverData.componentsMapExtraProps}
113
+ {...screenResolverData.extraData}
98
114
  />
99
115
  );
100
116
  }
@@ -106,6 +122,7 @@ export const River = (props: Props) => {
106
122
  isScreenWrappedInContainer={isInsideContainer}
107
123
  extraAnchorPointYOffset={extraAnchorPointYOffset}
108
124
  componentsMapExtraProps={componentsMapExtraProps}
125
+ groupId={groupId}
109
126
  />
110
127
  );
111
128
  };
@@ -1,11 +1,15 @@
1
- import { compose } from "ramda";
1
+ import { compose, identity } from "@applicaster/zapp-react-native-utils/utils";
2
+ import { isTvOSPlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
3
+
2
4
  import { River as RiverComponent } from "./River";
3
- import { withTvEventHandler } from "./withTVEventHandler";
4
5
  import { withComponentsMapOffsetContext } from "../../../Contexts/ComponentsMapOffsetContext";
5
6
  import { withRiverDataLoader } from "./withRiverDataLoader";
7
+ import { withFocusableGroupForContent } from "./withFocusableGroupForContent";
8
+
9
+ const isTVOS = isTvOSPlatform();
6
10
 
7
11
  export const River = compose(
8
- withTvEventHandler,
9
12
  withComponentsMapOffsetContext,
10
- withRiverDataLoader
13
+ withRiverDataLoader,
14
+ isTVOS ? withFocusableGroupForContent : identity
11
15
  )(RiverComponent);