@applicaster/zapp-react-native-ui-components 15.0.0-alpha.3514407021 → 15.0.0-alpha.3752284136

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 (85) 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 +8 -3
  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/useCurationAPI.ts +14 -3
  11. package/Components/HandlePlayable/HandlePlayable.tsx +17 -65
  12. package/Components/HandlePlayable/const.ts +3 -0
  13. package/Components/HandlePlayable/utils.ts +74 -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/Text/index.tsx +8 -8
  27. package/Components/MasterCell/index.tsx +2 -0
  28. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +82 -3
  29. package/Components/MasterCell/utils/index.ts +61 -31
  30. package/Components/MeasurmentsPortal/MeasurementsPortal.tsx +102 -87
  31. package/Components/MeasurmentsPortal/__tests__/MeasurementsPortal.test.tsx +355 -0
  32. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +2 -2
  33. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +17 -18
  34. package/Components/OfflineHandler/__tests__/index.test.tsx +27 -18
  35. package/Components/PlayerContainer/PlayerContainer.tsx +51 -55
  36. package/Components/PlayerContainer/useRestrictMobilePlayback.tsx +101 -0
  37. package/Components/PlayerImageBackground/index.tsx +3 -22
  38. package/Components/River/TV/River.tsx +31 -14
  39. package/Components/River/TV/index.tsx +8 -4
  40. package/Components/River/TV/utils/__tests__/toStringOrEmpty.test.ts +30 -0
  41. package/Components/River/TV/utils/index.ts +4 -0
  42. package/Components/River/TV/withFocusableGroupForContent.tsx +71 -0
  43. package/Components/Screen/TV/index.web.tsx +4 -2
  44. package/Components/Screen/__tests__/Screen.test.tsx +65 -42
  45. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +68 -44
  46. package/Components/Screen/hooks.ts +2 -3
  47. package/Components/Screen/index.tsx +2 -3
  48. package/Components/Screen/navigationHandler.ts +49 -24
  49. package/Components/Screen/orientationHandler.ts +3 -3
  50. package/Components/ScreenResolver/index.tsx +13 -7
  51. package/Components/ScreenRevealManager/ScreenRevealManager.ts +40 -8
  52. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +86 -69
  53. package/Components/Tabs/TV/Tabs.tsx +20 -3
  54. package/Components/Transitioner/Scene.tsx +15 -2
  55. package/Components/Transitioner/index.js +3 -3
  56. package/Components/VideoLive/__tests__/__snapshots__/PlayerLiveImageComponent.test.tsx.snap +1 -0
  57. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +118 -171
  58. package/Components/VideoModal/ModalAnimation/index.ts +2 -13
  59. package/Components/VideoModal/ModalAnimation/utils.ts +1 -327
  60. package/Components/VideoModal/PlayerWrapper.tsx +14 -88
  61. package/Components/VideoModal/VideoModal.tsx +1 -5
  62. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +1 -0
  63. package/Components/VideoModal/hooks/useModalSize.ts +10 -5
  64. package/Components/VideoModal/playerWrapperStyle.ts +70 -0
  65. package/Components/VideoModal/playerWrapperUtils.ts +91 -0
  66. package/Components/VideoModal/utils.ts +19 -9
  67. package/Decorators/Analytics/index.tsx +6 -5
  68. package/Decorators/ConfigurationWrapper/__tests__/__snapshots__/withConfigurationProvider.test.tsx.snap +1 -0
  69. package/Decorators/ConfigurationWrapper/const.ts +1 -0
  70. package/Decorators/ZappPipesDataConnector/__tests__/zappPipesDataConnector.test.js +1 -1
  71. package/Decorators/ZappPipesDataConnector/index.tsx +2 -2
  72. package/Decorators/ZappPipesDataConnector/resolvers/StaticFeedResolver.tsx +1 -1
  73. package/Helpers/DataSourceHelper/__tests__/itemLimitForData.test.ts +80 -0
  74. package/Helpers/DataSourceHelper/index.ts +19 -0
  75. package/index.d.ts +7 -0
  76. package/package.json +6 -5
  77. package/Components/River/TV/withTVEventHandler.tsx +0 -27
  78. package/Components/VideoModal/ModalAnimation/AnimatedPlayerModalWrapper.tsx +0 -60
  79. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.tsx +0 -417
  80. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.web.tsx +0 -294
  81. package/Components/VideoModal/ModalAnimation/AnimatedVideoPlayerComponent.tsx +0 -176
  82. package/Components/VideoModal/ModalAnimation/AnimatedVideoPlayerComponent.web.tsx +0 -93
  83. package/Components/VideoModal/ModalAnimation/AnimationComponent.tsx +0 -500
  84. package/Components/VideoModal/ModalAnimation/__tests__/getMoveUpValue.test.ts +0 -108
  85. package/Helpers/DataSourceHelper/index.js +0 -19
@@ -8,7 +8,7 @@ import {
8
8
  TouchableWithoutFeedback,
9
9
  } from "react-native";
10
10
 
11
- import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
11
+ import { usePlugins } from "@applicaster/zapp-react-native-redux";
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 } = usePickFromState(["plugins"]);
49
+ const plugins = usePlugins();
50
50
  const { statusHeight, notificationHeight } = useNotificationHeight();
51
51
 
52
52
  const offlinePlugin = R.find(
@@ -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", () => ({
@@ -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,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
  };
@@ -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);