@applicaster/zapp-react-native-utils 12.0.0-alpha.8417280639 → 12.0.0-alpha.8939308044

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 (42) hide show
  1. package/analyticsUtils/AnalyticsEvents/index.ts +6 -0
  2. package/analyticsUtils/AnalyticsEvents/searchEvents.ts +21 -0
  3. package/analyticsUtils/events.ts +6 -0
  4. package/analyticsUtils/index.tsx +6 -0
  5. package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +3 -3
  6. package/appUtils/focusManager/__tests__/focusManager.test.js +4 -24
  7. package/appUtils/focusManager/__tests__/index.test.ts +3 -3
  8. package/appUtils/focusManager/index.ts +141 -56
  9. package/appUtils/focusManager/treeDataStructure/Utils/index.js +73 -43
  10. package/appUtils/focusManager/utils/index.ts +94 -0
  11. package/appUtils/playerManager/player.ts +1 -0
  12. package/appUtils/playerManager/playerNative.ts +5 -0
  13. package/arrayUtils/__tests__/isFilledArray.test.ts +63 -0
  14. package/arrayUtils/index.ts +5 -0
  15. package/cellUtils/__tests__/cellUtils.test.ts +9 -20
  16. package/cellUtils/index.ts +6 -12
  17. package/dateUtils/index.ts +11 -0
  18. package/headersUtils/index.ts +1 -0
  19. package/inflatedUrlUtils/index.ts +4 -4
  20. package/localizationUtils/__tests__/localizationUtils.test.ts +72 -53
  21. package/localizationUtils/index.ts +13 -22
  22. package/numberUtils/__tests__/isLikeNumber.test.ts +18 -0
  23. package/numberUtils/index.ts +9 -18
  24. package/package.json +2 -2
  25. package/reactHooks/cell-click/__tests__/index.test.js +10 -2
  26. package/reactHooks/cell-click/index.ts +11 -11
  27. package/reactHooks/feed/__tests__/useBuildPipesUrl.test.tsx +30 -70
  28. package/reactHooks/feed/__tests__/useInflatedUrl.test.ts +18 -3
  29. package/reactHooks/feed/useBuildPipesUrl.ts +7 -10
  30. package/reactHooks/feed/useInflatedUrl.ts +44 -15
  31. package/reactHooks/navigation/__mocks__/index.ts +13 -0
  32. package/reactHooks/navigation/__mocks__/useNavigation.ts +3 -0
  33. package/reactHooks/navigation/index.ts +15 -0
  34. package/reactHooks/screen/__tests__/useCurrentScreenData.test.tsx +21 -23
  35. package/reactHooks/screen/index.ts +2 -0
  36. package/reactHooks/screen/useScreenBackgroundColor.ts +23 -0
  37. package/reactHooks/screen/useScreenContext.ts +4 -3
  38. package/screenState/index.ts +0 -8
  39. package/stringUtils/index.ts +5 -0
  40. package/testUtils/getByTestId.ts +14 -0
  41. package/testUtils/index.tsx +24 -22
  42. package/types.ts +4 -0
@@ -7,6 +7,10 @@ import { sendHeaderClickEvent } from "./sendHeaderClickEvent";
7
7
  import { sendHardwareBackButtonClickEvent } from "./sendHardwareBackButtonClickEvent";
8
8
  import { sendDownloadsEvent } from "./sendDownloadsEvent";
9
9
  import { sendScreenEvent } from "./sendScreenEvent";
10
+ import {
11
+ sendSearchCanceledEvent,
12
+ sendSearchExecutedEvent,
13
+ } from "./searchEvents";
10
14
 
11
15
  export {
12
16
  sendScreenEvent,
@@ -18,4 +22,6 @@ export {
18
22
  sendHeaderClickEvent,
19
23
  sendHardwareBackButtonClickEvent,
20
24
  sendDownloadsEvent,
25
+ sendSearchCanceledEvent,
26
+ sendSearchExecutedEvent,
21
27
  };
@@ -0,0 +1,21 @@
1
+ import { log_debug } from "../logger";
2
+ import { postAnalyticEvent } from "../manager";
3
+ import { ANALYTICS_CORE_EVENTS } from "../events";
4
+
5
+ export const sendSearchCanceledEvent = (analyticsProps) => {
6
+ const eventName = ANALYTICS_CORE_EVENTS.SEARCH_CANCELED;
7
+ postAnalyticEvent(eventName, analyticsProps);
8
+
9
+ log_debug(`sendSearchCanceledEvent: send event: ${eventName}`, {
10
+ analyticsProps,
11
+ });
12
+ };
13
+
14
+ export const sendSearchExecutedEvent = (analyticsProps) => {
15
+ const eventName = ANALYTICS_CORE_EVENTS.SEARCH_EXECUTED;
16
+ postAnalyticEvent(eventName, analyticsProps);
17
+
18
+ log_debug(`sendSearchExecutedEvent: send event: ${eventName}`, {
19
+ analyticsProps,
20
+ });
21
+ };
@@ -23,10 +23,16 @@ export const TAPPING_EVENTS = {
23
23
  TAP_NAVBAR_BACK_BUTTON: "tap_navbar_back_button",
24
24
  };
25
25
 
26
+ export const SEARCH_EVENTS = {
27
+ SEARCH_EXECUTED: "search_executed",
28
+ SEARCH_CANCELED: "search_canceled",
29
+ };
30
+
26
31
  export const ANALYTICS_CORE_EVENTS = {
27
32
  ...LEGACY_EVENTS,
28
33
  ...SCREEN_VIEW_EVENTS,
29
34
  ...TAPPING_EVENTS,
35
+ ...SEARCH_EVENTS,
30
36
  };
31
37
 
32
38
  export const WEBVIEW_EVENTS = {
@@ -18,6 +18,8 @@ import {
18
18
  sendHardwareBackButtonClickEvent,
19
19
  sendDownloadsEvent,
20
20
  sendScreenEvent,
21
+ sendSearchCanceledEvent,
22
+ sendSearchExecutedEvent,
21
23
  } from "./AnalyticsEvents";
22
24
 
23
25
  import { postAnalyticEvent } from "./manager";
@@ -66,6 +68,8 @@ type AnalyticsFunctions = {
66
68
  item: ZappEntry;
67
69
  state: OfflineItemState;
68
70
  }) => void;
71
+ sendSearchCanceledEvent: (props: AnalyticsDefaultProperties) => void;
72
+ sendSearchExecutedEvent: (props: AnalyticsDefaultProperties) => void;
69
73
  };
70
74
 
71
75
  type GetAnalyticsFunctions = (
@@ -102,6 +106,8 @@ export function getAnalyticsFunctions({
102
106
  sendHardwareBackButtonClickEvent,
103
107
  sendScreenEvent,
104
108
  sendDownloadsEvent,
109
+ sendSearchCanceledEvent,
110
+ sendSearchExecutedEvent,
105
111
  };
106
112
  }
107
113
 
@@ -3,9 +3,7 @@
3
3
  exports[`focusManager should be defined 1`] = `
4
4
  Object {
5
5
  "blur": [Function],
6
- "disableAutomaticFocus": [Function],
7
6
  "disableFocus": [Function],
8
- "enableAutomaticFocus": [Function],
9
7
  "enableFocus": [Function],
10
8
  "focusableTree": Tree {
11
9
  "loadingCounter": 0,
@@ -30,11 +28,13 @@ Object {
30
28
  "on": [Function],
31
29
  "press": [Function],
32
30
  "pressOut": [Function],
31
+ "recoverFocus": [Function],
33
32
  "register": [Function],
34
33
  "removeHandler": [Function],
35
34
  "setFocus": [Function],
35
+ "setFocusOnContent": [Function],
36
+ "setFocusOnMenu": [Function],
36
37
  "setInitialFocus": [Function],
37
- "setupInitialFocus": [Function],
38
38
  "unregister": [Function],
39
39
  }
40
40
  `;
@@ -14,10 +14,8 @@ describe("focusManager", () => {
14
14
  expect(focusManager).toMatchSnapshot();
15
15
  });
16
16
 
17
- it("enableAutomaticFocus should not block setting focus", () => {
17
+ it("manages to land initial focus", () => {
18
18
  const mockSetFocus = jest.fn();
19
- focusManager.setupInitialFocus("1", true);
20
- focusManager.enableAutomaticFocus();
21
19
 
22
20
  focusManager.register({
23
21
  id: "1",
@@ -31,31 +29,13 @@ describe("focusManager", () => {
31
29
  },
32
30
  });
33
31
 
32
+ const { success } = focusManager.setInitialFocus();
33
+
34
+ expect(success).toBe(true);
34
35
  expect(mockSetFocus).toBeCalledTimes(1);
35
36
  expect(mockSetFocus).toBeCalledWith(null);
36
37
  });
37
38
 
38
- it("enableAutomaticFocus should block setting focus", () => {
39
- const mockSetFocus = jest.fn();
40
- focusManager.setupInitialFocus("1", true);
41
- focusManager.disableAutomaticFocus();
42
-
43
- focusManager.register({
44
- id: "1",
45
- component: {
46
- setFocus: mockSetFocus,
47
- willReceiveFocus: function () {},
48
- isMounted: function () {
49
- return true;
50
- },
51
- props: { groupId: "121" },
52
- },
53
- });
54
-
55
- expect(mockSetFocus).toBeCalledTimes(0);
56
- expect(mockSetFocus).not.toBeCalledWith(null);
57
- });
58
-
59
39
  describe("register", () => {});
60
40
  describe("unregister", () => {});
61
41
  describe("find", () => {});
@@ -18,7 +18,6 @@ describe("focusManager", () => {
18
18
  "on",
19
19
  "invokeHandler",
20
20
  "removeHandler",
21
- "setupInitialFocus",
22
21
  "longPress",
23
22
  "pressOut",
24
23
  "enableFocus",
@@ -27,10 +26,11 @@ describe("focusManager", () => {
27
26
  "isFocusDisabled",
28
27
  "getCurrentFocusNode",
29
28
  "focusableTree",
30
- "disableAutomaticFocus",
31
- "enableAutomaticFocus",
32
29
  "getGroupRootById",
33
30
  "isGroupItemFocused",
31
+ "setFocusOnMenu",
32
+ "setFocusOnContent",
33
+ "recoverFocus",
34
34
  ];
35
35
 
36
36
  expectedMethods.forEach((method) => {
@@ -1,9 +1,24 @@
1
1
  import * as R from "ramda";
2
+ import {
3
+ isNilOrEmpty,
4
+ isNotNil,
5
+ } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
6
+
2
7
  import { Tree } from "./treeDataStructure/Tree";
3
- import { findFocusableNode, findPriorityItem } from "./treeDataStructure/Utils";
8
+ import {
9
+ findFocusableNode,
10
+ findPriorityItem,
11
+ isTransitionHappening,
12
+ } from "./treeDataStructure/Utils";
4
13
  import { subscriber } from "../../functionUtils";
5
14
  import * as FOCUS_EVENTS from "./events";
6
15
  import { coreLogger } from "../../logger";
16
+ import {
17
+ waitForActiveScreen,
18
+ waitForContent,
19
+ getContentNode,
20
+ getNavbarNode,
21
+ } from "./utils";
7
22
 
8
23
  const logger = coreLogger.addSubsystem("focusManager");
9
24
 
@@ -44,21 +59,9 @@ export const focusManager = (function () {
44
59
  // focusable node which currently holds the focus
45
60
  let currentFocusNode = null;
46
61
  let lastFocusedId = null;
47
- let initialFocusOnContent = null;
48
62
  let focusDisabled = false;
49
- let focusAutoEnabled = true;
50
63
  const eventHandler = subscriber();
51
64
 
52
- function disableAutomaticFocus() {
53
- logger.log({ message: "disableAutomaticFocus" });
54
- focusAutoEnabled = false;
55
- }
56
-
57
- function enableAutomaticFocus() {
58
- logger.log({ message: "focusManager: enableAutomaticFocus" });
59
- focusAutoEnabled = true;
60
- }
61
-
62
65
  function enableFocus() {
63
66
  focusDisabled = false;
64
67
  }
@@ -93,7 +96,7 @@ export const focusManager = (function () {
93
96
  !currentFocusable.isGroup &&
94
97
  currentFocusable.isMounted()
95
98
  ) {
96
- currentFocusable.blur(currentFocusable, direction);
99
+ currentFocusable.blur(direction);
97
100
  currentFocusable.hasLostFocus(getCurrentFocus(), direction);
98
101
  }
99
102
  }
@@ -226,32 +229,52 @@ export const focusManager = (function () {
226
229
 
227
230
  logger.log({ message: "setInitialFocus", data: { preferredFocus } });
228
231
 
229
- if (preferredFocus && preferredFocus.length > 0) {
230
- const focusCandidate = preferredFocus[0];
231
- const focusableItem = focusCandidate.component;
232
-
233
- if (
234
- focusableItem &&
235
- !focusableItem.isGroup &&
236
- isFocusEnabled(focusableItem)
237
- ) {
238
- if (initialFocusOnContent) {
239
- blur();
240
- initialFocusOnContent = false;
241
- }
232
+ if (isNilOrEmpty(preferredFocus)) {
233
+ return { success: false };
234
+ }
242
235
 
243
- logger.log({
244
- message: "setInitialFocus – currentFocusNode",
245
- data: {
246
- focusableItem,
247
- },
248
- });
236
+ const focusCandidate = preferredFocus[0];
237
+ const focusableItem = focusCandidate.component;
249
238
 
250
- currentFocusNode = focusCandidate;
251
- focusableItem.willReceiveFocus(focusableItem, null);
252
- focusableItem && setFocus(focusCandidate.id, null);
253
- }
239
+ const currentFocusedId = R.path(["props", "id"], getCurrentFocus());
240
+ const nextFocusedId = R.path(["props", "id"], focusableItem);
241
+
242
+ if (
243
+ isNotNil(currentFocusedId) &&
244
+ isNotNil(nextFocusedId) &&
245
+ currentFocusedId === nextFocusedId
246
+ ) {
247
+ // we are going to move focus to the same node(already focused) -> nothing to do
248
+ return { success: true };
254
249
  }
250
+
251
+ if (focusableItem?.isGroup) {
252
+ // focusCandidate is a FocusableGroup, we are not able to land focus on group(only on Focusable)
253
+ return { success: false };
254
+ }
255
+
256
+ if (
257
+ focusableItem &&
258
+ !focusableItem.isGroup &&
259
+ isFocusEnabled(focusableItem)
260
+ ) {
261
+ blur();
262
+
263
+ logger.log({
264
+ message: "setInitialFocus – currentFocusNode",
265
+ data: {
266
+ focusableItem,
267
+ },
268
+ });
269
+
270
+ currentFocusNode = focusCandidate;
271
+ focusableItem.willReceiveFocus(focusableItem, null);
272
+ focusableItem && setFocus(focusCandidate.id, null);
273
+
274
+ return { success: true };
275
+ }
276
+
277
+ return { success: false };
255
278
  }
256
279
 
257
280
  function treeLoaded(val, node) {
@@ -261,17 +284,8 @@ export const focusManager = (function () {
261
284
  val,
262
285
  node,
263
286
  currentFocusNode,
264
- initialFocusOnContent,
265
287
  },
266
288
  });
267
-
268
- if ((val && !currentFocusNode) || initialFocusOnContent) {
269
- logger.log({ message: `focusAutoEnabled ${focusAutoEnabled}` });
270
-
271
- if (focusAutoEnabled) {
272
- setInitialFocus(initialFocusOnContent ? node : undefined);
273
- }
274
- }
275
289
  }
276
290
 
277
291
  /**
@@ -402,11 +416,79 @@ export const focusManager = (function () {
402
416
  group ? unregisterGroup(id) : unregisterItem(id);
403
417
  }
404
418
 
405
- function setupInitialFocus(id: string, focusOnContent?: boolean) {
406
- if (focusOnContent) {
407
- initialFocusOnContent = focusOnContent;
408
- } else {
409
- setFocus(id);
419
+ async function setFocusOnMenu(currentRoute: string) {
420
+ try {
421
+ await waitForActiveScreen(currentRoute, focusableTree);
422
+
423
+ const navbar = getNavbarNode(focusableTree);
424
+
425
+ const selectedMenuItemId = R.find(
426
+ (child) => child.component.props.selected,
427
+ navbar.children
428
+ )?.id;
429
+
430
+ if (selectedMenuItemId) {
431
+ // set focus on selected menu item
432
+ setFocus(selectedMenuItemId);
433
+
434
+ return;
435
+ }
436
+
437
+ const firstMenuItemId = R.path(["children", 0, "id"], navbar);
438
+
439
+ if (firstMenuItemId) {
440
+ // set focus on first menu item
441
+ setFocus(firstMenuItemId);
442
+ }
443
+ } catch (error) {
444
+ logger.log({
445
+ message: "setFocusOnMenu – failed to put focus on top-menu",
446
+ data: {
447
+ currentRoute,
448
+ error,
449
+ },
450
+ });
451
+ }
452
+ }
453
+
454
+ async function setFocusOnContent(currentRoute: string) {
455
+ // we need to put focus on content here
456
+ // For doing it we have to wait registration of first focusable node into focusableTree
457
+ // we wait until content node starts having children
458
+ try {
459
+ await waitForActiveScreen(currentRoute, focusableTree);
460
+ await waitForContent(focusableTree);
461
+
462
+ const content = getContentNode(focusableTree);
463
+
464
+ const { success } = setInitialFocus(content);
465
+
466
+ if (!success) {
467
+ // we've failed to put initial focus on content, re-run "setFocusOnContent" again as a fallback
468
+ throw Error(
469
+ "we've failed to put initial focus on content, re-run 'setFocusOnContent' again as a fallback"
470
+ );
471
+ }
472
+ } catch (error) {
473
+ setFocusOnContent(currentRoute);
474
+ }
475
+ }
476
+
477
+ function isTransitionInProgress() {
478
+ const routes = R.pathOr([], ["root", "children"], focusableTree);
479
+
480
+ if (routes.length <= 1) {
481
+ // there is only one or zero routes
482
+ return false;
483
+ }
484
+
485
+ return isTransitionHappening(currentFocusNode, R.last(routes));
486
+ }
487
+
488
+ function recoverFocus() {
489
+ if (isTransitionInProgress()) {
490
+ // We've failed to set focused node on the new screen => run focus recovery
491
+ setInitialFocus();
410
492
  }
411
493
  }
412
494
 
@@ -416,6 +498,11 @@ export const focusManager = (function () {
416
498
  * @param {Object} direction of the scroll
417
499
  */
418
500
  function moveFocus(direction) {
501
+ if (isTransitionInProgress()) {
502
+ // We are opening new screen. We need to ignore movement of focus.
503
+ return false;
504
+ }
505
+
419
506
  if (focusDisabled) return false;
420
507
 
421
508
  if (currentFocusNode) {
@@ -444,8 +531,6 @@ export const focusManager = (function () {
444
531
  } else {
445
532
  currentFocusNode.component.failedLostFocus(currentFocusNode, direction);
446
533
  }
447
- } else {
448
- setInitialFocus();
449
534
  }
450
535
  }
451
536
 
@@ -505,7 +590,6 @@ export const focusManager = (function () {
505
590
  on,
506
591
  invokeHandler,
507
592
  removeHandler,
508
- setupInitialFocus,
509
593
  longPress,
510
594
  pressOut,
511
595
  enableFocus,
@@ -514,9 +598,10 @@ export const focusManager = (function () {
514
598
  isFocusDisabled,
515
599
  getCurrentFocusNode,
516
600
  focusableTree,
517
- disableAutomaticFocus,
518
- enableAutomaticFocus,
519
601
  getGroupRootById,
520
602
  isGroupItemFocused,
603
+ setFocusOnMenu,
604
+ setFocusOnContent,
605
+ recoverFocus,
521
606
  };
522
607
  })();
@@ -1,5 +1,6 @@
1
- import * as R from "ramda";
1
+ import { allPass, pathOr, startsWith, isNil } from "ramda";
2
2
  import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
3
+ import { isZero } from "@applicaster/zapp-react-native-utils/numberUtils";
3
4
 
4
5
  import { rejectItemByPath } from "../../../../objectUtils";
5
6
  import {
@@ -143,7 +144,47 @@ const hasChildren = (target) =>
143
144
 
144
145
  const hasComponent = (target) => !!target?.component;
145
146
 
146
- const hasComponentAndChildren = R.allPass([hasComponent, hasChildren]);
147
+ const hasComponentAndChildren = allPass([hasComponent, hasChildren]);
148
+
149
+ const positiveOrZero = (value) => value > 0 || isZero(value);
150
+
151
+ const excludeFromFocusSearching = pathOr(false, [
152
+ "component",
153
+ "props",
154
+ "excludeFromFocusSearching",
155
+ ]);
156
+
157
+ const isNavBar = (target) => startsWith("navbar", target?.id);
158
+
159
+ const ignoreMenuIfMultipleTargets = (nodes) => {
160
+ if (nodes.length > 1 && nodes.map((n) => n?.target?.id).includes("navbar")) {
161
+ return nodes.filter((n) => !isNavBar(n?.target));
162
+ }
163
+
164
+ return nodes;
165
+ };
166
+
167
+ const toDistance = ({ target, direction, comparisonRect }) => {
168
+ const componentRect = target.component?.getRect();
169
+
170
+ const orientationDistance = measureDistance(
171
+ direction,
172
+ comparisonRect,
173
+ componentRect
174
+ );
175
+
176
+ if (orientationDistance === -1) {
177
+ // ignores offscreen targets when moving up/down
178
+ return null;
179
+ }
180
+
181
+ const euclidDistance = measureEuclidianDistanceToCenter(
182
+ comparisonRect,
183
+ componentRect
184
+ );
185
+
186
+ return orientationDistance > 0 ? euclidDistance : orientationDistance;
187
+ };
147
188
 
148
189
  /**
149
190
  * Get target in shortest distance in specified direction
@@ -173,52 +214,18 @@ export function getTargetInDirection(
173
214
 
174
215
  const distances = targets
175
216
  .filter(hasComponentAndChildren)
176
- .map((target) => {
177
- const componentRect = target.component?.getRect();
178
-
179
- const distance = measureDistance(
180
- direction,
181
- comparisonRect,
182
- componentRect
183
- );
184
-
185
- if (distance === -1) {
186
- // ignores offscreen targets when moving up/down
187
- return null;
188
- }
189
-
190
- const orientationDistance = measureDistance(
191
- direction,
192
- comparisonRect,
193
- componentRect
194
- );
195
-
196
- const euclidDistance = measureEuclidianDistanceToCenter(
197
- comparisonRect,
198
- componentRect
199
- );
200
-
201
- return {
202
- distance:
203
- orientationDistance > 0 ? euclidDistance : orientationDistance,
204
- target,
205
- };
206
- })
207
- .filter((obj) => obj && obj.distance >= 0);
217
+ .filter((target) => !excludeFromFocusSearching(target))
218
+ .map((target) => ({
219
+ distance: toDistance({ target, direction, comparisonRect }),
220
+ target,
221
+ }))
222
+ .filter(({ distance }) => positiveOrZero(distance));
208
223
 
209
224
  const match = distances
210
225
  .sort((a, b) => a.distance - b.distance) // sorts the distances
211
226
  .filter(ignoreMenuIfMultipleTargets)[0]; // filters out multiple targets if necessary // gets the first element
212
227
 
213
- return distances && distances.length > 0 ? match.target : null;
214
- }
215
-
216
- function ignoreMenuIfMultipleTargets(nodes) {
217
- if (nodes.length > 1 && nodes.map((n) => n?.target?.id).includes("navbar")) {
218
- return nodes.filter((n) => n?.target?.id !== "navbar");
219
- }
220
-
221
- return nodes;
228
+ return match?.target;
222
229
  }
223
230
 
224
231
  /**
@@ -309,3 +316,26 @@ export function hasPriorityInDirection(direction, component) {
309
316
 
310
317
  return (prioritiseFocusOn & mappedDirection) > 0;
311
318
  }
319
+
320
+ export const getNodeBeforeRoot = (node) => {
321
+ if (isNil(node)) {
322
+ return undefined;
323
+ }
324
+
325
+ if (isNil(node.parent)) {
326
+ return undefined;
327
+ }
328
+
329
+ if (node.parent.id === "root") {
330
+ return node;
331
+ }
332
+
333
+ return getNodeBeforeRoot(node.parent);
334
+ };
335
+
336
+ export const isTransitionHappening = (currentNode, nextNode) => {
337
+ const routeForCurrentNode = getNodeBeforeRoot(currentNode);
338
+ const routeForNextNode = getNodeBeforeRoot(nextNode);
339
+
340
+ return routeForCurrentNode?.id !== routeForNextNode?.id;
341
+ };
@@ -0,0 +1,94 @@
1
+ import { isNotNil } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
2
+ import { find, last, startsWith, pathOr } from "ramda";
3
+
4
+ // run check each 300 ms
5
+ // run this check too often could lead to performance penalty on low-end devices
6
+ const HOW_OFTEN_TO_CHECK_CONDITION = 300; // ms
7
+
8
+ type Props = {
9
+ maxTimeout: number;
10
+ conditionFn: () => boolean;
11
+ };
12
+
13
+ export const waitUntil = ({ maxTimeout, conditionFn }: Props) => {
14
+ let interval;
15
+ let timeout;
16
+
17
+ return new Promise((resolve, reject) => {
18
+ // run interval check
19
+ interval = setInterval(() => {
20
+ if (conditionFn()) {
21
+ clearInterval(interval);
22
+ clearTimeout(timeout);
23
+
24
+ resolve(undefined);
25
+ }
26
+ }, HOW_OFTEN_TO_CHECK_CONDITION);
27
+
28
+ // max timeout when we need to stop interval check
29
+ timeout = setTimeout(() => {
30
+ clearInterval(interval);
31
+ clearTimeout(timeout);
32
+
33
+ const error = Error(
34
+ "Failed to wait satisfy condition. Max timeout is reached."
35
+ );
36
+
37
+ reject(error);
38
+ }, maxTimeout);
39
+ });
40
+ };
41
+
42
+ export const waitForActiveScreen = (currentRoute: string, focusableTree) => {
43
+ const isActiveScreenAvailableToFocus = (): boolean => {
44
+ const routes = focusableTree.root.children;
45
+
46
+ const route = find((route) => route.id === currentRoute, routes);
47
+
48
+ return isNotNil(route);
49
+ };
50
+
51
+ return waitUntil({
52
+ maxTimeout: HOW_OFTEN_TO_CHECK_CONDITION * 10, // ms
53
+ conditionFn: isActiveScreenAvailableToFocus,
54
+ });
55
+ };
56
+
57
+ const getActiveScreenChildren = (focusableTree) => {
58
+ return pathOr([], ["children"], last(focusableTree.root.children));
59
+ };
60
+
61
+ export const getContentNode = (focusableTree) => {
62
+ const content = find(
63
+ (child) => startsWith("content", child.id),
64
+ getActiveScreenChildren(focusableTree)
65
+ );
66
+
67
+ return content;
68
+ };
69
+
70
+ export const getNavbarNode = (focusableTree) => {
71
+ const navbar = find(
72
+ (child) => startsWith("navbar", child.id),
73
+ getActiveScreenChildren(focusableTree)
74
+ );
75
+
76
+ return navbar;
77
+ };
78
+
79
+ export const waitForContent = (focusableTree) => {
80
+ const contentHasAnyChildren = (): boolean => {
81
+ const countOfChildren = pathOr(
82
+ 0,
83
+ ["children", "length"],
84
+ getContentNode(focusableTree)
85
+ );
86
+
87
+ return countOfChildren > 0;
88
+ };
89
+
90
+ return waitUntil({
91
+ maxTimeout: HOW_OFTEN_TO_CHECK_CONDITION * 10, // ms
92
+ conditionFn: contentHasAnyChildren,
93
+ });
94
+ };
@@ -727,4 +727,5 @@ export class Player {
727
727
  }
728
728
 
729
729
  public supportsNativeControls = (): boolean => false;
730
+ public supportNativeCast = (): boolean => false;
730
731
  }