@applicaster/zapp-react-native-utils 12.0.0-alpha.5471145113 → 12.0.0-alpha.5521005225

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.
@@ -0,0 +1,50 @@
1
+ export const BUTTON_ACCESSIBILITY_KEYS = {
2
+ play: {
3
+ label: "accessibility_play_label",
4
+ hint: "accessibility_play_hint",
5
+ mini: {
6
+ label: "accessibility_play_mini_label",
7
+ hint: "accessibility_play_mini_hint",
8
+ },
9
+ },
10
+ pause: {
11
+ label: "accessibility_pause_label",
12
+ hint: "accessibility_pause_hint",
13
+ mini: {
14
+ label: "accessibility_pause_mini_label",
15
+ hint: "accessibility_pause_mini_hint",
16
+ },
17
+ },
18
+ rewind: {
19
+ label: "accessibility_rewind_label",
20
+ hint: "accessibility_rewind_hint",
21
+ },
22
+ forward: {
23
+ label: "accessibility_forward_label",
24
+ hint: "accessibility_forward_hint",
25
+ },
26
+ close: {
27
+ label: "accessibility_close_label",
28
+ hint: "accessibility_close_hint",
29
+ mini: {
30
+ label: "accessibility_close_mini_label",
31
+ hint: "accessibility_close_mini_hint",
32
+ },
33
+ },
34
+ maximize: {
35
+ label: "accessibility_maximize_label",
36
+ hint: "accessibility_maximize_hint",
37
+ },
38
+ subtitles: {
39
+ label: "accessibility_subtitle_label",
40
+ hint: "accessibility_subtitle_hint",
41
+ },
42
+ back: {
43
+ label: "accessibility_back_label",
44
+ hint: "accessibility_back_hint",
45
+ },
46
+ fullscreen: {
47
+ label: "accessibility_fullscreen_label",
48
+ hint: "accessibility_fullscreen_hint",
49
+ },
50
+ } as const;
@@ -0,0 +1,40 @@
1
+ import { useEffect, useMemo } from "react";
2
+ import { AccessibilityManager } from "./index";
3
+
4
+ /**
5
+ * This hook is used to instantiate the accessibility manager and update localizations used by the class.
6
+ * If the plugin config is not provided or it does not contain any localizations, the accessibility manager will not be updated.
7
+ * We use the plugin configuration because it offers a flattened structure where we can easily access the accessibility labels and hints
8
+ * i.e. pluginConfiguration = {
9
+ * accessibility_close_label: "Close",
10
+ * accessibility_close_hint: "Press here to close",
11
+ * }
12
+ */
13
+ export const useAccessibilityManager = (
14
+ pluginConfiguration: Record<string, any> = {}
15
+ ) => {
16
+ const accessibilityManager = useMemo(() => {
17
+ return AccessibilityManager.getInstance();
18
+ }, []);
19
+
20
+ useEffect(() => {
21
+ if (pluginConfiguration) {
22
+ accessibilityManager.updateLocalizations(pluginConfiguration);
23
+ }
24
+ }, [pluginConfiguration, accessibilityManager]);
25
+
26
+ useEffect(() => {
27
+ const subscription = accessibilityManager.getStateAsObservable().subscribe({
28
+ next: () => {
29
+ // TODO: handle accessibility states
30
+ // screenReaderEnabled: false
31
+ // reduceMotionEnabled: false
32
+ // boldTextEnabled: false
33
+ },
34
+ });
35
+
36
+ return () => subscription.unsubscribe();
37
+ }, [accessibilityManager]);
38
+
39
+ return accessibilityManager;
40
+ };
@@ -0,0 +1,155 @@
1
+ import { BehaviorSubject } from "rxjs";
2
+ import { accessibilityManagerLogger as logger } from "./logger";
3
+ import { TTSManager } from "../platform/platformUtils";
4
+ import { BUTTON_ACCESSIBILITY_KEYS } from "./const";
5
+ import { AccessibilityRole } from "react-native";
6
+
7
+ export class AccessibilityManager {
8
+ private static _instance: AccessibilityManager | null = null;
9
+ private state$ = new BehaviorSubject<AccessibilityState>({
10
+ screenReaderEnabled: false,
11
+ reduceMotionEnabled: false,
12
+ boldTextEnabled: false,
13
+ });
14
+
15
+ private announcements$ = new BehaviorSubject<{
16
+ message: string;
17
+ localizedMessage?: string | undefined;
18
+ timestamp: number;
19
+ } | null>(null);
20
+
21
+ private ttsManager = TTSManager.getInstance();
22
+ private localizations: { [key: string]: string } = {};
23
+
24
+ private constructor() {}
25
+
26
+ public static getInstance(): AccessibilityManager {
27
+ if (!AccessibilityManager._instance) {
28
+ AccessibilityManager._instance = new AccessibilityManager();
29
+ }
30
+
31
+ return AccessibilityManager._instance;
32
+ }
33
+
34
+ /**
35
+ * The method now accepts any object with localizations using a flattened structure
36
+ *
37
+ * i.e. { accessibility_close_label: "Close", accessibility_close_hint: "Press here to close" }
38
+ *
39
+ * No longer accepts:
40
+ *
41
+ * i.e. localizations: [{ en: { accessibility_close_label: "Close", accessibility_close_hint: "Press here to close" } }]
42
+ */
43
+ public updateLocalizations(localizations: { [key: string]: string }) {
44
+ this.localizations = localizations;
45
+ }
46
+
47
+ public getState(): AccessibilityState {
48
+ return this.state$.getValue();
49
+ }
50
+
51
+ public getStateAsObservable() {
52
+ return this.state$.asObservable();
53
+ }
54
+
55
+ /**
56
+ * text you want to be read, if you want to use localized text pass keyOfLocalizedText instead
57
+ * keyOfLocalizedText is the key to the localized text
58
+ */
59
+ public readText({
60
+ text,
61
+ keyOfLocalizedText,
62
+ }: {
63
+ text: string;
64
+ keyOfLocalizedText?: string;
65
+ }) {
66
+ let textToRead = text;
67
+
68
+ if (keyOfLocalizedText) {
69
+ if (!this.localizations) {
70
+ logger.error(
71
+ "Attempting to use localized key without initialized localizations"
72
+ );
73
+
74
+ return;
75
+ }
76
+
77
+ const localizedMessage = this.getLocalizedMessage(keyOfLocalizedText);
78
+
79
+ if (!localizedMessage) {
80
+ logger.warn(`No localization found for key: ${keyOfLocalizedText}`);
81
+
82
+ return;
83
+ }
84
+
85
+ textToRead = localizedMessage;
86
+ }
87
+
88
+ if (this.ttsManager) {
89
+ logger.debug({
90
+ message: `Reading text: ${textToRead}`,
91
+ data: {
92
+ textToRead,
93
+ text,
94
+ keyOfLocalizedText,
95
+ },
96
+ });
97
+
98
+ this.ttsManager.readText(textToRead);
99
+ }
100
+
101
+ this.announcements$.next({
102
+ message: textToRead,
103
+ localizedMessage: keyOfLocalizedText ? textToRead : undefined,
104
+ timestamp: Date.now(),
105
+ });
106
+ }
107
+
108
+ public getButtonAccessibilityProps(buttonName: string): AccessibilityProps {
109
+ const buttonConfig = BUTTON_ACCESSIBILITY_KEYS[buttonName];
110
+
111
+ if (!buttonConfig) {
112
+ return {
113
+ accessibilityLabel: buttonName,
114
+ accessibilityHint: `Press button to perform action on ${buttonName}`,
115
+ "aria-label": buttonName,
116
+ "aria-description": `Press button to perform action on ${buttonName}`,
117
+ accessibilityRole: "button" as AccessibilityRole,
118
+ "aria-role": "button",
119
+ };
120
+ }
121
+
122
+ const labelKey = buttonConfig.label;
123
+ const hintKey = buttonConfig.hint;
124
+
125
+ const label = this.getLocalizedMessage(labelKey) || buttonName;
126
+
127
+ const hint =
128
+ this.getLocalizedMessage(hintKey) ||
129
+ `Press button to perform action on ${buttonName}`;
130
+
131
+ return {
132
+ accessibilityLabel: label,
133
+ accessibilityHint: hint,
134
+ "aria-label": label,
135
+ "aria-description": hint,
136
+ accessibilityRole: "button" as AccessibilityRole,
137
+ "aria-role": "button",
138
+ };
139
+ }
140
+
141
+ public getLocalizedMessage(key: string): string | void {
142
+ if (!key) return;
143
+
144
+ if (!this.localizations) {
145
+ logger.error(
146
+ "Attempting to use localized key without initialized localizations",
147
+ { key }
148
+ );
149
+
150
+ return;
151
+ }
152
+
153
+ return this.localizations[key];
154
+ }
155
+ }
@@ -0,0 +1,5 @@
1
+ import { utilsLogger } from "../../logger";
2
+
3
+ export const accessibilityManagerLogger = utilsLogger.addSubsystem(
4
+ "accessibilityManager"
5
+ );
@@ -13,7 +13,6 @@ import {
13
13
  import { subscriber } from "../../functionUtils";
14
14
  import * as FOCUS_EVENTS from "./events";
15
15
  import { coreLogger } from "../../logger";
16
-
17
16
  const logger = coreLogger.addSubsystem("focusManager");
18
17
 
19
18
  const isFocusEnabled = (focusableItem): boolean => {
@@ -206,7 +206,7 @@ export function getTargetInDirection(
206
206
  const overrideTarget = currentNode?.component?.props[overridePropName];
207
207
 
208
208
  if (overrideTarget) {
209
- const target = targetNodes.find((n) => n.id === overrideTarget);
209
+ const target = targetNodes.find((n) => n?.id === overrideTarget);
210
210
 
211
211
  if (target) return target;
212
212
  }
package/appUtils/index.js CHANGED
@@ -9,6 +9,8 @@ export { KEYS, ARROW_KEYS } from "./keyCodes/keys";
9
9
 
10
10
  export { playerManager } from "./playerManager";
11
11
 
12
+ export { accessibilityManager } from "./accessibilityManager";
13
+
12
14
  export function deprecationMessage(fn, replacement) {
13
15
  if (__DEV__ && process.env.NODE_ENV !== "test") {
14
16
  console.warn("/** DEPRECATION WARNING **/");
@@ -16,3 +16,10 @@ export const PLATFORMS = {
16
16
  smartcast: "SmartCast",
17
17
  conjure: "Conjure",
18
18
  };
19
+
20
+ /**
21
+ * Keys used to store the platform / device state in the session storage
22
+ */
23
+ export const PLATFORM_KEYS = {
24
+ deviceClosedCaptioningEnabled: "device_closed_captioning_enabled",
25
+ };
@@ -1 +1,3 @@
1
1
  export * from "./platformUtils";
2
+
3
+ export * from "./const";
@@ -1 +1,185 @@
1
- export {};
1
+ import { BehaviorSubject } from "rxjs";
2
+
3
+ import { sessionStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/SessionStorage";
4
+
5
+ import { PLATFORM_KEYS, PLATFORMS } from "./const";
6
+ import { createLogger, utilsLogger } from "../../logger";
7
+
8
+ declare global {
9
+ interface Window {
10
+ VIZIO?: {
11
+ exitApplication: () => void;
12
+ Chromevox: {
13
+ play: (text: string) => void;
14
+ };
15
+ setClosedCaptionHandler: (
16
+ callback: (isCCEnabled: boolean) => void
17
+ ) => void;
18
+ };
19
+ }
20
+ }
21
+
22
+ const { log_debug } = createLogger({
23
+ category: "General",
24
+ subsystem: "PlatformUtils",
25
+ parent: utilsLogger,
26
+ });
27
+
28
+ export const getUserAgent = () =>
29
+ window?.navigator?.userAgent || global?.navigator?.userAgent;
30
+
31
+ /** VIZIO UTILS */
32
+
33
+ /**
34
+ * Checks if we are on the Vizio platform by using the userAgent string
35
+ *
36
+ * Use this method when you need to identify the platform on runtime
37
+ * before the VIZIO Companion Library has loaded
38
+ */
39
+ export const isVizioPlatform = () => {
40
+ const userAgent = getUserAgent();
41
+
42
+ const isVizioAgent =
43
+ userAgent.includes(PLATFORMS.vizio) ||
44
+ userAgent.includes(PLATFORMS.smartcast) ||
45
+ userAgent.includes(PLATFORMS.conjure);
46
+
47
+ return isVizioAgent;
48
+ };
49
+
50
+ /**
51
+ * Checks if we are on the Vizio platform by checking for the global VIZIO object
52
+ * and the VIZIO Companion Library. This method does not use the userAgent string,
53
+ * because it mainly checks whether we have access to apis, which may not be available yet
54
+ */
55
+ export const hasVizioAPIs = () => {
56
+ const hasAPIs =
57
+ typeof window.VIZIO !== "undefined" &&
58
+ window?.applicaster?.vizioLibraryDidLoad;
59
+
60
+ return hasAPIs;
61
+ };
62
+
63
+ /**
64
+ * Manages device based closed captioning state
65
+ * Returns an observable that emits the current state and updates whenever it changes
66
+ */
67
+ export class ClosedCaptioningManager {
68
+ private ccState$ = new BehaviorSubject<boolean>(false);
69
+ private static ccManagerInstance: ClosedCaptioningManager;
70
+
71
+ private constructor() {
72
+ this.initialize();
73
+ }
74
+
75
+ static getInstance(): ClosedCaptioningManager {
76
+ if (!ClosedCaptioningManager.ccManagerInstance) {
77
+ ClosedCaptioningManager.ccManagerInstance = new ClosedCaptioningManager();
78
+ }
79
+
80
+ return ClosedCaptioningManager.ccManagerInstance;
81
+ }
82
+
83
+ async initialize() {
84
+ const existingState = await sessionStorage.getItem(
85
+ PLATFORM_KEYS.deviceClosedCaptioningEnabled
86
+ );
87
+
88
+ log_debug("Initializing closed captioning manager", { existingState });
89
+
90
+ if (existingState !== null) {
91
+ this.ccState$.next(existingState === "true" || existingState === true);
92
+ }
93
+
94
+ if (hasVizioAPIs()) {
95
+ window.VIZIO!.setClosedCaptionHandler((isCCEnabled) => {
96
+ log_debug("CC state changed", { isCCEnabled });
97
+
98
+ sessionStorage.setItem(
99
+ PLATFORM_KEYS.deviceClosedCaptioningEnabled,
100
+ isCCEnabled
101
+ );
102
+
103
+ this.ccState$.next(isCCEnabled);
104
+ });
105
+ }
106
+ }
107
+
108
+ getCurrentState(): boolean {
109
+ return this.ccState$.getValue();
110
+ }
111
+
112
+ getStateAsObservable() {
113
+ return this.ccState$.asObservable();
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Subscribe to closed captioning state changes
119
+ * @returns Observable<boolean>
120
+ * @example
121
+ * const subscription = getClosedCaptionState().subscribe(
122
+ * isEnabled => setClosedCaptionState(isEnabled)
123
+ * );
124
+ * subscription.unsubscribe(), []);
125
+ */
126
+ export const getClosedCaptionState = () => {
127
+ const ccManager = ClosedCaptioningManager.getInstance();
128
+
129
+ return ccManager.getStateAsObservable();
130
+ };
131
+
132
+ /**
133
+ * Manages device based closed captioning state
134
+ * Returns an observable that emits the current state and updates whenever it changes
135
+ */
136
+ export class TTSManager {
137
+ private ttsState$ = new BehaviorSubject<boolean>(false);
138
+ private static ttsManagerInstance: TTSManager;
139
+
140
+ private constructor() {
141
+ this.initialize();
142
+ }
143
+
144
+ static getInstance(): TTSManager {
145
+ if (!TTSManager.ttsManagerInstance) {
146
+ TTSManager.ttsManagerInstance = new TTSManager();
147
+ }
148
+
149
+ return TTSManager.ttsManagerInstance;
150
+ }
151
+
152
+ async initialize() {
153
+ if (!isVizioPlatform()) return;
154
+
155
+ document.addEventListener(
156
+ "VIZIO_TTS_ENABLED",
157
+ () => {
158
+ this.ttsState$.next(true);
159
+ },
160
+ false
161
+ );
162
+
163
+ document.addEventListener(
164
+ "VIZIO_TTS_DISABLED",
165
+ () => {
166
+ this.ttsState$.next(false);
167
+ },
168
+ false
169
+ );
170
+ }
171
+
172
+ getCurrentState(): boolean {
173
+ return this.ttsState$.getValue();
174
+ }
175
+
176
+ getStateAsObservable() {
177
+ return this.ttsState$.asObservable();
178
+ }
179
+
180
+ readText(text: string) {
181
+ if (!window.VIZIO?.Chromevox) return;
182
+
183
+ window.VIZIO.Chromevox.play(text);
184
+ }
185
+ }
@@ -7,6 +7,7 @@ export enum SharedPlayerCallBacksKeys {
7
7
  OnPlayerPause = "onPlayerPause",
8
8
  OnLoad = "onLoad",
9
9
  OnError = "onError",
10
+ OnVideoError = "onVideoError",
10
11
  OnVideoEnd = "onVideoEnd",
11
12
  OnPlaybackRateChange = "onPlaybackRateChange",
12
13
  OnVideoLoad = "onVideoLoad",
@@ -366,9 +366,9 @@ export class PlayerManager {
366
366
  // TODO: Remove
367
367
  if (this.castingReceiver) {
368
368
  this.castingReceiver.rewind(deltaTime);
369
+ } else {
370
+ this.getActivePlayer()?.rewind(deltaTime);
369
371
  }
370
-
371
- this.getActivePlayer()?.rewind(deltaTime);
372
372
  }
373
373
 
374
374
  public forward(deltaTime = null) {
@@ -376,9 +376,9 @@ export class PlayerManager {
376
376
 
377
377
  if (this.castingReceiver) {
378
378
  this.castingReceiver.forward(deltaTime);
379
+ } else {
380
+ this.getActivePlayer()?.forward(deltaTime);
379
381
  }
380
-
381
- this.getActivePlayer()?.forward(deltaTime);
382
382
  }
383
383
 
384
384
  public seeking(time) {
@@ -441,7 +441,7 @@ export class PlayerManager {
441
441
  }
442
442
 
443
443
  // Call method on player just in case
444
- return this.getActivePlayer()?.setLanguage(track);
444
+ return this.getActivePlayer()?.selectTrack(track);
445
445
  }
446
446
 
447
447
  public isRegistered() {
@@ -359,6 +359,13 @@ export class Player {
359
359
  });
360
360
  };
361
361
 
362
+ onVideoError = (error: Error) => {
363
+ this.invokeListenersUpdate({
364
+ callbackName: SharedPlayerCallBacksKeys.OnVideoError,
365
+ event: error,
366
+ });
367
+ };
368
+
362
369
  onError = (error: Error) => {
363
370
  this.invokeListenersUpdate({
364
371
  callbackName: SharedPlayerCallBacksKeys.OnError,
@@ -650,6 +657,7 @@ export class Player {
650
657
  onPlayerSeekComplete: this.onSeekComplete,
651
658
  onVideoEnd: this.onVideoEnd,
652
659
  onError: this.onError,
660
+ onVideoError: this.onVideoError,
653
661
  onLoad: this.onLoad,
654
662
  onPlayerPause: this.onPlayerPause,
655
663
  onPlayerResume: this.onPlayerResume,
@@ -684,9 +692,6 @@ export class Player {
684
692
  play = () => {};
685
693
  pause = () => {};
686
694
  disableBufferAnimation = (): boolean => true;
687
- setLanguage = (
688
- _selected?: QuickBrickPlayer.TextTrack | QuickBrickPlayer.AudioTrack
689
- ) => {};
690
695
 
691
696
  setPlaybackRate = (_rate) => {};
692
697
  startSleepTimer = (_timestamp) => {};
@@ -728,4 +733,5 @@ export class Player {
728
733
 
729
734
  public supportsNativeControls = (): boolean => false;
730
735
  public supportNativeCast = (): boolean => false;
736
+ public destroy = () => {};
731
737
  }
@@ -188,10 +188,10 @@ export class PlayerNative extends Player {
188
188
 
189
189
  disableBufferAnimation = (): boolean => false;
190
190
 
191
- setLanguage = (
191
+ selectTrack = (
192
192
  selected: QuickBrickPlayer.TextTrack | QuickBrickPlayer.AudioTrack
193
193
  ) => {
194
- this.currentPlayerComponent()?.["setLanguage"]?.(selected);
194
+ this.currentPlayerComponent()?.["selectTrack"]?.(selected);
195
195
  };
196
196
 
197
197
  setPlaybackRate = (rate: number) => {
@@ -72,6 +72,7 @@ export const usePlayerControllerSetup = ({
72
72
 
73
73
  return () => {
74
74
  playerManager.unregisterPlayer(playerId);
75
+ playerController.destroy?.();
75
76
  };
76
77
  }, [playerId, playerController]);
77
78
 
@@ -31,13 +31,12 @@ export const getKeyForLabel = (dataKey, customKey) => {
31
31
  * This method will return true if the argument passed to it is either empty or nil
32
32
  * The method prevents zero from being evaluated as falsey in an || condition
33
33
  * i.e. - NaN, null, {}, "", [], should all return true
34
- * @param {*} argument - argument passed to it could be any type
35
34
  * @returns {Boolean} returns true if empty or nil
35
+ * @param value
36
36
  */
37
- export const isEmptyOrNil = R.either(
38
- R.equals(NaN),
39
- R.either(R.isEmpty, R.isNil)
40
- );
37
+
38
+ export const isEmptyOrNil = (value: any): boolean =>
39
+ Number.isNaN(value) || value == null || R.isEmpty(value);
41
40
 
42
41
  /**
43
42
  * This method is used to find the appropriate label for any given text field.
@@ -320,6 +319,7 @@ export const ifElseFocusedSelected = (
320
319
  * @param {Object} stylesObj styles object with keys from zapp { button_icon_width: "10" }
321
320
  * @returns {*} value, returns any type that can be used as styles
322
321
  */
322
+
323
323
  export const getValue = (stylesObj) => (key) => {
324
324
  return R.propOr(null, key, stylesObj);
325
325
  };
package/index.d.ts CHANGED
@@ -143,3 +143,18 @@ declare type BuilderData = {
143
143
  river?: ZappRiver;
144
144
  method?: string;
145
145
  };
146
+
147
+ declare type AccessibilityState = {
148
+ screenReaderEnabled: boolean;
149
+ reduceMotionEnabled: boolean;
150
+ boldTextEnabled: boolean;
151
+ };
152
+
153
+ declare type AccessibilityProps = {
154
+ accessibilityLabel?: string;
155
+ accessibilityHint?: string;
156
+ "aria-label"?: string;
157
+ "aria-description"?: string;
158
+ accessibilityRole?: string;
159
+ "aria-role"?: string;
160
+ };
@@ -592,3 +592,51 @@ export function isMenuDisabled(
592
592
  export function routeIsPlayerScreen(currentRoute) {
593
593
  return currentRoute?.includes("/playable");
594
594
  }
595
+
596
+ export const getNavBarProps =
597
+ (currentRiver: ZappRiver, pathname: string, title: string) => () => {
598
+ const props = getNavigationPropsV2({
599
+ currentRiver,
600
+ title,
601
+ category: "nav_bar",
602
+ });
603
+
604
+ if (props) {
605
+ return {
606
+ ...props,
607
+ id: pathname,
608
+ pathname: pathname,
609
+ };
610
+ }
611
+
612
+ return null;
613
+ };
614
+
615
+ export const findMenuPlugin = (
616
+ navigations: ZappNavigation[],
617
+ plugins
618
+ ): QuickBrickPlugin => {
619
+ return resolveNavigationPlugin({
620
+ category: "menu",
621
+ navigations,
622
+ plugins,
623
+ }) as QuickBrickPlugin;
624
+ };
625
+
626
+ export const shouldNavBarDisplayMenu = (currentRiver: ZappRiver, plugins) =>
627
+ findMenuPlugin(currentRiver.navigations, plugins)?.identifier ===
628
+ "quick_brick_side_menu" ||
629
+ currentRiver?.navigations.findIndex(
630
+ (nav) => nav.category === "nav_bar" && nav.general?.override_menu_target
631
+ ) > -1;
632
+
633
+ export const getScreenId = (
634
+ screenData: LegacyNavigationScreenData,
635
+ fallbackRiver: ZappRiver
636
+ ) => {
637
+ return screenData?.targetScreen
638
+ ? screenData?.targetScreen?.id ?? fallbackRiver.id
639
+ : "screen_id" in screenData
640
+ ? screenData.screen_id
641
+ : fallbackRiver.id;
642
+ };