@applicaster/zapp-react-native-ui-components 15.1.0-rc.3 → 16.0.0-alpha.7128076344

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 (144) hide show
  1. package/Components/BaseFocusable/index.ios.ts +12 -2
  2. package/Components/Cell/FocusableWrapper.tsx +3 -0
  3. package/Components/Cell/TvOSCellComponent.tsx +6 -3
  4. package/Components/Focusable/Focusable.tsx +4 -2
  5. package/Components/Focusable/FocusableTvOS.tsx +18 -1
  6. package/Components/Focusable/__tests__/__snapshots__/FocusableTvOS.test.tsx.snap +1 -0
  7. package/Components/FocusableGroup/FocusableTvOS.tsx +30 -1
  8. package/Components/GeneralContentScreen/utils/__tests__/useCurationAPI.test.js +1 -1
  9. package/Components/HandlePlayable/HandlePlayable.tsx +13 -8
  10. package/Components/Layout/TV/LayoutBackground.tsx +5 -2
  11. package/Components/Layout/TV/NavBarContainer.tsx +1 -10
  12. package/Components/Layout/TV/ScreenContainer.tsx +2 -6
  13. package/Components/Layout/TV/__tests__/__snapshots__/NavBarContainer.test.tsx.snap +7 -12
  14. package/Components/Layout/TV/__tests__/__snapshots__/ScreenContainer.test.tsx.snap +7 -12
  15. package/Components/Layout/TV/index.tsx +3 -4
  16. package/Components/Layout/TV/index.web.tsx +3 -4
  17. package/Components/LinkHandler/LinkHandler.tsx +2 -2
  18. package/Components/MasterCell/CONFIG_BUILDER_TO_REACT_COMPONENT.md +144 -0
  19. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/model.test.ts +80 -0
  20. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +187 -0
  21. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/selectors.test.ts +45 -0
  22. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/style.test.ts +49 -0
  23. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +165 -0
  24. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +405 -0
  25. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/index.ts +1 -0
  26. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/model.ts +47 -0
  27. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +170 -0
  28. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/selectors.ts +26 -0
  29. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/style.ts +29 -0
  30. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/types.ts +37 -0
  31. package/Components/MasterCell/DefaultComponents/BorderContainerView/index.tsx +4 -10
  32. package/Components/MasterCell/DefaultComponents/Button.tsx +0 -15
  33. package/Components/MasterCell/DefaultComponents/ButtonContainerView/components/HorizontalSeparator.tsx +8 -0
  34. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tsx +15 -0
  35. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tv.android.tsx +58 -0
  36. package/Components/MasterCell/DefaultComponents/{tv/ButtonContainerView/index.tsx → ButtonContainerView/index.tv.tsx} +3 -11
  37. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.web.ts +1 -0
  38. package/Components/MasterCell/DefaultComponents/ButtonContainerView/types.ts +40 -0
  39. package/Components/MasterCell/DefaultComponents/DataProvider/index.tsx +163 -0
  40. package/Components/MasterCell/DefaultComponents/FocusableView/index.android.tsx +2 -23
  41. package/Components/MasterCell/DefaultComponents/FocusableView/index.tsx +4 -22
  42. package/Components/MasterCell/DefaultComponents/Image/Image.android.tsx +8 -2
  43. package/Components/MasterCell/DefaultComponents/Image/Image.ios.tsx +11 -3
  44. package/Components/MasterCell/DefaultComponents/Image/Image.web.tsx +9 -1
  45. package/Components/MasterCell/DefaultComponents/Image/hooks/useImage.ts +15 -14
  46. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +1 -2
  47. package/Components/MasterCell/DefaultComponents/PressableView.tsx +34 -0
  48. package/Components/MasterCell/DefaultComponents/SecondaryImage/hooks/__tests__/useGetImageDimensions.test.ts +7 -6
  49. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +11 -0
  50. package/Components/MasterCell/DefaultComponents/__tests__/DataProvider.test.tsx +141 -0
  51. package/Components/MasterCell/DefaultComponents/index.ts +9 -3
  52. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +135 -0
  53. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Asset.ts +33 -0
  54. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +22 -0
  55. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +125 -0
  56. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +16 -0
  57. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabel.ts +67 -0
  58. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +37 -0
  59. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +393 -0
  60. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/builders.test.ts +141 -0
  61. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +343 -0
  62. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/helpers.ts +105 -0
  63. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +122 -0
  64. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +118 -0
  65. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +238 -0
  66. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/Asset.ts +4 -18
  67. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/Button.ts +24 -73
  68. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/TextLabelsContainer.ts +37 -18
  69. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/TvActionButton.tsx +27 -0
  70. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/index.test.ts +89 -0
  71. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/renderedTree.test.tsx +231 -0
  72. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +47 -48
  73. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/getPluginIdentifier.test.ts +115 -29
  74. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +101 -144
  75. package/Components/MasterCell/MappingFunctions/index.js +3 -2
  76. package/Components/MasterCell/README.md +4 -0
  77. package/Components/MasterCell/__tests__/__snapshots__/dataAdapter.test.js.snap +24 -0
  78. package/Components/MasterCell/__tests__/configInflater.test.js +1 -0
  79. package/Components/MasterCell/__tests__/elementMapper.test.js +46 -0
  80. package/Components/MasterCell/dataAdapter.ts +4 -1
  81. package/Components/MasterCell/elementMapper.tsx +52 -7
  82. package/Components/MasterCell/utils/__tests__/cloneChildrenWithIds.test.tsx +43 -0
  83. package/Components/MasterCell/utils/__tests__/useFilterChildren.test.tsx +80 -0
  84. package/Components/MasterCell/utils/index.ts +85 -15
  85. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +2 -2
  86. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +17 -18
  87. package/Components/OfflineHandler/__tests__/index.test.tsx +27 -18
  88. package/Components/PlayerContainer/PlayerContainer.tsx +14 -13
  89. package/Components/River/ComponentsMap/ComponentsMap.tsx +6 -19
  90. package/Components/River/ComponentsMap/hooks/__tests__/useLoadingState.test.ts +1 -1
  91. package/Components/River/RefreshControl.tsx +19 -88
  92. package/Components/River/River.tsx +9 -82
  93. package/Components/River/TV/River.tsx +31 -14
  94. package/Components/River/TV/index.tsx +8 -4
  95. package/Components/River/TV/utils/__tests__/toStringOrEmpty.test.ts +30 -0
  96. package/Components/River/TV/utils/index.ts +4 -0
  97. package/Components/River/TV/withFocusableGroupForContent.tsx +71 -0
  98. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +1 -0
  99. package/Components/River/__tests__/componentsMap.test.js +38 -0
  100. package/Components/River/hooks/__tests__/usePullToRefresh.test.ts +132 -0
  101. package/Components/River/hooks/index.ts +1 -0
  102. package/Components/River/hooks/usePullToRefresh.ts +51 -0
  103. package/Components/Screen/TV/index.web.tsx +4 -2
  104. package/Components/Screen/__tests__/Screen.test.tsx +65 -42
  105. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +68 -44
  106. package/Components/Screen/hooks.ts +2 -3
  107. package/Components/Screen/index.tsx +2 -3
  108. package/Components/Screen/orientationHandler.ts +3 -3
  109. package/Components/ScreenResolver/index.tsx +9 -5
  110. package/Components/ScreenRevealManager/ScreenRevealManager.ts +40 -8
  111. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +86 -69
  112. package/Components/Tabs/TabContent.tsx +7 -4
  113. package/Components/TopCutoffOverlay/__tests__/TopCutoffOverlay.test.tsx +201 -0
  114. package/Components/TopCutoffOverlay/hooks/__tests__/useMarginTop.test.ts +130 -0
  115. package/Components/TopCutoffOverlay/hooks/index.ts +1 -0
  116. package/Components/TopCutoffOverlay/hooks/useMarginTop.ts +59 -0
  117. package/Components/TopCutoffOverlay/index.tsx +55 -0
  118. package/Components/Transitioner/index.js +3 -3
  119. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +5 -5
  120. package/Components/VideoModal/hooks/__tests__/useDelayedPlayerDetails.test.ts +15 -7
  121. package/Components/VideoModal/utils.ts +12 -9
  122. package/Components/Viewport/ViewportAware/__tests__/viewportAware.test.js +0 -2
  123. package/Components/Viewport/ViewportAware/index.tsx +16 -7
  124. package/Components/Viewport/ViewportEvents/__tests__/viewportEvents.test.js +1 -1
  125. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +4 -6
  126. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  127. package/Components/default-cell-renderer/viewTrees/mobile/index.ts +0 -3
  128. package/Contexts/ScreenContext/index.tsx +25 -18
  129. package/Contexts/ScreenTrackedViewPositionsContext/__tests__/index.test.tsx +1 -1
  130. package/Decorators/Analytics/index.tsx +6 -5
  131. package/Decorators/ConfigurationWrapper/__tests__/__snapshots__/withConfigurationProvider.test.tsx.snap +1 -0
  132. package/Decorators/ConfigurationWrapper/const.ts +1 -0
  133. package/Decorators/ZappPipesDataConnector/__tests__/UrlFeedResolver.test.tsx +39 -21
  134. package/Decorators/ZappPipesDataConnector/__tests__/zappPipesDataConnector.test.js +1 -1
  135. package/Decorators/ZappPipesDataConnector/index.tsx +2 -2
  136. package/Decorators/ZappPipesDataConnector/resolvers/StaticFeedResolver.tsx +1 -1
  137. package/Decorators/ZappPipesDataConnector/resolvers/UrlFeedResolver.tsx +18 -7
  138. package/Helpers/DataSourceHelper/__tests__/itemLimitForData.test.ts +80 -0
  139. package/Helpers/DataSourceHelper/index.ts +19 -0
  140. package/package.json +5 -5
  141. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/index.android.tsx +0 -135
  142. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/types.ts +0 -25
  143. package/Components/River/TV/withTVEventHandler.tsx +0 -36
  144. package/Helpers/DataSourceHelper/index.js +0 -19
@@ -4,9 +4,7 @@ import {
4
4
  RefreshControl as RNRefreshControl,
5
5
  StyleSheet,
6
6
  } from "react-native";
7
- import * as R from "ramda";
8
- import { path } from "@applicaster/zapp-react-native-utils/utils";
9
- import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
7
+ import { get } from "@applicaster/zapp-react-native-utils/utils";
10
8
  import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
11
9
  import { useLocalizedStrings } from "@applicaster/zapp-react-native-utils/localizationUtils";
12
10
  import { useAnalytics } from "@applicaster/zapp-react-native-utils/analyticsUtils";
@@ -15,7 +13,7 @@ import { useCurrentScreenData } from "@applicaster/zapp-react-native-utils/react
15
13
  import { useShallow } from "zustand/react/shallow";
16
14
  import { useScreenContextV2 } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
17
15
  import { useSafeAreaInsets } from "react-native-safe-area-context";
18
- import { useLoadPipesDataDispatch } from "@applicaster/zapp-react-native-utils/reactHooks";
16
+ import { usePullToRefresh } from "./hooks";
19
17
 
20
18
  const BRIGHTNESS_THRESHOLD = 160;
21
19
  const ABOVE_DEFAULT_COLOR = "gray";
@@ -55,66 +53,6 @@ const getBrightness = (RGBcolor) => {
55
53
  );
56
54
  };
57
55
 
58
- export const usePullToRefresh = (
59
- riverComponents,
60
- pullToRefreshPipesV1RefreshingStateUpdater,
61
- refreshingPipesV1
62
- ) => {
63
- const isPipesV1 = !!pullToRefreshPipesV1RefreshingStateUpdater;
64
-
65
- const [refreshing, setRefreshing] = React.useState(false);
66
-
67
- const feeds: string[] = React.useMemo(
68
- () =>
69
- (riverComponents || [])
70
- .map((riverComponent) => path(["data", "source"], riverComponent))
71
- .filter((feed) => !isNilOrEmpty(feed)),
72
- [riverComponents]
73
- );
74
-
75
- const feedsLength = feeds.length;
76
-
77
- const [requestsCompletedCounter, setRequestsCompletedCounter] =
78
- React.useState(0);
79
-
80
- const loadPipesDataDispatcher = useLoadPipesDataDispatch();
81
-
82
- React.useEffect(() => {
83
- // will not work for pipes v1 on 1st level screens
84
- if (refreshing && !isPipesV1) {
85
- feeds.forEach((feed) => {
86
- loadPipesDataDispatcher(feed, {
87
- silentRefresh: true,
88
- clearCache: true,
89
- callback: () => {
90
- setRequestsCompletedCounter(R.inc);
91
- },
92
- });
93
- });
94
- }
95
- }, [refreshing, isPipesV1, feeds, loadPipesDataDispatcher]);
96
-
97
- React.useEffect(() => {
98
- if (requestsCompletedCounter === feedsLength) {
99
- setRefreshing(false);
100
- }
101
- }, [requestsCompletedCounter, feedsLength]);
102
-
103
- const onRefresh = React.useCallback(() => {
104
- if (isPipesV1) {
105
- pullToRefreshPipesV1RefreshingStateUpdater(true);
106
- } else {
107
- setRefreshing(true);
108
- setRequestsCompletedCounter(0);
109
- }
110
- }, [isPipesV1]);
111
-
112
- return {
113
- refreshing: isPipesV1 ? refreshingPipesV1 : refreshing,
114
- onRefresh,
115
- };
116
- };
117
-
118
56
  /** Returns the offset for the progress view of the RefreshControl component
119
57
  * based on navbar content position */
120
58
  export const useGetProgressViewOffset = () => {
@@ -143,16 +81,12 @@ export const useGetProgressViewOffset = () => {
143
81
  }
144
82
  };
145
83
 
146
- export function RefreshControl(props: {
147
- pullToRefreshPipesV1RefreshingStateUpdater?: (refreshing: boolean) => void;
148
- refreshingPipesV1?: boolean;
149
- }) {
84
+ export function RefreshControl(props) {
150
85
  const screenData = useCurrentScreenData();
151
86
 
152
87
  const { refreshing, onRefresh } = usePullToRefresh(
153
- screenData.ui_components,
154
- props.pullToRefreshPipesV1RefreshingStateUpdater,
155
- props.refreshingPipesV1
88
+ screenData.id,
89
+ screenData.ui_components
156
90
  );
157
91
 
158
92
  const { app_background_color: themeBackgroundColor } = useTheme();
@@ -170,29 +104,26 @@ export function RefreshControl(props: {
170
104
  displayTitleIOS,
171
105
  } = React.useMemo(
172
106
  () => ({
173
- indicatorColor: R.prop(
174
- "pull_to_refresh_indicator_color",
175
- screenData.styles
176
- ),
177
- titleUnderIndicatorColor: R.prop(
178
- "pull_to_refresh_title_color_under_indicator",
179
- screenData.styles
107
+ indicatorColor: get(screenData.styles, "pull_to_refresh_indicator_color"),
108
+ titleUnderIndicatorColor: get(
109
+ screenData.styles,
110
+ "pull_to_refresh_title_color_under_indicator"
180
111
  ),
181
- indicatorBackgroundColor: R.prop(
182
- "pull_to_refresh_indicator_bg_color",
183
- screenData.styles
112
+ indicatorBackgroundColor: get(
113
+ screenData.styles,
114
+ "pull_to_refresh_indicator_bg_color"
184
115
  ),
185
116
  indicatorSize:
186
- R.prop("pull_to_refresh_indicator_size", screenData.styles) === "large"
117
+ get(screenData.styles, "pull_to_refresh_indicator_size") === "large"
187
118
  ? "large"
188
119
  : "default",
189
- generalContentBackgroungColor: R.prop(
190
- "screen_background_color",
191
- screenData.styles
120
+ generalContentBackgroungColor: get(
121
+ screenData.styles,
122
+ "screen_background_color"
192
123
  ),
193
- displayTitleIOS: R.prop(
194
- "pull_to_refresh_display_title_ios",
195
- screenData.styles
124
+ displayTitleIOS: get(
125
+ screenData.styles,
126
+ "pull_to_refresh_display_title_ios"
196
127
  ),
197
128
  }),
198
129
  [screenData]
@@ -2,7 +2,6 @@ import * as React from "react";
2
2
  import * as R from "ramda";
3
3
  import { GeneralContentScreen } from "@applicaster/zapp-react-native-ui-components/Components/GeneralContentScreen";
4
4
 
5
- import { FeedLoader } from "../FeedLoader";
6
5
  import { ScreenResolver } from "../ScreenResolver";
7
6
 
8
7
  type Props = ZappScreenProps & {
@@ -23,24 +22,13 @@ type Props = ZappScreenProps & {
23
22
  river: ZappRiver;
24
23
  };
25
24
 
26
- type State = {
27
- refreshing: boolean;
28
- };
29
-
30
- export class RiverComponent extends React.Component<Props, State> {
25
+ export class RiverComponent extends React.Component<Props> {
31
26
  private currentScreenTitle: string;
32
27
  private currentScreenSummary: string;
33
28
  constructor(props: Props) {
34
29
  super(props);
35
30
 
36
- this.state = {
37
- refreshing: false,
38
- };
39
-
40
31
  this.applyContexts();
41
-
42
- this.pullToRefreshPipesV1RefreshingStateUpdater =
43
- this.pullToRefreshPipesV1RefreshingStateUpdater.bind(this);
44
32
  }
45
33
 
46
34
  applyContexts() {
@@ -69,20 +57,9 @@ export class RiverComponent extends React.Component<Props, State> {
69
57
  }
70
58
  }
71
59
 
72
- usesPipesV1Layout() {
73
- return this.props?.appData?.layoutVersion === "v1";
74
- }
75
-
76
- // Method added to keep pipes v1 logic up to date with the pullToRefresh state.
77
- // TODO: Remove when pipes v1 is deprecated.
78
- pullToRefreshPipesV1RefreshingStateUpdater(refreshing) {
79
- this.setState({ refreshing });
80
- }
81
-
82
60
  render() {
83
61
  const {
84
62
  river,
85
- feedUrl,
86
63
  screenData,
87
64
  isInsideContainer,
88
65
  groupId,
@@ -91,22 +68,10 @@ export class RiverComponent extends React.Component<Props, State> {
91
68
 
92
69
  const { id, type } = river;
93
70
 
94
- const connectedFeedURL = R.path(["content", "src"], screenData);
95
- const _feedUrl = feedUrl || connectedFeedURL;
96
-
97
71
  if (type !== "general_content") {
98
- let riverWithConnectedDatasource;
99
-
100
- if (_feedUrl && this.usesPipesV1Layout()) {
101
- riverWithConnectedDatasource = {
102
- ...river,
103
- data: R.merge(river.data || {}, { source: _feedUrl }),
104
- };
105
- }
106
-
107
72
  return (
108
73
  <ScreenResolver
109
- screenData={R.mergeLeft(riverWithConnectedDatasource || river, {
74
+ screenData={R.mergeLeft(river, {
110
75
  groupId,
111
76
  ...screenData,
112
77
  })}
@@ -116,53 +81,15 @@ export class RiverComponent extends React.Component<Props, State> {
116
81
  );
117
82
  }
118
83
 
119
- if (!_feedUrl || !this.usesPipesV1Layout()) {
120
- this.currentScreenTitle = (screenData && screenData.title) || null;
121
-
122
- return (
123
- <GeneralContentScreen
124
- screenId={id}
125
- isScreenWrappedInContainer={isInsideContainer}
126
- groupId={groupId}
127
- scrollViewExtraProps={scrollViewExtraProps}
128
- />
129
- );
130
- }
84
+ this.currentScreenTitle = (screenData && screenData.title) || null;
131
85
 
132
86
  return (
133
- <FeedLoader
134
- feedUrl={_feedUrl}
135
- refreshing={this.state.refreshing}
136
- refreshCallback={() =>
137
- this.pullToRefreshPipesV1RefreshingStateUpdater(false)
138
- }
139
- >
140
- {(feed) => {
141
- if (!feed) {
142
- return null;
143
- }
144
-
145
- this.currentScreenSummary = (feed && feed.summary) || null;
146
-
147
- this.currentScreenTitle =
148
- (feed && feed.title) || (screenData && screenData.title) || null;
149
-
150
- return (
151
- <GeneralContentScreen
152
- screenId={id}
153
- groupId={groupId}
154
- feed={feed}
155
- isScreenWrappedInContainer={isInsideContainer}
156
- scrollViewExtraProps={scrollViewExtraProps}
157
- componentsMapExtraProps={{
158
- pullToRefreshPipesV1RefreshingStateUpdater:
159
- this.pullToRefreshPipesV1RefreshingStateUpdater,
160
- refreshingPipesV1: this.state.refreshing,
161
- }}
162
- />
163
- );
164
- }}
165
- </FeedLoader>
87
+ <GeneralContentScreen
88
+ screenId={id}
89
+ isScreenWrappedInContainer={isInsideContainer}
90
+ groupId={groupId}
91
+ scrollViewExtraProps={scrollViewExtraProps}
92
+ />
166
93
  );
167
94
  }
168
95
  }
@@ -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);
@@ -0,0 +1,71 @@
1
+ import * as React from "react";
2
+ import { View, StyleSheet } from "react-native";
3
+
4
+ import { FocusableGroup } from "@applicaster/zapp-react-native-ui-components/Components/FocusableGroup";
5
+ import { riverFocusManager } from "@applicaster/zapp-react-native-utils/appUtils/RiverFocusManager";
6
+
7
+ import { topMenuLayoutChange$ } from "@applicaster/zapp-react-native-tvos-app/Layout/topMenu";
8
+
9
+ const styles = StyleSheet.create({
10
+ flexOne: {
11
+ flex: 1,
12
+ },
13
+ });
14
+
15
+ export const withFocusableGroupForContent = (Component) => {
16
+ return function WithFocusableGroupForContent(props) {
17
+ const { screenId, isInsideContainer } = props;
18
+
19
+ const [topMenuHeight, setTopMenuHeight] = React.useState(0);
20
+
21
+ React.useEffect(() => {
22
+ const subscription = topMenuLayoutChange$.subscribe((layout) => {
23
+ setTopMenuHeight(layout.height);
24
+ });
25
+
26
+ return () => {
27
+ subscription.unsubscribe();
28
+ };
29
+ }, []);
30
+
31
+ const focusableId = React.useMemo(
32
+ () =>
33
+ riverFocusManager.screenFocusableGroupId({
34
+ screenId,
35
+ isInsideContainer,
36
+ }),
37
+ [screenId, isInsideContainer]
38
+ );
39
+
40
+ if (isInsideContainer) {
41
+ return <Component {...props} />;
42
+ }
43
+
44
+ return (
45
+ <FocusableGroup
46
+ key={focusableId}
47
+ id={focusableId}
48
+ // The top menu is rendered in its own FocusableGroup, anchored at the top of the screen.
49
+ // When the "content" FocusableGroup starts at y = 0 as well, the two groups visually overlap.
50
+ // On TvOS platform this overlap can confuse the focus engine, because the focusable bounds of
51
+ // the top-menu group and the content group intersect, leading to erratic navigation between
52
+ // the menu and the content (e.g. unexpected jumps or focus getting "stuck").
53
+ //
54
+ // To avoid this, we shift the entire content FocusableGroup down by the dynamic top menu
55
+ // height (marginTop: topMenuHeight). This separates the focus regions of the two groups in
56
+ // focus space, so they no longer intersect.
57
+ //
58
+ // The inner <View> below then applies the inverse margin (marginTop: -topMenuHeight) so that
59
+ // the actual visual position of the content on screen does not change; only the focusable
60
+ // bounds of the outer group are offset.
61
+ style={[styles.flexOne, { marginTop: topMenuHeight }]}
62
+ // this group does not have parent
63
+ groupId={undefined}
64
+ >
65
+ <View style={[styles.flexOne, { marginTop: -1 * topMenuHeight }]}>
66
+ <Component {...props} groupId={focusableId} />
67
+ </View>
68
+ </FocusableGroup>
69
+ );
70
+ };
71
+ };
@@ -155,6 +155,7 @@ exports[`componentsMap renders renders components map correctly 1`] = `
155
155
  }
156
156
  }
157
157
  stickyHeaderIndices={[]}
158
+ testID="components-map-flat-list"
158
159
  viewabilityConfigCallbackPairs={[]}
159
160
  windowSize={12}
160
161
  >
@@ -139,7 +139,13 @@ jest.mock(
139
139
  })
140
140
  );
141
141
 
142
+ jest.mock("@applicaster/zapp-react-native-ui-components/events", () => ({
143
+ ...jest.requireActual("@applicaster/zapp-react-native-ui-components/events"),
144
+ emitScrollEndReached: jest.fn(),
145
+ }));
146
+
142
147
  const { View } = require("react-native");
148
+ const events = require("@applicaster/zapp-react-native-ui-components/events");
143
149
  const { ComponentsMap } = require("../ComponentsMap/ComponentsMap");
144
150
  const theme = require("./theme-mock.json");
145
151
 
@@ -190,4 +196,36 @@ describe("componentsMap", () => {
190
196
 
191
197
  expect(toJSON()).toMatchSnapshot();
192
198
  });
199
+
200
+ it("calls emitScrollEndReached when onScroll was called and isScreenWrappedInContainer is false", () => {
201
+ themeSpy = jest
202
+ .spyOn(themeUtils, "useTheme")
203
+ .mockImplementation(() => () => theme);
204
+
205
+ events.emitScrollEndReached.mockClear();
206
+
207
+ const { getByTestId } = render(
208
+ <Provider store={store}>
209
+ <ComponentsMap
210
+ {...props}
211
+ isScreenWrappedInContainer={false}
212
+ feed={{ entry: [] }}
213
+ />
214
+ </Provider>
215
+ );
216
+
217
+ const flatList = getByTestId("components-map-flat-list");
218
+
219
+ flatList.props.onScroll({
220
+ nativeEvent: {
221
+ contentOffset: { y: 0 },
222
+ layoutMeasurement: { height: 100 },
223
+ contentSize: { height: 200 },
224
+ },
225
+ });
226
+
227
+ flatList.props.onEndReached();
228
+
229
+ expect(events.emitScrollEndReached).toHaveBeenCalledTimes(1);
230
+ });
193
231
  });