@applicaster/zapp-react-native-ui-components 15.0.0-rc.1 → 15.0.0-rc.100

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 (123) hide show
  1. package/Components/AnimatedInOut/index.tsx +69 -26
  2. package/Components/BaseFocusable/index.ios.ts +12 -2
  3. package/Components/Cell/Cell.tsx +14 -3
  4. package/Components/Cell/CellWithFocusable.tsx +9 -0
  5. package/Components/Cell/FocusableWrapper.tsx +47 -0
  6. package/Components/Cell/TvOSCellComponent.tsx +106 -19
  7. package/Components/Focusable/Focusable.tsx +4 -2
  8. package/Components/Focusable/FocusableTvOS.tsx +18 -1
  9. package/Components/Focusable/__tests__/__snapshots__/FocusableTvOS.test.tsx.snap +1 -0
  10. package/Components/FocusableGroup/FocusableTvOS.tsx +32 -1
  11. package/Components/GeneralContentScreen/utils/__tests__/useCurationAPI.test.js +1 -1
  12. package/Components/GeneralContentScreen/utils/useCurationAPI.ts +31 -17
  13. package/Components/HandlePlayable/HandlePlayable.tsx +17 -65
  14. package/Components/HandlePlayable/const.ts +3 -0
  15. package/Components/HandlePlayable/utils.ts +74 -0
  16. package/Components/Layout/TV/LayoutBackground.tsx +5 -2
  17. package/Components/Layout/TV/ScreenContainer.tsx +2 -6
  18. package/Components/Layout/TV/index.tsx +3 -4
  19. package/Components/Layout/TV/index.web.tsx +3 -4
  20. package/Components/LinkHandler/LinkHandler.tsx +2 -2
  21. package/Components/MasterCell/DefaultComponents/BorderContainerView/__tests__/index.test.tsx +16 -1
  22. package/Components/MasterCell/DefaultComponents/BorderContainerView/index.tsx +30 -2
  23. package/Components/MasterCell/DefaultComponents/Image/Image.android.tsx +5 -1
  24. package/Components/MasterCell/DefaultComponents/Image/Image.ios.tsx +11 -3
  25. package/Components/MasterCell/DefaultComponents/Image/Image.web.tsx +9 -1
  26. package/Components/MasterCell/DefaultComponents/Image/hooks/useImage.ts +15 -14
  27. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +10 -6
  28. package/Components/MasterCell/DefaultComponents/SecondaryImage/Image.tsx +40 -39
  29. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/Image.test.tsx +95 -0
  30. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/__snapshots__/Image.test.tsx.snap +86 -0
  31. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/index.test.ts +141 -0
  32. package/Components/MasterCell/DefaultComponents/SecondaryImage/hooks/__tests__/useGetImageDimensions.test.ts +7 -6
  33. package/Components/MasterCell/DefaultComponents/SecondaryImage/index.ts +1 -1
  34. package/Components/MasterCell/DefaultComponents/Text/index.tsx +8 -8
  35. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +6 -2
  36. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/getPluginIdentifier.test.ts +233 -11
  37. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +19 -15
  38. package/Components/MasterCell/hoc/__tests__/withAsyncRender.test.tsx +219 -0
  39. package/Components/MasterCell/hoc/withAsyncRender.tsx +9 -7
  40. package/Components/MasterCell/index.tsx +2 -0
  41. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +82 -3
  42. package/Components/MasterCell/utils/index.ts +61 -31
  43. package/Components/MeasurmentsPortal/MeasurementsPortal.tsx +102 -87
  44. package/Components/MeasurmentsPortal/__tests__/MeasurementsPortal.test.tsx +355 -0
  45. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +17 -9
  46. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +16 -8
  47. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +2 -2
  48. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +17 -18
  49. package/Components/OfflineHandler/NotificationView/utils.ts +34 -0
  50. package/Components/OfflineHandler/__tests__/index.test.tsx +27 -18
  51. package/Components/PlayerContainer/PlayerContainer.tsx +51 -55
  52. package/Components/PlayerContainer/useRestrictMobilePlayback.tsx +101 -0
  53. package/Components/PlayerImageBackground/index.tsx +3 -22
  54. package/Components/River/ComponentsMap/ComponentsMap.tsx +65 -42
  55. package/Components/River/ComponentsMap/ContextProviders/ComponentsMapHeightContext.ts +8 -0
  56. package/Components/River/ComponentsMap/ContextProviders/ComponentsMapRefContext.ts +8 -0
  57. package/Components/River/ComponentsMap/hooks/__tests__/useLoadingState.test.ts +1 -1
  58. package/Components/River/TV/River.tsx +31 -14
  59. package/Components/River/TV/index.tsx +8 -4
  60. package/Components/River/TV/utils/__tests__/toStringOrEmpty.test.ts +30 -0
  61. package/Components/River/TV/utils/index.ts +4 -0
  62. package/Components/River/TV/withFocusableGroupForContent.tsx +71 -0
  63. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +2 -0
  64. package/Components/River/__tests__/componentsMap.test.js +38 -0
  65. package/Components/Screen/TV/hooks/useInitialFocus.ts +14 -4
  66. package/Components/Screen/TV/index.web.tsx +4 -2
  67. package/Components/Screen/__tests__/Screen.test.tsx +65 -42
  68. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +68 -42
  69. package/Components/Screen/hooks.ts +2 -3
  70. package/Components/Screen/index.tsx +24 -8
  71. package/Components/Screen/navigationHandler.ts +49 -24
  72. package/Components/Screen/orientationHandler.ts +10 -13
  73. package/Components/ScreenResolver/index.tsx +21 -9
  74. package/Components/ScreenRevealManager/ScreenRevealManager.ts +40 -8
  75. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +86 -69
  76. package/Components/ScreenRevealManager/utils/index.ts +23 -0
  77. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +54 -24
  78. package/Components/Tabs/TV/Tabs.tsx +20 -3
  79. package/Components/Tabs/TabContent.tsx +7 -4
  80. package/Components/Transitioner/Scene.tsx +15 -2
  81. package/Components/Transitioner/index.js +3 -3
  82. package/Components/VideoLive/__tests__/__snapshots__/PlayerLiveImageComponent.test.tsx.snap +1 -0
  83. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +118 -171
  84. package/Components/VideoModal/ModalAnimation/index.ts +2 -13
  85. package/Components/VideoModal/ModalAnimation/utils.ts +1 -327
  86. package/Components/VideoModal/PlayerDetails.tsx +5 -5
  87. package/Components/VideoModal/PlayerWrapper.tsx +14 -88
  88. package/Components/VideoModal/VideoModal.tsx +1 -5
  89. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +1 -0
  90. package/Components/VideoModal/hooks/__tests__/useDelayedPlayerDetails.test.ts +15 -7
  91. package/Components/VideoModal/hooks/useModalSize.ts +10 -5
  92. package/Components/VideoModal/playerWrapperStyle.ts +70 -0
  93. package/Components/VideoModal/playerWrapperUtils.ts +91 -0
  94. package/Components/VideoModal/utils.ts +19 -9
  95. package/Components/Viewport/ViewportEvents/__tests__/viewportEvents.test.js +1 -1
  96. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +4 -6
  97. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  98. package/Contexts/ScreenContext/index.tsx +25 -18
  99. package/Contexts/ScreenTrackedViewPositionsContext/__tests__/index.test.tsx +1 -1
  100. package/Contexts/ZappHookModalContext/index.tsx +37 -61
  101. package/Contexts/index.ts +0 -2
  102. package/Decorators/Analytics/index.tsx +6 -5
  103. package/Decorators/ConfigurationWrapper/__tests__/__snapshots__/withConfigurationProvider.test.tsx.snap +1 -0
  104. package/Decorators/ConfigurationWrapper/const.ts +1 -0
  105. package/Decorators/RiverFeedLoader/utils/getDatasourceUrl.ts +6 -10
  106. package/Decorators/ZappPipesDataConnector/__tests__/zappPipesDataConnector.test.js +1 -1
  107. package/Decorators/ZappPipesDataConnector/index.tsx +2 -2
  108. package/Decorators/ZappPipesDataConnector/resolvers/StaticFeedResolver.tsx +1 -1
  109. package/Helpers/DataSourceHelper/__tests__/itemLimitForData.test.ts +80 -0
  110. package/Helpers/DataSourceHelper/index.ts +19 -0
  111. package/events/index.ts +3 -0
  112. package/events/scrollEndReached.ts +15 -0
  113. package/index.d.ts +7 -0
  114. package/package.json +6 -5
  115. package/Components/River/TV/withTVEventHandler.tsx +0 -27
  116. package/Components/VideoModal/ModalAnimation/AnimatedPlayerModalWrapper.tsx +0 -60
  117. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.tsx +0 -415
  118. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.web.tsx +0 -294
  119. package/Components/VideoModal/ModalAnimation/AnimatedVideoPlayerComponent.tsx +0 -176
  120. package/Components/VideoModal/ModalAnimation/AnimatedVideoPlayerComponent.web.tsx +0 -93
  121. package/Components/VideoModal/ModalAnimation/AnimationComponent.tsx +0 -500
  122. package/Components/VideoModal/ModalAnimation/__tests__/getMoveUpValue.test.ts +0 -108
  123. package/Helpers/DataSourceHelper/index.js +0 -19
@@ -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,21 @@ 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
+ pluginConfiguration,
281
+ close,
282
+ });
275
283
 
276
284
  const playEntry = (entry) => navigator.replaceTop(entry, { mode });
277
285
 
@@ -463,13 +471,6 @@ const PlayerContainerComponent = (props: Props) => {
463
471
  }
464
472
  }, []);
465
473
 
466
- const pluginConfiguration = React.useMemo(() => {
467
- return (
468
- playerManager.getPluginConfiguration() ||
469
- R.prop("__plugin_configuration", Player)
470
- );
471
- }, [playerManager.isRegistered()]);
472
-
473
474
  const disableMiniPlayer = React.useMemo(() => {
474
475
  return pluginConfiguration?.disable_mini_player_when_inline;
475
476
  }, [pluginConfiguration]);
@@ -482,8 +483,6 @@ const PlayerContainerComponent = (props: Props) => {
482
483
  if (isModal && mode === VideoModalMode.MAXIMIZED) {
483
484
  if (disableMiniPlayer) {
484
485
  navigator.closeVideoModal();
485
- } else {
486
- setPlayerAnimationState(PlayerAnimationStateEnum.minimize);
487
486
  }
488
487
  }
489
488
 
@@ -671,41 +670,38 @@ const PlayerContainerComponent = (props: Props) => {
671
670
  <PlayerFocusableWrapperView
672
671
  nextFocusDown={context.bottomFocusableId}
673
672
  >
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>
673
+ {isRestricted ? null : (
674
+ <Player
675
+ source={{
676
+ uri,
677
+ entry: item,
678
+ }}
679
+ focused={isInlineTV ? true : undefined}
680
+ autoplay={true}
681
+ controls={false}
682
+ disableCastAction={disableCastAction}
683
+ docked={navigator.isVideoModalDocked()}
684
+ entry={item}
685
+ fullscreen={mode === VideoModalMode.FULLSCREEN}
686
+ inline={inline}
687
+ isModal={isModal}
688
+ isTabletPortrait={isTabletPortrait}
689
+ muted={false}
690
+ playableItem={item}
691
+ playerEvent={playerEvent}
692
+ playerId={state.playerId}
693
+ pluginConfiguration={pluginConfiguration}
694
+ ref={playerRef}
695
+ toggleFullscreen={toggleFullscreen}
696
+ style={videoStyle}
697
+ playNextData={playNextData}
698
+ setNextVideoPreloadThresholdPercentage={
699
+ setNextVideoPreloadThresholdPercentage
700
+ }
701
+ >
702
+ {renderApplePlayer(applePlayerProps)}
703
+ </Player>
704
+ )}
709
705
  </PlayerFocusableWrapperView>
710
706
 
711
707
  {state.error ? <ErrorDisplay error={state.error} /> : null}
@@ -0,0 +1,101 @@
1
+ import { Player } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/player";
2
+ import NetInfo from "@react-native-community/netinfo";
3
+
4
+ import { useEffect, useMemo, useRef, useState } from "react";
5
+ import { showAlertDialog } from "@applicaster/zapp-react-native-utils/alertUtils";
6
+ import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
7
+ import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
8
+ import { log_info } from "./logger";
9
+
10
+ type RestrictMobilePlaybackProps = {
11
+ player?: Player;
12
+ entry?: ZappEntry;
13
+ pluginConfiguration?: Record<string, string>;
14
+ close: () => void;
15
+ };
16
+
17
+ export const useRestrictMobilePlayback = ({
18
+ player,
19
+ entry,
20
+ pluginConfiguration,
21
+ close,
22
+ }: RestrictMobilePlaybackProps): { isRestricted: boolean } => {
23
+ const dialogVisibleRef = useRef<boolean>(false);
24
+ const theme = useTheme();
25
+
26
+ useEffect(() => {
27
+ return () => {
28
+ if (isTV()) {
29
+ return;
30
+ }
31
+
32
+ dialogVisibleRef.current = false;
33
+ };
34
+ }, []);
35
+
36
+ const isConnectionRestricted = useMemo(() => {
37
+ if (isTV()) {
38
+ return false;
39
+ }
40
+
41
+ return player && entry?.extensions?.connection_restricted;
42
+ }, [player, entry]);
43
+
44
+ const [isRestricted, setIsRestricted] = useState<boolean>(
45
+ isConnectionRestricted
46
+ );
47
+
48
+ useEffect(() => {
49
+ if (!isConnectionRestricted) {
50
+ return;
51
+ }
52
+
53
+ const stopPlayer = () => {
54
+ log_info(
55
+ "Stopping player due to mobile restriction, connection_restricted: true"
56
+ );
57
+
58
+ player?.close();
59
+
60
+ dialogVisibleRef.current = true;
61
+
62
+ showAlertDialog({
63
+ title:
64
+ pluginConfiguration?.mobile_connection_restricted_alert_title ||
65
+ "Restricted Connection Type",
66
+ message:
67
+ pluginConfiguration?.mobile_connection_restricted_alert_message ||
68
+ "This content can only be viewed over a Wi-Fi or LAN network.",
69
+ okButtonText: theme.ok_button || "OK",
70
+ completion: () => {
71
+ dialogVisibleRef.current = false;
72
+
73
+ close();
74
+ },
75
+ });
76
+ };
77
+
78
+ return NetInfo.addEventListener((state) => {
79
+ if (state.type === "cellular") {
80
+ setIsRestricted(true);
81
+
82
+ if (dialogVisibleRef.current) {
83
+ return;
84
+ }
85
+
86
+ stopPlayer();
87
+ } else {
88
+ setIsRestricted(false);
89
+ }
90
+ });
91
+ }, [
92
+ close,
93
+ entry?.extensions?.connection_restricted,
94
+ pluginConfiguration,
95
+ player,
96
+ theme.ok_button,
97
+ isConnectionRestricted,
98
+ ]);
99
+
100
+ return { isRestricted };
101
+ };
@@ -2,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
  };
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import * as R from "ramda";
3
- import { View, StyleSheet, FlatList } from "react-native";
3
+ import { FlatList, StyleSheet, 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";
@@ -9,8 +9,8 @@ import { useScreenConfiguration } from "../useScreenConfiguration";
9
9
  import { RefreshControl } from "../RefreshControl";
10
10
  import { ifEmptyUseFallback } from "@applicaster/zapp-react-native-utils/cellUtils";
11
11
  import {
12
- useProfilerLogging,
13
12
  usePipesCacheReset,
13
+ useProfilerLogging,
14
14
  } from "@applicaster/zapp-react-native-utils/reactHooks";
15
15
  import { useLoadingState } from "./hooks/useLoadingState";
16
16
  import { ViewportTracker } from "../../Viewport";
@@ -23,8 +23,11 @@ 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";
29
+ import { ComponentsMapHeightContext } from "./ContextProviders/ComponentsMapHeightContext";
30
+ import { ComponentsMapRefContext } from "./ContextProviders/ComponentsMapRefContext";
28
31
 
29
32
  const isAndroid = isAndroidPlatform();
30
33
 
@@ -70,6 +73,8 @@ function ComponentsMapComponent(props: Props) {
70
73
  } = props;
71
74
 
72
75
  const flatListRef = React.useRef<FlatList | null>(null);
76
+ const flatListWrapperRef = React.useRef<View | null>(null);
77
+ const hasUserScrolledRef = React.useRef(false);
73
78
  const screenConfig = useScreenConfiguration(riverId);
74
79
  const screenData = useScreenData(riverId);
75
80
  const pullToRefreshEnabled = screenData?.rules?.pull_to_refresh_enabled;
@@ -233,6 +238,8 @@ function ComponentsMapComponent(props: Props) {
233
238
  }, []);
234
239
 
235
240
  const onScroll = React.useCallback((event) => {
241
+ hasUserScrolledRef.current = true;
242
+
236
243
  const {
237
244
  nativeEvent: {
238
245
  contentOffset: { y },
@@ -265,47 +272,63 @@ function ComponentsMapComponent(props: Props) {
265
272
  // The Screen Picker in Mobile is completly different than the TV
266
273
  // so the various offsets / margins in TV do not apply here.
267
274
  return (
268
- <View style={styles.container}>
269
- <ScreenLoadingMeasurements
270
- riverId={riverId}
271
- numberOfComponents={riverComponents.length}
272
- >
273
- <ViewportTracker>
274
- <FlatList
275
- ref={(ref) => {
276
- flatListRef.current = ref;
277
- }}
278
- // Fix for WebView rerender crashes on Android API 28+
279
- // https://github.com/react-native-webview/react-native-webview/issues/1915#issuecomment-964035468
280
- overScrollMode={isAndroid ? "never" : "auto"}
281
- scrollIndicatorInsets={scrollIndicatorInsets}
282
- extraData={feed}
283
- stickyHeaderIndices={stickyHeaderIndices}
284
- removeClippedSubviews={isAndroid}
285
- onLayout={handleOnLayout}
286
- initialNumToRender={3}
287
- maxToRenderPerBatch={10}
288
- windowSize={12}
289
- keyExtractor={keyExtractor}
290
- renderItem={renderRiverItem}
291
- data={riverComponents}
292
- contentContainerStyle={contentContainerStyle}
293
- ListFooterComponent={
294
- <RiverFooter
295
- flatListHeight={flatListHeight}
296
- loadingState={loadingState}
275
+ <View style={styles.container} ref={flatListWrapperRef}>
276
+ <ComponentsMapHeightContext.Provider value={flatListHeight}>
277
+ <ComponentsMapRefContext.Provider value={flatListWrapperRef}>
278
+ <ScreenLoadingMeasurements
279
+ riverId={riverId}
280
+ numberOfComponents={riverComponents.length}
281
+ >
282
+ <ViewportTracker>
283
+ <FlatList
284
+ testID="components-map-flat-list"
285
+ ref={(ref) => {
286
+ flatListRef.current = ref;
287
+ }}
288
+ // Fix for WebView rerender crashes on Android API 28+
289
+ // https://github.com/react-native-webview/react-native-webview/issues/1915#issuecomment-964035468
290
+ overScrollMode={isAndroid ? "never" : "auto"}
291
+ scrollIndicatorInsets={scrollIndicatorInsets}
292
+ extraData={feed}
293
+ stickyHeaderIndices={stickyHeaderIndices}
294
+ removeClippedSubviews={isAndroid}
295
+ onLayout={handleOnLayout}
296
+ initialNumToRender={3}
297
+ maxToRenderPerBatch={10}
298
+ windowSize={12}
299
+ keyExtractor={keyExtractor}
300
+ renderItem={renderRiverItem}
301
+ data={riverComponents}
302
+ contentContainerStyle={contentContainerStyle}
303
+ ListFooterComponent={
304
+ <RiverFooter
305
+ flatListHeight={flatListHeight}
306
+ loadingState={loadingState}
307
+ />
308
+ }
309
+ refreshControl={refreshControl}
310
+ onScrollBeginDrag={onScrollBeginDrag}
311
+ onScroll={onScroll}
312
+ onMomentumScrollEnd={_onMomentumScrollEnd}
313
+ onScrollEndDrag={_onScrollEndDrag}
314
+ scrollEventThrottle={16}
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
+ }
297
327
  />
298
- }
299
- refreshControl={refreshControl}
300
- onScrollBeginDrag={onScrollBeginDrag}
301
- onScroll={onScroll}
302
- onMomentumScrollEnd={_onMomentumScrollEnd}
303
- onScrollEndDrag={_onScrollEndDrag}
304
- scrollEventThrottle={16}
305
- {...scrollViewExtraProps}
306
- />
307
- </ViewportTracker>
308
- </ScreenLoadingMeasurements>
328
+ </ViewportTracker>
329
+ </ScreenLoadingMeasurements>
330
+ </ComponentsMapRefContext.Provider>
331
+ </ComponentsMapHeightContext.Provider>
309
332
  </View>
310
333
  );
311
334
  }
@@ -0,0 +1,8 @@
1
+ import * as React from "react";
2
+
3
+ export const ComponentsMapHeightContext = React.createContext<number | null>(
4
+ null
5
+ );
6
+
7
+ export const useComponentsMapHeight = () =>
8
+ React.useContext(ComponentsMapHeightContext);
@@ -0,0 +1,8 @@
1
+ import * as React from "react";
2
+ import { View } from "react-native";
3
+
4
+ export const ComponentsMapRefContext =
5
+ React.createContext<React.RefObject<View | null> | null>(null);
6
+
7
+ export const useComponentsMapRef = () =>
8
+ React.useContext(ComponentsMapRefContext);
@@ -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);
@@ -0,0 +1,30 @@
1
+ import { toStringOrEmpty } from "..";
2
+
3
+ describe("toStringOrEmpty", () => {
4
+ test("returns empty string for undefined", () => {
5
+ expect(toStringOrEmpty(undefined)).toBe("");
6
+ });
7
+
8
+ test("returns empty string for null", () => {
9
+ expect(toStringOrEmpty(null)).toBe("");
10
+ });
11
+
12
+ test("converts number to string", () => {
13
+ expect(toStringOrEmpty(0)).toBe("0");
14
+ expect(toStringOrEmpty(123)).toBe("123");
15
+ expect(toStringOrEmpty(-42)).toBe("-42");
16
+ });
17
+
18
+ test("returns string as is", () => {
19
+ expect(toStringOrEmpty("hello")).toBe("hello");
20
+ expect(toStringOrEmpty("")).toBe("");
21
+ });
22
+
23
+ test("works with numeric strings", () => {
24
+ expect(toStringOrEmpty("123")).toBe("123");
25
+ });
26
+
27
+ test("does not throw on falsy values like 0", () => {
28
+ expect(toStringOrEmpty(0)).toBe("0");
29
+ });
30
+ });
@@ -0,0 +1,4 @@
1
+ import { isNil } from "@applicaster/zapp-react-native-utils/utils";
2
+
3
+ export const toStringOrEmpty = (value: unknown): string =>
4
+ isNil(value) ? "" : String(value);