@applicaster/zapp-react-native-utils 15.0.0-alpha.1121049279 → 15.0.0-alpha.1308474364

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.
@@ -17,6 +17,9 @@ export const useAccessibilityManager = (
17
17
  return AccessibilityManager.getInstance();
18
18
  }, []);
19
19
 
20
+ const [accessibilityManagerState, setAccessibilityManagerState] =
21
+ useState<AccessibilityState>(accessibilityManager.getState());
22
+
20
23
  useEffect(() => {
21
24
  if (pluginConfiguration) {
22
25
  accessibilityManager.updateLocalizations(pluginConfiguration);
@@ -25,18 +28,17 @@ export const useAccessibilityManager = (
25
28
 
26
29
  useEffect(() => {
27
30
  const subscription = accessibilityManager.getStateAsObservable().subscribe({
28
- next: () => {
29
- // TODO: handle accessibility states
30
- // screenReaderEnabled: false
31
- // reduceMotionEnabled: false
32
- // boldTextEnabled: false
31
+ next: (newState) => {
32
+ setAccessibilityManagerState(newState);
33
33
  },
34
34
  });
35
35
 
36
36
  return () => subscription.unsubscribe();
37
37
  }, [accessibilityManager]);
38
38
 
39
- return accessibilityManager;
39
+ return Object.assign(accessibilityManager, {
40
+ accessibilityManagerState,
41
+ });
40
42
  };
41
43
 
42
44
  export const useInitialAnnouncementReady = (
@@ -36,7 +36,20 @@ export class AccessibilityManager {
36
36
  false
37
37
  );
38
38
 
39
- private constructor() {}
39
+ private constructor() {
40
+ this.ttsManager
41
+ .getScreenReaderEnabledAsObservable()
42
+ .subscribe((enabled) => {
43
+ const state = this.state$.getValue();
44
+
45
+ if (state.screenReaderEnabled !== enabled) {
46
+ this.state$.next({
47
+ ...state,
48
+ screenReaderEnabled: enabled,
49
+ });
50
+ }
51
+ });
52
+ }
40
53
 
41
54
  public static getInstance(): AccessibilityManager {
42
55
  if (!AccessibilityManager._instance) {
@@ -92,8 +105,15 @@ export class AccessibilityManager {
92
105
  /**
93
106
  * Adds a heading to the queue, headings will be read before the next text
94
107
  * Each heading will be read once and removed from the queue
108
+ * Does nothing if screen reader is not enabled
95
109
  */
96
110
  public addHeading(heading: string) {
111
+ const state = this.state$.getValue();
112
+
113
+ if (!state.screenReaderEnabled) {
114
+ return;
115
+ }
116
+
97
117
  if (!this.pendingFocusId) {
98
118
  this.pendingFocusId = Date.now().toString();
99
119
  }
@@ -108,6 +128,7 @@ export class AccessibilityManager {
108
128
  *
109
129
  * Implements a delay mechanism to reduce noise during rapid navigation.
110
130
  * Only the most recent announcement will be read after the delay period.
131
+ * Does nothing if screen reader is not enabled
111
132
  */
112
133
  public readText({
113
134
  text,
@@ -116,6 +137,12 @@ export class AccessibilityManager {
116
137
  text: string;
117
138
  keyOfLocalizedText?: string;
118
139
  }) {
140
+ const state = this.state$.getValue();
141
+
142
+ if (!state.screenReaderEnabled) {
143
+ return;
144
+ }
145
+
119
146
  let textToRead = text;
120
147
 
121
148
  if (keyOfLocalizedText) {
@@ -12,13 +12,44 @@ export function calculateReadingTime(
12
12
  minimumPause: number = 500,
13
13
  announcementDelay: number = 700
14
14
  ): number {
15
- const words = text
16
- .trim()
15
+ const trimmed = text.trim();
16
+
17
+ // Count words (split on whitespace and punctuation, keep alnum boundaries)
18
+ const words = trimmed
17
19
  .split(/(?<=\d)(?=[a-zA-Z])|(?<=[a-zA-Z])(?=\d)|[^\w\s]+|\s+/)
18
20
  .filter(Boolean).length;
19
21
 
20
- return (
21
- Math.max(minimumPause, (words / wordsPerMinute) * 60 * 1000) +
22
- announcementDelay
22
+ // Count spaces - multiple consecutive spaces add extra pause time
23
+ const spaceMatches: string[] = trimmed.match(/\s+/g) || [];
24
+
25
+ const totalSpaces = spaceMatches.reduce(
26
+ (sum: number, match: string) => sum + match.length,
27
+ 0
23
28
  );
29
+
30
+ const extraSpaces = Math.max(0, totalSpaces - (words - 1)); // words-1 is normal spacing
31
+
32
+ // Heuristic: punctuation increases TTS duration beyond word-based WPM.
33
+ // Commas typically introduce short pauses, sentence terminators longer ones.
34
+ const commaCount = (trimmed.match(/,/g) || []).length;
35
+ const semicolonCount = (trimmed.match(/;/g) || []).length;
36
+ const colonCount = (trimmed.match(/:/g) || []).length;
37
+ const dashCount = (trimmed.match(/\u2013|\u2014|-/g) || []).length; // – — -
38
+ const sentenceEndCount = (trimmed.match(/[.!?](?!\d)/g) || []).length;
39
+
40
+ const commaPauseMs = 220; // short prosody pause for ","
41
+ const midPauseMs = 260; // for ";", ":", dashes
42
+ const sentenceEndPauseMs = 420; // for ".", "!", "?"
43
+ const extraSpacePauseMs = 50; // per extra space beyond normal spacing
44
+
45
+ const punctuationPause =
46
+ commaCount * commaPauseMs +
47
+ (semicolonCount + colonCount + dashCount) * midPauseMs +
48
+ sentenceEndCount * sentenceEndPauseMs +
49
+ extraSpaces * extraSpacePauseMs;
50
+
51
+ const baseByWordsMs = (words / wordsPerMinute) * 60 * 1000;
52
+ const estimatedMs = Math.max(minimumPause, baseByWordsMs + punctuationPause);
53
+
54
+ return estimatedMs + announcementDelay;
24
55
  }
@@ -69,6 +69,7 @@ exports[`focusManagerIOS should be defined 1`] = `
69
69
  "getGroupRootById": [Function],
70
70
  "getPreferredFocusChild": [Function],
71
71
  "invokeHandler": [Function],
72
+ "isChildOf": [Function],
72
73
  "isFocusOn": [Function],
73
74
  "isGroupItemFocused": [Function],
74
75
  "moveFocus": [Function],
@@ -1,12 +1,20 @@
1
1
  import { NativeModules } from "react-native";
2
2
  import * as R from "ramda";
3
3
 
4
- import { isCurrentFocusOn } from "../focusManagerAux/utils";
4
+ import {
5
+ isCurrentFocusOn,
6
+ isChildOf as isChildOfUtils,
7
+ } from "../focusManagerAux/utils";
5
8
  import { Tree } from "./treeDataStructure/Tree";
6
9
  import { findFocusableNode } from "./treeDataStructure/Utils";
7
10
  import { subscriber } from "../../functionUtils";
8
11
  import { findChild } from "./utils";
9
12
 
13
+ import {
14
+ emitRegistered,
15
+ emitUnregistered,
16
+ } from "../focusManagerAux/utils/utils.ios";
17
+
10
18
  const { FocusableManagerModule } = NativeModules;
11
19
 
12
20
  /**
@@ -180,10 +188,14 @@ export const focusManager = (function () {
180
188
  function register({ id, component }) {
181
189
  const { isGroup = false } = component;
182
190
 
191
+ emitRegistered(id);
192
+
183
193
  return isGroup ? registerGroup(id, component) : registerItem(id, component);
184
194
  }
185
195
 
186
196
  function unregister(id, { group = false } = {}) {
197
+ emitUnregistered(id);
198
+
187
199
  group ? unregisterGroup(id) : unregisterItem(id);
188
200
  }
189
201
 
@@ -400,6 +412,10 @@ export const focusManager = (function () {
400
412
  return id && isCurrentFocusOn(id, currentFocusNode);
401
413
  }
402
414
 
415
+ function isChildOf(childId, parentId): boolean {
416
+ return isChildOfUtils(focusableTree, childId, parentId);
417
+ }
418
+
403
419
  return {
404
420
  on,
405
421
  invokeHandler,
@@ -422,5 +438,6 @@ export const focusManager = (function () {
422
438
  isGroupItemFocused,
423
439
  getPreferredFocusChild,
424
440
  isFocusOn,
441
+ isChildOf,
425
442
  };
426
443
  })();
@@ -190,3 +190,21 @@ export const isCurrentFocusOn = (id, node) => {
190
190
 
191
191
  return isCurrentFocusOn(id, node.parent);
192
192
  };
193
+
194
+ export const isChildOf = (focusableTree, childId, parentId) => {
195
+ if (isNil(childId) || isNil(parentId)) {
196
+ return false;
197
+ }
198
+
199
+ const childNode = focusableTree.findInTree(childId);
200
+
201
+ if (isNil(childNode)) {
202
+ return false;
203
+ }
204
+
205
+ if (childNode.parent?.id === parentId) {
206
+ return true;
207
+ }
208
+
209
+ return isChildOf(focusableTree, childNode.parent?.id, parentId);
210
+ };
@@ -0,0 +1,35 @@
1
+ import { ReplaySubject } from "rxjs";
2
+ import { filter } from "rxjs/operators";
3
+ import { BUTTON_PREFIX } from "@applicaster/zapp-react-native-ui-components/Components/MasterCell/DefaultComponents/tv/TvActionButtons/const";
4
+ import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios";
5
+
6
+ type FocusableID = string;
7
+ type RegistrationEvent = {
8
+ id: FocusableID;
9
+ registered: boolean;
10
+ };
11
+
12
+ const isFocusableButton = (id: Option<FocusableID>): boolean =>
13
+ id && id.includes?.(BUTTON_PREFIX);
14
+
15
+ const registeredSubject$ = new ReplaySubject<RegistrationEvent>(1);
16
+
17
+ export const focusableButtonsRegistration$ = (focusableGroupId: string) =>
18
+ registeredSubject$.pipe(
19
+ filter(
20
+ (value) =>
21
+ value.registered && focusManager.isChildOf(value.id, focusableGroupId)
22
+ )
23
+ );
24
+
25
+ export const emitRegistered = (id: Option<FocusableID>): void => {
26
+ if (isFocusableButton(id)) {
27
+ registeredSubject$.next({ id, registered: true });
28
+ }
29
+ };
30
+
31
+ export const emitUnregistered = (id: Option<FocusableID>): void => {
32
+ if (isFocusableButton(id)) {
33
+ registeredSubject$.next({ id, registered: false });
34
+ }
35
+ };
@@ -170,7 +170,9 @@ export const getClosedCaptionState = () => {
170
170
  */
171
171
  export class TTSManager {
172
172
  private ttsState$ = new BehaviorSubject<boolean>(false);
173
+ private screenReaderEnabled$ = new BehaviorSubject<boolean>(false);
173
174
  private static ttsManagerInstance: TTSManager;
175
+ private samsungListenerId: number | null = null;
174
176
 
175
177
  private constructor() {
176
178
  this.initialize();
@@ -185,23 +187,114 @@ export class TTSManager {
185
187
  }
186
188
 
187
189
  async initialize() {
188
- if (!isVizioPlatform()) return;
190
+ if (isVizioPlatform()) {
191
+ document.addEventListener(
192
+ "VIZIO_TTS_ENABLED",
193
+ () => {
194
+ log_debug("Vizio screen reader enabled");
195
+ this.screenReaderEnabled$.next(true);
196
+ },
197
+ false
198
+ );
189
199
 
190
- document.addEventListener(
191
- "VIZIO_TTS_ENABLED",
192
- () => {
193
- this.ttsState$.next(true);
194
- },
195
- false
196
- );
200
+ document.addEventListener(
201
+ "VIZIO_TTS_DISABLED",
202
+ () => {
203
+ log_debug("Vizio screen reader disabled");
204
+ this.screenReaderEnabled$.next(false);
205
+ },
206
+ false
207
+ );
208
+ }
197
209
 
198
- document.addEventListener(
199
- "VIZIO_TTS_DISABLED",
200
- () => {
201
- this.ttsState$.next(false);
202
- },
203
- false
204
- );
210
+ if (isLgPlatform() && window.webOS?.service) {
211
+ try {
212
+ // https://webostv.developer.lge.com/develop/references/settings-service
213
+ window.webOS.service.request("luna://com.webos.settingsservice", {
214
+ method: "getSystemSettings",
215
+ parameters: {
216
+ category: "option",
217
+ keys: ["audioGuidance"],
218
+ subscribe: true, // Request a subscription to changes
219
+ },
220
+ onSuccess: (response: any) => {
221
+ const isEnabled = response?.settings?.audioGuidance === "on";
222
+
223
+ log_debug("LG Audio Guidance status changed", {
224
+ isEnabled,
225
+ response,
226
+ });
227
+
228
+ this.screenReaderEnabled$.next(isEnabled);
229
+ },
230
+ onFailure: (error: any) => {
231
+ log_debug("webOS settings subscription failed", { error });
232
+ this.screenReaderEnabled$.next(false);
233
+ },
234
+ });
235
+ } catch (error) {
236
+ log_debug("webOS settings service request error", { error });
237
+ this.screenReaderEnabled$.next(false);
238
+ }
239
+ }
240
+
241
+ if (isSamsungPlatform() && typeof window.webapis !== "undefined") {
242
+ try {
243
+ if (
244
+ window.webapis?.tvinfo &&
245
+ typeof window.webapis.tvinfo.getMenuValue === "function" &&
246
+ typeof window.webapis.tvinfo.addCaptionChangeListener === "function"
247
+ ) {
248
+ // Get initial Voice Guide status
249
+ const initialStatus = window.webapis.tvinfo.getMenuValue(
250
+ window.webapis.tvinfo.TvInfoMenuKey.VOICE_GUIDE_KEY
251
+ );
252
+
253
+ const isEnabled =
254
+ initialStatus === window.webapis.tvinfo.TvInfoMenuValue.ON;
255
+
256
+ log_debug("Samsung Voice Guide initial status", {
257
+ isEnabled,
258
+ initialStatus,
259
+ });
260
+
261
+ this.screenReaderEnabled$.next(isEnabled);
262
+
263
+ // Listen for Voice Guide status changes
264
+ const onChange = () => {
265
+ const currentStatus = window.webapis.tvinfo.getMenuValue(
266
+ window.webapis.tvinfo.TvInfoMenuKey.VOICE_GUIDE_KEY
267
+ );
268
+
269
+ const enabled =
270
+ currentStatus === window.webapis.tvinfo.TvInfoMenuValue.ON;
271
+
272
+ log_debug("Samsung Voice Guide status changed", {
273
+ enabled,
274
+ currentStatus,
275
+ });
276
+
277
+ this.screenReaderEnabled$.next(enabled);
278
+ };
279
+
280
+ this.samsungListenerId =
281
+ window.webapis.tvinfo.addCaptionChangeListener(
282
+ window.webapis.tvinfo.TvInfoMenuKey.VOICE_GUIDE_KEY,
283
+ onChange
284
+ );
285
+
286
+ log_debug("Samsung Voice Guide listener registered", {
287
+ listenerId: this.samsungListenerId,
288
+ });
289
+ } else {
290
+ log_debug("Samsung TvInfo API not available");
291
+ this.screenReaderEnabled$.next(false);
292
+ }
293
+ } catch (error) {
294
+ log_debug("Samsung Voice Guide listener error", { error });
295
+ this.screenReaderEnabled$.next(false);
296
+ }
297
+ }
205
298
  }
206
299
 
207
300
  getCurrentState(): boolean {
@@ -212,31 +305,52 @@ export class TTSManager {
212
305
  return this.ttsState$.asObservable();
213
306
  }
214
307
 
308
+ getScreenReaderEnabledAsObservable() {
309
+ return this.screenReaderEnabled$.asObservable();
310
+ }
311
+
215
312
  readText(text: string) {
216
313
  this.ttsState$.next(true);
217
314
 
218
- if (isSamsungPlatform() && window.speechSynthesis) {
219
- const utterance = new SpeechSynthesisUtterance(text);
315
+ if (
316
+ isSamsungPlatform() &&
317
+ typeof window.tizen !== "undefined" &&
318
+ window.tizen.speech
319
+ ) {
320
+ try {
321
+ const successCallback = () => {
322
+ log_debug("Samsung TTS play started successfully");
323
+ // Estimate reading time and set inactive when done
324
+ this.scheduleTTSComplete(text);
325
+ };
220
326
 
221
- window.speechSynthesis.cancel(); // Cancel previous speech before speaking new text
222
- window.speechSynthesis.speak(utterance);
327
+ const errorCallback = (error: any) => {
328
+ log_debug("Samsung TTS error", { error: error?.message || error });
329
+ this.ttsState$.next(false);
330
+ };
223
331
 
224
- // Estimate reading time and set inactive when done
225
- this.scheduleTTSComplete(text);
332
+ // Clear any previous speech before speaking new text
333
+ window.tizen.speech.stop();
334
+
335
+ window.tizen.speech.speak(text, successCallback, errorCallback);
336
+ } catch (error) {
337
+ log_debug("Samsung TTS speak() error", { error });
338
+ this.ttsState$.next(false);
339
+ }
226
340
  }
227
341
 
228
342
  if (isLgPlatform() && window.webOS?.service) {
229
343
  try {
230
344
  window.webOS.service.request("luna://com.webos.service.tts", {
231
345
  method: "speak",
232
- onFailure(error: any) {
346
+ onFailure: (error: any) => {
233
347
  log_debug("There was a failure setting up webOS TTS service", {
234
348
  error,
235
349
  });
236
350
 
237
351
  this.ttsState$.next(false);
238
352
  },
239
- onSuccess(response: any) {
353
+ onSuccess: (response: any) => {
240
354
  log_debug("webOS TTS service is configured successfully", {
241
355
  response,
242
356
  });
@@ -0,0 +1,24 @@
1
+ import { allTruthy } from "..";
2
+
3
+ describe("allTruthy", () => {
4
+ it("should return true when all values are true", () => {
5
+ expect(allTruthy([true, true, true])).toBe(true);
6
+ });
7
+
8
+ it("should return false when at least one value is false", () => {
9
+ expect(allTruthy([true, false, true])).toBe(false);
10
+ });
11
+
12
+ it("should return false when all values are false", () => {
13
+ expect(allTruthy([false, false, false])).toBe(false);
14
+ });
15
+
16
+ it("should return false for an empty array", () => {
17
+ expect(allTruthy([])).toBe(false);
18
+ });
19
+
20
+ it("should handle single-element arrays correctly", () => {
21
+ expect(allTruthy([true])).toBe(true);
22
+ expect(allTruthy([false])).toBe(false);
23
+ });
24
+ });
@@ -0,0 +1,24 @@
1
+ import { anyTruthy } from "..";
2
+
3
+ describe("anyTruthy", () => {
4
+ it("should return true when at least one value is true", () => {
5
+ expect(anyTruthy([false, true, false])).toBe(true);
6
+ });
7
+
8
+ it("should return false when all values are false", () => {
9
+ expect(anyTruthy([false, false, false])).toBe(false);
10
+ });
11
+
12
+ it("should return true when all values are true", () => {
13
+ expect(anyTruthy([true, true, true])).toBe(true);
14
+ });
15
+
16
+ it("should return false for an empty array", () => {
17
+ expect(anyTruthy([])).toBe(false);
18
+ });
19
+
20
+ it("should handle single-element arrays correctly", () => {
21
+ expect(anyTruthy([true])).toBe(true);
22
+ expect(anyTruthy([false])).toBe(false);
23
+ });
24
+ });
@@ -116,3 +116,8 @@ export const sample = (xs: unknown[]): unknown => {
116
116
 
117
117
  return xs[index];
118
118
  };
119
+
120
+ export const allTruthy = (xs: boolean[]) =>
121
+ isFilledArray(xs) && xs.every(Boolean);
122
+
123
+ export const anyTruthy = (xs: boolean[]) => xs.some(Boolean);
@@ -9,7 +9,10 @@ import {
9
9
  } from "@applicaster/zapp-react-native-utils/stringUtils";
10
10
  import { cellUtilsLogger } from "@applicaster/zapp-react-native-utils/cellUtils/logger";
11
11
  import { isWeb } from "@applicaster/zapp-react-native-utils/reactUtils";
12
- import { isNotNil } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
12
+ import {
13
+ isNotNil,
14
+ isNilOrEmpty,
15
+ } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
13
16
 
14
17
  import { toNumberWithDefault, toNumberWithDefaultZero } from "../numberUtils";
15
18
 
@@ -505,3 +508,39 @@ export const getImageContainerMarginStyles = ({ value }: { value: any }) => {
505
508
  marginRight: value("image_margin_right"),
506
509
  };
507
510
  };
511
+
512
+ export const withoutNilOrEmpty = (arr: string[]): string[] =>
513
+ arr.filter((item) => !isNilOrEmpty(item));
514
+
515
+ export const isTextLabel = (key: string): boolean =>
516
+ key.includes("text_label_") && key.endsWith("_switch");
517
+
518
+ export const hasTextLabelsEnabled = (
519
+ configuration: Record<string, any>
520
+ ): boolean => {
521
+ const textLabelsKeys = Object.keys(configuration).filter(isTextLabel);
522
+
523
+ const picked = textLabelsKeys.reduce(
524
+ (acc, key) => {
525
+ acc[key] = configuration[key];
526
+
527
+ return acc;
528
+ },
529
+ {} as Record<string, any>
530
+ );
531
+
532
+ const pickedValues = Object.values(picked);
533
+
534
+ // Check if any switch value is truthy (true, "true", "1", etc.)
535
+ return pickedValues.some((value) => {
536
+ if (typeof value === "boolean") {
537
+ return value === true;
538
+ }
539
+
540
+ if (typeof value === "string") {
541
+ return value !== "" && value.toLowerCase() !== "false";
542
+ }
543
+
544
+ return Boolean(value);
545
+ });
546
+ };
@@ -959,6 +959,27 @@ const TV_CELL_LABEL_FIELDS = [
959
959
  rules: "conditional",
960
960
  conditions: [{ key: "switch", section: "styles", value: true }],
961
961
  },
962
+ {
963
+ type: ZAPPIFEST_FIELDS.font_selector.roku,
964
+ suffix: "roku font family",
965
+ tooltip: "",
966
+ rules: "conditional",
967
+ conditions: [{ key: "switch", section: "styles", value: true }],
968
+ },
969
+ {
970
+ type: ZAPPIFEST_FIELDS.number_input,
971
+ suffix: "roku font size",
972
+ tooltip: "",
973
+ rules: "conditional",
974
+ conditions: [{ key: "switch", section: "styles", value: true }],
975
+ },
976
+ {
977
+ type: ZAPPIFEST_FIELDS.number_input,
978
+ suffix: "roku line height",
979
+ tooltip: "",
980
+ rules: "conditional",
981
+ conditions: [{ key: "switch", section: "styles", value: true }],
982
+ },
962
983
  {
963
984
  type: ZAPPIFEST_FIELDS.select,
964
985
  suffix: "text alignment",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "15.0.0-alpha.1121049279",
3
+ "version": "15.0.0-alpha.1308474364",
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": "15.0.0-alpha.1121049279",
30
+ "@applicaster/applicaster-types": "15.0.0-alpha.1308474364",
31
31
  "buffer": "^5.2.1",
32
32
  "camelize": "^1.0.0",
33
33
  "dayjs": "^1.11.10",
@@ -7,8 +7,11 @@ export const usePlayerTTS = () => {
7
7
  const player = usePlayer();
8
8
  const accessibilityManager = useAccessibilityManager({});
9
9
 
10
+ const isScreenReaderEnabled =
11
+ accessibilityManager.accessibilityManagerState.screenReaderEnabled;
12
+
10
13
  React.useEffect(() => {
11
- if (player && accessibilityManager) {
14
+ if (player && accessibilityManager && isScreenReaderEnabled) {
12
15
  const playerTTS = new PlayerTTS(player, accessibilityManager);
13
16
  const unsubscribe = playerTTS.init();
14
17
 
@@ -17,5 +20,5 @@ export const usePlayerTTS = () => {
17
20
  playerTTS.destroy();
18
21
  };
19
22
  }
20
- }, [player, accessibilityManager]);
23
+ }, [player, accessibilityManager, isScreenReaderEnabled]);
21
24
  };
@@ -151,7 +151,13 @@ export const useBatchLoading = (
151
151
  }
152
152
  }
153
153
  });
154
- }, [feedUrls, feeds, loadPipesDataDispatcher]);
154
+ }, [
155
+ batchComponents,
156
+ feeds,
157
+ getUrl,
158
+ loadPipesDataDispatcher,
159
+ options.riverId,
160
+ ]);
155
161
 
156
162
  React.useEffect(() => {
157
163
  runBatchLoading();
@@ -34,7 +34,9 @@ export const usePipesCacheReset = (riverId, riverComponents) => {
34
34
  component
35
35
  );
36
36
 
37
- dispatch(clearPipesData(url, { riverId }));
37
+ if (url) {
38
+ dispatch(clearPipesData(url, { riverId }));
39
+ }
38
40
  }
39
41
  });
40
42
  };
@@ -44,7 +44,7 @@ export function useStatusBarHeight() {
44
44
 
45
45
  return platformSelect({
46
46
  ios: StatusBarHeight,
47
- android: StatusBar.currentHeight,
47
+ android: StatusBar.currentHeight ?? 0,
48
48
  default: 0,
49
49
  });
50
50
  }
@@ -0,0 +1,73 @@
1
+ import { mapAccum } from "../mapAccum";
2
+
3
+ describe("mapAccum", () => {
4
+ it("using standard ramda test", () => {
5
+ const digits = ["1", "2", "3", "4"];
6
+ const appender = (a, b) => [a + b, a + b];
7
+
8
+ const [acc, result] = mapAccum(appender, 0, digits); //= > ['01234', ['01', '012', '0123', '01234']]
9
+ expect(acc).toBe("01234");
10
+ expect(result).toEqual(["01", "012", "0123", "01234"]);
11
+ });
12
+
13
+ it("maps and accumulates over an array", () => {
14
+ const fn = (acc, x) => [acc + x, x * 2];
15
+ const [acc, result] = mapAccum(fn, 0, [1, 2, 3]);
16
+
17
+ expect(acc).toBe(6); // final accumulator (0 + 1 + 2 + 3)
18
+ expect(result).toEqual([2, 4, 6]); // mapped values (acc + x*2 at each step)
19
+ });
20
+
21
+ it("returns initial accumulator for empty array", () => {
22
+ const fn = (acc, x) => [acc + x, acc * x];
23
+ const [acc, result] = mapAccum(fn, 10, []);
24
+
25
+ expect(acc).toBe(10);
26
+ expect(result).toEqual([]);
27
+ });
28
+
29
+ it("accumulates strings correctly", () => {
30
+ const fn = (acc, x) => [acc + x, acc + x];
31
+ const [acc, result] = mapAccum(fn, "A", ["B", "C", "D"]);
32
+
33
+ expect(acc).toBe("ABCD");
34
+ expect(result).toEqual(["AB", "ABC", "ABCD"]);
35
+ });
36
+
37
+ it("works with objects as accumulator", () => {
38
+ const fn = (acc, x) => {
39
+ const newAcc = { sum: acc.sum + x };
40
+
41
+ return [newAcc, newAcc.sum];
42
+ };
43
+
44
+ const [acc, result] = mapAccum(fn, { sum: 0 }, [1, 2, 3]);
45
+
46
+ expect(acc).toEqual({ sum: 6 });
47
+ expect(result).toEqual([1, 3, 6]);
48
+ });
49
+
50
+ it("is curried", () => {
51
+ const fn = (acc, x) => [acc + x, x * 2];
52
+ const mapWithFn = mapAccum(fn);
53
+ const withInit = mapWithFn(2);
54
+ const [acc, result] = withInit([1, 2, 3]);
55
+
56
+ expect(acc).toBe(8);
57
+ expect(result).toEqual([2, 4, 6]);
58
+ });
59
+
60
+ it("does not mutate the original array", () => {
61
+ const arr = [1, 2, 3];
62
+ mapAccum((acc, x) => [acc + x, acc + x], 0, arr);
63
+ expect(arr).toEqual([1, 2, 3]);
64
+ });
65
+
66
+ it("handles mixed types in accumulator and result", () => {
67
+ const fn = (acc, x) => [acc + x.length, acc + "-" + x];
68
+ const [acc, result] = mapAccum(fn, 0, ["a", "bb", "ccc"]);
69
+
70
+ expect(acc).toBe(6);
71
+ expect(result).toEqual(["0-a", "1-bb", "3-ccc"]);
72
+ });
73
+ });
package/utils/index.ts CHANGED
@@ -16,6 +16,8 @@ export { endsWith } from "./endsWith";
16
16
 
17
17
  export { take } from "./take";
18
18
 
19
+ export { mapAccum } from "./mapAccum";
20
+
19
21
  export {
20
22
  cloneDeep as clone,
21
23
  flatten,
@@ -34,4 +36,9 @@ export {
34
36
  last,
35
37
  toLower,
36
38
  isEqual as equals,
39
+ uniq,
40
+ uniqWith,
41
+ flowRight as compose,
42
+ partial,
43
+ reverse,
37
44
  } from "lodash";
@@ -0,0 +1,23 @@
1
+ import { curry } from "lodash";
2
+
3
+ /**
4
+ * A native reimplementation of Ramda's mapAccum.
5
+ *
6
+ * @template A, B, C
7
+ * @param {(acc: A, value: B) => [A, C]} fn - Function returning [newAcc, mappedValue]
8
+ * @param {A} acc - Initial accumulator
9
+ * @param {B[]} list - List to process
10
+ * @returns {[A, C[]]} - Tuple of [final accumulator, mapped array]
11
+ */
12
+ export const mapAccum = curry((fn, acc, list) => {
13
+ const result = [];
14
+ let currentAcc = acc;
15
+
16
+ for (let i = 0; i < list.length; i++) {
17
+ const [nextAcc, mapped] = fn(currentAcc, list[i]);
18
+ currentAcc = nextAcc;
19
+ result.push(mapped);
20
+ }
21
+
22
+ return [currentAcc, result];
23
+ });
@@ -56,9 +56,23 @@ const legacyMappingKeys = {
56
56
  actionType: "logout_completion_action",
57
57
  targetScreen: "navigate_to_logout_screen",
58
58
  },
59
+ "quick-brick-storefront": {
60
+ actionType: "purchase_completion_action",
61
+ targetScreen: "navigate_to_screen_after_purchase",
62
+ },
63
+ "zapp_login_plugin_oauth_tv_2_0.login": {
64
+ actionType: "login_completion_action",
65
+ targetScreen: "navigate_to_login_screen",
66
+ },
67
+ "zapp_login_plugin_oauth_tv_2_0.logout": {
68
+ actionType: "logout_completion_action",
69
+ targetScreen: "navigate_to_logout_screen",
70
+ },
59
71
  };
60
72
 
61
- const NAV_ACTIONS = Object.values(NavigationCallbackOptions) as string[];
73
+ const NAV_ACTIONS = (
74
+ Object.values(NavigationCallbackOptions) as string[]
75
+ ).filter((value) => value !== NavigationCallbackOptions.DEFAULT);
62
76
 
63
77
  const isNavAction = (v: unknown): v is NavigationCallbackOptions =>
64
78
  typeof v === "string" && NAV_ACTIONS.includes(v);