@applicaster/zapp-react-native-utils 14.0.0-alpha.5243406255 → 14.0.0-alpha.5351122050

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 (40) hide show
  1. package/actionsExecutor/ActionExecutorContext.tsx +60 -84
  2. package/actionsExecutor/ScreenActions.ts +164 -0
  3. package/actionsExecutor/StorageActions.ts +110 -0
  4. package/actionsExecutor/feedDecorator.ts +171 -0
  5. package/actionsExecutor/screenResolver.ts +11 -0
  6. package/analyticsUtils/AnalyticsEvents/helper.ts +1 -1
  7. package/analyticsUtils/__tests__/analyticsUtils.test.js +0 -11
  8. package/appUtils/contextKeysManager/contextResolver.ts +42 -1
  9. package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +2 -1
  10. package/appUtils/focusManager/index.ios.ts +10 -0
  11. package/appUtils/focusManager/index.ts +35 -36
  12. package/appUtils/focusManagerAux/utils/index.ts +80 -23
  13. package/configurationUtils/__tests__/manifestKeyParser.test.ts +0 -1
  14. package/navigationUtils/index.ts +1 -1
  15. package/package.json +2 -3
  16. package/reactHooks/cell-click/__tests__/index.test.js +3 -0
  17. package/reactHooks/cell-click/index.ts +8 -1
  18. package/reactHooks/debugging/__tests__/index.test.js +0 -1
  19. package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +8 -2
  20. package/reactHooks/feed/__tests__/useFeedLoader.test.tsx +71 -31
  21. package/reactHooks/feed/index.ts +2 -0
  22. package/reactHooks/feed/useBatchLoading.ts +14 -9
  23. package/reactHooks/feed/useFeedLoader.tsx +36 -38
  24. package/reactHooks/feed/useLoadPipesDataDispatch.ts +57 -0
  25. package/reactHooks/navigation/useRoute.ts +7 -2
  26. package/reactHooks/navigation/useScreenStateStore.ts +8 -0
  27. package/reactHooks/state/index.ts +1 -1
  28. package/reactHooks/state/useHomeRiver.ts +4 -2
  29. package/screenPickerUtils/index.ts +7 -0
  30. package/storage/ScreenSingleValueProvider.ts +204 -0
  31. package/storage/ScreenStateMultiSelectProvider.ts +293 -0
  32. package/storage/StorageMultiSelectProvider.ts +192 -0
  33. package/storage/StorageSingleSelectProvider.ts +108 -0
  34. package/utils/__tests__/find.test.ts +36 -0
  35. package/utils/__tests__/pathOr.test.ts +37 -0
  36. package/utils/__tests__/startsWith.test.ts +30 -0
  37. package/utils/find.ts +3 -0
  38. package/utils/index.ts +7 -0
  39. package/utils/pathOr.ts +5 -0
  40. package/utils/startsWith.ts +9 -0
@@ -1,10 +1,13 @@
1
1
  import { ContextKeysManager } from "./index";
2
2
  import * as R from "ramda";
3
+ import * as _ from "lodash";
4
+ import { useScreenStateStore } from "../../reactHooks/navigation/useScreenStateStore";
3
5
 
4
- interface IResolver {
6
+ export interface IResolver {
5
7
  resolve: (string) => Promise<string | number | object>;
6
8
  }
7
9
 
10
+ // TODO: Rename to ObjectKeyResolver or similar
8
11
  export class EntryResolver implements IResolver {
9
12
  entry: ZappEntry;
10
13
 
@@ -21,6 +24,28 @@ export class EntryResolver implements IResolver {
21
24
  }
22
25
  }
23
26
 
27
+ // TODO: Move to proper place
28
+
29
+ export class ScreenStateResolver implements IResolver {
30
+ constructor(
31
+ private screenStateStore: ReturnType<typeof useScreenStateStore>
32
+ ) {}
33
+
34
+ async resolve(key: string) {
35
+ const screenState = this.screenStateStore.getState().data;
36
+
37
+ if (!key || key.length === 0) {
38
+ return screenState;
39
+ }
40
+
41
+ if (key.includes(".")) {
42
+ return R.view(R.lensPath(key.split(".")), screenState);
43
+ }
44
+
45
+ return screenState?.[key];
46
+ }
47
+ }
48
+
24
49
  export class ContextResolver implements IResolver {
25
50
  resolve = async (compositeKey: string) =>
26
51
  ContextKeysManager.instance.getKey(compositeKey);
@@ -64,3 +89,19 @@ export const resolveObjectValues = async (
64
89
 
65
90
  return Object.fromEntries(resolvedEntries);
66
91
  };
92
+
93
+ export const extractAtValues = _.memoize((input: any): string[] => {
94
+ return _.flatMapDeep(input, (value: any) => {
95
+ if (_.isString(value)) {
96
+ const matches = value.match(/@\{([^}]*)\}/g);
97
+
98
+ return matches ? matches.map((match) => match.slice(2, -1)) : [];
99
+ }
100
+
101
+ if (_.isObject(value)) {
102
+ return extractAtValues(_.values(value));
103
+ }
104
+
105
+ return [];
106
+ });
107
+ });
@@ -25,10 +25,10 @@ exports[`focusManager should be defined 1`] = `
25
25
  "invokeHandler": [Function],
26
26
  "isCurrentFocusOnTheTopScreen": [Function],
27
27
  "isFocusDisabled": [Function],
28
+ "isFocusOn": [Function],
28
29
  "isFocusOnContent": [Function],
29
30
  "isFocusOnMenu": [Function],
30
31
  "isGroupItemFocused": [Function],
31
- "isOnRootScreen": [Function],
32
32
  "longPress": [Function],
33
33
  "moveFocus": [Function],
34
34
  "on": [Function],
@@ -67,6 +67,7 @@ exports[`focusManagerIOS should be defined 1`] = `
67
67
  "getGroupRootById": [Function],
68
68
  "getPreferredFocusChild": [Function],
69
69
  "invokeHandler": [Function],
70
+ "isFocusOn": [Function],
70
71
  "isGroupItemFocused": [Function],
71
72
  "moveFocus": [Function],
72
73
  "on": [Function],
@@ -1,6 +1,7 @@
1
1
  import { NativeModules } from "react-native";
2
2
  import * as R from "ramda";
3
3
 
4
+ import { isCurrentFocusOn } from "../focusManagerAux/utils";
4
5
  import { Tree } from "./treeDataStructure/Tree";
5
6
  import { findFocusableNode } from "./treeDataStructure/Utils";
6
7
  import { subscriber } from "../../functionUtils";
@@ -391,6 +392,14 @@ export const focusManager = (function () {
391
392
  return node;
392
393
  }
393
394
 
395
+ function isFocusOn(id): boolean {
396
+ const currentFocusNode = focusableTree.findInTree(
397
+ getCurrentFocus()?.props?.id
398
+ );
399
+
400
+ return id && isCurrentFocusOn(id, currentFocusNode);
401
+ }
402
+
394
403
  return {
395
404
  on,
396
405
  invokeHandler,
@@ -412,5 +421,6 @@ export const focusManager = (function () {
412
421
  getGroupRootById,
413
422
  isGroupItemFocused,
414
423
  getPreferredFocusChild,
424
+ isFocusOn,
415
425
  };
416
426
  })();
@@ -15,12 +15,12 @@ import { coreLogger } from "../../logger";
15
15
  import { ACTION } from "./utils/enums";
16
16
 
17
17
  import {
18
- isTabsScreen,
19
18
  findSelectedTabId,
20
19
  findSelectedMenuId,
21
- isTabsMenuFocused,
20
+ isTabsScreenContentFocused,
22
21
  isCurrentFocusOnContent,
23
22
  isCurrentFocusOnMenu,
23
+ isCurrentFocusOn,
24
24
  } from "../focusManagerAux/utils";
25
25
 
26
26
  const logger = coreLogger.addSubsystem("focusManager");
@@ -300,48 +300,46 @@ export const focusManager = (function () {
300
300
  return isCurrentFocusOnMenu(currentFocusNode);
301
301
  }
302
302
 
303
- function landFocusTo(id) {
304
- if (id) {
305
- // set focus on selected menu item
306
- const direction = undefined;
303
+ // Move focus to appropriate top navigation tab with context
304
+ function focusTopNavigation(isTabsScreen: boolean, item: ZappEntry) {
305
+ const landFocusTo = (id) => {
306
+ if (id) {
307
+ // set focus on selected menu item
308
+ const direction = undefined;
307
309
 
308
- const context: FocusManager.FocusContext = {
309
- source: "back",
310
- preserveScroll: true,
311
- };
310
+ const context: FocusManager.FocusContext = {
311
+ source: "back",
312
+ preserveScroll: true,
313
+ };
312
314
 
313
- blur(direction);
314
- setFocus(id, direction, context);
315
- }
316
- }
315
+ logger.log({ message: "landFocusTo", data: { id } });
317
316
 
318
- // Move focus to appropriate top navigation tab with context
319
- function focusTopNavigation() {
320
- // Store current focus for restoration
321
- // this.storeFocusState();
322
-
323
- if (isTabsScreen(focusableTree) && !isTabsMenuFocused(currentFocusNode)) {
324
- const selectedTabId = findSelectedTabId(focusableTree);
317
+ blur(direction);
318
+ setFocus(id, direction, context);
319
+ }
320
+ };
325
321
 
326
- console.log("debug_2", "FM - moveFocusToSelectedTab", { selectedTabId });
322
+ if (isTabsScreen && isTabsScreenContentFocused(currentFocusNode)) {
323
+ const selectedTabId = findSelectedTabId(item);
327
324
 
325
+ // Set focus with back button context to tabs-menu
328
326
  landFocusTo(selectedTabId);
329
327
 
330
328
  return;
331
329
  }
332
330
 
333
- // Set focus with back button context
334
331
  const selectedMenuItemId = findSelectedMenuId(focusableTree);
335
-
336
- console.log("debug_2", "IM - moveFocusToTopMenu", { selectedMenuItemId });
337
-
332
+ // Set focus with back button context to top-menu
338
333
  landFocusTo(selectedMenuItemId);
339
334
  }
340
335
 
341
336
  /**
342
337
  * sets the initial focus when the screen loads, or when focus is lost
343
338
  */
344
- function setInitialFocus(lastAddedParentNode?: any) {
339
+ function setInitialFocus(
340
+ lastAddedParentNode?: any,
341
+ context?: FocusManager.FocusContext
342
+ ) {
345
343
  const preferredFocus = findPriorityItem(
346
344
  lastAddedParentNode?.children || focusableTree.root.children
347
345
  );
@@ -387,7 +385,7 @@ export const focusManager = (function () {
387
385
  },
388
386
  });
389
387
 
390
- focusableItem && setFocus(focusCandidate.id, null);
388
+ focusableItem && setFocus(focusCandidate.id, null, context);
391
389
 
392
390
  return { success: true };
393
391
  }
@@ -544,12 +542,6 @@ export const focusManager = (function () {
544
542
  return haveSameParentBeforeRoot(currentFocusNode, R.last(routes));
545
543
  }
546
544
 
547
- function isOnRootScreen() {
548
- const routes = R.pathOr([], ["root", "children"], focusableTree);
549
-
550
- return routes.length <= 1;
551
- }
552
-
553
545
  function recoverFocus() {
554
546
  if (!isCurrentFocusOnTheTopScreen()) {
555
547
  // We've failed to set focused node on the new screen => run focus recovery
@@ -613,6 +605,14 @@ export const focusManager = (function () {
613
605
  return preferredFocus[0];
614
606
  }
615
607
 
608
+ function isFocusOn(id): boolean {
609
+ return (
610
+ id &&
611
+ isCurrentFocusOnTheTopScreen() &&
612
+ isCurrentFocusOn(id, currentFocusNode)
613
+ );
614
+ }
615
+
616
616
  /**
617
617
  * this is the list of the functions available externally
618
618
  * when importing the focus manager
@@ -643,10 +643,9 @@ export const focusManager = (function () {
643
643
  recoverFocus,
644
644
  isCurrentFocusOnTheTopScreen,
645
645
  findPreferredFocusChild,
646
-
647
646
  focusTopNavigation,
648
647
  isFocusOnContent,
649
648
  isFocusOnMenu,
650
- isOnRootScreen,
649
+ isFocusOn,
651
650
  };
652
651
  })();
@@ -1,15 +1,30 @@
1
- import { isNil } from "@applicaster/zapp-react-native-utils/utils";
2
- import { find, last, pathOr, startsWith } from "ramda";
1
+ import {
2
+ isNil,
3
+ last,
4
+ startsWith,
5
+ find,
6
+ pathOr,
7
+ } from "@applicaster/zapp-react-native-utils/utils";
8
+
3
9
  import {
4
10
  QUICK_BRICK_CONTENT,
5
11
  QUICK_BRICK_NAVBAR,
6
12
  } from "@applicaster/quick-brick-core/const";
7
13
 
14
+ import {
15
+ getFocusableId,
16
+ SCREEN_PICKER_CONTAINER,
17
+ } from "@applicaster/zapp-react-native-utils/screenPickerUtils";
18
+
8
19
  // run check each 300 ms
9
20
  // run this check too often could lead to performance penalty on low-end devices
10
21
  const HOW_OFTEN_TO_CHECK_CONDITION = 300; // ms
11
22
 
12
- const TABS_GROUP_ID = "PickerSelector.sp-river";
23
+ const isTopMenu = (node) => startsWith(QUICK_BRICK_NAVBAR, node?.id);
24
+ const isContent = (node) => startsWith(QUICK_BRICK_CONTENT, node?.id);
25
+ const isRoot = (node) => node?.id === "root";
26
+
27
+ const isScrenPicker = (node) => startsWith(SCREEN_PICKER_CONTAINER, node?.id);
13
28
 
14
29
  type Props = {
15
30
  maxTimeout: number;
@@ -102,19 +117,8 @@ export const waitForContent = (focusableTree) => {
102
117
  });
103
118
  };
104
119
 
105
- export function isTabsScreen(focusableTree) {
106
- const tabsGroup = focusableTree.findInTree(TABS_GROUP_ID);
107
-
108
- return !!tabsGroup;
109
- }
110
-
111
- export const findSelectedTabId = (focusableTree) => {
112
- // FIXME - find elegant way how to get ID of selected tab
113
- const tabsGroup = focusableTree.findInTree(TABS_GROUP_ID);
114
-
115
- const selectedTabId = tabsGroup.children.find(
116
- (child) => child.component.props.preferredFocus // ?? selected
117
- )?.id;
120
+ export const findSelectedTabId = (item: ZappEntry): string => {
121
+ const selectedTabId = getFocusableId(item.id);
118
122
 
119
123
  return selectedTabId;
120
124
  };
@@ -131,17 +135,70 @@ export const findSelectedMenuId = (focusableTree) => {
131
135
  return selectedMenuItemId;
132
136
  };
133
137
 
134
- export const isTabsMenuFocused = (node) => {
135
- // FIXME - find elegant way how to get ID of selected tab
136
- return node.parent.id === TABS_GROUP_ID;
138
+ export const isTabsScreenContentFocused = (node) => {
139
+ if (isRoot(node)) {
140
+ return false;
141
+ }
142
+
143
+ if (isTopMenu(node)) {
144
+ return false;
145
+ }
146
+
147
+ if (isContent(node)) {
148
+ return false;
149
+ }
150
+
151
+ if (isScrenPicker(node)) {
152
+ return true;
153
+ }
154
+
155
+ return isTabsScreenContentFocused(node.parent);
137
156
  };
138
157
 
139
158
  export const isCurrentFocusOnMenu = (node) => {
140
- // FIXME
141
- return node.parent.id.startsWith(QUICK_BRICK_NAVBAR);
159
+ if (isRoot(node)) {
160
+ return false;
161
+ }
162
+
163
+ if (isTopMenu(node)) {
164
+ return true;
165
+ }
166
+
167
+ if (isContent(node)) {
168
+ return false;
169
+ }
170
+
171
+ return isCurrentFocusOnMenu(node.parent);
142
172
  };
143
173
 
144
174
  export const isCurrentFocusOnContent = (node) => {
145
- // FIXME
146
- return !isCurrentFocusOnMenu(node);
175
+ if (isRoot(node)) {
176
+ return false;
177
+ }
178
+
179
+ if (isTopMenu(node)) {
180
+ return false;
181
+ }
182
+
183
+ if (isContent(node)) {
184
+ return true;
185
+ }
186
+
187
+ return isCurrentFocusOnContent(node.parent);
188
+ };
189
+
190
+ export const isCurrentFocusOn = (id, node) => {
191
+ if (!node) {
192
+ return false;
193
+ }
194
+
195
+ if (isRoot(node)) {
196
+ return false;
197
+ }
198
+
199
+ if (node?.id === id) {
200
+ return true;
201
+ }
202
+
203
+ return isCurrentFocusOn(id, node.parent);
147
204
  };
@@ -1,6 +1,5 @@
1
1
  import { getAllSpecificStyles } from "../manifestKeyParser";
2
2
 
3
- // Mock the dependencies
4
3
  jest.mock("@applicaster/zapp-react-native-utils/reactUtils", () => ({
5
4
  platformSelect: jest.fn((platforms) => platforms.samsung_tv), // Default to samsung for tests
6
5
  }));
@@ -42,7 +42,7 @@ export function getNavigationType(
42
42
  R.unless(R.isNil, R.prop("navigation_type")),
43
43
  R.defaultTo(undefined),
44
44
  R.find(R.propEq("category", category))
45
- )(navigations);
45
+ )(navigations || []);
46
46
  }
47
47
 
48
48
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "14.0.0-alpha.5243406255",
3
+ "version": "14.0.0-alpha.5351122050",
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.5243406255",
30
+ "@applicaster/applicaster-types": "14.0.0-alpha.5351122050",
31
31
  "buffer": "^5.2.1",
32
32
  "camelize": "^1.0.0",
33
33
  "dayjs": "^1.11.10",
@@ -38,7 +38,6 @@
38
38
  "peerDependencies": {
39
39
  "@applicaster/zapp-pipes-v2-client": "*",
40
40
  "@react-native-community/netinfo": "*",
41
- "immer": "*",
42
41
  "react": "*",
43
42
  "react-native": "*",
44
43
  "uglify-js": "*",
@@ -26,6 +26,9 @@ jest.mock("@applicaster/zapp-react-native-utils/analyticsUtils/", () => ({
26
26
  }));
27
27
 
28
28
  jest.mock("@applicaster/zapp-react-native-utils/reactHooks/screen", () => ({
29
+ ...jest.requireActual(
30
+ "@applicaster/zapp-react-native-utils/reactHooks/screen"
31
+ ),
29
32
  useTargetScreenData: jest.fn(() => ({})),
30
33
  useCurrentScreenData: jest.fn(() => ({})),
31
34
  }));
@@ -16,7 +16,8 @@ import { ActionExecutorContext } from "@applicaster/zapp-react-native-utils/acti
16
16
  import { isFunction, noop } from "../../functionUtils";
17
17
  import { useSendAnalyticsOnPress } from "../analytics";
18
18
  import { logOnPress, warnEmptyContentType } from "./helpers";
19
- import { useCurrentScreenData } from "../screen";
19
+ import { useCurrentScreenData, useScreenContext } from "../screen";
20
+ import { useScreenStateStore } from "../navigation/useScreenStateStore";
20
21
 
21
22
  /**
22
23
  * If onCellTap is defined execute the function and
@@ -42,10 +43,12 @@ export const useCellClick = ({
42
43
  }: Props): onPressReturnFn => {
43
44
  const { push, currentRoute } = useNavigation();
44
45
  const { pathname } = useRoute();
46
+ const screenStateStore = useScreenStateStore();
45
47
 
46
48
  const onCellTap: Option<Function> = React.useContext(CellTapContext);
47
49
  const actionExecutor = React.useContext(ActionExecutorContext);
48
50
  const screenData = useCurrentScreenData();
51
+ const screenState = useScreenContext()?.options;
49
52
 
50
53
  const cellSelectable = toBooleanWithDefaultTrue(
51
54
  component?.rules?.component_cells_selectable
@@ -83,6 +86,9 @@ export const useCellClick = ({
83
86
  await actionExecutor?.handleEntryActions(selectedItem, {
84
87
  component,
85
88
  screenData,
89
+ screenState,
90
+ screenRoute: pathname,
91
+ screenStateStore,
86
92
  });
87
93
  }
88
94
 
@@ -117,6 +123,7 @@ export const useCellClick = ({
117
123
  push,
118
124
  sendAnalyticsOnPress,
119
125
  screenData,
126
+ screenState,
120
127
  ]
121
128
  );
122
129
 
@@ -12,7 +12,6 @@ describe("Debug utils", () => {
12
12
  // Clear the timers object
13
13
  Object.keys(timers).forEach((key) => delete timers[key]);
14
14
 
15
- // Mock performance.now()
16
15
  // eslint-disable-next-line no-undef
17
16
  performanceNowMock = jest.spyOn(performance, "now");
18
17
  performanceNowMock.mockReturnValue(0); // Initial value
@@ -2,12 +2,16 @@ import { renderHook } from "@testing-library/react-hooks";
2
2
  import { allFeedsIsReady, useBatchLoading } from "../useBatchLoading";
3
3
  import { WrappedWithProviders } from "@applicaster/zapp-react-native-utils/testUtils";
4
4
  import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
5
+ import { waitFor } from "@testing-library/react-native";
5
6
 
6
7
  jest.mock("../../navigation");
7
8
 
8
9
  jest.mock(
9
10
  "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext",
10
11
  () => ({
12
+ ...jest.requireActual(
13
+ "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext"
14
+ ),
11
15
  useScreenContext: jest.fn().mockReturnValue({ screen: {}, entry: {} }),
12
16
  })
13
17
  );
@@ -33,7 +37,7 @@ describe("useBatchLoading", () => {
33
37
  jest.clearAllMocks();
34
38
  });
35
39
 
36
- it("loadPipesData start loading not started requests", () => {
40
+ it("loadPipesData start loading not started requests", async () => {
37
41
  const store = {
38
42
  zappPipes: {
39
43
  url1: {
@@ -65,7 +69,9 @@ describe("useBatchLoading", () => {
65
69
 
66
70
  const actions = (appStore.getStore() as any).getActions();
67
71
 
68
- expect(actions).toHaveLength(2);
72
+ await waitFor(() => {
73
+ expect(actions).toHaveLength(2);
74
+ });
69
75
 
70
76
  expect(actions[0]).toMatchObject({
71
77
  type: "ZAPP_PIPES_REQUEST_START",