@applicaster/zapp-react-native-ui-components 14.0.0-alpha.8387612031 → 14.0.0-alpha.8557119261

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 (124) hide show
  1. package/Components/AnimatedInOut/index.tsx +5 -3
  2. package/Components/AudioPlayer/mobile/Layout.tsx +7 -4
  3. package/Components/AudioPlayer/mobile/__tests__/__snapshots__/audioPlayerMobileLayout.test.js.snap +1 -1
  4. package/Components/AudioPlayer/mobile/__tests__/audioPlayerMobileLayout.test.js +1 -1
  5. package/Components/AudioPlayer/mobile/index.tsx +7 -12
  6. package/Components/AudioPlayer/tv/Artwork.tsx +3 -2
  7. package/Components/AudioPlayer/tv/Channel.tsx +7 -7
  8. package/Components/AudioPlayer/tv/Layout.tsx +100 -93
  9. package/Components/AudioPlayer/tv/Runtime.tsx +7 -1
  10. package/Components/AudioPlayer/tv/Summary.tsx +6 -2
  11. package/Components/AudioPlayer/tv/Title.tsx +6 -2
  12. package/Components/AudioPlayer/tv/__tests__/__snapshots__/Runtime.test.js.snap +2 -2
  13. package/Components/AudioPlayer/tv/__tests__/__snapshots__/audioPlayer.test.js.snap +21 -27
  14. package/Components/AudioPlayer/tv/__tests__/__snapshots__/channel.test.js.snap +8 -17
  15. package/Components/AudioPlayer/tv/__tests__/__snapshots__/summary.test.js.snap +1 -2
  16. package/Components/AudioPlayer/tv/__tests__/__snapshots__/title.test.js.snap +1 -2
  17. package/Components/AudioPlayer/tv/__tests__/audioPlayer.test.js +4 -0
  18. package/Components/AudioPlayer/tv/helpers.tsx +10 -3
  19. package/Components/AudioPlayer/tv/index.tsx +9 -11
  20. package/Components/BaseFocusable/index.tsx +23 -12
  21. package/Components/Cell/__tests__/CellWIthFocusable.test.js +3 -2
  22. package/Components/Cell/index.js +7 -3
  23. package/Components/ComponentResolver/index.ts +1 -1
  24. package/Components/FeedLoader/FeedLoader.tsx +6 -15
  25. package/Components/FeedLoader/FeedLoaderHOC.tsx +21 -0
  26. package/Components/FeedLoader/index.js +2 -8
  27. package/Components/Focusable/Focusable.tsx +5 -3
  28. package/Components/Focusable/FocusableTvOS.tsx +3 -3
  29. package/Components/Focusable/FocusableiOS.tsx +2 -2
  30. package/Components/Focusable/__tests__/index.android.test.tsx +3 -0
  31. package/Components/Focusable/index.android.tsx +12 -8
  32. package/Components/Focusable/index.tsx +1 -1
  33. package/Components/FocusableList/index.tsx +4 -0
  34. package/Components/FreezeWithCallback/__tests__/index.test.tsx +67 -43
  35. package/Components/GeneralContentScreen/utils/__tests__/useCurationAPI.test.js +42 -59
  36. package/Components/GeneralContentScreen/utils/useCurationAPI.ts +13 -10
  37. package/Components/HandlePlayable/HandlePlayable.tsx +25 -9
  38. package/Components/Layout/TV/LayoutBackground.tsx +1 -1
  39. package/Components/Layout/TV/__tests__/index.test.tsx +0 -1
  40. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +2 -0
  41. package/Components/MasterCell/DefaultComponents/Button.tsx +1 -1
  42. package/Components/MasterCell/DefaultComponents/FocusableView/index.tsx +4 -27
  43. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +1 -1
  44. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +1 -1
  45. package/Components/MasterCell/DefaultComponents/__tests__/image.test.js +10 -10
  46. package/Components/MasterCell/DefaultComponents/__tests__/text.test.tsx +18 -18
  47. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/index.test.tsx +10 -10
  48. package/Components/MasterCell/elementMapper.tsx +1 -2
  49. package/Components/MasterCell/index.tsx +1 -1
  50. package/Components/MasterCell/utils/behaviorProvider.ts +82 -14
  51. package/Components/MasterCell/utils/index.ts +11 -5
  52. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +13 -18
  53. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +9 -0
  54. package/Components/OfflineHandler/__tests__/index.test.tsx +26 -35
  55. package/Components/PlayerContainer/ErrorDisplay/index.ts +1 -1
  56. package/Components/PlayerContainer/PlayerContainer.tsx +41 -28
  57. package/Components/PlayerContainer/ProgramInfo/index.tsx +1 -1
  58. package/Components/PlayerContainer/index.ts +1 -1
  59. package/Components/River/ComponentsMap/ComponentsMap.tsx +0 -1
  60. package/Components/River/ComponentsMap/hooks/__tests__/useLoadingState.test.ts +378 -0
  61. package/Components/River/ComponentsMap/hooks/useLoadingState.ts +2 -2
  62. package/Components/River/RefreshControl.tsx +11 -17
  63. package/Components/River/TV/River.tsx +2 -17
  64. package/Components/River/TV/index.tsx +3 -1
  65. package/Components/River/TV/withPipesV1DataLoader.tsx +43 -0
  66. package/Components/River/TV/withRiverDataLoader.tsx +17 -0
  67. package/Components/River/TV/withTVEventHandler.tsx +1 -1
  68. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +2 -0
  69. package/Components/River/__tests__/river.test.js +12 -26
  70. package/Components/River/index.tsx +1 -1
  71. package/Components/Screen/__tests__/Screen.test.tsx +28 -29
  72. package/Components/ScreenRevealManager/ScreenRevealManager.ts +76 -0
  73. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +107 -0
  74. package/Components/ScreenRevealManager/__tests__/withScreenRevealManager.test.tsx +96 -0
  75. package/Components/ScreenRevealManager/index.ts +1 -0
  76. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +79 -0
  77. package/Components/Tabs/TV/Tabs.android.tsx +0 -2
  78. package/Components/Tabs/Tabs.tsx +2 -3
  79. package/Components/Touchable/__tests__/__snapshots__/touchable.test.tsx.snap +34 -0
  80. package/Components/Touchable/__tests__/touchable.test.tsx +12 -17
  81. package/Components/Transitioner/__tests__/__snapshots__/Scene.test.js.snap +15 -9
  82. package/Components/VideoLive/animationUtils.ts +3 -3
  83. package/Components/VideoModal/ModalAnimation/AnimatedScrollModal.tsx +3 -9
  84. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +32 -8
  85. package/Components/VideoModal/PlayerDetails.tsx +24 -2
  86. package/Components/VideoModal/PlayerWrapper.tsx +26 -142
  87. package/Components/VideoModal/VideoModal.tsx +3 -17
  88. package/Components/VideoModal/__tests__/PlayerDetails.test.tsx +5 -5
  89. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +1 -7
  90. package/Components/VideoModal/__tests__/__snapshots__/PlayerWrapper.test.tsx.snap +44 -240
  91. package/Components/VideoModal/hooks/index.ts +0 -2
  92. package/Components/VideoModal/hooks/useModalSize.ts +18 -2
  93. package/Components/VideoModal/utils.ts +6 -0
  94. package/Components/Viewport/ViewportAware/__tests__/viewportAware.test.js +12 -16
  95. package/Components/Viewport/ViewportTracker/__tests__/viewportTracker.test.js +84 -24
  96. package/Components/Viewport/VisibilitySensor/VisibilitySensor.tsx +3 -3
  97. package/Components/default-cell-renderer/viewTrees/tv/DefaultCell/index.ts +3 -3
  98. package/Contexts/ConfigutaionContext/__tests__/ConfigurationProvider.test.tsx +3 -3
  99. package/Contexts/ScreenContext/index.tsx +46 -6
  100. package/Decorators/ConfigurationWrapper/__tests__/withConfigurationProvider.test.tsx +3 -3
  101. package/Decorators/ConfigurationWrapper/withConfigurationProvider.tsx +2 -2
  102. package/Decorators/RiverFeedLoader/__tests__/__snapshots__/riverFeedLoader.test.tsx.snap +221 -209
  103. package/Decorators/RiverFeedLoader/__tests__/riverFeedLoader.test.tsx +14 -16
  104. package/Decorators/RiverFeedLoader/__tests__/utils.test.ts +0 -20
  105. package/Decorators/RiverFeedLoader/index.tsx +22 -4
  106. package/Decorators/RiverFeedLoader/utils/index.ts +0 -18
  107. package/Decorators/RiverResolver/__tests__/riverResolver.test.tsx +3 -6
  108. package/Decorators/ZappPipesDataConnector/ResolverSelector.tsx +25 -0
  109. package/Decorators/ZappPipesDataConnector/__tests__/NullFeedResolver.test.tsx +78 -0
  110. package/Decorators/ZappPipesDataConnector/__tests__/ResolverSelector.test.tsx +205 -0
  111. package/Decorators/ZappPipesDataConnector/__tests__/StaticFeedResolver.test.tsx +251 -0
  112. package/Decorators/ZappPipesDataConnector/__tests__/UrlFeedResolver.test.tsx +368 -0
  113. package/Decorators/ZappPipesDataConnector/__tests__/utils.test.ts +39 -0
  114. package/Decorators/ZappPipesDataConnector/index.tsx +26 -293
  115. package/Decorators/ZappPipesDataConnector/resolvers/NullFeedResolver.tsx +25 -0
  116. package/Decorators/ZappPipesDataConnector/resolvers/StaticFeedResolver.tsx +87 -0
  117. package/Decorators/ZappPipesDataConnector/resolvers/UrlFeedResolver.tsx +266 -0
  118. package/Decorators/ZappPipesDataConnector/types.ts +29 -0
  119. package/Decorators/ZappPipesDataConnector/utils/mongoFilter.ts +738 -0
  120. package/Decorators/ZappPipesDataConnector/utils/useFilter.tsx +157 -0
  121. package/events/index.ts +1 -0
  122. package/package.json +5 -10
  123. package/Components/River/__tests__/__snapshots__/river.test.js.snap +0 -27
  124. package/Components/VideoModal/hooks/useBackgroundColor.ts +0 -10
@@ -1,7 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { useEffect, useReducer } from "react";
3
- // @ts-ignore
4
- import { TVMenuControl, View, ViewStyle } from "react-native";
3
+ import { View, ViewStyle } from "react-native";
5
4
  import * as R from "ramda";
6
5
  import uuid from "uuid/v4";
7
6
  import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils/playerManager";
@@ -62,6 +61,11 @@ import {
62
61
  useModalAnimationContext,
63
62
  } from "@applicaster/zapp-react-native-ui-components/Components/VideoModal/ModalAnimation";
64
63
 
64
+ import {
65
+ PlayerNativeCommandTypes,
66
+ PlayerNativeSendCommand,
67
+ } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand";
68
+
65
69
  type Props = {
66
70
  Player: React.ComponentType<any>;
67
71
  PlayerLoadingView?: React.ComponentType<any>; // 👀 we are not receiving this prop
@@ -88,7 +92,7 @@ export const VideoModalMode = {
88
92
  MAXIMIZED: "MAXIMIZED",
89
93
  MINIMIZED: "MINIMIZED",
90
94
  FULLSCREEN: "FULLSCREEN",
91
- };
95
+ } as const;
92
96
 
93
97
  export type PlayNextData = {
94
98
  state: PlayNextState;
@@ -127,7 +131,7 @@ const webStyles = {
127
131
  playerScreen: {
128
132
  flex: 1,
129
133
  height: "100vh",
130
- background: "black",
134
+ backgroundColor: "black",
131
135
  },
132
136
  playerWrapper: {
133
137
  height: "100%",
@@ -145,7 +149,6 @@ const nativeStyles = {
145
149
  },
146
150
  playerScreen: {
147
151
  flex: 1,
148
- backgroundColor: "black",
149
152
  overflow: "hidden",
150
153
  },
151
154
  playerWrapper: {
@@ -260,9 +263,15 @@ const PlayerContainerComponent = (props: Props) => {
260
263
  return;
261
264
  }
262
265
 
266
+ // send command to clear and stop player
267
+ PlayerNativeSendCommand(
268
+ PlayerNativeCommandTypes.clearPlayerData,
269
+ state.playerId
270
+ );
271
+
263
272
  showNavBar(true);
264
273
  navigator.goBack();
265
- }, [isModal, navigator.goBack, showNavBar]);
274
+ }, [isModal, navigator.goBack, state.playerId, showNavBar]);
266
275
 
267
276
  const playEntry = (entry) => navigator.replaceTop(entry, { mode });
268
277
 
@@ -390,13 +399,17 @@ const PlayerContainerComponent = (props: Props) => {
390
399
  }
391
400
  };
392
401
 
393
- const playerRemoteHandler = (event, isLanguageOverlayVisible) => {
394
- const { eventType } = event;
402
+ const playerRemoteHandler = React.useCallback(
403
+ (isLanguageOverlayVisible = false) =>
404
+ (event) => {
405
+ const { eventType } = event;
395
406
 
396
- if (!isLanguageOverlayVisible && eventType === "menu") {
397
- close();
398
- }
399
- };
407
+ if (!isLanguageOverlayVisible && eventType === "menu") {
408
+ close();
409
+ }
410
+ },
411
+ [close]
412
+ );
400
413
 
401
414
  // Effects
402
415
  useEffect(() => {
@@ -509,16 +522,6 @@ const PlayerContainerComponent = (props: Props) => {
509
522
  }
510
523
  }, [isAudioContent]);
511
524
 
512
- // Needs to handle back button on Apple TV
513
- // https://github.com/facebook/react-native/issues/18930
514
- useEffect(() => {
515
- TVMenuControl?.enableTVMenuKey();
516
-
517
- return () => {
518
- TVMenuControl?.disableTVMenuKey();
519
- };
520
- }, []);
521
-
522
525
  useEffect(() => {
523
526
  playerEvent("source_changed", { item });
524
527
 
@@ -565,8 +568,9 @@ const PlayerContainerComponent = (props: Props) => {
565
568
  const isInlineTV = isInlineTVUtil(screenData);
566
569
 
567
570
  const inline =
568
- [VideoModalMode.MAXIMIZED, VideoModalMode.MINIMIZED].includes(mode) ||
569
- isInlineTV;
571
+ [VideoModalMode.MAXIMIZED, VideoModalMode.MINIMIZED].includes(
572
+ mode as any
573
+ ) || isInlineTV;
570
574
 
571
575
  const value = React.useMemo(
572
576
  () => ({ playerId: state.playerId }),
@@ -587,7 +591,11 @@ const PlayerContainerComponent = (props: Props) => {
587
591
  );
588
592
  }
589
593
 
590
- if (screen_background_color && mode !== VideoModalMode.FULLSCREEN) {
594
+ if (
595
+ screen_background_color &&
596
+ mode !== VideoModalMode.FULLSCREEN &&
597
+ isTV()
598
+ ) {
591
599
  updatedStyles.playerScreen.backgroundColor = screen_background_color;
592
600
  }
593
601
 
@@ -617,6 +625,8 @@ const PlayerContainerComponent = (props: Props) => {
617
625
  playNextData,
618
626
  };
619
627
 
628
+ const pointerEventsProp = mode === "MINIMIZED" ? "box-none" : "auto";
629
+
620
630
  return (
621
631
  <PlayerStateContext.Provider value={value}>
622
632
  <PlayerContainerContextProvider
@@ -627,9 +637,9 @@ const PlayerContainerComponent = (props: Props) => {
627
637
  <PlayerContainerContext.Consumer>
628
638
  {(context) => (
629
639
  <TVEventHandlerComponent
630
- tvEventHandler={(_component, event) =>
631
- playerRemoteHandler(event, context.isLanguageOverlayVisible)
632
- }
640
+ tvEventHandler={playerRemoteHandler(
641
+ context.isLanguageOverlayVisible
642
+ )}
633
643
  >
634
644
  <FocusableGroup
635
645
  id={FocusableGroupMainContainerId}
@@ -637,14 +647,17 @@ const PlayerContainerComponent = (props: Props) => {
637
647
  preferredFocus
638
648
  shouldUsePreferredFocus
639
649
  groupId={groupId}
650
+ pointerEvents={pointerEventsProp}
640
651
  >
641
652
  {/* Video player and components */}
642
653
  <View
643
654
  style={styles.playerScreen}
644
655
  testID={"player-screen-container"}
656
+ pointerEvents={pointerEventsProp}
645
657
  >
646
658
  {/* Player container */}
647
659
  <View
660
+ pointerEvents={pointerEventsProp}
648
661
  style={[
649
662
  styles.playerWrapper,
650
663
  // eslint-disable-next-line react-native/no-inline-styles, react-native/no-color-literals
@@ -1,4 +1,4 @@
1
- import { connectToStore } from "@applicaster/zapp-react-native-redux";
1
+ import { connectToStore } from "@applicaster/zapp-react-native-redux/utils/connectToStore";
2
2
  import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
3
3
  import { styleKeys } from "@applicaster/zapp-react-native-utils/styleKeysUtils";
4
4
  import { transformColorCode as fixColorHexCode } from "@applicaster/zapp-react-native-utils/transform";
@@ -1,6 +1,6 @@
1
1
  import * as R from "ramda";
2
2
 
3
- import { connectToStore } from "@applicaster/zapp-react-native-redux";
3
+ import { connectToStore } from "@applicaster/zapp-react-native-redux/utils/connectToStore";
4
4
  import { loadPipesData } from "@applicaster/zapp-react-native-redux/ZappPipes";
5
5
 
6
6
  import { PlayerContainer as PlayerContainerComponent } from "./PlayerContainer";
@@ -286,7 +286,6 @@ function ComponentsMapComponent(props: Props) {
286
286
  initialNumToRender={3}
287
287
  maxToRenderPerBatch={10}
288
288
  windowSize={12}
289
- listKey={riverId}
290
289
  keyExtractor={keyExtractor}
291
290
  renderItem={renderRiverItem}
292
291
  data={riverComponents}
@@ -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) {