@applicaster/zapp-react-native-utils 14.0.0-alpha.5333112458 → 14.0.0-alpha.5365702091

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 (84) hide show
  1. package/actionsExecutor/ActionExecutorContext.tsx +0 -1
  2. package/actionsExecutor/ScreenActions.ts +20 -19
  3. package/analyticsUtils/AnalyticsEvents/sendHeaderClickEvent.ts +1 -1
  4. package/analyticsUtils/AnalyticsEvents/sendMenuClickEvent.ts +2 -1
  5. package/analyticsUtils/__tests__/analyticsUtils.test.js +0 -11
  6. package/analyticsUtils/index.tsx +3 -4
  7. package/analyticsUtils/manager.ts +1 -1
  8. package/analyticsUtils/playerAnalyticsTracker.ts +2 -1
  9. package/appUtils/HooksManager/Hook.ts +4 -4
  10. package/appUtils/HooksManager/index.ts +11 -1
  11. package/appUtils/accessibilityManager/const.ts +13 -0
  12. package/appUtils/accessibilityManager/hooks.ts +35 -1
  13. package/appUtils/accessibilityManager/index.ts +142 -30
  14. package/appUtils/accessibilityManager/utils.ts +24 -0
  15. package/appUtils/contextKeysManager/contextResolver.ts +29 -1
  16. package/appUtils/focusManager/__tests__/__snapshots__/focusManager.test.js.snap +7 -0
  17. package/appUtils/focusManager/__tests__/focusManager.test.js +1 -1
  18. package/appUtils/focusManager/events.ts +2 -0
  19. package/appUtils/focusManager/index.ios.ts +10 -0
  20. package/appUtils/focusManager/index.ts +86 -11
  21. package/appUtils/focusManager/treeDataStructure/Tree/index.js +1 -1
  22. package/appUtils/focusManagerAux/utils/index.ts +94 -3
  23. package/appUtils/platform/platformUtils.ts +31 -1
  24. package/arrayUtils/index.ts +1 -1
  25. package/componentsUtils/__tests__/isTabsScreen.test.ts +38 -0
  26. package/componentsUtils/index.ts +4 -1
  27. package/configurationUtils/__tests__/manifestKeyParser.test.ts +0 -1
  28. package/configurationUtils/index.ts +1 -1
  29. package/focusManager/FocusManager.ts +78 -4
  30. package/focusManager/aux/index.ts +98 -0
  31. package/focusManager/utils.ts +12 -6
  32. package/index.d.ts +1 -13
  33. package/manifestUtils/defaultManifestConfigurations/player.js +188 -2
  34. package/manifestUtils/index.js +4 -0
  35. package/manifestUtils/keys.js +12 -0
  36. package/manifestUtils/sharedConfiguration/screenPicker/stylesFields.js +6 -0
  37. package/navigationUtils/__tests__/mapContentTypesToRivers.test.ts +130 -0
  38. package/navigationUtils/index.ts +7 -5
  39. package/package.json +2 -3
  40. package/playerUtils/PlayerTTS/PlayerTTS.ts +359 -0
  41. package/playerUtils/PlayerTTS/index.ts +1 -0
  42. package/playerUtils/getPlayerActionButtons.ts +1 -1
  43. package/playerUtils/usePlayerTTS.ts +21 -0
  44. package/reactHooks/autoscrolling/__tests__/useTrackedView.test.tsx +15 -14
  45. package/reactHooks/cell-click/__tests__/index.test.js +3 -0
  46. package/reactHooks/debugging/__tests__/index.test.js +0 -1
  47. package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +47 -90
  48. package/reactHooks/feed/__tests__/useFeedLoader.test.tsx +57 -37
  49. package/reactHooks/feed/index.ts +2 -0
  50. package/reactHooks/feed/useBatchLoading.ts +15 -8
  51. package/reactHooks/feed/useFeedLoader.tsx +39 -44
  52. package/reactHooks/feed/useLoadPipesDataDispatch.ts +63 -0
  53. package/reactHooks/feed/usePipesCacheReset.ts +2 -2
  54. package/reactHooks/flatList/useSequentialRenderItem.tsx +3 -3
  55. package/reactHooks/layout/__tests__/index.test.tsx +3 -1
  56. package/reactHooks/layout/useDimensions/__tests__/useDimensions.test.ts +34 -36
  57. package/reactHooks/layout/useDimensions/useDimensions.ts +2 -3
  58. package/reactHooks/layout/useLayoutVersion.ts +5 -5
  59. package/reactHooks/navigation/index.ts +5 -7
  60. package/reactHooks/navigation/useScreenStateStore.ts +3 -3
  61. package/reactHooks/resolvers/__tests__/useCellResolver.test.tsx +4 -0
  62. package/reactHooks/state/index.ts +1 -1
  63. package/reactHooks/state/useHomeRiver.ts +4 -2
  64. package/reactHooks/state/useRivers.ts +7 -8
  65. package/screenPickerUtils/index.ts +13 -0
  66. package/storage/ScreenSingleValueProvider.ts +25 -22
  67. package/storage/ScreenStateMultiSelectProvider.ts +26 -23
  68. package/testUtils/index.tsx +7 -8
  69. package/time/BackgroundTimer.ts +1 -1
  70. package/utils/__tests__/endsWith.test.ts +30 -0
  71. package/utils/__tests__/find.test.ts +36 -0
  72. package/utils/__tests__/omit.test.ts +19 -0
  73. package/utils/__tests__/path.test.ts +33 -0
  74. package/utils/__tests__/pathOr.test.ts +37 -0
  75. package/utils/__tests__/startsWith.test.ts +30 -0
  76. package/utils/__tests__/take.test.ts +40 -0
  77. package/utils/endsWith.ts +9 -0
  78. package/utils/find.ts +3 -0
  79. package/utils/index.ts +18 -1
  80. package/utils/omit.ts +5 -0
  81. package/utils/path.ts +5 -0
  82. package/utils/pathOr.ts +5 -0
  83. package/utils/startsWith.ts +9 -0
  84. package/utils/take.ts +5 -0
@@ -0,0 +1,359 @@
1
+ import uuidv4 from "uuid/v4";
2
+ import { AccessibilityManager } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager";
3
+ import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
4
+ import { Player } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/player";
5
+
6
+ const { log_debug, log_error } = createLogger({
7
+ subsystem: "Player",
8
+ category: "PlayerTTS",
9
+ });
10
+
11
+ enum SEEK_DIRECTION {
12
+ FORWARD = "forward",
13
+ REWIND = "back",
14
+ }
15
+
16
+ const hasPrerollAds = (entry: ZappEntry): boolean => {
17
+ const videoAds = entry?.extensions?.video_ads;
18
+
19
+ if (!videoAds) {
20
+ return false;
21
+ }
22
+
23
+ // If it's a string (VMAP URL), assume it might have preroll
24
+ if (typeof videoAds === "string") {
25
+ return true;
26
+ }
27
+
28
+ // If it's an array, check for preroll offset
29
+ if (Array.isArray(videoAds)) {
30
+ return videoAds.some(
31
+ (ad: ZappVideoAdExtension) => ad.offset === "preroll" || ad.offset === 0
32
+ );
33
+ }
34
+
35
+ return false;
36
+ };
37
+
38
+ export class PlayerTTS {
39
+ private player: Player;
40
+ private accessibilityManager: AccessibilityManager;
41
+ private seekStartPosition: number | null = null;
42
+ private isSeeking: boolean = false;
43
+ private listenerId: string;
44
+ private isInitialPlayerOpen: boolean = true;
45
+ private isPrerollActive: boolean = false;
46
+ private hasPrerollAds: boolean = false; // Track if preroll ads are expected
47
+
48
+ constructor(player: Player, accessibilityManager: AccessibilityManager) {
49
+ this.player = player;
50
+ this.accessibilityManager = accessibilityManager;
51
+ this.listenerId = `player-tts-${uuidv4()}`;
52
+ this.hasPrerollAds = hasPrerollAds(player.entry);
53
+
54
+ log_debug("PlayerTTS initialized", {
55
+ hasPrerollAds: this.hasPrerollAds,
56
+ listenerId: this.listenerId,
57
+ entryTitle: player.entry.title,
58
+ });
59
+ }
60
+
61
+ private numberToWords(num: number): string {
62
+ const ones = [
63
+ "",
64
+ "one",
65
+ "two",
66
+ "three",
67
+ "four",
68
+ "five",
69
+ "six",
70
+ "seven",
71
+ "eight",
72
+ "nine",
73
+ "ten",
74
+ "eleven",
75
+ "twelve",
76
+ "thirteen",
77
+ "fourteen",
78
+ "fifteen",
79
+ "sixteen",
80
+ "seventeen",
81
+ "eighteen",
82
+ "nineteen",
83
+ ];
84
+
85
+ const tens = [
86
+ "",
87
+ "",
88
+ "twenty",
89
+ "thirty",
90
+ "forty",
91
+ "fifty",
92
+ "sixty",
93
+ "seventy",
94
+ "eighty",
95
+ "ninety",
96
+ ];
97
+
98
+ if (num === 0) return "zero";
99
+ if (num < 20) return ones[num];
100
+
101
+ const ten = Math.floor(num / 10);
102
+ const one = num % 10;
103
+
104
+ return one === 0 ? tens[ten] : `${tens[ten]} ${ones[one]}`;
105
+ }
106
+
107
+ private secondsToTime(
108
+ seconds: number,
109
+ format: "natural" | "standard" = "natural"
110
+ ): string {
111
+ if (seconds < 0) return format === "natural" ? "zero" : "0";
112
+
113
+ const minutes = Math.floor(seconds / 60);
114
+ const remainingSeconds = Math.floor(seconds % 60);
115
+
116
+ if (format === "standard") {
117
+ const parts = [];
118
+
119
+ if (minutes > 0) {
120
+ parts.push(`${minutes} minute${minutes !== 1 ? "s" : ""}`);
121
+ }
122
+
123
+ if (remainingSeconds > 0) {
124
+ parts.push(
125
+ `${remainingSeconds} second${remainingSeconds !== 1 ? "s" : ""}`
126
+ );
127
+ }
128
+
129
+ return parts.length > 0 ? parts.join(" ") : "0";
130
+ } else {
131
+ if (minutes === 0) {
132
+ if (remainingSeconds === 0) return "zero";
133
+
134
+ if (remainingSeconds < 10) {
135
+ return `zero o ${this.numberToWords(remainingSeconds)}`;
136
+ }
137
+
138
+ return `zero ${this.numberToWords(remainingSeconds)}`;
139
+ }
140
+
141
+ if (remainingSeconds === 0) {
142
+ return `${this.numberToWords(minutes)}`;
143
+ }
144
+
145
+ if (remainingSeconds < 10) {
146
+ return `${this.numberToWords(minutes)} o ${this.numberToWords(remainingSeconds)}`;
147
+ }
148
+
149
+ return `${this.numberToWords(minutes)} ${this.numberToWords(remainingSeconds)}`;
150
+ }
151
+ }
152
+
153
+ private announcePause = () => {
154
+ if (!this.isSeeking) {
155
+ this.accessibilityManager.addHeading(
156
+ `Paused - ${this.secondsToTime(this.player.playerState.contentPosition, "standard")}`
157
+ );
158
+ }
159
+ };
160
+
161
+ private announceContentStart(
162
+ options: {
163
+ currentTime?: number;
164
+ duration?: number;
165
+ useReadText?: boolean;
166
+ } = {}
167
+ ): void {
168
+ const { currentTime, duration, useReadText = false } = options;
169
+ const state = this.player.playerState;
170
+
171
+ const timeRemaining =
172
+ (duration || state?.contentDuration || 0) -
173
+ (currentTime || state?.contentPosition || 0);
174
+
175
+ const title = (this.player.entry.title as string) || "";
176
+ const summary = (this.player.entry.summary as string) || "";
177
+
178
+ log_debug("Announcing content start", {
179
+ title,
180
+ currentTime: currentTime || state?.contentPosition || 0,
181
+ duration: duration || state?.contentDuration || 0,
182
+ timeRemaining,
183
+ useReadText,
184
+ });
185
+
186
+ this.accessibilityManager.addHeading(`Playing - ${title}`);
187
+ if (summary) this.accessibilityManager.addHeading(summary);
188
+
189
+ this.accessibilityManager.addHeading(
190
+ `Playing from ${this.secondsToTime(currentTime || state?.contentPosition || 0, "standard")}`
191
+ );
192
+
193
+ const remainingText = `${this.secondsToTime(Math.max(0, Math.floor(timeRemaining)), "standard")} remaining.`;
194
+
195
+ if (useReadText) {
196
+ this.accessibilityManager.readText({ text: remainingText });
197
+ } else {
198
+ this.accessibilityManager.addHeading(remainingText);
199
+ }
200
+
201
+ this.accessibilityManager.setInitialPlayerAnnouncementReady();
202
+ this.isInitialPlayerOpen = false;
203
+ }
204
+
205
+ private announceBufferComplete = (event: any) => {
206
+ // If preroll ads are expected, wait for them to finish before announcing content
207
+ if (this.hasPrerollAds && this.isInitialPlayerOpen) {
208
+ log_debug("Waiting for preroll ads to finish", {
209
+ hasPrerollAds: this.hasPrerollAds,
210
+ isInitialPlayerOpen: this.isInitialPlayerOpen,
211
+ });
212
+
213
+ return;
214
+ }
215
+
216
+ // Gate content announcement until preroll finishes
217
+ if (this.isInitialPlayerOpen && !this.isPrerollActive) {
218
+ log_debug("Buffer complete - announcing content", {
219
+ currentTime: event.currentTime,
220
+ duration: event.duration,
221
+ isPrerollActive: this.isPrerollActive,
222
+ });
223
+
224
+ this.announceContentStart({
225
+ currentTime: event.currentTime,
226
+ duration: event.duration,
227
+ });
228
+ }
229
+ };
230
+
231
+ private announceResume = () => {
232
+ if (!this.isSeeking && !this.isInitialPlayerOpen) {
233
+ log_debug("Player resumed", {
234
+ contentPosition: this.player.playerState.contentPosition,
235
+ isSeeking: this.isSeeking,
236
+ isInitialPlayerOpen: this.isInitialPlayerOpen,
237
+ });
238
+
239
+ this.accessibilityManager.addHeading(
240
+ `Playing - ${this.secondsToTime(this.player.playerState.contentPosition, "standard")}`
241
+ );
242
+ }
243
+ };
244
+
245
+ private handleVideoProgress = (event: any) => {
246
+ if (event.currentTime > 0) {
247
+ this.seekStartPosition = event.currentTime;
248
+ }
249
+ };
250
+
251
+ private handleSeekComplete = (event: any) => {
252
+ if (this.seekStartPosition !== null) {
253
+ const seekDirection =
254
+ event.currentTime > this.seekStartPosition
255
+ ? SEEK_DIRECTION.FORWARD
256
+ : SEEK_DIRECTION.REWIND;
257
+
258
+ const seekAmount = Math.round(
259
+ Math.abs(event.currentTime - this.seekStartPosition)
260
+ );
261
+
262
+ log_debug("Seek completed", {
263
+ seekDirection,
264
+ seekAmount,
265
+ fromPosition: this.seekStartPosition,
266
+ toPosition: event.currentTime,
267
+ });
268
+
269
+ this.accessibilityManager.readText({
270
+ text: `Skipped ${seekDirection} ${this.secondsToTime(seekAmount, "standard")}`,
271
+ });
272
+
273
+ this.seekStartPosition = event.currentTime;
274
+ }
275
+
276
+ this.isSeeking = false;
277
+ };
278
+
279
+ private handleSeekStart = () => {
280
+ log_debug("Seek started");
281
+ this.isSeeking = true;
282
+ };
283
+
284
+ private handlePlayerClose = () => {
285
+ log_debug("Player closed - resetting state");
286
+ this.isInitialPlayerOpen = true;
287
+ this.accessibilityManager.resetInitialPlayerAnnouncementReady();
288
+ };
289
+
290
+ private announceAdBegin = (event: any) => {
291
+ this.isPrerollActive = true;
292
+
293
+ log_debug("Ad started", {
294
+ adDuration: event?.ad?.data?.duration,
295
+ isPrerollActive: this.isPrerollActive,
296
+ });
297
+
298
+ if (event?.ad?.data?.duration) {
299
+ this.accessibilityManager.readText({
300
+ text: `Sponsored. Ends in ${this.secondsToTime(event.ad.data.duration, "standard")}`,
301
+ });
302
+ }
303
+ };
304
+
305
+ private handleAdEnd = (_event: any) => {
306
+ this.isPrerollActive = false;
307
+
308
+ log_debug("Ad ended", {
309
+ isPrerollActive: this.isPrerollActive,
310
+ isInitialPlayerOpen: this.isInitialPlayerOpen,
311
+ });
312
+
313
+ // If initial entry still pending, trigger content announcement using latest player state
314
+ if (this.isInitialPlayerOpen) {
315
+ this.announceContentStart({ useReadText: true });
316
+ }
317
+ };
318
+
319
+ public init(): () => void {
320
+ if (!this.player) {
321
+ log_error("Failed to initialize PlayerTTS - no player provided");
322
+
323
+ return () => {};
324
+ }
325
+
326
+ log_debug("Initializing PlayerTTS listeners", {
327
+ listenerId: this.listenerId,
328
+ });
329
+
330
+ return this.player.addListener({
331
+ id: this.listenerId,
332
+ listener: {
333
+ onBufferComplete: this.announceBufferComplete,
334
+ onPlayerResume: this.announceResume,
335
+ onPlayerPause: this.announcePause,
336
+ onVideoProgress: this.handleVideoProgress,
337
+ onPlayerSeekStart: this.handleSeekStart,
338
+ onPlayerSeekComplete: this.handleSeekComplete,
339
+ onPlayerClose: this.handlePlayerClose,
340
+ onAdBegin: this.announceAdBegin,
341
+ onAdEnd: this.handleAdEnd,
342
+ onAdBreakEnd: this.handleAdEnd,
343
+ },
344
+ });
345
+ }
346
+
347
+ public destroy(): void {
348
+ log_debug("Destroying PlayerTTS", {
349
+ listenerId: this.listenerId,
350
+ });
351
+
352
+ if (this.player) {
353
+ this.player.removeListener(this.listenerId);
354
+ }
355
+
356
+ this.seekStartPosition = null;
357
+ this.handlePlayerClose();
358
+ }
359
+ }
@@ -0,0 +1 @@
1
+ export { PlayerTTS } from "./PlayerTTS";
@@ -13,5 +13,5 @@ export const getPlayerActionButtons = (configuration: any) => {
13
13
  return [];
14
14
  }
15
15
 
16
- return take(map(buttonsString.split(","), trim), 2);
16
+ return take(2, map(buttonsString.split(","), trim));
17
17
  };
@@ -0,0 +1,21 @@
1
+ import * as React from "react";
2
+ import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer";
3
+ import { useAccessibilityManager } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager/hooks";
4
+ import { PlayerTTS } from "@applicaster/zapp-react-native-utils/playerUtils/PlayerTTS";
5
+
6
+ export const usePlayerTTS = () => {
7
+ const player = usePlayer();
8
+ const accessibilityManager = useAccessibilityManager({});
9
+
10
+ React.useEffect(() => {
11
+ if (player && accessibilityManager) {
12
+ const playerTTS = new PlayerTTS(player, accessibilityManager);
13
+ const unsubscribe = playerTTS.init();
14
+
15
+ return () => {
16
+ unsubscribe();
17
+ playerTTS.destroy();
18
+ };
19
+ }
20
+ }, [player, accessibilityManager]);
21
+ };
@@ -1,27 +1,31 @@
1
1
  import React from "react";
2
2
 
3
- import { act, renderHook } from "@testing-library/react-hooks";
3
+ import { renderHook } from "@testing-library/react-hooks";
4
+ import { act, waitFor } from "@testing-library/react-native";
4
5
  import { Provider } from "react-redux";
5
6
  import configureStore from "redux-mock-store";
7
+ import { useTrackedView } from "../useTrackedView";
6
8
 
7
9
  const mockUpdateComponentsPositions = jest.fn();
8
10
 
9
11
  jest.mock(
10
12
  "@applicaster/zapp-react-native-ui-components/Contexts/ScreenTrackedViewPositionsContext",
11
13
  () => ({
12
- useScreenTrackedViewPositionsContext: jest.fn().mockReturnValue({
14
+ useScreenTrackedViewPositionsContext: jest.fn(() => ({
13
15
  updateComponentsPositions: mockUpdateComponentsPositions,
14
16
  value: {
15
17
  "123": { componentId: "123", centerX: 0.4, centerY: 0.5 },
16
18
  "124": { componentId: "124", centerX: 0.2, centerY: 0.3 },
17
19
  },
18
- }),
20
+ })),
19
21
  })
20
22
  );
21
23
 
22
- jest.useFakeTimers({ legacyFakeTimers: true });
24
+ jest.useFakeTimers();
23
25
 
24
- jest.mock("@applicaster/zapp-react-native-utils/reactHooks/navigation");
26
+ jest.mock(
27
+ "@applicaster/zapp-react-native-utils/reactHooks/navigation/useNavigation"
28
+ );
25
29
 
26
30
  const mockStore = configureStore();
27
31
 
@@ -32,10 +36,8 @@ const Wrapper = ({ children }: { children: React.ReactChild }) => (
32
36
  <Provider store={store}>{children}</Provider>
33
37
  );
34
38
 
35
- const { useTrackedView } = require("../useTrackedView");
36
-
37
39
  describe("useTrackCurrentAutoScrollingElement", () => {
38
- it("should update position for selected component - onViewportEnter", () => {
40
+ it("should update position for selected component - onViewportEnter", async () => {
39
41
  const { result } = renderHook(() => useTrackedView("123"), {
40
42
  wrapper: Wrapper,
41
43
  });
@@ -46,14 +48,13 @@ describe("useTrackCurrentAutoScrollingElement", () => {
46
48
  rect: { left: 1, right: 1, top: 1, bottom: 1 },
47
49
  };
48
50
 
49
- act(async () => {
50
- await result.current.onPositionUpdated(mockRect);
51
+ act(() => {
52
+ result.current.onPositionUpdated(mockRect);
51
53
  });
52
54
 
53
- // Fast-forward until all timers have been executed
54
- jest.runAllTimers();
55
-
56
- expect(result.current.inViewPort).toBe(true);
55
+ await waitFor(() => {
56
+ expect(result.current.inViewPort).toBe(true);
57
+ });
57
58
 
58
59
  expect(mockUpdateComponentsPositions).toHaveBeenCalledWith(
59
60
  "123",
@@ -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
  }));
@@ -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