@applicaster/zapp-react-native-ui-components 15.0.0-alpha.1690332913 → 15.0.0-alpha.1693300296

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 (118) hide show
  1. package/Components/Cell/Cell.tsx +6 -0
  2. package/Components/GeneralContentScreen/GeneralContentScreen.tsx +39 -28
  3. package/Components/GeneralContentScreen/__tests__/GeneralContentScreen.test.tsx +104 -0
  4. package/Components/GeneralContentScreen/utils/__tests__/getScreenDataSource.test.ts +19 -0
  5. package/Components/GeneralContentScreen/utils/__tests__/useCurationAPI.test.js +1 -1
  6. package/Components/GeneralContentScreen/utils/getScreenDataSource.ts +9 -0
  7. package/Components/GeneralContentScreen/utils/useCurationAPI.ts +7 -7
  8. package/Components/HandlePlayable/HandlePlayable.tsx +16 -29
  9. package/Components/HandlePlayable/utils.ts +31 -0
  10. package/Components/HookRenderer/HookRenderer.tsx +40 -10
  11. package/Components/HookRenderer/__tests__/HookRenderer.test.tsx +60 -0
  12. package/Components/Layout/TV/NavBarContainer.tsx +1 -10
  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/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/model.test.ts +80 -0
  16. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +187 -0
  17. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/selectors.test.ts +45 -0
  18. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/style.test.ts +49 -0
  19. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/model.ts +47 -0
  20. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +170 -0
  21. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/selectors.ts +26 -0
  22. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/style.ts +29 -0
  23. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/types.ts +37 -0
  24. package/Components/MasterCell/DefaultComponents/Button.tsx +0 -15
  25. package/Components/MasterCell/DefaultComponents/LiveImage/__tests__/prepareEntry.test.ts +352 -0
  26. package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +136 -0
  27. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +33 -16
  28. package/Components/MasterCell/DefaultComponents/PressableView.tsx +196 -0
  29. package/Components/MasterCell/DefaultComponents/SecondaryImage/Image.tsx +40 -39
  30. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/Image.test.tsx +95 -0
  31. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/__snapshots__/Image.test.tsx.snap +86 -0
  32. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/index.test.ts +141 -0
  33. package/Components/MasterCell/DefaultComponents/SecondaryImage/hooks/__tests__/useGetImageDimensions.test.ts +7 -6
  34. package/Components/MasterCell/DefaultComponents/SecondaryImage/index.ts +1 -1
  35. package/Components/MasterCell/DefaultComponents/Text/index.tsx +2 -6
  36. package/Components/MasterCell/DefaultComponents/index.ts +2 -0
  37. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Asset.ts +46 -0
  38. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +126 -0
  39. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ButtonContainerView.ts +23 -0
  40. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +16 -0
  41. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabel.ts +67 -0
  42. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +32 -0
  43. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +191 -0
  44. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/builders.test.ts +140 -0
  45. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +222 -0
  46. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/helpers.ts +105 -0
  47. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +104 -0
  48. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +118 -0
  49. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +73 -0
  50. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/index.test.ts +86 -0
  51. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +35 -48
  52. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/getPluginIdentifier.test.ts +115 -29
  53. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +39 -144
  54. package/Components/MasterCell/elementMapper.tsx +1 -0
  55. package/Components/MasterCell/hoc/__tests__/withAsyncRender.test.tsx +219 -0
  56. package/Components/MasterCell/hoc/withAsyncRender.tsx +9 -7
  57. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +17 -9
  58. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +16 -8
  59. package/Components/OfflineHandler/NotificationView/utils.ts +34 -0
  60. package/Components/PlayerContainer/PlayerContainer.tsx +1 -18
  61. package/Components/PreloaderWrapper/__tests__/index.test.tsx +26 -0
  62. package/Components/PreloaderWrapper/index.tsx +15 -0
  63. package/Components/River/ComponentsMap/ComponentsMap.tsx +16 -0
  64. package/Components/River/ComponentsMap/hooks/__tests__/useLoadingState.test.ts +1 -1
  65. package/Components/River/RefreshControl.tsx +36 -13
  66. package/Components/River/RiverItem.tsx +26 -20
  67. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +2 -0
  68. package/Components/River/__tests__/componentsMap.test.js +38 -0
  69. package/Components/Screen/__tests__/Screen.test.tsx +1 -0
  70. package/Components/Screen/hooks.ts +73 -3
  71. package/Components/Screen/index.tsx +7 -1
  72. package/Components/Screen/orientationHandler.ts +7 -10
  73. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +46 -0
  74. package/Components/ScreenFeedLoader/__tests__/ScreenFeedLoader.test.tsx +94 -0
  75. package/Components/ScreenFeedLoader/index.ts +1 -0
  76. package/Components/ScreenResolver/__tests__/screenResolver.test.js +24 -0
  77. package/Components/ScreenResolver/hooks/index.ts +3 -0
  78. package/Components/ScreenResolver/hooks/useGetComponent.ts +15 -0
  79. package/Components/ScreenResolver/hooks/useScreenComponentResolver.tsx +90 -0
  80. package/Components/ScreenResolver/index.tsx +15 -117
  81. package/Components/ScreenResolver/utils/__tests__/getScreenTypeProps.test.ts +45 -0
  82. package/Components/ScreenResolver/utils/getScreenTypeProps.ts +43 -0
  83. package/Components/ScreenResolver/utils/index.ts +1 -0
  84. package/Components/ScreenResolver/withDefaultScreenContext.tsx +16 -0
  85. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +25 -0
  86. package/Components/ScreenResolverFeedProvider/__tests__/ScreenResolverFeedProvider.test.tsx +44 -0
  87. package/Components/ScreenResolverFeedProvider/index.ts +1 -0
  88. package/Components/Tabs/TabContent.tsx +7 -4
  89. package/Components/Transitioner/Scene.tsx +9 -15
  90. package/Components/VideoLive/LiveImageManager.ts +199 -54
  91. package/Components/VideoLive/PlayerLiveImageComponent.tsx +31 -33
  92. package/Components/VideoLive/__tests__/PlayerLiveImageComponent.test.tsx +2 -17
  93. package/Components/VideoModal/hooks/__tests__/useDelayedPlayerDetails.test.ts +15 -7
  94. package/Components/Viewport/ViewportAware/__tests__/viewportAware.test.js +0 -2
  95. package/Components/Viewport/ViewportAware/index.tsx +16 -7
  96. package/Components/Viewport/ViewportEvents/__tests__/viewportEvents.test.js +1 -1
  97. package/Components/ZappUIComponent/index.tsx +12 -6
  98. package/Components/default-cell-renderer/viewTrees/mobile/index.ts +0 -3
  99. package/Components/index.js +1 -1
  100. package/Contexts/ScreenContext/__tests__/index.test.tsx +57 -0
  101. package/Contexts/ScreenContext/index.tsx +71 -19
  102. package/Contexts/ScreenTrackedViewPositionsContext/__tests__/index.test.tsx +1 -1
  103. package/Contexts/ZappHookModalContext/index.tsx +37 -61
  104. package/Contexts/ZappPipesContext/ZappPipesContextFactory.tsx +18 -7
  105. package/Contexts/index.ts +0 -2
  106. package/Decorators/ZappPipesDataConnector/ResolverSelector.tsx +25 -7
  107. package/Decorators/ZappPipesDataConnector/__tests__/ResolverSelector.test.tsx +212 -5
  108. package/Decorators/ZappPipesDataConnector/__tests__/UrlFeedResolver.test.tsx +39 -21
  109. package/Decorators/ZappPipesDataConnector/resolvers/UrlFeedResolver.tsx +18 -7
  110. package/events/index.ts +3 -0
  111. package/events/scrollEndReached.ts +15 -0
  112. package/package.json +5 -5
  113. package/Components/MasterCell/DefaultComponents/Text/utils/__tests__/withAdjustedLineHeight.test.ts +0 -46
  114. package/Components/MasterCell/DefaultComponents/Text/utils/index.ts +0 -21
  115. package/Components/PlayerContainer/ErrorDisplay/ErrorDisplay.tsx +0 -57
  116. package/Components/PlayerContainer/ErrorDisplay/index.ts +0 -9
  117. package/Components/PlayerContainer/useRestrictMobilePlayback.tsx +0 -101
  118. /package/Components/HookRenderer/{index.tsx → index.ts} +0 -0
@@ -0,0 +1,219 @@
1
+ import React from "react";
2
+ import { render } from "@testing-library/react-native";
3
+ import { Text, View } from "react-native";
4
+ import { withAsyncRenderHOC } from "../withAsyncRender";
5
+
6
+ describe("withAsyncRenderHOC", () => {
7
+ // Test component that requires onAsyncRender
8
+ const TestComponent = ({
9
+ onAsyncRender,
10
+ text,
11
+ }: {
12
+ onAsyncRender: () => void;
13
+ text?: string;
14
+ }) => {
15
+ React.useEffect(() => {
16
+ onAsyncRender();
17
+ }, [onAsyncRender]);
18
+
19
+ return (
20
+ <View>
21
+ <Text>{text || "Test"}</Text>
22
+ </View>
23
+ );
24
+ };
25
+
26
+ it("should wrap component and provide onAsyncRender callback", () => {
27
+ const WrappedComponent = withAsyncRenderHOC(TestComponent);
28
+
29
+ const wrapper = render(<WrappedComponent text="Hello" />);
30
+
31
+ expect(wrapper.toJSON()).not.toBeNull();
32
+ expect(wrapper.getByText("Hello")).toBeDefined();
33
+ });
34
+
35
+ it("should call emitAsyncElementRegistrate on mount", () => {
36
+ const mockRegistrate = jest.fn();
37
+ const WrappedComponent = withAsyncRenderHOC(TestComponent);
38
+
39
+ render(<WrappedComponent emitAsyncElementRegistrate={mockRegistrate} />);
40
+
41
+ expect(mockRegistrate).toHaveBeenCalledTimes(1);
42
+ });
43
+
44
+ it("should call emitAsyncElementLayout when onAsyncRender is called", () => {
45
+ const mockLayout = jest.fn();
46
+
47
+ const ComponentWithAsyncCall = ({
48
+ onAsyncRender,
49
+ }: {
50
+ onAsyncRender: () => void;
51
+ }) => {
52
+ return (
53
+ <View onLayout={onAsyncRender}>
54
+ <Text>Async Component</Text>
55
+ </View>
56
+ );
57
+ };
58
+
59
+ const WrappedComponent = withAsyncRenderHOC(ComponentWithAsyncCall);
60
+
61
+ const wrapper = render(
62
+ <WrappedComponent emitAsyncElementLayout={mockLayout} />
63
+ );
64
+
65
+ // Trigger onLayout
66
+ const view = wrapper.UNSAFE_getByType(View);
67
+ view.props.onLayout();
68
+
69
+ expect(mockLayout).toHaveBeenCalled();
70
+ });
71
+
72
+ it("should return unsubscribe function from emitAsyncElementRegistrate", () => {
73
+ const mockUnsubscribe = jest.fn();
74
+ const mockRegistrate = jest.fn(() => mockUnsubscribe);
75
+ const WrappedComponent = withAsyncRenderHOC(TestComponent);
76
+
77
+ const { unmount } = render(
78
+ <WrappedComponent emitAsyncElementRegistrate={mockRegistrate} />
79
+ );
80
+
81
+ expect(mockRegistrate).toHaveBeenCalledTimes(1);
82
+
83
+ // Unmount should trigger cleanup
84
+ unmount();
85
+
86
+ expect(mockUnsubscribe).toHaveBeenCalled();
87
+ });
88
+
89
+ it("should work without emitAsyncElementRegistrate prop", () => {
90
+ const WrappedComponent = withAsyncRenderHOC(TestComponent);
91
+
92
+ const wrapper = render(<WrappedComponent text="No Registration" />);
93
+
94
+ expect(wrapper.toJSON()).not.toBeNull();
95
+ expect(wrapper.getByText("No Registration")).toBeDefined();
96
+ });
97
+
98
+ it("should work without emitAsyncElementLayout prop", () => {
99
+ const ComponentWithLayout = ({
100
+ onAsyncRender,
101
+ }: {
102
+ onAsyncRender: () => void;
103
+ }) => {
104
+ return (
105
+ <View onLayout={onAsyncRender}>
106
+ <Text>Test Layout</Text>
107
+ </View>
108
+ );
109
+ };
110
+
111
+ const WrappedComponent = withAsyncRenderHOC(ComponentWithLayout);
112
+
113
+ const wrapper = render(<WrappedComponent />);
114
+
115
+ // Should not throw when calling onLayout without emitAsyncElementLayout
116
+ const view = wrapper.UNSAFE_getByType(View);
117
+ expect(() => view.props.onLayout()).not.toThrow();
118
+ });
119
+
120
+ it("should pass through all other props to wrapped component", () => {
121
+ const ComponentWithProps = ({
122
+ customProp,
123
+ anotherProp,
124
+ }: {
125
+ onAsyncRender: () => void;
126
+ customProp: string;
127
+ anotherProp: number;
128
+ }) => {
129
+ return (
130
+ <View>
131
+ <Text>{customProp}</Text>
132
+ <Text>{anotherProp}</Text>
133
+ </View>
134
+ );
135
+ };
136
+
137
+ const WrappedComponent = withAsyncRenderHOC(ComponentWithProps);
138
+
139
+ const wrapper = render(
140
+ <WrappedComponent customProp="custom value" anotherProp={42} />
141
+ );
142
+
143
+ expect(wrapper.getByText("custom value")).toBeDefined();
144
+ expect(wrapper.getByText("42")).toBeDefined();
145
+ });
146
+
147
+ it("should call emitAsyncElementLayout multiple times if onAsyncRender is called multiple times", () => {
148
+ const mockLayout = jest.fn();
149
+
150
+ const ComponentWithMultipleCalls = ({
151
+ onAsyncRender,
152
+ }: {
153
+ onAsyncRender: () => void;
154
+ }) => {
155
+ return (
156
+ <View>
157
+ <View onLayout={onAsyncRender} testID="view1">
158
+ <Text>First</Text>
159
+ </View>
160
+ <View onLayout={onAsyncRender} testID="view2">
161
+ <Text>Second</Text>
162
+ </View>
163
+ </View>
164
+ );
165
+ };
166
+
167
+ const WrappedComponent = withAsyncRenderHOC(ComponentWithMultipleCalls);
168
+
169
+ const wrapper = render(
170
+ <WrappedComponent emitAsyncElementLayout={mockLayout} />
171
+ );
172
+
173
+ // Trigger both onLayout callbacks
174
+ const view1 = wrapper.getByTestId("view1");
175
+ const view2 = wrapper.getByTestId("view2");
176
+
177
+ view1.props.onLayout();
178
+ view2.props.onLayout();
179
+
180
+ expect(mockLayout).toHaveBeenCalledTimes(2);
181
+ });
182
+
183
+ it("should handle components that use onAsyncRender in effects", () => {
184
+ const mockLayout = jest.fn();
185
+
186
+ const ComponentWithEffect = ({
187
+ onAsyncRender,
188
+ }: {
189
+ onAsyncRender: () => void;
190
+ }) => {
191
+ React.useEffect(() => {
192
+ // Simulate async operation completing
193
+ const timer = setTimeout(() => {
194
+ onAsyncRender();
195
+ }, 0);
196
+
197
+ return () => clearTimeout(timer);
198
+ }, [onAsyncRender]);
199
+
200
+ return (
201
+ <View>
202
+ <Text>Effect Component</Text>
203
+ </View>
204
+ );
205
+ };
206
+
207
+ const WrappedComponent = withAsyncRenderHOC(ComponentWithEffect);
208
+
209
+ render(<WrappedComponent emitAsyncElementLayout={mockLayout} />);
210
+
211
+ // The effect should have been triggered
212
+ return new Promise((resolve) => {
213
+ setTimeout(() => {
214
+ expect(mockLayout).toHaveBeenCalled();
215
+ resolve(true);
216
+ }, 10);
217
+ });
218
+ });
219
+ });
@@ -6,12 +6,12 @@ type withAsyncRenderHOCProps = {
6
6
  emitAsyncElementLayout?: () => void;
7
7
  };
8
8
 
9
- type withAsyncRenderHOCT = (
10
- Component: React.ComponentType<{ onAsyncRender: () => void }>
11
- ) => React.FC<withAsyncRenderHOCProps>;
12
-
13
- export const withAsyncRenderHOC: withAsyncRenderHOCT = (Component) => {
14
- const WithAsyncRender = (props: withAsyncRenderHOCProps) => {
9
+ export const withAsyncRenderHOC = <P extends { onAsyncRender: () => void }>(
10
+ Component: React.ComponentType<P>
11
+ ): React.FC<Omit<P, "onAsyncRender"> & withAsyncRenderHOCProps> => {
12
+ const WithAsyncRender = (
13
+ props: Omit<P, "onAsyncRender"> & withAsyncRenderHOCProps
14
+ ) => {
15
15
  const { emitAsyncElementRegistrate = noop, emitAsyncElementLayout = noop } =
16
16
  props;
17
17
 
@@ -27,7 +27,9 @@ export const withAsyncRenderHOC: withAsyncRenderHOCT = (Component) => {
27
27
  }
28
28
  }, [emitAsyncElementLayout]);
29
29
 
30
- return <Component {...props} onAsyncRender={onAsyncRender} />;
30
+ return (
31
+ <Component {...(props as unknown as P)} onAsyncRender={onAsyncRender} />
32
+ );
31
33
  };
32
34
 
33
35
  return WithAsyncRender;
@@ -1,4 +1,6 @@
1
1
  import * as React from "react";
2
+ import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
3
+ import { useNetworkStatusLocalizations } from "./utils";
2
4
 
3
5
  type Props = {
4
6
  children?: React.ReactNode;
@@ -10,13 +12,6 @@ type Props = {
10
12
 
11
13
  const NOTIF_DURATION = 4500;
12
14
 
13
- export const ONLINE_MSG = "You are back online";
14
-
15
- export const OFFLINE_MSG = "No internet connection";
16
-
17
- const EXTRA_OFFLINE_MSG = "Please check your connection";
18
- const EXTRA_ONLINE_MSG = "Feel free to continue where you left off";
19
-
20
15
  const styles: Record<any, React.CSSProperties> = {
21
16
  body: {
22
17
  position: "absolute",
@@ -47,6 +42,9 @@ const styles: Record<any, React.CSSProperties> = {
47
42
  export const NotificationView = (props: Props) => {
48
43
  const { children, hidden, dismiss, previousOnline, online } = props;
49
44
 
45
+ const theme = useTheme<BaseThemePropertiesTV>();
46
+ const storedLocalizations = useNetworkStatusLocalizations();
47
+
50
48
  const [open, setOpen] = React.useState<boolean | null>(null);
51
49
 
52
50
  const timeout = React.useRef<NodeJS.Timeout>();
@@ -84,8 +82,18 @@ export const NotificationView = (props: Props) => {
84
82
  }, [hidden, online]);
85
83
 
86
84
  const showOnlineMsg = wentOnline || online;
87
- const MSG = showOnlineMsg ? ONLINE_MSG : OFFLINE_MSG;
88
- const EXTRA_MSG = showOnlineMsg ? EXTRA_ONLINE_MSG : EXTRA_OFFLINE_MSG;
85
+
86
+ const MSG = showOnlineMsg
87
+ ? (theme?.online_notification_title ??
88
+ storedLocalizations?.online_notification_title)
89
+ : (theme?.offline_notification_title ??
90
+ storedLocalizations?.offline_notification_title);
91
+
92
+ const EXTRA_MSG = showOnlineMsg
93
+ ? (theme?.online_notification_subtitle ??
94
+ storedLocalizations?.online_notification_subtitle)
95
+ : (theme?.offline_notification_subtitle ??
96
+ storedLocalizations?.offline_notification_subtitle);
89
97
 
90
98
  return (
91
99
  <div onClick={onClose}>
@@ -1,4 +1,6 @@
1
1
  import * as React from "react";
2
+ import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
3
+ import { useNetworkStatusLocalizations } from "./utils";
2
4
 
3
5
  type Props = {
4
6
  children?: React.ReactNode;
@@ -9,12 +11,6 @@ type Props = {
9
11
 
10
12
  const DURATION_TO_HIDE_AFTER_BACK_TO_ONLINE = 4500; // ms
11
13
 
12
- const ONLINE_TITLE = "You are back online";
13
- const ONLINE_SUBTITLE = "Feel free to continue where you left off";
14
-
15
- const OFFLINE_TITLE = "No internet connection";
16
- const OFFLINE_SUBTITLE = "Please check your connection";
17
-
18
14
  const styles: Record<any, React.CSSProperties> = {
19
15
  body: {
20
16
  position: "absolute",
@@ -47,6 +43,9 @@ let timer: NodeJS.Timeout;
47
43
  export const NotificationView = (props: Props) => {
48
44
  const { children, dismiss, previousOnline = true, online } = props;
49
45
 
46
+ const theme = useTheme<BaseThemePropertiesTV>();
47
+ const storedLocalizations = useNetworkStatusLocalizations();
48
+
50
49
  const [shown, setShown] = React.useState<boolean>(false);
51
50
 
52
51
  const onClose = () => {
@@ -79,8 +78,17 @@ export const NotificationView = (props: Props) => {
79
78
  }
80
79
  }, [online, previousOnline]);
81
80
 
82
- const title = online ? ONLINE_TITLE : OFFLINE_TITLE;
83
- const subtitle = online ? ONLINE_SUBTITLE : OFFLINE_SUBTITLE;
81
+ const title = online
82
+ ? (theme?.online_notification_title ??
83
+ storedLocalizations?.online_notification_title)
84
+ : (theme?.offline_notification_title ??
85
+ storedLocalizations?.offline_notification_title);
86
+
87
+ const subtitle = online
88
+ ? (theme?.online_notification_subtitle ??
89
+ storedLocalizations?.online_notification_subtitle)
90
+ : (theme?.offline_notification_subtitle ??
91
+ storedLocalizations?.offline_notification_subtitle);
84
92
 
85
93
  return (
86
94
  <div onClick={onClose}>
@@ -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
+ }
@@ -46,7 +46,6 @@ import {
46
46
  PlayerContainerContextProvider,
47
47
  } from "./PlayerContainerContext";
48
48
  import { FocusableGroup } from "@applicaster/zapp-react-native-ui-components/Components/FocusableGroup";
49
- import { ErrorDisplay } from "./ErrorDisplay";
50
49
  import { PlayerFocusableWrapperView } from "./WappersView/PlayerFocusableWrapperView";
51
50
  import { FocusableGroupMainContainerId } from "./index";
52
51
  import { isPlayable } from "@applicaster/zapp-react-native-utils/navigationUtils/itemTypeMatchers";
@@ -61,7 +60,6 @@ import {
61
60
  PlayerNativeSendCommand,
62
61
  } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand";
63
62
  import { useAppData } from "@applicaster/zapp-react-native-redux";
64
- import { useRestrictMobilePlayback } from "./useRestrictMobilePlayback";
65
63
 
66
64
  type Props = {
67
65
  Player: React.ComponentType<any>;
@@ -274,13 +272,6 @@ const PlayerContainerComponent = (props: Props) => {
274
272
  );
275
273
  }, [playerManager.isRegistered()]);
276
274
 
277
- const { isRestricted } = useRestrictMobilePlayback({
278
- player,
279
- entry: item,
280
- pluginConfiguration,
281
- close,
282
- });
283
-
284
275
  const playEntry = (entry) => navigator.replaceTop(entry, { mode });
285
276
 
286
277
  const onPlayNextPerformNextVideoPlay = React.useCallback(() => {
@@ -347,12 +338,6 @@ const PlayerContainerComponent = (props: Props) => {
347
338
  playerContainerLogger.error(errorObj);
348
339
 
349
340
  setState({ error: errorObj });
350
-
351
- if (!isTvOS) {
352
- setTimeout(() => {
353
- close();
354
- }, 800);
355
- }
356
341
  },
357
342
  [close]
358
343
  );
@@ -670,7 +655,7 @@ const PlayerContainerComponent = (props: Props) => {
670
655
  <PlayerFocusableWrapperView
671
656
  nextFocusDown={context.bottomFocusableId}
672
657
  >
673
- {isRestricted ? null : (
658
+ {!Player ? null : (
674
659
  <Player
675
660
  source={{
676
661
  uri,
@@ -703,8 +688,6 @@ const PlayerContainerComponent = (props: Props) => {
703
688
  </Player>
704
689
  )}
705
690
  </PlayerFocusableWrapperView>
706
-
707
- {state.error ? <ErrorDisplay error={state.error} /> : null}
708
691
  </View>
709
692
  {/* Components container */}
710
693
  {isInlineTV && context.showComponentsContainer ? (
@@ -0,0 +1,26 @@
1
+ import React from "react";
2
+ import { Text } from "react-native";
3
+ import { render } from "@testing-library/react-native";
4
+ import { PreloaderWrapper } from "..";
5
+
6
+ describe("PreloaderWrapper", () => {
7
+ it("renders children when preloader is hidden", () => {
8
+ const { getByText } = render(
9
+ <PreloaderWrapper showPreloader={false}>
10
+ <Text>content</Text>
11
+ </PreloaderWrapper>
12
+ );
13
+
14
+ expect(getByText("content")).toBeDefined();
15
+ });
16
+
17
+ it("renders null when preloader is shown", () => {
18
+ const { queryByText } = render(
19
+ <PreloaderWrapper showPreloader>
20
+ <Text>content</Text>
21
+ </PreloaderWrapper>
22
+ );
23
+
24
+ expect(queryByText("content")).toBeNull();
25
+ });
26
+ });
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+
3
+ type PreloaderWrapperProps = {
4
+ showPreloader?: boolean;
5
+ children?: React.ReactNode;
6
+ };
7
+
8
+ export const PreloaderWrapper: React.FC<PreloaderWrapperProps> = ({
9
+ showPreloader = false,
10
+ children,
11
+ }) => {
12
+ return !showPreloader ? children : null;
13
+ };
14
+
15
+ export default PreloaderWrapper;
@@ -23,6 +23,7 @@ import { isLast } from "@applicaster/zapp-react-native-utils/arrayUtils";
23
23
  import { withComponentsMapProvider } from "@applicaster/zapp-react-native-ui-components/Decorators/ComponentsMapWrapper";
24
24
  import { useScreenContextV2 } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
25
25
  import { useShallow } from "zustand/react/shallow";
26
+ import { emitScrollEndReached } from "@applicaster/zapp-react-native-ui-components/events";
26
27
 
27
28
  import { isAndroidPlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
28
29
  import { ComponentsMapHeightContext } from "./ContextProviders/ComponentsMapHeightContext";
@@ -73,6 +74,7 @@ function ComponentsMapComponent(props: Props) {
73
74
 
74
75
  const flatListRef = React.useRef<FlatList | null>(null);
75
76
  const flatListWrapperRef = React.useRef<View | null>(null);
77
+ const hasUserScrolledRef = React.useRef(false);
76
78
  const screenConfig = useScreenConfiguration(riverId);
77
79
  const screenData = useScreenData(riverId);
78
80
  const pullToRefreshEnabled = screenData?.rules?.pull_to_refresh_enabled;
@@ -236,6 +238,8 @@ function ComponentsMapComponent(props: Props) {
236
238
  }, []);
237
239
 
238
240
  const onScroll = React.useCallback((event) => {
241
+ hasUserScrolledRef.current = true;
242
+
239
243
  const {
240
244
  nativeEvent: {
241
245
  contentOffset: { y },
@@ -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
 
@@ -5,6 +5,8 @@ import {
5
5
  StyleSheet,
6
6
  } from "react-native";
7
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";
8
10
  import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
9
11
  import { useLocalizedStrings } from "@applicaster/zapp-react-native-utils/localizationUtils";
10
12
  import { useAnalytics } from "@applicaster/zapp-react-native-utils/analyticsUtils";
@@ -14,6 +16,7 @@ import { useShallow } from "zustand/react/shallow";
14
16
  import { useScreenContextV2 } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
15
17
  import { useSafeAreaInsets } from "react-native-safe-area-context";
16
18
  import { useLoadPipesDataDispatch } from "@applicaster/zapp-react-native-utils/reactHooks";
19
+ import { useGetUrlInflater } from "@applicaster/zapp-react-native-utils/reactHooks/feed/useInflatedUrl";
17
20
 
18
21
  const BRIGHTNESS_THRESHOLD = 160;
19
22
  const ABOVE_DEFAULT_COLOR = "gray";
@@ -62,31 +65,51 @@ export const usePullToRefresh = (
62
65
 
63
66
  const [refreshing, setRefreshing] = React.useState(false);
64
67
 
65
- const feeds: string[] =
66
- riverComponents?.map(R.path(["data", "source"])).filter((feed) => !!feed) ??
67
- [];
68
+ const feedSources = React.useMemo(
69
+ () =>
70
+ (riverComponents || [])
71
+ .map((riverComponent) => ({
72
+ source: path(["data", "source"], riverComponent),
73
+ mapping: path(["data", "mapping"], riverComponent),
74
+ }))
75
+ .filter(({ source }) => !isNilOrEmpty(source)),
76
+ [riverComponents]
77
+ );
68
78
 
69
- const feedsLength = feeds.length;
79
+ const feedsLength = feedSources.length;
70
80
 
71
81
  const [requestsCompletedCounter, setRequestsCompletedCounter] =
72
82
  React.useState(0);
73
83
 
74
84
  const loadPipesDataDispatcher = useLoadPipesDataDispatch();
85
+ const urlInflater = useGetUrlInflater();
75
86
 
76
87
  React.useEffect(() => {
77
88
  // will not work for pipes v1 on 1st level screens
78
89
  if (refreshing && !isPipesV1) {
79
- feeds.forEach((feed) => {
80
- loadPipesDataDispatcher(feed, {
81
- silentRefresh: true,
82
- clearCache: true,
83
- callback: () => {
84
- setRequestsCompletedCounter(R.inc);
85
- },
86
- });
90
+ feedSources.forEach(({ source, mapping }) => {
91
+ const inflatedUrl = urlInflater(source, mapping);
92
+
93
+ if (inflatedUrl) {
94
+ loadPipesDataDispatcher(inflatedUrl, {
95
+ silentRefresh: true,
96
+ clearCache: true,
97
+ callback: () => {
98
+ setRequestsCompletedCounter(R.inc);
99
+ },
100
+ });
101
+ } else {
102
+ setRequestsCompletedCounter(R.inc);
103
+ }
87
104
  });
88
105
  }
89
- }, [refreshing, isPipesV1, feeds, loadPipesDataDispatcher]);
106
+ }, [
107
+ refreshing,
108
+ isPipesV1,
109
+ feedSources,
110
+ loadPipesDataDispatcher,
111
+ urlInflater,
112
+ ]);
90
113
 
91
114
  React.useEffect(() => {
92
115
  if (requestsCompletedCounter === feedsLength) {