@applicaster/zapp-react-native-ui-components 14.0.0-rc.9 → 15.0.0-alpha.2239032089

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 (166) hide show
  1. package/Components/AnimatedInOut/index.tsx +5 -3
  2. package/Components/AudioPlayer/index.tsx +15 -0
  3. package/Components/AudioPlayer/mobile/Layout.tsx +66 -0
  4. package/Components/AudioPlayer/{__tests__/__snapshots__/audioPlayer.test.js.snap → mobile/__tests__/__snapshots__/audioPlayerMobileLayout.test.js.snap} +2 -2
  5. package/Components/AudioPlayer/mobile/__tests__/audioPlayerMobileLayout.test.js +18 -0
  6. package/Components/AudioPlayer/mobile/index.tsx +18 -0
  7. package/Components/AudioPlayer/{Artwork.tsx → tv/Artwork.tsx} +3 -2
  8. package/Components/AudioPlayer/{Channel.tsx → tv/Channel.tsx} +7 -7
  9. package/Components/AudioPlayer/tv/Layout.tsx +168 -0
  10. package/Components/AudioPlayer/{Runtime.tsx → tv/Runtime.tsx} +7 -1
  11. package/Components/AudioPlayer/{Summary.tsx → tv/Summary.tsx} +6 -2
  12. package/Components/AudioPlayer/{Title.tsx → tv/Title.tsx} +6 -2
  13. package/Components/AudioPlayer/{__tests__ → tv/__tests__}/__snapshots__/Runtime.test.js.snap +2 -2
  14. package/Components/AudioPlayer/tv/__tests__/__snapshots__/audioPlayer.test.js.snap +164 -0
  15. package/Components/AudioPlayer/tv/__tests__/__snapshots__/channel.test.js.snap +19 -0
  16. package/Components/AudioPlayer/{__tests__ → tv/__tests__}/__snapshots__/summary.test.js.snap +1 -2
  17. package/Components/AudioPlayer/{__tests__ → tv/__tests__}/__snapshots__/title.test.js.snap +1 -2
  18. package/Components/AudioPlayer/{__tests__ → tv/__tests__}/audioPlayer.test.js +7 -3
  19. package/Components/AudioPlayer/{helpers.tsx → tv/helpers.tsx} +11 -5
  20. package/Components/AudioPlayer/{AudioPlayer.tsx → tv/index.tsx} +17 -58
  21. package/Components/AudioPlayer/types.ts +40 -0
  22. package/Components/BaseFocusable/index.tsx +23 -12
  23. package/Components/Cell/Cell.tsx +91 -64
  24. package/Components/Cell/CellWithFocusable.tsx +3 -0
  25. package/Components/Cell/__tests__/CellWIthFocusable.test.js +3 -2
  26. package/Components/Cell/index.js +7 -3
  27. package/Components/ComponentResolver/index.ts +1 -1
  28. package/Components/FeedLoader/FeedLoader.tsx +7 -16
  29. package/Components/FeedLoader/FeedLoaderHOC.tsx +21 -0
  30. package/Components/FeedLoader/index.js +2 -8
  31. package/Components/Focusable/Focusable.tsx +16 -5
  32. package/Components/Focusable/FocusableTvOS.tsx +10 -6
  33. package/Components/Focusable/FocusableiOS.tsx +2 -2
  34. package/Components/Focusable/Touchable.tsx +5 -3
  35. package/Components/Focusable/__tests__/index.android.test.tsx +3 -0
  36. package/Components/Focusable/index.android.tsx +19 -11
  37. package/Components/Focusable/index.tsx +1 -1
  38. package/Components/FocusableGroup/FocusableTvOS.tsx +6 -1
  39. package/Components/FocusableList/FocusableItem.tsx +4 -3
  40. package/Components/FocusableList/FocusableListItemWrapper.tsx +2 -1
  41. package/Components/FocusableList/hooks/useCellState.android.ts +13 -3
  42. package/Components/FocusableList/index.tsx +20 -9
  43. package/Components/FreezeWithCallback/__tests__/index.test.tsx +67 -43
  44. package/Components/GeneralContentScreen/utils/__tests__/useCurationAPI.test.js +42 -59
  45. package/Components/GeneralContentScreen/utils/useCurationAPI.ts +13 -10
  46. package/Components/HandlePlayable/HandlePlayable.tsx +25 -9
  47. package/Components/HookRenderer/HookRenderer.tsx +5 -1
  48. package/Components/Layout/TV/LayoutBackground.tsx +1 -1
  49. package/Components/Layout/TV/__tests__/index.test.tsx +0 -1
  50. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +6 -2
  51. package/Components/MasterCell/DefaultComponents/Button.tsx +1 -1
  52. package/Components/MasterCell/DefaultComponents/FocusableView/index.tsx +4 -39
  53. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +1 -1
  54. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +1 -1
  55. package/Components/MasterCell/DefaultComponents/SecondaryImage/Image.tsx +65 -17
  56. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/Image.test.tsx +21 -3
  57. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/__snapshots__/Image.test.tsx.snap +6 -3
  58. package/Components/MasterCell/DefaultComponents/Text/index.tsx +26 -6
  59. package/Components/MasterCell/DefaultComponents/__tests__/image.test.js +10 -10
  60. package/Components/MasterCell/DefaultComponents/__tests__/text.test.tsx +18 -18
  61. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/index.test.tsx +10 -10
  62. package/Components/MasterCell/elementMapper.tsx +1 -2
  63. package/Components/MasterCell/index.tsx +1 -1
  64. package/Components/MasterCell/utils/behaviorProvider.ts +82 -14
  65. package/Components/MasterCell/utils/index.ts +11 -5
  66. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +13 -18
  67. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +9 -0
  68. package/Components/OfflineHandler/__tests__/index.test.tsx +26 -35
  69. package/Components/PlayerContainer/ErrorDisplay/index.ts +1 -1
  70. package/Components/PlayerContainer/PlayerContainer.tsx +46 -33
  71. package/Components/PlayerContainer/ProgramInfo/index.tsx +1 -1
  72. package/Components/PlayerContainer/index.ts +1 -1
  73. package/Components/PlayerImageBackground/index.tsx +1 -1
  74. package/Components/River/ComponentsMap/ComponentsMap.tsx +49 -43
  75. package/Components/River/ComponentsMap/ContextProviders/ComponentsMapHeightContext.ts +8 -0
  76. package/Components/River/ComponentsMap/ContextProviders/ComponentsMapRefContext.ts +8 -0
  77. package/Components/River/ComponentsMap/hooks/__tests__/useLoadingState.test.ts +378 -0
  78. package/Components/River/ComponentsMap/hooks/useLoadingState.ts +2 -2
  79. package/Components/River/RefreshControl.tsx +11 -17
  80. package/Components/River/RiverItem.tsx +3 -0
  81. package/Components/River/TV/River.tsx +11 -20
  82. package/Components/River/TV/index.tsx +5 -3
  83. package/Components/River/TV/withFocusableGroupForContent.tsx +60 -0
  84. package/Components/River/TV/withPipesV1DataLoader.tsx +43 -0
  85. package/Components/River/TV/withRiverDataLoader.tsx +17 -0
  86. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +2 -0
  87. package/Components/River/__tests__/river.test.js +12 -26
  88. package/Components/River/index.tsx +1 -1
  89. package/Components/Screen/__tests__/Screen.test.tsx +28 -29
  90. package/Components/Screen/__tests__/navigationHandler.test.ts +133 -22
  91. package/Components/Screen/navigationHandler.ts +20 -2
  92. package/Components/ScreenResolver/index.tsx +15 -0
  93. package/Components/ScreenRevealManager/ScreenRevealManager.ts +76 -0
  94. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +107 -0
  95. package/Components/ScreenRevealManager/__tests__/withScreenRevealManager.test.tsx +96 -0
  96. package/Components/ScreenRevealManager/index.ts +1 -0
  97. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +79 -0
  98. package/Components/Tabs/TV/Tabs.android.tsx +1 -3
  99. package/Components/Tabs/Tabs.tsx +2 -3
  100. package/Components/TextInputTv/__tests__/__snapshots__/TextInputTv.test.js.snap +13 -0
  101. package/Components/TextInputTv/index.tsx +11 -0
  102. package/Components/Touchable/__tests__/__snapshots__/touchable.test.tsx.snap +34 -0
  103. package/Components/Touchable/__tests__/touchable.test.tsx +12 -17
  104. package/Components/Transitioner/__tests__/__snapshots__/Scene.test.js.snap +15 -9
  105. package/Components/VideoLive/animationUtils.ts +3 -3
  106. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.tsx +6 -10
  107. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.web.tsx +294 -0
  108. package/Components/VideoModal/ModalAnimation/AnimatedVideoPlayerComponent.web.tsx +93 -0
  109. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +73 -29
  110. package/Components/VideoModal/PlayerDetails.tsx +29 -7
  111. package/Components/VideoModal/PlayerWrapper.tsx +26 -142
  112. package/Components/VideoModal/VideoModal.tsx +3 -17
  113. package/Components/VideoModal/__tests__/PlayerDetails.test.tsx +5 -5
  114. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +1 -7
  115. package/Components/VideoModal/__tests__/__snapshots__/PlayerWrapper.test.tsx.snap +44 -240
  116. package/Components/VideoModal/hooks/__tests__/useDelayedPlayerDetails.test.ts +9 -1
  117. package/Components/VideoModal/hooks/index.ts +0 -2
  118. package/Components/VideoModal/hooks/useDelayedPlayerDetails.ts +40 -15
  119. package/Components/VideoModal/hooks/useModalSize.ts +18 -2
  120. package/Components/VideoModal/hooks/utils/__tests__/showDetails.test.ts +2 -2
  121. package/Components/VideoModal/hooks/utils/index.ts +4 -0
  122. package/Components/VideoModal/utils.ts +6 -0
  123. package/Components/Viewport/ViewportAware/__tests__/viewportAware.test.js +12 -16
  124. package/Components/Viewport/ViewportTracker/__tests__/viewportTracker.test.js +84 -24
  125. package/Components/Viewport/VisibilitySensor/VisibilitySensor.tsx +3 -3
  126. package/Components/default-cell-renderer/viewTrees/tv/DefaultCell/index.ts +3 -3
  127. package/Contexts/CellFocusedStateContext/index.tsx +27 -0
  128. package/Contexts/ConfigutaionContext/__tests__/ConfigurationProvider.test.tsx +3 -3
  129. package/Contexts/ScreenContext/index.tsx +46 -6
  130. package/Decorators/ConfigurationWrapper/__tests__/withConfigurationProvider.test.tsx +3 -3
  131. package/Decorators/ConfigurationWrapper/withConfigurationProvider.tsx +2 -2
  132. package/Decorators/RiverFeedLoader/__tests__/__snapshots__/riverFeedLoader.test.tsx.snap +221 -209
  133. package/Decorators/RiverFeedLoader/__tests__/riverFeedLoader.test.tsx +14 -16
  134. package/Decorators/RiverFeedLoader/__tests__/utils.test.ts +0 -20
  135. package/Decorators/RiverFeedLoader/index.tsx +22 -4
  136. package/Decorators/RiverFeedLoader/utils/index.ts +0 -18
  137. package/Decorators/RiverResolver/__tests__/riverResolver.test.tsx +3 -6
  138. package/Decorators/ZappPipesDataConnector/ResolverSelector.tsx +25 -0
  139. package/Decorators/ZappPipesDataConnector/__tests__/NullFeedResolver.test.tsx +78 -0
  140. package/Decorators/ZappPipesDataConnector/__tests__/ResolverSelector.test.tsx +205 -0
  141. package/Decorators/ZappPipesDataConnector/__tests__/StaticFeedResolver.test.tsx +251 -0
  142. package/Decorators/ZappPipesDataConnector/__tests__/UrlFeedResolver.test.tsx +368 -0
  143. package/Decorators/ZappPipesDataConnector/__tests__/utils.test.ts +39 -0
  144. package/Decorators/ZappPipesDataConnector/index.tsx +26 -293
  145. package/Decorators/ZappPipesDataConnector/resolvers/NullFeedResolver.tsx +25 -0
  146. package/Decorators/ZappPipesDataConnector/resolvers/StaticFeedResolver.tsx +87 -0
  147. package/Decorators/ZappPipesDataConnector/resolvers/UrlFeedResolver.tsx +266 -0
  148. package/Decorators/ZappPipesDataConnector/types.ts +29 -0
  149. package/Decorators/ZappPipesDataConnector/utils/mongoFilter.ts +738 -0
  150. package/Decorators/ZappPipesDataConnector/utils/useFilter.tsx +157 -0
  151. package/events/index.ts +5 -0
  152. package/package.json +5 -10
  153. package/Components/AudioPlayer/AudioPlayerLayout.tsx +0 -202
  154. package/Components/AudioPlayer/__tests__/__snapshots__/audioPlayerLayout.test.js.snap +0 -66
  155. package/Components/AudioPlayer/__tests__/__snapshots__/channel.test.js.snap +0 -28
  156. package/Components/AudioPlayer/__tests__/audioPlayerLayout.test.js +0 -26
  157. package/Components/AudioPlayer/index.ts +0 -1
  158. package/Components/River/TV/withTVEventHandler.tsx +0 -27
  159. package/Components/River/__tests__/__snapshots__/river.test.js.snap +0 -27
  160. package/Components/VideoModal/hooks/useBackgroundColor.ts +0 -10
  161. /package/Components/AudioPlayer/{__tests__ → tv/__tests__}/Runtime.test.js +0 -0
  162. /package/Components/AudioPlayer/{__tests__ → tv/__tests__}/__snapshots__/artWork.test.js.snap +0 -0
  163. /package/Components/AudioPlayer/{__tests__ → tv/__tests__}/artWork.test.js +0 -0
  164. /package/Components/AudioPlayer/{__tests__ → tv/__tests__}/channel.test.js +0 -0
  165. /package/Components/AudioPlayer/{__tests__ → tv/__tests__}/summary.test.js +0 -0
  166. /package/Components/AudioPlayer/{__tests__ → tv/__tests__}/title.test.js +0 -0
@@ -0,0 +1,378 @@
1
+ import { renderHook, act } from "@testing-library/react-hooks";
2
+ import { BehaviorSubject } from "rxjs";
3
+ import { useLoadingState } from "../useLoadingState";
4
+
5
+ jest.mock(
6
+ "@applicaster/zapp-react-native-utils/reactHooks/state/useRefWithInitialValue",
7
+ () => ({
8
+ useRefWithInitialValue: jest.fn((initializer) => ({
9
+ current: initializer(),
10
+ })),
11
+ })
12
+ );
13
+
14
+ describe("useLoadingState", () => {
15
+ let onLoadDone: jest.Mock;
16
+
17
+ beforeEach(() => {
18
+ onLoadDone = jest.fn();
19
+ jest.clearAllMocks();
20
+ });
21
+
22
+ describe("initialization", () => {
23
+ it("should initialize with correct default state for zero components", () => {
24
+ const { result } = renderHook(() => useLoadingState(0, onLoadDone));
25
+
26
+ const initialState = result.current.loadingState.getValue();
27
+
28
+ expect(initialState).toEqual({
29
+ index: -1,
30
+ done: true,
31
+ waitForAllComponents: false,
32
+ });
33
+
34
+ expect(result.current.shouldShowLoadingError).toBe(false);
35
+ });
36
+
37
+ it("should initialize with correct default state for multiple components", () => {
38
+ const { result } = renderHook(() => useLoadingState(3, onLoadDone));
39
+
40
+ const initialState = result.current.loadingState.getValue();
41
+
42
+ expect(initialState).toEqual({
43
+ index: -1,
44
+ done: false,
45
+ waitForAllComponents: false,
46
+ });
47
+
48
+ expect(result.current.shouldShowLoadingError).toBe(false);
49
+ });
50
+
51
+ it("should return a BehaviorSubject for loadingState", () => {
52
+ const { result } = renderHook(() => useLoadingState(3, onLoadDone));
53
+
54
+ expect(result.current.loadingState).toBeInstanceOf(BehaviorSubject);
55
+ });
56
+ });
57
+
58
+ describe("arePreviousComponentsLoaded", () => {
59
+ it("should return true for index 0 (first component)", () => {
60
+ const { result } = renderHook(() => useLoadingState(3, onLoadDone));
61
+
62
+ expect(result.current.arePreviousComponentsLoaded(0)).toBe(true);
63
+ });
64
+
65
+ it("should return false when previous components are not loaded", () => {
66
+ const { result } = renderHook(() => useLoadingState(3, onLoadDone));
67
+
68
+ expect(result.current.arePreviousComponentsLoaded(1)).toBe(false);
69
+ expect(result.current.arePreviousComponentsLoaded(2)).toBe(false);
70
+ });
71
+
72
+ it("should return true when all previous components are loaded", () => {
73
+ const { result } = renderHook(() => useLoadingState(3, onLoadDone));
74
+
75
+ act(() => {
76
+ result.current.onLoadFinished(0);
77
+ });
78
+
79
+ expect(result.current.arePreviousComponentsLoaded(1)).toBe(true);
80
+ expect(result.current.arePreviousComponentsLoaded(2)).toBe(false);
81
+
82
+ act(() => {
83
+ result.current.onLoadFinished(1);
84
+ });
85
+
86
+ expect(result.current.arePreviousComponentsLoaded(2)).toBe(true);
87
+ });
88
+ });
89
+
90
+ describe("onLoadFinished", () => {
91
+ it("should update component state and loading state when component finishes loading", () => {
92
+ const { result } = renderHook(() => useLoadingState(3, onLoadDone));
93
+
94
+ act(() => {
95
+ result.current.onLoadFinished(0);
96
+ });
97
+
98
+ const state = result.current.loadingState.getValue();
99
+ expect(state.index).toBe(0);
100
+ expect(state.done).toBe(false);
101
+ });
102
+
103
+ it("should update index to highest loaded component", () => {
104
+ const { result } = renderHook(() => useLoadingState(3, onLoadDone));
105
+
106
+ act(() => {
107
+ result.current.onLoadFinished(2);
108
+ });
109
+
110
+ let state = result.current.loadingState.getValue();
111
+ expect(state.index).toBe(2);
112
+
113
+ act(() => {
114
+ result.current.onLoadFinished(1);
115
+ });
116
+
117
+ state = result.current.loadingState.getValue();
118
+ expect(state.index).toBe(2); // Should remain 2, not decrease to 1
119
+ });
120
+
121
+ it("should mark as done when all components are loaded", () => {
122
+ const { result } = renderHook(() => useLoadingState(2, onLoadDone));
123
+
124
+ act(() => {
125
+ result.current.onLoadFinished(0);
126
+ });
127
+
128
+ let state = result.current.loadingState.getValue();
129
+ expect(state.done).toBe(true); // True because arePreviousComponentsLoaded(1) returns true when component 0 is loaded
130
+ expect(onLoadDone).toHaveBeenCalledTimes(1);
131
+
132
+ act(() => {
133
+ result.current.onLoadFinished(1);
134
+ });
135
+
136
+ state = result.current.loadingState.getValue();
137
+ expect(state.done).toBe(true);
138
+ expect(onLoadDone).toHaveBeenCalledTimes(2); // Called again
139
+ });
140
+
141
+ it("should call onLoadDone when count is 0", () => {
142
+ const { result } = renderHook(() => useLoadingState(0, onLoadDone));
143
+
144
+ const state = result.current.loadingState.getValue();
145
+ expect(state.done).toBe(true);
146
+ // onLoadDone is not called on initialization for count 0, only when all components are loaded via dispatch
147
+ });
148
+
149
+ it("should handle loading components out of order", () => {
150
+ const { result } = renderHook(() => useLoadingState(3, onLoadDone));
151
+
152
+ // Load component 2 first
153
+ act(() => {
154
+ result.current.onLoadFinished(2);
155
+ });
156
+
157
+ let state = result.current.loadingState.getValue();
158
+ expect(state.done).toBe(false);
159
+
160
+ // Load component 0
161
+ act(() => {
162
+ result.current.onLoadFinished(0);
163
+ });
164
+
165
+ state = result.current.loadingState.getValue();
166
+ expect(state.done).toBe(false);
167
+
168
+ // Load component 1 - should complete loading
169
+ act(() => {
170
+ result.current.onLoadFinished(1);
171
+ });
172
+
173
+ state = result.current.loadingState.getValue();
174
+ expect(state.done).toBe(true);
175
+ expect(onLoadDone).toHaveBeenCalledTimes(1);
176
+ });
177
+
178
+ it("should call onLoadDone again on subsequent dispatches", () => {
179
+ const { result } = renderHook(() => useLoadingState(2, onLoadDone));
180
+
181
+ act(() => {
182
+ result.current.onLoadFinished(0);
183
+ result.current.onLoadFinished(1);
184
+ });
185
+
186
+ expect(onLoadDone).toHaveBeenCalledTimes(2); // Called for each dispatch when done is true
187
+
188
+ // Try loading again - onLoadDone will be called again because dispatch runs again
189
+ act(() => {
190
+ result.current.onLoadFinished(0);
191
+ });
192
+
193
+ expect(onLoadDone).toHaveBeenCalledTimes(3); // Will be called again
194
+ });
195
+ });
196
+
197
+ describe("onLoadFailed", () => {
198
+ it("should treat failed components as loaded when SHOULD_FAIL_ON_COMPONENT_LOADING is false", () => {
199
+ const { result } = renderHook(() => useLoadingState(2, onLoadDone));
200
+
201
+ const error = new Error("Load failed");
202
+
203
+ act(() => {
204
+ result.current.onLoadFailed({ error, index: 0 });
205
+ });
206
+
207
+ const state = result.current.loadingState.getValue();
208
+ expect(state.index).toBe(0);
209
+ expect(result.current.shouldShowLoadingError).toBe(false);
210
+ });
211
+
212
+ it("should complete loading when all components fail", () => {
213
+ const { result } = renderHook(() => useLoadingState(2, onLoadDone));
214
+
215
+ const error = new Error("Load failed");
216
+
217
+ act(() => {
218
+ result.current.onLoadFailed({ error, index: 0 });
219
+ result.current.onLoadFailed({ error, index: 1 });
220
+ });
221
+
222
+ const state = result.current.loadingState.getValue();
223
+ expect(state.done).toBe(true);
224
+ expect(onLoadDone).toHaveBeenCalledTimes(2); // Called for each failed component
225
+ });
226
+
227
+ it("should handle mixed success and failure", () => {
228
+ const { result } = renderHook(() => useLoadingState(3, onLoadDone));
229
+
230
+ const error = new Error("Load failed");
231
+
232
+ act(() => {
233
+ result.current.onLoadFinished(0);
234
+ result.current.onLoadFailed({ error, index: 1 });
235
+ result.current.onLoadFinished(2);
236
+ });
237
+
238
+ const state = result.current.loadingState.getValue();
239
+ expect(state.done).toBe(true);
240
+ expect(onLoadDone).toHaveBeenCalledTimes(2); // Called when all components 0,1,2 are handled
241
+ });
242
+ });
243
+
244
+ describe("loading state observable", () => {
245
+ it("should emit state changes through BehaviorSubject", () => {
246
+ const { result } = renderHook(() => useLoadingState(3, onLoadDone)); // Use 3 components so loading component 0 doesn't complete everything
247
+ const mockSubscriber = jest.fn();
248
+
249
+ result.current.loadingState.subscribe(mockSubscriber);
250
+
251
+ act(() => {
252
+ result.current.onLoadFinished(0);
253
+ });
254
+
255
+ // Should have been called twice: initial state + update
256
+ expect(mockSubscriber).toHaveBeenCalledTimes(2);
257
+
258
+ expect(mockSubscriber).toHaveBeenLastCalledWith({
259
+ index: 0,
260
+ done: false, // Will be false because we need components 1 and 2 as well
261
+ waitForAllComponents: false,
262
+ });
263
+ });
264
+
265
+ it("should preserve waitForAllComponents flag in state updates", () => {
266
+ const { result } = renderHook(() => useLoadingState(2, onLoadDone));
267
+
268
+ act(() => {
269
+ result.current.onLoadFinished(0);
270
+ });
271
+
272
+ const state = result.current.loadingState.getValue();
273
+ expect(state.waitForAllComponents).toBe(false);
274
+ });
275
+ });
276
+
277
+ describe("memoization", () => {
278
+ it("should return stable references for functions", () => {
279
+ const { result, rerender } = renderHook(() =>
280
+ useLoadingState(2, onLoadDone)
281
+ );
282
+
283
+ const firstRender = {
284
+ onLoadFinished: result.current.onLoadFinished,
285
+ onLoadFailed: result.current.onLoadFailed,
286
+ arePreviousComponentsLoaded: result.current.arePreviousComponentsLoaded,
287
+ };
288
+
289
+ rerender();
290
+
291
+ expect(result.current.onLoadFinished).toBe(firstRender.onLoadFinished);
292
+ expect(result.current.onLoadFailed).toBe(firstRender.onLoadFailed);
293
+
294
+ expect(result.current.arePreviousComponentsLoaded).toBe(
295
+ firstRender.arePreviousComponentsLoaded
296
+ );
297
+ });
298
+
299
+ it("should return stable function references (current behavior)", () => {
300
+ const { result, rerender } = renderHook(
301
+ ({ onLoadDone }) => useLoadingState(2, onLoadDone),
302
+ { initialProps: { onLoadDone } }
303
+ );
304
+
305
+ const firstResult = result.current;
306
+
307
+ const newOnLoadDone = jest.fn();
308
+ rerender({ onLoadDone: newOnLoadDone });
309
+
310
+ // Functions should remain the same due to empty dependency arrays (this is the current behavior)
311
+ expect(result.current.onLoadFinished).toBe(firstResult.onLoadFinished);
312
+ expect(result.current.onLoadFailed).toBe(firstResult.onLoadFailed);
313
+ });
314
+ });
315
+
316
+ describe("edge cases", () => {
317
+ it("should handle duplicate load finished calls gracefully", () => {
318
+ const { result } = renderHook(() => useLoadingState(2, onLoadDone));
319
+
320
+ act(() => {
321
+ result.current.onLoadFinished(0);
322
+ result.current.onLoadFinished(0); // Duplicate call
323
+ });
324
+
325
+ const state = result.current.loadingState.getValue();
326
+ expect(state.index).toBe(0);
327
+ expect(state.done).toBe(true); // True because loading component 0 makes it done in a 2-component setup
328
+ });
329
+
330
+ it("should handle loading index greater than component count", () => {
331
+ const { result } = renderHook(() => useLoadingState(2, onLoadDone));
332
+
333
+ act(() => {
334
+ result.current.onLoadFinished(5); // Index out of bounds
335
+ });
336
+
337
+ const state = result.current.loadingState.getValue();
338
+ expect(state.index).toBe(5);
339
+ expect(state.done).toBe(false); // Should not be done as not all components loaded
340
+ });
341
+
342
+ it("should handle negative indices", () => {
343
+ const { result } = renderHook(() => useLoadingState(2, onLoadDone));
344
+
345
+ act(() => {
346
+ result.current.onLoadFinished(-1);
347
+ });
348
+
349
+ const state = result.current.loadingState.getValue();
350
+ expect(state.index).toBe(-1); // Should remain -1
351
+ });
352
+ });
353
+
354
+ describe("component count changes", () => {
355
+ it("should handle changing component count", () => {
356
+ const { result, rerender } = renderHook(
357
+ ({ count }) => useLoadingState(count, onLoadDone),
358
+ { initialProps: { count: 2 } }
359
+ );
360
+
361
+ act(() => {
362
+ result.current.onLoadFinished(0);
363
+ });
364
+
365
+ // Change count
366
+ rerender({ count: 3 });
367
+
368
+ // The hook should work with the new count
369
+ act(() => {
370
+ result.current.onLoadFinished(1);
371
+ result.current.onLoadFinished(2);
372
+ });
373
+
374
+ const state = result.current.loadingState.getValue();
375
+ expect(state.done).toBe(true);
376
+ });
377
+ });
378
+ });
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import { isNil, set, lensIndex, T, slice } from "ramda";
2
+ import { isNil, set, lensIndex, slice } from "ramda";
3
3
  import { BehaviorSubject } from "rxjs";
4
4
  import { useRefWithInitialValue } from "@applicaster/zapp-react-native-utils/reactHooks/state/useRefWithInitialValue";
5
5
 
@@ -53,7 +53,7 @@ export const useLoadingState = (
53
53
 
54
54
  const componentsBefore = slice(0, index, componentStateRef.current);
55
55
 
56
- return componentsBefore.every(T);
56
+ return componentsBefore.every(Boolean);
57
57
  }, []);
58
58
 
59
59
  const dispatch = React.useCallback(({ payload }) => {
@@ -10,11 +10,10 @@ import { useLocalizedStrings } from "@applicaster/zapp-react-native-utils/locali
10
10
  import { useAnalytics } from "@applicaster/zapp-react-native-utils/analyticsUtils";
11
11
  import { useSendAnalyticsEventWithFunction } from "@applicaster/zapp-react-native-utils/analyticsUtils/helpers/hooks";
12
12
  import { useCurrentScreenData } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useCurrentScreenData";
13
- import { loadPipesData } from "@applicaster/zapp-react-native-redux/ZappPipes";
14
- import { useDispatch } from "react-redux";
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";
16
+ import { useLoadPipesDataDispatch } from "@applicaster/zapp-react-native-utils/reactHooks";
18
17
 
19
18
  const BRIGHTNESS_THRESHOLD = 160;
20
19
  const ABOVE_DEFAULT_COLOR = "gray";
@@ -61,38 +60,33 @@ export const usePullToRefresh = (
61
60
  ) => {
62
61
  const isPipesV1 = !!pullToRefreshPipesV1RefreshingStateUpdater;
63
62
 
64
- const dispatch = useDispatch();
65
-
66
63
  const [refreshing, setRefreshing] = React.useState(false);
67
64
 
68
65
  const feeds: string[] =
69
66
  riverComponents?.map(R.path(["data", "source"])).filter((feed) => !!feed) ??
70
67
  [];
71
68
 
72
- const screenData = useCurrentScreenData();
73
-
74
69
  const feedsLength = feeds.length;
75
70
 
76
71
  const [requestsCompletedCounter, setRequestsCompletedCounter] =
77
72
  React.useState(0);
78
73
 
74
+ const loadPipesDataDispatcher = useLoadPipesDataDispatch();
75
+
79
76
  React.useEffect(() => {
80
77
  // will not work for pipes v1 on 1st level screens
81
78
  if (refreshing && !isPipesV1) {
82
79
  feeds.forEach((feed) => {
83
- dispatch(
84
- loadPipesData(feed, {
85
- silentRefresh: true,
86
- clearCache: true,
87
- callback: () => {
88
- setRequestsCompletedCounter(R.inc);
89
- },
90
- riverId: screenData.id,
91
- })
92
- );
80
+ loadPipesDataDispatcher(feed, {
81
+ silentRefresh: true,
82
+ clearCache: true,
83
+ callback: () => {
84
+ setRequestsCompletedCounter(R.inc);
85
+ },
86
+ });
93
87
  });
94
88
  }
95
- }, [refreshing, isPipesV1]);
89
+ }, [refreshing, isPipesV1, feeds, loadPipesDataDispatcher]);
96
90
 
97
91
  React.useEffect(() => {
98
92
  if (requestsCompletedCounter === feedsLength) {
@@ -13,6 +13,7 @@ import { riverLogger } from "./logger";
13
13
  import { tvPluginsWithCellRenderer } from "../../const";
14
14
  import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
15
15
  import type { BehaviorSubject } from "rxjs";
16
+ import { useCallbackActions } from "@applicaster/zapp-react-native-utils/zappFrameworkUtils/HookCallback/useCallbackActions";
16
17
 
17
18
  export type RiverItemType = {
18
19
  item: ZappUIComponent;
@@ -85,6 +86,7 @@ function RiverItemComponent(props: RiverItemType) {
85
86
  loadingState,
86
87
  } = props;
87
88
 
89
+ const callbackAction = useCallbackActions(item);
88
90
  const readyToBeDisplayed = useLoadingState(index, loadingState);
89
91
 
90
92
  const feedUrl = getFeedUrl(feed, index);
@@ -155,6 +157,7 @@ function RiverItemComponent(props: RiverItemType) {
155
157
  groupId={groupId}
156
158
  feedUrl={feedUrl}
157
159
  isLast={isLast}
160
+ callback={callbackAction}
158
161
  />
159
162
  );
160
163
  }
@@ -4,10 +4,7 @@ import * as React from "react";
4
4
  import { Text } from "react-native";
5
5
  import * as R from "ramda";
6
6
 
7
- import {
8
- useFeedLoader,
9
- useLayoutVersion,
10
- } from "@applicaster/zapp-react-native-utils/reactHooks";
7
+ import { isNil } from "@applicaster/zapp-react-native-utils/utils";
11
8
  import { GeneralContentScreen } from "../../GeneralContentScreen";
12
9
  import { ScreenResolver } from "@applicaster/zapp-react-native-ui-components/Components/ScreenResolver";
13
10
  import { utilsLogger } from "@applicaster/zapp-react-native-utils/logger";
@@ -21,23 +18,26 @@ type Props = {
21
18
  screenId: string;
22
19
  screenData: ZappRiver | ZappEntry;
23
20
  feedUrl?: string;
21
+ feedData?: PipesDataObject["data"];
24
22
  extraProps?: any;
25
23
  screenResolverExtraProps?: any;
26
24
  componentsMapExtraProps?: any;
27
25
  isInsideContainer?: boolean;
28
26
  extraAnchorPointYOffset: number;
29
27
  river?: ZappRiver | ZappEntry;
28
+ groupId: string;
30
29
  };
31
30
 
32
31
  export const River = (props: Props) => {
33
32
  const {
34
33
  screenId,
35
- feedUrl,
34
+ feedData,
36
35
  extraProps,
37
36
  screenResolverExtraProps,
38
37
  componentsMapExtraProps,
39
38
  isInsideContainer,
40
39
  extraAnchorPointYOffset,
40
+ groupId,
41
41
  } = props;
42
42
 
43
43
  const { title: screenTitle, summary: screenSummary } = useNavbarState();
@@ -46,7 +46,6 @@ export const River = (props: Props) => {
46
46
  useSetNavbarState();
47
47
 
48
48
  const rivers = useRivers();
49
- const isV2 = useLayoutVersion({ isV2: true });
50
49
 
51
50
  const river = React.useMemo(() => rivers?.[screenId], [screenId]);
52
51
 
@@ -55,19 +54,8 @@ export const River = (props: Props) => {
55
54
  [screenId]
56
55
  );
57
56
 
58
- const connectedScreenUrl = React.useMemo(() => {
59
- // Avoid using feedUrl or content.src on layouts v2
60
- if (isV2) return null;
61
-
62
- return feedUrl || R.path(["content", "src"], screenData);
63
- }, [feedUrl, screenData]);
64
-
65
- const { data: feedData } = useFeedLoader({
66
- feedUrl: connectedScreenUrl,
67
- });
68
-
69
57
  const stringOrEmpty = (value: string | number | undefined): string =>
70
- R.isNil(value) ? "" : String(value);
58
+ isNil(value) ? "" : String(value);
71
59
 
72
60
  React.useEffect(() => {
73
61
  if (!isInsideContainer) {
@@ -107,8 +95,10 @@ export const River = (props: Props) => {
107
95
  <ScreenResolver
108
96
  screenType={river.type}
109
97
  screenId={screenId}
110
- screenData={R.merge(river, { groupId: extraData?.groupId })}
111
- componentsMapExtraProps={componentsMapExtraProps}
98
+ screenData={Object.assign(river || {}, { groupId: extraData?.groupId })}
99
+ componentsMapExtraProps={Object.assign(componentsMapExtraProps || {}, {
100
+ groupId,
101
+ })}
112
102
  {...extraData}
113
103
  />
114
104
  );
@@ -121,6 +111,7 @@ export const River = (props: Props) => {
121
111
  isScreenWrappedInContainer={isInsideContainer}
122
112
  extraAnchorPointYOffset={extraAnchorPointYOffset}
123
113
  componentsMapExtraProps={componentsMapExtraProps}
114
+ groupId={groupId}
124
115
  />
125
116
  );
126
117
  };
@@ -1,9 +1,11 @@
1
1
  import { compose } from "ramda";
2
2
  import { River as RiverComponent } from "./River";
3
- import { withTvEventHandler } from "./withTVEventHandler";
4
3
  import { withComponentsMapOffsetContext } from "../../../Contexts/ComponentsMapOffsetContext";
4
+ import { withRiverDataLoader } from "./withRiverDataLoader";
5
+ import { withFocusableGroupForContent } from "./withFocusableGroupForContent";
5
6
 
6
7
  export const River = compose(
7
- withTvEventHandler,
8
- withComponentsMapOffsetContext
8
+ withComponentsMapOffsetContext,
9
+ withRiverDataLoader,
10
+ withFocusableGroupForContent
9
11
  )(RiverComponent);
@@ -0,0 +1,60 @@
1
+ import * as React from "react";
2
+ import { View } 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
+ import { toNumberWithDefaultZero } from "@applicaster/zapp-react-native-utils/numberUtils";
7
+
8
+ import { useSubscriberFor } from "@applicaster/zapp-react-native-utils/reactHooks/useSubscriberFor";
9
+ import { QBUIComponentEvents } from "@applicaster/zapp-react-native-ui-components/events";
10
+
11
+ import { QUICK_BRICK_TOP_CONTAINER } from "@applicaster/quick-brick-core/const";
12
+
13
+ const useTopMenuLayout = () => {
14
+ const [layout, setLayout] = React.useState(undefined);
15
+
16
+ const handleLayout = React.useCallback((layout) => {
17
+ setLayout(layout);
18
+ }, []);
19
+
20
+ useSubscriberFor(QBUIComponentEvents.topMenuBarTV_onLayout, handleLayout);
21
+
22
+ return layout;
23
+ };
24
+
25
+ export const withFocusableGroupForContent = (Component) => {
26
+ return function (props) {
27
+ const { screenId, isInsideContainer } = props;
28
+
29
+ const topMenuLayout = useTopMenuLayout();
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
+ const topMenuHeight = toNumberWithDefaultZero(topMenuLayout?.height);
45
+
46
+ return (
47
+ <FocusableGroup
48
+ key={focusableId}
49
+ id={focusableId}
50
+ // workaround to avoid intersection between FocusableGroup for content and FocusableGroup for top-menu
51
+ style={{ flex: 1, marginTop: topMenuHeight }}
52
+ groupId={QUICK_BRICK_TOP_CONTAINER}
53
+ >
54
+ <View style={{ flex: 1, marginTop: -1 * topMenuHeight }}>
55
+ <Component {...props} groupId={focusableId} />
56
+ </View>
57
+ </FocusableGroup>
58
+ );
59
+ };
60
+ };
@@ -0,0 +1,43 @@
1
+ import React, { useMemo } from "react";
2
+ import { path } from "ramda";
3
+
4
+ import {
5
+ useFeedLoader,
6
+ useRivers,
7
+ } from "@applicaster/zapp-react-native-utils/reactHooks";
8
+
9
+ type Props = {
10
+ screenId: string;
11
+ screenData: ZappRiver | ZappEntry;
12
+ feedUrl?: string;
13
+ river?: ZappRiver | ZappEntry;
14
+ };
15
+
16
+ export const withPipesV1DataLoader = (
17
+ WrappedComponent: React.ComponentType<any>
18
+ ) => {
19
+ return function WithPipesV1DataLoaderComponent(props: Props) {
20
+ const { screenId, feedUrl } = props;
21
+
22
+ const rivers = useRivers();
23
+
24
+ const river = React.useMemo(() => rivers?.[screenId], [screenId]);
25
+
26
+ const screenData = React.useMemo(
27
+ () => props.screenData || props.river || river,
28
+ [screenId]
29
+ );
30
+
31
+ const connectedScreenUrl = useMemo(() => {
32
+ // Avoid using feedUrl or content.src on layouts v2
33
+
34
+ return feedUrl || path(["content", "src"], screenData);
35
+ }, [feedUrl, screenData]);
36
+
37
+ const { data: feedData } = useFeedLoader({
38
+ feedUrl: connectedScreenUrl,
39
+ });
40
+
41
+ return <WrappedComponent {...props} feedData={feedData} />;
42
+ };
43
+ };