@applicaster/zapp-react-native-utils 14.0.0-alpha.4385101226 → 14.0.0-alpha.4520122433

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 (69) 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/analyticsUtils/playerAnalyticsTracker.ts +2 -1
  9. package/appUtils/accessibilityManager/const.ts +13 -0
  10. package/appUtils/accessibilityManager/hooks.ts +35 -1
  11. package/appUtils/accessibilityManager/index.ts +151 -30
  12. package/appUtils/accessibilityManager/utils.ts +24 -0
  13. package/appUtils/contextKeysManager/contextResolver.ts +42 -1
  14. package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +7 -0
  15. package/appUtils/focusManager/__tests__/focusManager.test.js +1 -1
  16. package/appUtils/focusManager/events.ts +2 -0
  17. package/appUtils/focusManager/index.ios.ts +10 -0
  18. package/appUtils/focusManager/index.ts +86 -11
  19. package/appUtils/focusManagerAux/utils/index.ts +94 -3
  20. package/appUtils/platform/platformUtils.ts +31 -1
  21. package/configurationUtils/__tests__/manifestKeyParser.test.ts +0 -1
  22. package/configurationUtils/index.ts +1 -1
  23. package/focusManager/FocusManager.ts +78 -4
  24. package/focusManager/aux/index.ts +98 -0
  25. package/focusManager/utils.ts +12 -6
  26. package/index.d.ts +1 -1
  27. package/manifestUtils/defaultManifestConfigurations/player.js +188 -2
  28. package/manifestUtils/index.js +4 -0
  29. package/manifestUtils/keys.js +12 -0
  30. package/manifestUtils/sharedConfiguration/screenPicker/stylesFields.js +6 -0
  31. package/navigationUtils/index.ts +1 -1
  32. package/package.json +2 -2
  33. package/playerUtils/PlayerTTS/PlayerTTS.ts +359 -0
  34. package/playerUtils/PlayerTTS/index.ts +1 -0
  35. package/playerUtils/getPlayerActionButtons.ts +1 -1
  36. package/playerUtils/usePlayerTTS.ts +21 -0
  37. package/reactHooks/cell-click/__tests__/index.test.js +3 -0
  38. package/reactHooks/cell-click/index.ts +8 -1
  39. package/reactHooks/debugging/__tests__/index.test.js +0 -1
  40. package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +8 -2
  41. package/reactHooks/feed/__tests__/useFeedLoader.test.tsx +71 -31
  42. package/reactHooks/feed/index.ts +2 -0
  43. package/reactHooks/feed/useBatchLoading.ts +14 -9
  44. package/reactHooks/feed/useFeedLoader.tsx +36 -38
  45. package/reactHooks/feed/useLoadPipesDataDispatch.ts +63 -0
  46. package/reactHooks/navigation/useRoute.ts +7 -2
  47. package/reactHooks/navigation/useScreenStateStore.ts +8 -0
  48. package/reactHooks/state/index.ts +1 -1
  49. package/reactHooks/state/useHomeRiver.ts +4 -2
  50. package/screenPickerUtils/index.ts +13 -0
  51. package/storage/ScreenSingleValueProvider.ts +204 -0
  52. package/storage/ScreenStateMultiSelectProvider.ts +293 -0
  53. package/storage/StorageMultiSelectProvider.ts +192 -0
  54. package/storage/StorageSingleSelectProvider.ts +108 -0
  55. package/utils/__tests__/endsWith.test.ts +30 -0
  56. package/utils/__tests__/find.test.ts +36 -0
  57. package/utils/__tests__/omit.test.ts +19 -0
  58. package/utils/__tests__/path.test.ts +33 -0
  59. package/utils/__tests__/pathOr.test.ts +37 -0
  60. package/utils/__tests__/startsWith.test.ts +30 -0
  61. package/utils/__tests__/take.test.ts +40 -0
  62. package/utils/endsWith.ts +9 -0
  63. package/utils/find.ts +3 -0
  64. package/utils/index.ts +17 -1
  65. package/utils/omit.ts +5 -0
  66. package/utils/path.ts +5 -0
  67. package/utils/pathOr.ts +5 -0
  68. package/utils/startsWith.ts +9 -0
  69. package/utils/take.ts +5 -0
@@ -1,14 +1,31 @@
1
- import { isNotNil } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
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
 
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);
28
+
12
29
  type Props = {
13
30
  maxTimeout: number;
14
31
  conditionFn: () => boolean;
@@ -49,7 +66,7 @@ export const waitForActiveScreen = (currentRoute: string, focusableTree) => {
49
66
 
50
67
  const route = find((route) => route.id === currentRoute, routes);
51
68
 
52
- return isNotNil(route);
69
+ return !isNil(route);
53
70
  };
54
71
 
55
72
  return waitUntil({
@@ -99,3 +116,77 @@ export const waitForContent = (focusableTree) => {
99
116
  conditionFn: contentHasAnyChildren,
100
117
  });
101
118
  };
119
+
120
+ export const findSelectedTabId = (item: ZappEntry): string => {
121
+ const selectedTabId = getFocusableId(item.id);
122
+
123
+ return selectedTabId;
124
+ };
125
+
126
+ export const isTabsScreenOnContentFocused = (node) => {
127
+ if (isRoot(node)) {
128
+ return false;
129
+ }
130
+
131
+ if (isTopMenu(node)) {
132
+ return false;
133
+ }
134
+
135
+ if (isContent(node)) {
136
+ return false;
137
+ }
138
+
139
+ if (isScrenPicker(node)) {
140
+ return true;
141
+ }
142
+
143
+ return isTabsScreenOnContentFocused(node.parent);
144
+ };
145
+
146
+ export const isCurrentFocusOnMenu = (node) => {
147
+ if (isRoot(node)) {
148
+ return false;
149
+ }
150
+
151
+ if (isTopMenu(node)) {
152
+ return true;
153
+ }
154
+
155
+ if (isContent(node)) {
156
+ return false;
157
+ }
158
+
159
+ return isCurrentFocusOnMenu(node.parent);
160
+ };
161
+
162
+ export const isCurrentFocusOnContent = (node) => {
163
+ if (isRoot(node)) {
164
+ return false;
165
+ }
166
+
167
+ if (isTopMenu(node)) {
168
+ return false;
169
+ }
170
+
171
+ if (isContent(node)) {
172
+ return true;
173
+ }
174
+
175
+ return isCurrentFocusOnContent(node.parent);
176
+ };
177
+
178
+ export const isCurrentFocusOn = (id, node) => {
179
+ if (!node) {
180
+ return false;
181
+ }
182
+
183
+ if (isRoot(node)) {
184
+ return false;
185
+ }
186
+
187
+ if (node?.id === id) {
188
+ return true;
189
+ }
190
+
191
+ return isCurrentFocusOn(id, node.parent);
192
+ };
@@ -10,6 +10,7 @@ import { PLATFORM_KEYS, PLATFORMS, ZappPlatform } from "./const";
10
10
  import { createLogger, utilsLogger } from "../../logger";
11
11
  import { getPlatform } from "../../reactUtils";
12
12
  import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
13
+ import { calculateReadingTime } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager/utils";
13
14
 
14
15
  const { log_debug } = createLogger({
15
16
  category: "General",
@@ -212,9 +213,16 @@ export class TTSManager {
212
213
  }
213
214
 
214
215
  readText(text: string) {
216
+ this.ttsState$.next(true);
217
+
215
218
  if (isSamsungPlatform() && window.speechSynthesis) {
216
219
  const utterance = new SpeechSynthesisUtterance(text);
220
+
221
+ window.speechSynthesis.cancel(); // Cancel previous speech before speaking new text
217
222
  window.speechSynthesis.speak(utterance);
223
+
224
+ // Estimate reading time and set inactive when done
225
+ this.scheduleTTSComplete(text);
218
226
  }
219
227
 
220
228
  if (isLgPlatform() && window.webOS?.service) {
@@ -225,23 +233,45 @@ export class TTSManager {
225
233
  log_debug("There was a failure setting up webOS TTS service", {
226
234
  error,
227
235
  });
236
+
237
+ this.ttsState$.next(false);
228
238
  },
229
239
  onSuccess(response: any) {
230
240
  log_debug("webOS TTS service is configured successfully", {
231
241
  response,
232
242
  });
243
+
244
+ // Estimate reading time and set inactive when done
245
+ this.scheduleTTSComplete(text);
233
246
  },
234
247
  parameters: {
235
248
  text,
249
+ clear: true, // Clear any previous speech before speaking new text
236
250
  },
237
251
  });
238
252
  } catch (error) {
239
253
  log_debug("webOS TTS service error", { error });
254
+ this.ttsState$.next(false);
240
255
  }
241
256
  }
242
257
 
243
- if (!window.VIZIO?.Chromevox) return;
258
+ if (!window.VIZIO?.Chromevox) {
259
+ // For platforms without TTS, estimate reading time
260
+ this.scheduleTTSComplete(text);
261
+
262
+ return;
263
+ }
244
264
 
245
265
  window.VIZIO.Chromevox.play(text);
266
+ // Estimate reading time and set inactive when done
267
+ this.scheduleTTSComplete(text);
268
+ }
269
+
270
+ private scheduleTTSComplete(text: string) {
271
+ const readingTime = calculateReadingTime(text);
272
+
273
+ setTimeout(() => {
274
+ this.ttsState$.next(false);
275
+ }, readingTime);
246
276
  }
247
277
  }
@@ -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
  }));
@@ -399,7 +399,7 @@ export const populateConfigurationValues =
399
399
  flattenAndPopulateFields(fields, configuration, skipDefaults)
400
400
  );
401
401
 
402
- export const getAccesabilityProps = (item: ZappEntry) => ({
402
+ export const getAccessibilityProps = (item: ZappEntry) => ({
403
403
  accessible: item?.extensions?.accessibility,
404
404
  accessibilityLabel: item?.extensions?.accessibility?.label || item?.title,
405
405
  accessibilityHint: item?.extensions?.accessibility?.hint,
@@ -2,11 +2,24 @@ import { path } from "ramda";
2
2
  import { isString } from "@applicaster/zapp-react-native-utils/stringUtils";
3
3
  import * as FOCUS_EVENTS from "@applicaster/zapp-react-native-utils/appUtils/focusManager/events";
4
4
 
5
+ import {
6
+ QUICK_BRICK_CONTENT,
7
+ QUICK_BRICK_NAVBAR,
8
+ } from "@applicaster/quick-brick-core/const";
9
+
5
10
  import { logger } from "./logger";
6
11
  import { TreeNode } from "./TreeNode";
7
12
  import { Tree } from "./Tree";
8
13
  import { subscriber } from "../functionUtils";
9
14
  import { getFocusableId, toFocusDirection } from "./utils";
15
+ import {
16
+ findSelectedMenuId,
17
+ isTabsScreenContentFocused,
18
+ findSelectedTabId,
19
+ contextWithoutScrolling,
20
+ } from "./aux";
21
+
22
+ export { contextWithoutScrolling } from "./aux";
10
23
 
11
24
  export {
12
25
  toFocusDirection,
@@ -221,7 +234,8 @@ class FocusManager {
221
234
 
222
235
  private setNextFocus(
223
236
  nextFocus: FocusManager.TouchableReactRef,
224
- options?: FocusManager.Android.CallbackOptions
237
+ options?: FocusManager.Android.CallbackOptions,
238
+ context?: FocusManager.FocusContext
225
239
  ) {
226
240
  if (nextFocus?.current?.props?.blockFocus) {
227
241
  return;
@@ -250,7 +264,7 @@ class FocusManager {
250
264
 
251
265
  FocusManager.instance.setPreviousNavigationDirection(options ?? null);
252
266
 
253
- nextFocus?.current?.onFocus?.(nextFocus.current, options ?? {});
267
+ nextFocus?.current?.onFocus?.(nextFocus.current, options ?? {}, context);
254
268
  }
255
269
  }
256
270
 
@@ -291,7 +305,8 @@ class FocusManager {
291
305
 
292
306
  setFocus(
293
307
  newFocus: FocusManager.TouchableReactRef | string,
294
- options?: FocusManager.Android.CallbackOptions
308
+ options?: FocusManager.Android.CallbackOptions,
309
+ context?: FocusManager.FocusContext
295
310
  ) {
296
311
  // Checks if element is focusable
297
312
  const { isFocusable, error } = FocusManager.isFocusable(newFocus);
@@ -316,7 +331,7 @@ class FocusManager {
316
331
  }
317
332
 
318
333
  if (newFocusRef) {
319
- FocusManager.instance.setNextFocus(newFocusRef, options);
334
+ FocusManager.instance.setNextFocus(newFocusRef, options, context);
320
335
  }
321
336
  }
322
337
  }
@@ -351,6 +366,11 @@ class FocusManager {
351
366
  FocusManager.instance.focused.onBlur(FocusManager.instance.focused, {});
352
367
  }
353
368
 
369
+ // send reset event to some handler to reset their internal state, before real reset happens
370
+ this.eventHandler?.invokeHandler?.(FOCUS_EVENTS.RESET, {
371
+ focusedId: FocusManager.instance.focusedId,
372
+ });
373
+
354
374
  FocusManager.instance.setFocusLocal({ current: null });
355
375
  }
356
376
 
@@ -417,6 +437,60 @@ class FocusManager {
417
437
  throw new Error(`Group with id ${id} not found`);
418
438
  }
419
439
  }
440
+
441
+ isFocusOnMenu(): boolean {
442
+ return this.isFocusableChildOf(
443
+ FocusManager.instance.focusedId,
444
+ QUICK_BRICK_NAVBAR
445
+ );
446
+ }
447
+
448
+ isFocusOnContent(): boolean {
449
+ return this.isFocusableChildOf(
450
+ FocusManager.instance.focusedId,
451
+ QUICK_BRICK_CONTENT
452
+ );
453
+ }
454
+
455
+ private landFocusToWithoutScrolling = (id) => {
456
+ if (id) {
457
+ // set focus on selected menu item
458
+ const direction = undefined;
459
+
460
+ const context: FocusManager.FocusContext =
461
+ contextWithoutScrolling("back");
462
+
463
+ logger.log({ message: "landFocusToWithoutScrolling", data: { id } });
464
+
465
+ this.setFocus(id, direction, context);
466
+ }
467
+ };
468
+
469
+ // Move focus to appropriate top navigation tab with context
470
+ focusOnSelectedTab(index: number): void {
471
+ const selectedTabId = findSelectedTabId(this.tree, index);
472
+
473
+ // Set focus with back button context to tabs-menu
474
+ this.landFocusToWithoutScrolling(selectedTabId);
475
+ }
476
+
477
+ // Move focus to appropriate top navigation tab with context
478
+ focusOnSelectedTopMenuItem(index: number, sectionKey: string): void {
479
+ const selectedMenuItemId = findSelectedMenuId(this.tree, {
480
+ index,
481
+ sectionKey,
482
+ });
483
+
484
+ // Set focus with back button context to top-menu
485
+ this.landFocusToWithoutScrolling(selectedMenuItemId);
486
+ }
487
+
488
+ isTabsScreenContentFocused(): boolean {
489
+ return isTabsScreenContentFocused(
490
+ this.tree,
491
+ FocusManager.instance.focusedId
492
+ );
493
+ }
420
494
  }
421
495
 
422
496
  export const focusManager = FocusManager.getInstance();
@@ -0,0 +1,98 @@
1
+ import { isNil, pathOr } from "@applicaster/zapp-react-native-utils/utils";
2
+
3
+ import {
4
+ QUICK_BRICK_CONTENT,
5
+ QUICK_BRICK_NAVBAR,
6
+ QUICK_BRICK_NAVBAR_SECTIONS,
7
+ } from "@applicaster/quick-brick-core/const";
8
+
9
+ const isNavBar = (node) => QUICK_BRICK_NAVBAR === node?.id;
10
+ const isContent = (node) => QUICK_BRICK_CONTENT === node?.id;
11
+
12
+ // SCREEN_PICKER_SELECTOR_CONTAINER(we assume there is only one SCREEN_PICKER)
13
+ let screenPickerSelectorContainerId;
14
+
15
+ export const onRegisterScreenPickerSelectorContainer = (id) => {
16
+ screenPickerSelectorContainerId = id;
17
+ };
18
+
19
+ export const onUnregisterScreenPickerSelectorContainer = (id) => {
20
+ // reset screenSelectorId on unregistration
21
+ if (screenPickerSelectorContainerId === id) {
22
+ screenPickerSelectorContainerId = undefined;
23
+ }
24
+ };
25
+ // SCREEN_PICKER_SELECTOR_CONTAINER
26
+
27
+ // SCREEN_PICKER_CONTENT_CONTAINER(we assume there is only one SCREEN_PICKER)
28
+ let screenPickerContentContainerId;
29
+
30
+ export const onRegisterScreenPickerContentContainer = (id) => {
31
+ screenPickerContentContainerId = id;
32
+ };
33
+
34
+ export const onUnregisterScreenPickerContentContainer = (id) => {
35
+ // reset screenSelectorId on unregistration
36
+ if (screenPickerContentContainerId === id) {
37
+ screenPickerContentContainerId = undefined;
38
+ }
39
+ };
40
+
41
+ const isScreenPickerContentContainer = (node) =>
42
+ screenPickerContentContainerId === node?.id;
43
+
44
+ // SCREEN_PICKER_CONTENT_CONTAINER
45
+
46
+ export const findSelectedMenuId = (
47
+ focusableTree,
48
+ { index, sectionKey }: { index: number; sectionKey: string }
49
+ ) => {
50
+ const sectionName = QUICK_BRICK_NAVBAR_SECTIONS[sectionKey];
51
+
52
+ return pathOr(
53
+ undefined,
54
+ ["children", index, "id"],
55
+ focusableTree.find(sectionName)
56
+ );
57
+ };
58
+
59
+ export const findSelectedTabId = (focusableTree, index: number): string => {
60
+ const screenSelectorContainerNode = focusableTree.find(
61
+ screenPickerSelectorContainerId
62
+ );
63
+
64
+ const selectedTabId = screenSelectorContainerNode.children[index]?.id;
65
+
66
+ return selectedTabId;
67
+ };
68
+
69
+ export const isTabsScreenContentFocused = (focusableTree, id) => {
70
+ const node = focusableTree.find(id);
71
+
72
+ if (isNil(node)) {
73
+ return false;
74
+ }
75
+
76
+ if (isNavBar(node)) {
77
+ return false;
78
+ }
79
+
80
+ if (isContent(node)) {
81
+ return false;
82
+ }
83
+
84
+ if (isScreenPickerContentContainer(node)) {
85
+ return true;
86
+ }
87
+
88
+ return isTabsScreenContentFocused(focusableTree, node.parentId);
89
+ };
90
+
91
+ export const contextWithoutScrolling = (
92
+ source: FocusManager.FocusContext["source"]
93
+ ): FocusManager.FocusContext => {
94
+ return {
95
+ source,
96
+ preserveScroll: true,
97
+ };
98
+ };
@@ -4,11 +4,11 @@ import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/he
4
4
 
5
5
  export const getFocusableId = (ref) => ref?.current?.props.id;
6
6
 
7
- type Direction = "up" | "down" | "left" | "right";
8
-
9
7
  const normalizeDirection = (direction) => direction.toLowerCase();
10
8
 
11
- const checkDirection = (direction: Direction) => {
9
+ const checkDirection = (
10
+ direction: FocusManager.Android.FocusNavigationDirections
11
+ ) => {
12
12
  invariant(!isNilOrEmpty(direction), "direction should not be empty");
13
13
 
14
14
  invariant(
@@ -17,19 +17,25 @@ const checkDirection = (direction: Direction) => {
17
17
  );
18
18
  };
19
19
 
20
- export const toFocusDirection = (direction: Direction) => {
20
+ export const toFocusDirection = (
21
+ direction: FocusManager.Android.FocusNavigationDirections
22
+ ) => {
21
23
  checkDirection(direction);
22
24
 
23
25
  return `nextFocus${capitalize(normalizeDirection(direction))}`;
24
26
  };
25
27
 
26
- export const isHorizontalDirection = (direction: Direction) => {
28
+ export const isHorizontalDirection = (
29
+ direction: FocusManager.Android.FocusNavigationDirections
30
+ ) => {
27
31
  checkDirection(direction);
28
32
 
29
33
  return ["left", "right"].includes(normalizeDirection(direction));
30
34
  };
31
35
 
32
- export const isVerticalDirection = (direction: Direction) => {
36
+ export const isVerticalDirection = (
37
+ direction: FocusManager.Android.FocusNavigationDirections
38
+ ) => {
33
39
  checkDirection(direction);
34
40
 
35
41
  return ["up", "down"].includes(normalizeDirection(direction));
package/index.d.ts CHANGED
@@ -69,7 +69,7 @@ declare type ExtraProps = ZappUIComponentProps & {
69
69
  };
70
70
 
71
71
  declare type WebConfirmationDialog = {
72
- message: string;
72
+ message?: string;
73
73
  confirmCompletion?: () => void;
74
74
  cancelCompletion?: () => void;
75
75
  };