@applicaster/zapp-react-native-utils 14.0.0-alpha.4274952546 → 14.0.0-alpha.4517121861

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 (29) hide show
  1. package/actionsExecutor/ActionExecutorContext.tsx +1 -1
  2. package/analyticsUtils/AnalyticsEvents/helper.ts +81 -0
  3. package/analyticsUtils/AnalyticsEvents/sendHeaderClickEvent.ts +1 -1
  4. package/analyticsUtils/AnalyticsEvents/sendMenuClickEvent.ts +1 -2
  5. package/analyticsUtils/AnalyticsEvents/sendOnClickEvent.ts +14 -4
  6. package/analyticsUtils/__tests__/analyticsUtils.test.js +14 -0
  7. package/analyticsUtils/events.ts +8 -0
  8. package/analyticsUtils/index.tsx +4 -3
  9. package/analyticsUtils/manager.ts +1 -1
  10. package/appUtils/accessibilityManager/index.ts +3 -3
  11. package/focusManager/FocusManager.ts +26 -16
  12. package/focusManager/Tree.ts +25 -21
  13. package/focusManager/__tests__/FocusManager.test.ts +50 -8
  14. package/manifestUtils/defaultManifestConfigurations/player.js +8 -0
  15. package/package.json +2 -2
  16. package/playerUtils/getPlayerActionButtons.ts +1 -1
  17. package/reactHooks/autoscrolling/__tests__/useTrackedView.test.tsx +13 -12
  18. package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +88 -39
  19. package/reactHooks/feed/useBatchLoading.ts +1 -1
  20. package/reactHooks/feed/usePipesCacheReset.ts +1 -1
  21. package/reactHooks/navigation/index.ts +2 -2
  22. package/reactHooks/navigation/useIsScreenActive.ts +9 -5
  23. package/reactHooks/screen/useScreenContext.ts +1 -1
  24. package/reactHooks/state/__tests__/ZStoreProvider.test.tsx +2 -1
  25. package/riverComponetsMeasurementProvider/index.tsx +1 -1
  26. package/services/js2native.ts +1 -0
  27. package/testUtils/index.tsx +8 -7
  28. package/time/BackgroundTimer.ts +5 -3
  29. package/utils/index.ts +4 -0
@@ -25,7 +25,7 @@ import {
25
25
  resolveObjectValues,
26
26
  } from "../appUtils/contextKeysManager/contextResolver";
27
27
  import { useNavigation } from "../reactHooks";
28
- import { get } from "lodash";
28
+ import { get } from "../utils";
29
29
 
30
30
  import {
31
31
  useContentTypes,
@@ -4,9 +4,12 @@ import {
4
4
  ANALYTICS_COMPONENT_EVENTS,
5
5
  ANALYTICS_CORE_EVENTS,
6
6
  ANALYTICS_ENTRY_EVENTS,
7
+ ANALYTICS_PREFERENCES_EVENTS,
7
8
  DOWNLOADS_EVENTS,
8
9
  } from "../events";
9
10
  import { isEmptyOrNil } from "../../cellUtils";
11
+ import { get } from "lodash";
12
+ import { StorageMultiSelectProvider } from "@applicaster/zapp-react-native-bridge/ZappStorage/StorageMultiSelectProvider";
10
13
 
11
14
  export enum OfflineItemState {
12
15
  notExist = "NOT_EXISTS",
@@ -102,6 +105,84 @@ export function eventForComponent(
102
105
  return analyticsProps;
103
106
  }
104
107
 
108
+ /**
109
+ * Checks if an item is currently selected in localStorage based on its actions
110
+ * @param item - The item to check
111
+ * @returns boolean indicating if the item is currently selected
112
+ */
113
+ function isItemPreviouslySelected(item: any): boolean {
114
+ const actions = item?.extensions?.tap_actions?.actions;
115
+
116
+ if (!actions) {
117
+ return false;
118
+ }
119
+
120
+ const localStorageAction = actions.find(
121
+ (action) => action?.type === "localStorageToggleFlag"
122
+ );
123
+
124
+ if (!localStorageAction?.options?.key) {
125
+ return false;
126
+ }
127
+
128
+ const keyNamespace = localStorageAction.options.key;
129
+
130
+ const tag = localStorageAction.options?.selector
131
+ ? get(item, localStorageAction.options.selector)
132
+ : (item.extensions?.tag ?? item.id);
133
+
134
+ if (!tag) {
135
+ return false;
136
+ }
137
+
138
+ try {
139
+ const multiSelectProvider =
140
+ StorageMultiSelectProvider.getProvider(keyNamespace);
141
+
142
+ const selectedItems = multiSelectProvider.getSelectedItems();
143
+
144
+ return selectedItems.includes(tag);
145
+ } catch (error) {
146
+ return false;
147
+ }
148
+ }
149
+
150
+ export function getLocalStorageSetPayload(extraProps) {
151
+ const { item } = extraProps;
152
+
153
+ const hasLocalStorageSetAction = item?.extensions?.tap_actions?.actions?.some(
154
+ (action) => action?.type === "localStorageSet"
155
+ );
156
+
157
+ if (!hasLocalStorageSetAction) {
158
+ return null;
159
+ }
160
+
161
+ return {
162
+ [ANALYTICS_PREFERENCES_EVENTS.ITEM_SELECTED_STATUS]: true,
163
+ };
164
+ }
165
+
166
+ export function getLocalStorageToggleFlagPayload(extraProps) {
167
+ const { item } = extraProps;
168
+
169
+ const hasLocalStorageToggleAction =
170
+ item?.extensions?.tap_actions?.actions?.some(
171
+ (action) => action?.type === "localStorageToggleFlag"
172
+ );
173
+
174
+ if (!hasLocalStorageToggleAction) {
175
+ return null;
176
+ }
177
+
178
+ const previouslySelected = isItemPreviouslySelected(item);
179
+
180
+ return {
181
+ [ANALYTICS_PREFERENCES_EVENTS.ITEM_SELECTED_STATUS]: !previouslySelected,
182
+ [ANALYTICS_PREFERENCES_EVENTS.PREVIOUS_SELECTED_STATE]: previouslySelected,
183
+ };
184
+ }
185
+
105
186
  export function playEventForType(item) {
106
187
  const itemType = item?.type && item.type?.value;
107
188
 
@@ -4,7 +4,7 @@ import { postAnalyticEvent } from "../manager";
4
4
  import { ANALYTICS_CORE_EVENTS } from "../events";
5
5
 
6
6
  type SendHeaderClickEventProps = {
7
- extraProps: Record<string, any>;
7
+ extraProps: ExtraProps;
8
8
  component?: ZappUIComponent;
9
9
  zappPipesData?: ZappPipesData;
10
10
  item?: ZappEntry;
@@ -1,11 +1,10 @@
1
- /// <reference types="../../" />
2
1
  import { log_error, log_debug } from "../logger";
3
2
  import { replaceAnalyticsPropsNils } from "./helper";
4
3
  import { postAnalyticEvent } from "../manager";
5
4
 
6
5
  import { ANALYTICS_CORE_EVENTS } from "../events";
7
6
 
8
- type AnalyticsDefaultHelperProperties = {
7
+ declare type AnalyticsDefaultHelperProperties = {
9
8
  analyticsScreenData: AnalyticsScreenProperties;
10
9
  extraProps: any;
11
10
  props;
@@ -1,13 +1,13 @@
1
1
  import { log_error, log_debug } from "../logger";
2
-
3
- import { ANALYTICS_CORE_EVENTS } from "../events";
4
-
2
+ import { ANALYTICS_CORE_EVENTS, ACTION_TYPE } from "../events";
5
3
  import { postAnalyticEvent } from "../manager";
6
4
  import {
7
5
  replaceAnalyticsPropsNils,
8
6
  eventForEntry,
9
7
  eventForComponent,
10
8
  extensionsEvents,
9
+ getLocalStorageSetPayload,
10
+ getLocalStorageToggleFlagPayload,
11
11
  } from "./helper";
12
12
 
13
13
  declare type AnalyticsDefaultHelperProperties = {
@@ -26,7 +26,16 @@ export const sendOnClickEvent = ({
26
26
  const castedExtraProps: ExtraProps = extraProps;
27
27
  const componentData = component || extraProps.component;
28
28
  const data = zappPipesData || extraProps.zappPipesData;
29
- const eventName = ANALYTICS_CORE_EVENTS.TAP_CELL;
29
+
30
+ const actionCellPayload =
31
+ extraProps?.item?.type?.value === ACTION_TYPE
32
+ ? getLocalStorageSetPayload(extraProps) ||
33
+ getLocalStorageToggleFlagPayload(extraProps)
34
+ : null;
35
+
36
+ const eventName = actionCellPayload
37
+ ? ANALYTICS_CORE_EVENTS.TAP_SELECTABLE_CELL
38
+ : ANALYTICS_CORE_EVENTS.TAP_CELL;
30
39
 
31
40
  if (!analyticsScreenData) {
32
41
  log_error(
@@ -44,6 +53,7 @@ export const sendOnClickEvent = ({
44
53
  ...replaceAnalyticsPropsNils({
45
54
  ...analyticsScreenData,
46
55
  }),
56
+ ...actionCellPayload,
47
57
  };
48
58
 
49
59
  if (analyticsCustomProperties) {
@@ -3,8 +3,22 @@ import { ANALYTICS_CORE_EVENTS } from "../events";
3
3
 
4
4
  jest.mock("@applicaster/zapp-react-native-utils/reactUtils", () => ({
5
5
  isWeb: jest.fn(),
6
+ platformSelect: jest.fn(
7
+ (options) => options.android || options.ios || options.web
8
+ ),
6
9
  }));
7
10
 
11
+ jest.mock(
12
+ "@applicaster/zapp-react-native-bridge/ZappStorage/StorageMultiSelectProvider",
13
+ () => ({
14
+ StorageMultiSelectProvider: {
15
+ getProvider: jest.fn(() => ({
16
+ getSelectedItems: jest.fn(() => []),
17
+ })),
18
+ },
19
+ })
20
+ );
21
+
8
22
  const mock_postAnalyticEvent = jest.fn();
9
23
  const mock_startAnalyticsTimedEvent = jest.fn();
10
24
  const mock_endAnalyticsTimedEvent = jest.fn();
@@ -17,8 +17,11 @@ export const SCREEN_VIEW_EVENTS = {
17
17
  TIME_ON_SCREEN: "time_on_screen",
18
18
  };
19
19
 
20
+ export const ACTION_TYPE = "action";
21
+
20
22
  export const TAPPING_EVENTS = {
21
23
  TAP_CELL: "tap_cell",
24
+ TAP_SELECTABLE_CELL: "tap_selectable_cell",
22
25
  TAP_MENU: "tap_menu",
23
26
  TAP_NAVBAR_BACK_BUTTON: "tap_navbar_back_button",
24
27
  };
@@ -99,6 +102,11 @@ export const ANALYTICS_COMPONENT_EVENTS = {
99
102
  COMPONENT_SOURCE: "component_source",
100
103
  };
101
104
 
105
+ export const ANALYTICS_PREFERENCES_EVENTS = {
106
+ ITEM_SELECTED_STATUS: "item_selected_status",
107
+ PREVIOUS_SELECTED_STATE: "previous_selected_state",
108
+ };
109
+
102
110
  // ---------------- EVENTS ---------------------
103
111
  export const AD_EVENT = {
104
112
  ad_break_start: "player_ad_break_start",
@@ -1,3 +1,4 @@
1
+ /// <reference types="@applicaster/zapp-react-native-utils" />
1
2
  import * as R from "ramda";
2
3
  import * as React from "react";
3
4
  import { isWeb } from "@applicaster/zapp-react-native-utils/reactUtils";
@@ -30,7 +31,7 @@ import { ANALYTICS_CORE_EVENTS } from "./events";
30
31
  import { noop } from "../functionUtils";
31
32
 
32
33
  type ComponentWithChildrenProps = {
33
- children: React.ReactElement;
34
+ children: React.ReactChildren;
34
35
  };
35
36
 
36
37
  export function sendSelectCellEvent(item, component, headerTitle, itemIndex) {
@@ -119,11 +120,11 @@ export function getAnalyticsFunctions({
119
120
  export const AnalyticsContext =
120
121
  React.createContext<GetAnalyticsFunctions>(noop);
121
122
 
122
- export function AnalyticsProvider({ children }: ComponentWithChildrenProps) {
123
+ export function AnalyticsProvider(props: ComponentWithChildrenProps) {
123
124
  return (
124
125
  // @ts-ignore - this is a valid context provider
125
126
  <AnalyticsContext.Provider value={getAnalyticsFunctions}>
126
- {children}
127
+ {props?.children}
127
128
  </AnalyticsContext.Provider>
128
129
  );
129
130
  }
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable @typescript-eslint/no-use-before-define */
2
2
  import * as R from "ramda";
3
3
  import { NativeModules } from "react-native";
4
- import { ANALYTICS_CORE_EVENTS } from "./events";
4
+ import { ANALYTICS_CORE_EVENTS } from "@applicaster/zapp-react-native-utils/analyticsUtils/events";
5
5
 
6
6
  import { analyticsUtilsLogger } from "./logger";
7
7
 
@@ -1,9 +1,9 @@
1
1
  import { BehaviorSubject } from "rxjs";
2
2
  import { accessibilityManagerLogger as logger } from "./logger";
3
- import { TTSManager } from "../platform/platformUtils";
3
+ import { TTSManager } from "../platform";
4
4
  import { BUTTON_ACCESSIBILITY_KEYS } from "./const";
5
5
  import { AccessibilityRole } from "react-native";
6
- import _ from "lodash";
6
+ import { toString } from "../../utils";
7
7
 
8
8
  export class AccessibilityManager {
9
9
  private static _instance: AccessibilityManager | null = null;
@@ -137,7 +137,7 @@ export class AccessibilityManager {
137
137
  }
138
138
 
139
139
  public getButtonAccessibilityProps(name: string): AccessibilityProps {
140
- const buttonName = _.toString(name);
140
+ const buttonName = toString(name);
141
141
 
142
142
  const buttonConfig = BUTTON_ACCESSIBILITY_KEYS[buttonName];
143
143
 
@@ -176,18 +176,30 @@ class FocusManager {
176
176
  }
177
177
  }
178
178
 
179
- registerFocusable(
180
- component: FocusManager.TouchableReactRef,
181
- parentFocusable: FocusManager.TouchableReactRef,
182
- isFocusableCell: boolean
183
- ) {
184
- const focusableId = getFocusableId(component);
179
+ registerFocusable({
180
+ touchableRef,
181
+ parentFocusableRef,
182
+ isFocusableCell,
183
+ parentFocusableId,
184
+ }: {
185
+ touchableRef: FocusManager.TouchableReactRef;
186
+ parentFocusableRef: FocusManager.TouchableReactRef;
187
+ isFocusableCell: boolean;
188
+ parentFocusableId: string;
189
+ }) {
190
+ const focusableId = getFocusableId(touchableRef);
191
+
185
192
  const focusableComponent = FocusManager.findFocusable(focusableId);
186
193
 
187
- if (!focusableComponent && component) {
188
- this.focusableComponents.push(component);
194
+ if (!focusableComponent && touchableRef) {
195
+ this.focusableComponents.push(touchableRef);
189
196
 
190
- this.tree.add(component, parentFocusable, isFocusableCell);
197
+ this.tree.add(
198
+ touchableRef,
199
+ parentFocusableRef,
200
+ isFocusableCell,
201
+ parentFocusableId
202
+ );
191
203
  } else {
192
204
  logger.warning("Focusable component already registered", {
193
205
  id: focusableId,
@@ -243,12 +255,10 @@ class FocusManager {
243
255
  }
244
256
 
245
257
  blurPrevious(options?: FocusManager.Android.CallbackOptions) {
246
- if (options) {
247
- FocusManager.instance.prevFocused?.onBlur?.(
248
- FocusManager.instance.prevFocused,
249
- options
250
- );
251
- }
258
+ FocusManager.instance.prevFocused?.onBlur?.(
259
+ FocusManager.instance.prevFocused,
260
+ options ?? {} // Adding fallback to avoid potential regression caused by #7509
261
+ );
252
262
  }
253
263
 
254
264
  onDisableFocusChange = (id) => {
@@ -269,7 +279,7 @@ class FocusManager {
269
279
 
270
280
  if (nextFocus) {
271
281
  // HACK: hack to fix the hack below
272
- // HACK: putting call to the end of the event loop so the next component has a chane to be registered
282
+ // HACK: putting call to the end of the event loop so the next component has a chance to be registered
273
283
  setTimeout(() => {
274
284
  FocusManager.instance.setFocus(nextFocus, {
275
285
  direction: "down",
@@ -8,37 +8,41 @@ export class Tree {
8
8
  this.tree = focusManagerTree;
9
9
  }
10
10
 
11
- add(component, parentFocusable, isFocusableCell) {
12
- const focusableId = getFocusableId(component);
13
- const parentId = getFocusableId(parentFocusable);
11
+ add(
12
+ touchableRef: FocusManager.TouchableReactRef,
13
+ parentFocusableRef: FocusManager.TouchableReactRef,
14
+ isFocusableCell: boolean,
15
+ parentFocusableId: string
16
+ ) {
17
+ const focusableId = getFocusableId(touchableRef);
18
+ const parentId = getFocusableId(parentFocusableRef) || parentFocusableId;
14
19
  const focusableComponentInTree = this.find(focusableId);
15
20
 
16
21
  // update node if it already exists
17
22
  if (focusableComponentInTree) {
18
- focusableComponentInTree.updateNode(component);
23
+ focusableComponentInTree.updateNode(touchableRef);
19
24
  }
20
25
 
21
- if (parentFocusable?.current) {
22
- if (!this.find(parentId)) {
23
- this.tree.push(new TreeNode(null, parentId, null, isFocusableCell));
24
- }
26
+ if (!this.find(parentId)) {
27
+ // create temporary node to the root of the tree
28
+ this.tree.push(new TreeNode(null, parentId, null, isFocusableCell));
29
+ }
25
30
 
26
- const parentNode = this.find(parentId);
31
+ const parentNode = this.find(parentId);
27
32
 
28
- if (parentNode) {
29
- if (focusableComponentInTree) {
30
- focusableComponentInTree.isFocusableCell = isFocusableCell;
31
- focusableComponentInTree.parentId = parentNode.id;
33
+ if (parentNode) {
34
+ if (focusableComponentInTree) {
35
+ focusableComponentInTree.isFocusableCell = isFocusableCell;
36
+ focusableComponentInTree.parentId = parentNode.id;
32
37
 
33
- parentNode.addChild(focusableComponentInTree);
38
+ parentNode.addChild(focusableComponentInTree);
34
39
 
35
- // remove root object from the list
36
- this.tree = this.tree.filter(
37
- (node) => node !== focusableComponentInTree
38
- );
39
- } else {
40
- parentNode.addChild(component, focusableId, isFocusableCell);
41
- }
40
+ // remove root object from the list
41
+ this.tree = this.tree.filter(
42
+ (node) => node !== focusableComponentInTree
43
+ );
44
+ } else {
45
+ parentNode.addChild(touchableRef, focusableId, isFocusableCell);
42
46
  }
43
47
  }
44
48
  }
@@ -1,5 +1,8 @@
1
1
  import { focusManager } from "../FocusManager";
2
2
 
3
+ const isFocusableCell = true;
4
+ const parentFocusableId = "parentFocusableId";
5
+
3
6
  const group = {
4
7
  current: {
5
8
  props: {
@@ -62,13 +65,47 @@ jest.useFakeTimers();
62
65
 
63
66
  describe("FocusManager", () => {
64
67
  beforeAll(() => {
65
- focusManager.registerFocusable(group, { current: null });
66
- focusManager.registerFocusable(child1, group);
67
- focusManager.registerFocusable(child2, group);
68
- focusManager.registerFocusable(child3, child2);
69
-
70
- focusManager.registerFocusable(child4, child2);
71
- focusManager.registerFocusable(child5, child2);
68
+ focusManager.registerFocusable({
69
+ touchableRef: group,
70
+ parentFocusableRef: { current: null },
71
+ isFocusableCell,
72
+ parentFocusableId,
73
+ });
74
+
75
+ focusManager.registerFocusable({
76
+ touchableRef: child1,
77
+ parentFocusableRef: group,
78
+ isFocusableCell,
79
+ parentFocusableId,
80
+ });
81
+
82
+ focusManager.registerFocusable({
83
+ touchableRef: child2,
84
+ parentFocusableRef: group,
85
+ isFocusableCell,
86
+ parentFocusableId,
87
+ });
88
+
89
+ focusManager.registerFocusable({
90
+ touchableRef: child3,
91
+ parentFocusableRef: child2,
92
+ isFocusableCell,
93
+ parentFocusableId,
94
+ });
95
+
96
+ focusManager.registerFocusable({
97
+ touchableRef: child4,
98
+ parentFocusableRef: child2,
99
+ isFocusableCell,
100
+ parentFocusableId,
101
+ });
102
+
103
+ focusManager.registerFocusable({
104
+ touchableRef: child5,
105
+ parentFocusableRef: child2,
106
+ isFocusableCell,
107
+ parentFocusableId,
108
+ });
72
109
  });
73
110
 
74
111
  it("focusManager should be defined", () => {
@@ -199,7 +236,12 @@ describe("FocusManager", () => {
199
236
  });
200
237
 
201
238
  it("focusManager registerFocusable should register", () => {
202
- focusManager.registerFocusable(child5, child2);
239
+ focusManager.registerFocusable({
240
+ touchableRef: child5,
241
+ parentFocusableRef: child2,
242
+ isFocusableCell,
243
+ parentFocusableId,
244
+ });
203
245
 
204
246
  expect(
205
247
  focusManager.isFocusableChildOf(child5.current.props.id, child2)
@@ -2985,6 +2985,14 @@ function getPlayerConfiguration({ platform, version }) {
2985
2985
  type: "uploader",
2986
2986
  default: "",
2987
2987
  },
2988
+ {
2989
+ key: "audio_player_background_image_overlay",
2990
+ label: "Background Image Overlay",
2991
+ label_tooltip:
2992
+ "Add a semi-transparent color overlay to improve text readability over the background image.",
2993
+ type: "color_picker_rgba",
2994
+ initial_value: "rgba(17, 17, 17, 0.5)",
2995
+ },
2988
2996
  {
2989
2997
  type: "text_input",
2990
2998
  label: "Item Image Key",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "14.0.0-alpha.4274952546",
3
+ "version": "14.0.0-alpha.4517121861",
4
4
  "description": "Applicaster Zapp React Native utilities package",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "homepage": "https://github.com/applicaster/quickbrick#readme",
29
29
  "dependencies": {
30
- "@applicaster/applicaster-types": "14.0.0-alpha.4274952546",
30
+ "@applicaster/applicaster-types": "14.0.0-alpha.4517121861",
31
31
  "buffer": "^5.2.1",
32
32
  "camelize": "^1.0.0",
33
33
  "dayjs": "^1.11.10",
@@ -1,4 +1,4 @@
1
- import { take, map, trim } from "lodash";
1
+ import { map, take, trim } from "../utils";
2
2
  import { selectActionButtons } from "../conf/player/selectors";
3
3
 
4
4
  /**
@@ -1,27 +1,25 @@
1
1
  import React from "react";
2
2
 
3
- import { renderHook } from "@testing-library/react-hooks";
4
- import { act, waitFor } from "@testing-library/react-native";
3
+ import { act, renderHook } from "@testing-library/react-hooks";
5
4
  import { Provider } from "react-redux";
6
5
  import configureStore from "redux-mock-store";
7
- import { useTrackedView } from "../useTrackedView";
8
6
 
9
7
  const mockUpdateComponentsPositions = jest.fn();
10
8
 
11
9
  jest.mock(
12
10
  "@applicaster/zapp-react-native-ui-components/Contexts/ScreenTrackedViewPositionsContext",
13
11
  () => ({
14
- useScreenTrackedViewPositionsContext: jest.fn(() => ({
12
+ useScreenTrackedViewPositionsContext: jest.fn().mockReturnValue({
15
13
  updateComponentsPositions: mockUpdateComponentsPositions,
16
14
  value: {
17
15
  "123": { componentId: "123", centerX: 0.4, centerY: 0.5 },
18
16
  "124": { componentId: "124", centerX: 0.2, centerY: 0.3 },
19
17
  },
20
- })),
18
+ }),
21
19
  })
22
20
  );
23
21
 
24
- jest.useFakeTimers();
22
+ jest.useFakeTimers({ legacyFakeTimers: true });
25
23
 
26
24
  jest.mock("@applicaster/zapp-react-native-utils/reactHooks/navigation");
27
25
 
@@ -34,8 +32,10 @@ const Wrapper = ({ children }: { children: React.ReactChild }) => (
34
32
  <Provider store={store}>{children}</Provider>
35
33
  );
36
34
 
35
+ const { useTrackedView } = require("../useTrackedView");
36
+
37
37
  describe("useTrackCurrentAutoScrollingElement", () => {
38
- it("should update position for selected component - onViewportEnter", async () => {
38
+ it("should update position for selected component - onViewportEnter", () => {
39
39
  const { result } = renderHook(() => useTrackedView("123"), {
40
40
  wrapper: Wrapper,
41
41
  });
@@ -46,13 +46,14 @@ describe("useTrackCurrentAutoScrollingElement", () => {
46
46
  rect: { left: 1, right: 1, top: 1, bottom: 1 },
47
47
  };
48
48
 
49
- act(() => {
50
- result.current.onPositionUpdated(mockRect);
49
+ act(async () => {
50
+ await result.current.onPositionUpdated(mockRect);
51
51
  });
52
52
 
53
- await waitFor(() => {
54
- expect(result.current.inViewPort).toBe(true);
55
- });
53
+ // Fast-forward until all timers have been executed
54
+ jest.runAllTimers();
55
+
56
+ expect(result.current.inViewPort).toBe(true);
56
57
 
57
58
  expect(mockUpdateComponentsPositions).toHaveBeenCalledWith(
58
59
  "123",
@@ -1,7 +1,8 @@
1
1
  import { renderHook } from "@testing-library/react-hooks";
2
- import { allFeedsIsReady, useBatchLoading } from "../useBatchLoading";
3
- import { WrappedWithProviders } from "@applicaster/zapp-react-native-utils/testUtils";
4
- import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
2
+ import * as reduxMockStore from "redux-mock-store";
3
+ import thunk from "redux-thunk";
4
+ import React from "react";
5
+ import * as ReactRedux from "react-redux";
5
6
 
6
7
  jest.mock("../../navigation");
7
8
 
@@ -12,18 +13,17 @@ jest.mock(
12
13
  })
13
14
  );
14
15
 
15
- const wrapper = WrappedWithProviders;
16
+ const useBatchLoading = require("../useBatchLoading").useBatchLoading;
17
+ const allFeedsIsReady = require("../useBatchLoading").allFeedsIsReady;
18
+
19
+ const mockStore = reduxMockStore.default([thunk]);
20
+
21
+ const wrapper: React.FC<any> = ({ children, store }) => (
22
+ <ReactRedux.Provider store={store}>{children}</ReactRedux.Provider>
23
+ );
16
24
 
17
25
  describe("useBatchLoading", () => {
18
- const data = [
19
- { data: { source: "url1" }, component_type: "any" },
20
- { data: { source: "url2" }, component_type: "any" },
21
- { data: { source: "url3" }, component_type: "any" },
22
- { data: { source: "url4" }, component_type: "any" },
23
- { data: { source: "url5" }, component_type: "any" },
24
- { data: { source: "url6" }, component_type: "any" },
25
- // ... more items
26
- ];
26
+ const useDispatchSpy = jest.spyOn(ReactRedux, "useDispatch");
27
27
 
28
28
  beforeAll(() => {
29
29
  jest.useFakeTimers();
@@ -34,7 +34,7 @@ describe("useBatchLoading", () => {
34
34
  });
35
35
 
36
36
  it("loadPipesData start loading not started requests", () => {
37
- const store = {
37
+ const store = mockStore({
38
38
  zappPipes: {
39
39
  url1: {
40
40
  loading: true,
@@ -53,17 +53,29 @@ describe("useBatchLoading", () => {
53
53
  },
54
54
  },
55
55
  test: "true",
56
- };
56
+ });
57
+
58
+ useDispatchSpy.mockReturnValue(store.dispatch);
57
59
 
58
60
  const initialBatchSize = 3;
59
61
  const riverId = "123";
60
62
 
63
+ const data = [
64
+ { data: { source: "url1" } },
65
+ { data: { source: "url2" } },
66
+ { data: { source: "url3" } },
67
+ { data: { source: "url4" } },
68
+ { data: { source: "url5" } },
69
+ { data: { source: "url6" } },
70
+ // ... more items
71
+ ];
72
+
61
73
  renderHook(() => useBatchLoading(data, { initialBatchSize, riverId }), {
62
74
  wrapper,
63
75
  initialProps: { store },
64
76
  });
65
77
 
66
- const actions = (appStore.getStore() as any).getActions();
78
+ const actions = store.getActions();
67
79
 
68
80
  expect(actions).toHaveLength(2);
69
81
 
@@ -79,7 +91,7 @@ describe("useBatchLoading", () => {
79
91
  });
80
92
 
81
93
  it("loadPipesData start loading new feed when 1 feed is done loading and 1 is in loading state", () => {
82
- const store = {
94
+ const store = mockStore({
83
95
  zappPipes: {
84
96
  url1: {
85
97
  loading: false,
@@ -98,17 +110,31 @@ describe("useBatchLoading", () => {
98
110
  },
99
111
  },
100
112
  test: "true",
101
- };
113
+ });
114
+
115
+ useDispatchSpy.mockReturnValue(store.dispatch);
102
116
 
103
117
  const initialBatchSize = 3;
104
118
  const riverId = "123";
105
119
 
120
+ const data = [
121
+ { data: { source: "url1" } },
122
+ { data: { source: "url2" } },
123
+ { data: { source: "url3" } },
124
+ { data: { source: "url4" } },
125
+ { data: { source: "url5" } },
126
+ { data: { source: "url6" } },
127
+ // ... more items
128
+ ];
129
+
130
+ expect(useDispatchSpy).toBeCalledTimes(0);
131
+
106
132
  renderHook(() => useBatchLoading(data, { initialBatchSize, riverId }), {
107
133
  wrapper,
108
134
  initialProps: { store },
109
135
  });
110
136
 
111
- const actions = (appStore.getStore() as any).getActions();
137
+ const actions = store.getActions();
112
138
 
113
139
  expect(actions).toHaveLength(1);
114
140
 
@@ -119,26 +145,38 @@ describe("useBatchLoading", () => {
119
145
  });
120
146
 
121
147
  it("loadPipesData has been called when no data cached", () => {
122
- const store = {
148
+ const store = mockStore({
123
149
  zappPipes: {},
124
150
  test: "true",
125
- };
151
+ });
152
+
153
+ useDispatchSpy.mockReturnValue(store.dispatch);
126
154
 
127
155
  const initialBatchSize = 3;
128
156
  const riverId = "123";
129
157
 
158
+ const data = [
159
+ { data: { source: "url1" } },
160
+ { data: { source: "url2" } },
161
+ { data: { source: "url3" } },
162
+ { data: { source: "url4" } },
163
+ { data: { source: "url5" } },
164
+ { data: { source: "url6" } },
165
+ // ... more items
166
+ ];
167
+
130
168
  renderHook(() => useBatchLoading(data, { initialBatchSize, riverId }), {
131
169
  wrapper,
132
170
  initialProps: { store },
133
171
  });
134
172
 
135
- const actions = (appStore.getStore() as any).getActions();
173
+ const actions = store.getActions();
136
174
 
137
175
  expect(actions).toHaveLength(3);
138
176
  });
139
177
 
140
178
  it("initial batch ready when all initial items loaded", () => {
141
- const store = {
179
+ const store = mockStore({
142
180
  zappPipes: {
143
181
  url1: {
144
182
  loading: false,
@@ -156,11 +194,19 @@ describe("useBatchLoading", () => {
156
194
  data: {},
157
195
  },
158
196
  },
159
- };
197
+ });
198
+
199
+ useDispatchSpy.mockReturnValue(store.dispatch);
160
200
 
161
201
  const initialBatchSize = 3;
162
202
  const riverId = "123";
163
203
 
204
+ const data: Partial<ZappUIComponent>[] = [
205
+ { data: { source: "url1" } },
206
+ { data: { source: "url2" } },
207
+ { data: { source: "url3" } },
208
+ ];
209
+
164
210
  const { result } = renderHook(
165
211
  () => useBatchLoading(data, { initialBatchSize, riverId }),
166
212
  { wrapper, initialProps: { store } }
@@ -170,10 +216,12 @@ describe("useBatchLoading", () => {
170
216
  });
171
217
 
172
218
  it("gallery-qb: loadPipesData should be called only once for first component in the gallery", () => {
173
- const store = {
219
+ const store = mockStore({
174
220
  zappPipes: {},
175
221
  test: "true",
176
- };
222
+ });
223
+
224
+ useDispatchSpy.mockReturnValue(store.dispatch);
177
225
 
178
226
  const initialBatchSize = 3;
179
227
  const riverId = "123";
@@ -183,11 +231,11 @@ describe("useBatchLoading", () => {
183
231
  component_type: "gallery-qb",
184
232
  ui_components: [{ data: { source: "url1" } }],
185
233
  },
186
- { data: { source: "url2" }, component_type: "any" },
187
- { data: { source: "url3" }, component_type: "any" },
188
- { data: { source: "url4" }, component_type: "any" },
189
- { data: { source: "url5" }, component_type: "any" },
190
- { data: { source: "url6" }, component_type: "any" },
234
+ { data: { source: "url2" } },
235
+ { data: { source: "url3" } },
236
+ { data: { source: "url4" } },
237
+ { data: { source: "url5" } },
238
+ { data: { source: "url6" } },
191
239
  // ... more items
192
240
  ];
193
241
 
@@ -196,13 +244,13 @@ describe("useBatchLoading", () => {
196
244
  initialProps: { store },
197
245
  });
198
246
 
199
- const actions = (appStore.getStore() as any).getActions();
247
+ const actions = store.getActions();
200
248
 
201
249
  expect(actions).toHaveLength(1);
202
250
  });
203
251
 
204
252
  it("gallery-qb: initial batch ready when all initial items loaded", () => {
205
- const store = {
253
+ const store = mockStore({
206
254
  zappPipes: {
207
255
  url1: {
208
256
  loading: false,
@@ -210,19 +258,20 @@ describe("useBatchLoading", () => {
210
258
  data: {},
211
259
  },
212
260
  },
213
- };
261
+ });
262
+
263
+ useDispatchSpy.mockReturnValue(store.dispatch);
214
264
 
215
265
  const initialBatchSize = 3;
216
266
  const riverId = "123";
217
267
 
218
- const data = [
268
+ const data: Partial<ZappUIComponent>[] = [
219
269
  {
220
270
  component_type: "gallery-qb",
221
- data: {},
222
- ui_components: [{ data: { source: "url1" } }] as any,
271
+ ui_components: [{ data: { source: "url1" } }],
223
272
  },
224
- { data: { source: "url2" }, component_type: "any" },
225
- { data: { source: "url3" }, component_type: "any" },
273
+ { data: { source: "url2" } },
274
+ { data: { source: "url3" } },
226
275
  ];
227
276
 
228
277
  const { result } = renderHook(
@@ -10,7 +10,7 @@ import {
10
10
  getSearchContext,
11
11
  } from "@applicaster/zapp-react-native-utils/reactHooks";
12
12
  import { isGallery } from "@applicaster/zapp-react-native-utils/componentsUtils";
13
- import { useScreenContext } from "../screen/useScreenContext";
13
+ import { useScreenContext } from "../screen";
14
14
 
15
15
  type Options = {
16
16
  initialBatchSize?: number;
@@ -5,7 +5,7 @@ import { getDatasourceUrl } from "@applicaster/zapp-react-native-ui-components/D
5
5
  import { usePipesContexts } from "@applicaster/zapp-react-native-ui-components/Decorators/RiverFeedLoader/utils/usePipesContexts";
6
6
  import { clearPipesData } from "@applicaster/zapp-react-native-redux/ZappPipes";
7
7
 
8
- import { useRoute } from "../navigation/useRoute";
8
+ import { useRoute } from "../navigation";
9
9
 
10
10
  /**
11
11
  * reset river components cache when screen is unmounted
@@ -127,10 +127,10 @@ export function isNavBarVisible(
127
127
 
128
128
  export const useBackHandler = (cb: () => boolean) => {
129
129
  useEffect(() => {
130
- const unsubscribe = BackHandler.addEventListener("hardwareBackPress", cb);
130
+ BackHandler.addEventListener("hardwareBackPress", cb);
131
131
 
132
132
  return () => {
133
- unsubscribe.remove();
133
+ BackHandler.removeEventListener("hardwareBackPress", cb);
134
134
  };
135
135
  }, [cb]);
136
136
  };
@@ -1,3 +1,4 @@
1
+ import { ROUTE_TYPES } from "@applicaster/zapp-react-native-utils/navigationUtils/routeTypes";
1
2
  import { useNavigation } from "./useNavigation";
2
3
  import { usePathname } from "./usePathname";
3
4
 
@@ -6,11 +7,14 @@ export const useIsScreenActive = () => {
6
7
  const pathname = usePathname();
7
8
  const { currentRoute, videoModalState } = useNavigation();
8
9
 
9
- if (
10
- videoModalState.visible &&
11
- ["FULLSCREEN", "MAXIMIZED", "PIP"].includes(videoModalState.mode)
12
- ) {
13
- return false;
10
+ if (videoModalState.visible) {
11
+ if (pathname.includes(ROUTE_TYPES.VIDEO_MODAL)) {
12
+ return true;
13
+ }
14
+
15
+ if (["FULLSCREEN", "MAXIMIZED", "PIP"].includes(videoModalState.mode)) {
16
+ return false;
17
+ }
14
18
  }
15
19
 
16
20
  return pathname === currentRoute;
@@ -2,7 +2,7 @@ import { useContext, useMemo } from "react";
2
2
 
3
3
  import { useModalNavigationContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ModalNavigationContext";
4
4
  import { useNestedNavigationContext } from "@applicaster/zapp-react-native-ui-components/Contexts/NestedNavigationContext";
5
- import { useNavigation } from "../navigation/useNavigation";
5
+ import { useNavigation } from "../navigation";
6
6
 
7
7
  import { ScreenContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ScreenContext";
8
8
  import { ScreenDataContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ScreenDataContext";
@@ -1,7 +1,8 @@
1
+ /* eslint-disable no-console */
1
2
  import React from "react";
2
3
  import { render, screen } from "@testing-library/react-native";
3
4
  import { Text } from "react-native";
4
- import { ZStoreProvider, useZStore } from "../ZStoreProvider";
5
+ import { useZStore, ZStoreProvider } from "../ZStoreProvider";
5
6
  import { useStore } from "zustand";
6
7
 
7
8
  interface TestState {
@@ -3,7 +3,7 @@ import { NativeModules, StyleSheet, View } from "react-native";
3
3
  import { getXray } from "@applicaster/zapp-react-native-utils/logger";
4
4
 
5
5
  import { isApplePlatform, isWeb } from "../reactUtils";
6
- import { useRivers } from "../reactHooks/state";
6
+ import { useRivers } from "../reactHooks";
7
7
 
8
8
  const layoutReducer = (state, { payload }) => {
9
9
  return state.map((item, index, _state) => ({
@@ -496,6 +496,7 @@ async function removeStorageListenerHandler(payload: { listenerId?: string }) {
496
496
  function log({ level, messages }) {
497
497
  try {
498
498
  const parsedMessages = parseJsonIfNeeded(messages);
499
+ // eslint-disable-next-line no-console
499
500
  const logFn = console[level] || console.log;
500
501
 
501
502
  if (Array.isArray(parsedMessages)) {
@@ -1,16 +1,17 @@
1
- import { SafeAreaProvider } from "react-native-safe-area-context";
2
- import { render } from "@testing-library/react-native";
1
+ import * as R from "ramda";
2
+
3
3
  import React, { PropsWithChildren } from "react";
4
- import configureStore from "redux-mock-store";
5
- import { Provider } from "react-redux";
6
4
  import { View } from "react-native";
7
- import thunk from "redux-thunk";
8
- import * as R from "ramda";
9
5
 
6
+ import { Provider } from "react-redux";
7
+ import thunk from "redux-thunk";
8
+ import configureStore from "redux-mock-store";
9
+ import { SafeAreaProvider } from "react-native-safe-area-context";
10
10
  import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
11
11
 
12
- import { ThemeContext } from "../theme";
12
+ import { render } from "@testing-library/react-native";
13
13
  import { AnalyticsProvider } from "../analyticsUtils";
14
+ import { ThemeContext } from "../theme";
14
15
 
15
16
  export { getByTestId } from "./getByTestId";
16
17
 
@@ -13,13 +13,15 @@ class BackgroundTimer {
13
13
 
14
14
  const EventEmitter = platformSelect({
15
15
  android: DeviceEventEmitter,
16
- ios: undefined,
16
+ android_tv: DeviceEventEmitter,
17
+ amazon: DeviceEventEmitter, // probably does not exist and uses android_tv
17
18
  default: undefined,
18
19
  });
19
20
 
20
21
  EventEmitter?.addListener("BackgroundTimer.timer.fired", (id: number) => {
21
- if (this.callbacks[id]) {
22
- const callback = this.callbacks[id];
22
+ const callback = this.callbacks[id];
23
+
24
+ if (callback) {
23
25
  delete this.callbacks[id];
24
26
  callback();
25
27
  }
package/utils/index.ts CHANGED
@@ -13,4 +13,8 @@ export {
13
13
  has,
14
14
  flatMap,
15
15
  difference,
16
+ take,
17
+ map,
18
+ trim,
19
+ toString,
16
20
  } from "lodash";