@applicaster/zapp-react-dom-app 12.0.0-alpha.5812543239 → 12.0.0-alpha.5840700068

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.
@@ -1,6 +1,8 @@
1
1
  import * as React from "react";
2
2
  import * as R from "ramda";
3
3
 
4
+ import { connectToStore } from "@applicaster/zapp-react-native-redux";
5
+
4
6
  import {
5
7
  QUICK_BRICK_EVENTS,
6
8
  sendQuickBrickEvent,
@@ -31,14 +33,28 @@ import {
31
33
  import { PlayerContentContext } from "@applicaster/zapp-react-dom-ui-components/Contexts";
32
34
 
33
35
  import { withNavigator } from "@applicaster/zapp-react-native-ui-components/Decorators/Navigator";
34
- import { Platform } from "react-native";
35
- import { LONG_KEY_PRESS_TIMEOUT } from "@applicaster/quick-brick-core/const";
36
36
  import { getXray } from "@applicaster/zapp-react-native-utils/logger";
37
- import { confirmationDialogStore } from "@applicaster/zapp-react-native-ui-components/Contexts/ConfirmDialogState";
37
+
38
+ import {
39
+ confirmationDialogStore,
40
+ ConfirmationDialogState,
41
+ } from "@applicaster/zapp-react-native-ui-components/Contexts/ConfirmDialogState";
42
+
43
+ import {
44
+ isLgPlatform,
45
+ isSamsungPlatform,
46
+ isVizioPlatform,
47
+ } from "@applicaster/zapp-react-native-utils/reactUtils";
48
+
49
+ import { OnScreenKeyboard } from "@applicaster/zapp-react-dom-ui-components/Components/OnScreenKeyboard";
50
+ import { KeyboardLongPressManager } from "@applicaster/zapp-react-dom-ui-components/Utils/KeyboardLongPressManager";
38
51
 
39
52
  const { withXray } = getXray();
40
53
  const globalAny: any = global;
41
54
 
55
+ const shouldUseOnScreenKeyboard = () =>
56
+ isVizioPlatform() || window.applicaster.useOnScreenKeyboard;
57
+
42
58
  type Props = {
43
59
  displayState: string;
44
60
  setDisplayState: (state: string) => void;
@@ -54,620 +70,694 @@ type Props = {
54
70
  onRewindPressOut: () => void;
55
71
  onForwardPressOut: () => void;
56
72
  };
73
+ confirmDialog: typeof confirmationDialogStore;
57
74
  };
58
75
 
59
- type State = { isKeyboardVisible: boolean };
76
+ interface State {
77
+ isKeyboardVisible: boolean;
78
+ keyboardInput: string;
79
+ activeInputRef: HTMLInputElement | null;
80
+ }
60
81
 
61
82
  type KeyCode = { code: string; keyCode: number | string };
62
83
 
63
84
  export const alertConfirmationMesage =
64
85
  "Are you sure you want to exit application?";
65
86
 
66
- export function withInteractionManager(Component: React.FC) {
67
- class InteractionManager extends React.Component<Props, State> {
68
- onKeyDownListener: (keyCode: KeyCode) => void;
69
- onMouseDownListener: (event: MouseEvent) => void;
70
- onMouseMoveListener: (event: MouseEvent) => void;
71
- onScrollListener: (event: MouseEvent) => void;
72
-
73
- longKeyPressTimeout: any | null;
74
- longPressActive: boolean;
75
- timeout: any | null;
76
- sequenceTimeout: NodeJS.Timeout | null; // timeoutID
77
- userSequence: number[] = [];
78
- confirmDialog: any;
87
+ class InteractionManagerClass extends React.Component<Props, State> {
88
+ onKeyDownListener: (keyCode: KeyCode) => void;
89
+ onMouseDownListener: (event: MouseEvent) => void;
90
+ onMouseMoveListener: (event: MouseEvent) => void;
91
+ onScrollListener: (event: MouseEvent) => void;
92
+
93
+ private longPressDetector: KeyboardLongPressManager;
94
+ private longPressActive: boolean = false;
95
+ private timeout: any | null = null;
96
+ private sequenceTimeout: NodeJS.Timeout | null;
97
+ private userSequence: number[] = [];
98
+ private isDismissing = false;
99
+ private confirmDialog: ConfirmationDialogState =
100
+ confirmationDialogStore.getState();
101
+ // private logger: LoggerService = createLogger({
102
+ // category: "zapp-react-dom-app",
103
+ // subsystem: "InteractionManager",
104
+ // });
105
+
106
+ state: State = {
107
+ isKeyboardVisible: false,
108
+ keyboardInput: "",
109
+ activeInputRef: null,
110
+ };
79
111
 
80
- constructor(props) {
81
- super(props);
112
+ constructor(props) {
113
+ super(props);
114
+
115
+ this.onMouseMoveListener = debounce({
116
+ fn: this.onMouseMove,
117
+ context: this,
118
+ });
119
+
120
+ this.onScrollListener = debounce({
121
+ fn: this.onScroll,
122
+ wait: 100,
123
+ context: this,
124
+ });
125
+
126
+ this.onMouseDownListener = debounce({
127
+ fn: this.onMouseDown,
128
+ context: this,
129
+ });
130
+
131
+ this.onKeyDownListener = this.onKeyDown.bind(this);
132
+ this.resetHudTimeout = this.resetHudTimeout.bind(this);
133
+ this.cursorVisibilityChange = this.cursorVisibilityChange.bind(this);
134
+ this.onKeyDown = this.onKeyDown.bind(this);
135
+ this.onKeyUp = this.onKeyUp.bind(this);
136
+ this.onMouseMove = this.onMouseMove.bind(this);
137
+ this.onScroll = this.onScroll.bind(this);
138
+ this.onMouseDown = this.onMouseDown.bind(this);
139
+ this.onKeyboardStateChange = this.onKeyboardStateChange.bind(this);
140
+ this.onConfirmDialogOpen = this.onConfirmDialogOpen.bind(this);
141
+ this.onConfirmDialogClose = this.onConfirmDialogClose.bind(this);
142
+ this.performExit = this.performExit.bind(this);
143
+ this.handleInputFocus = this.handleInputFocus.bind(this);
144
+ this.handleInputBlur = this.handleInputBlur.bind(this);
145
+ this.handleKeyboardInput = this.handleKeyboardInput.bind(this);
146
+ this.handleKeyboardDismiss = this.handleKeyboardDismiss.bind(this);
147
+ this.onKeyDownListener = this.onKeyDown.bind(this);
148
+ this.onKeyUp = this.onKeyUp.bind(this);
149
+ this.onMouseMoveListener = this.onMouseMove.bind(this);
150
+ this.onScrollListener = this.onScroll.bind(this);
151
+ this.onMouseDownListener = this.onMouseDown.bind(this);
152
+ this.onKeyboardStateChange = this.onKeyboardStateChange.bind(this);
153
+ this.onConfirmDialogOpen = this.onConfirmDialogOpen.bind(this);
154
+ this.onConfirmDialogClose = this.onConfirmDialogClose.bind(this);
155
+
156
+ this.confirmDialog.setConfirmAction(this.performExit);
157
+ this.confirmDialog.setCancelAction(this.onConfirmDialogClose);
158
+ this.longPressDetector = new KeyboardLongPressManager();
159
+ }
82
160
 
83
- this.onKeyDownListener = this.onKeyDown.bind(this);
161
+ componentDidMount() {
162
+ window.addEventListener("keydown", this.onKeyDownListener);
163
+ window.addEventListener("keyup", this.onKeyUp);
164
+ window.addEventListener("mousemove", this.onMouseMoveListener);
84
165
 
85
- this.state = {
86
- isKeyboardVisible: false,
87
- };
166
+ document.addEventListener("wheel", this.onScrollListener, {
167
+ passive: true,
168
+ });
88
169
 
89
- this.onMouseMoveListener = debounce({
90
- fn: this.onMouseMove,
91
- context: this,
92
- });
170
+ document.addEventListener("mousedown", this.onMouseDownListener);
93
171
 
94
- this.onScrollListener = debounce({
95
- fn: this.onScroll,
96
- wait: 100,
97
- context: this,
98
- });
172
+ document.addEventListener(
173
+ "keyboardStateChange",
174
+ this.onKeyboardStateChange,
175
+ false
176
+ );
99
177
 
100
- this.onMouseDownListener = debounce({
101
- fn: this.onMouseDown,
102
- context: this,
103
- });
178
+ document.addEventListener("focusin", this.handleInputFocus);
104
179
 
105
- this.longKeyPressTimeout = null;
106
- this.longPressActive = false;
107
- this.confirmDialog = confirmationDialogStore.getState();
108
- this.resetHudTimeout = this.resetHudTimeout.bind(this);
109
- this.cursorVisibilityChange = this.cursorVisibilityChange.bind(this);
110
- this.onKeyDown = this.onKeyDown.bind(this);
111
- this.onKeyUp = this.onKeyUp.bind(this);
112
- this.onMouseMove = this.onMouseMove.bind(this);
113
- this.onScroll = this.onScroll.bind(this);
114
- this.onMouseDown = this.onMouseDown.bind(this);
115
- this.onKeyboardStateChange = this.onKeyboardStateChange.bind(this);
116
- this.onConfirmDialogOpen = this.onConfirmDialogOpen.bind(this);
117
- this.onConfirmDialogClose = this.onConfirmDialogClose.bind(this);
118
- this.performExit = this.performExit.bind(this);
119
-
120
- this.confirmDialog.setConfirmAction(this.performExit);
121
- this.confirmDialog.setCancelAction(this.onConfirmDialogClose);
122
- }
180
+ document.addEventListener(
181
+ "cursorStateChange",
182
+ this.cursorVisibilityChange,
183
+ false
184
+ );
123
185
 
124
- componentDidMount() {
125
- window.addEventListener("keydown", this.onKeyDownListener);
126
- window.addEventListener("keyup", this.onKeyUp);
127
- window.addEventListener("mousemove", this.onMouseMoveListener);
186
+ window.addEventListener("onLongPress", this.handleLongPress.bind(this));
128
187
 
129
- document.addEventListener("wheel", this.onScrollListener, {
130
- passive: true,
131
- });
188
+ window.addEventListener(
189
+ "onLongPressRelease",
190
+ this.handleLongPressRelease.bind(this)
191
+ );
192
+ }
132
193
 
133
- document.addEventListener("mousedown", this.onMouseDownListener);
194
+ componentWillUnmount() {
195
+ window.removeEventListener("keydown", this.onKeyDownListener);
196
+ window.removeEventListener("keyup", this.onKeyUp);
197
+ document.removeEventListener("wheel", this.onScrollListener);
134
198
 
135
- document.addEventListener(
136
- "keyboardStateChange",
137
- this.onKeyboardStateChange,
138
- false
139
- );
199
+ document.removeEventListener(
200
+ "cursorStateChange",
201
+ this.cursorVisibilityChange
202
+ );
140
203
 
141
- document.addEventListener(
142
- "cursorStateChange",
143
- this.cursorVisibilityChange,
144
- false
145
- );
146
- }
204
+ document.removeEventListener("focusin", this.handleInputFocus);
147
205
 
148
- componentWillUnmount() {
149
- window.removeEventListener("keydown", this.onKeyDownListener);
150
- window.removeEventListener("keyup", this.onKeyUp);
151
- document.removeEventListener("wheel", this.onScrollListener);
206
+ document.removeEventListener("mousedown", this.onMouseDownListener);
152
207
 
153
- document.removeEventListener(
154
- "cursorStateChange",
155
- this.cursorVisibilityChange
156
- );
208
+ if (this.timeout) {
209
+ this.clearTimeout();
210
+ }
157
211
 
158
- document.removeEventListener(
159
- "keyboardStateChange",
160
- this.onKeyboardStateChange
161
- );
212
+ this.isDismissing = false;
213
+ this.longPressDetector.cleanup();
214
+ window.removeEventListener("onLongPress", this.handleLongPress);
162
215
 
163
- document.removeEventListener("mousedown", this.onMouseDownListener);
216
+ window.removeEventListener(
217
+ "onLongPressRelease",
218
+ this.handleLongPressRelease
219
+ );
220
+ }
164
221
 
165
- if (this.timeout) {
166
- this.clearTimeout();
167
- }
222
+ performExit() {
223
+ if (isSamsungPlatform()) {
224
+ this.onConfirmDialogClose();
225
+ sendQuickBrickEvent(QUICK_BRICK_EVENTS.MOVE_APP_TO_BACKGROUND);
168
226
  }
169
227
 
170
- performExit() {
171
- if (Platform.OS === ("samsung_tv" as QuickBrickPlatforms)) {
172
- this.onConfirmDialogClose();
173
- sendQuickBrickEvent(QUICK_BRICK_EVENTS.MOVE_APP_TO_BACKGROUND);
174
- }
228
+ if (isLgPlatform()) {
229
+ // webOS recommend to use this method when implemention a custom exit
230
+ globalAny.close();
231
+ }
175
232
 
176
- if (Platform.OS === ("lg_tv" as QuickBrickPlatforms)) {
177
- // webOS recommend to use this method when implemention a custom exit
178
- globalAny.close();
179
- }
233
+ if (isVizioPlatform()) {
234
+ window?.VIZIO?.exitApplication();
180
235
  }
236
+ }
181
237
 
182
- handleWebOsBack() {
183
- const systemBack = globalAny?.webOS?.platformBack;
238
+ handleWebOsBack() {
239
+ const systemBack = globalAny?.webOS?.platformBack;
184
240
 
185
- if (systemBack) {
186
- systemBack();
187
- } else {
188
- /**
189
- * In case webOS failed to show the exit pop-up, we show a regular promtp as fallback
190
- */
191
- showConfirmationDialog({
192
- title: "",
193
- message: alertConfirmationMesage,
194
- confirmCompletion: this.performExit,
195
- });
196
- }
241
+ if (systemBack) {
242
+ systemBack();
243
+ } else {
244
+ /**
245
+ * In case webOS failed to show the exit pop-up, we show a regular promtp as fallback
246
+ */
247
+ showConfirmationDialog({
248
+ title: "",
249
+ message: alertConfirmationMesage,
250
+ confirmCompletion: this.performExit,
251
+ });
197
252
  }
253
+ }
198
254
 
199
- /**
200
- * This methods sends the app to background in whatever way is appropriate for each platform
201
- * In the case of Tizen it shows a custom dialog on confirm it exits the app
202
- * WebOS behavior depends on the SDK version, WebOS 4 would pull up the launcher menu
203
- * following versions would show a native exit prompt
204
- */
205
- platformBack() {
206
- try {
207
- // TODO: temporary hack until this code is refactored
208
- if (Platform.OS === ("samsung_tv" as QuickBrickPlatforms)) {
209
- const { isDialogVisible } = confirmationDialogStore.getState();
210
-
211
- if (isDialogVisible) {
212
- this.onConfirmDialogClose();
213
- } else {
214
- showConfirmationDialog({
215
- title: "",
216
- message: alertConfirmationMesage,
217
- confirmCompletion: this.performExit,
218
- cancelCompletion: this.onConfirmDialogClose,
219
- });
220
- }
221
- } else if (Platform.OS === ("lg_tv" as QuickBrickPlatforms)) {
222
- this.handleWebOsBack();
255
+ /**
256
+ * This methods sends the app to background in whatever way is appropriate for each platform
257
+ * In the case of Tizen it shows a custom dialog on confirm it exits the app
258
+ * WebOS behavior depends on the SDK version, WebOS 4 would pull up the launcher menu
259
+ * following versions would show a native exit prompt
260
+ */
261
+ platformBack() {
262
+ try {
263
+ // TODO: temporary hack until this code is refactored
264
+ if (isSamsungPlatform() || isVizioPlatform()) {
265
+ const { isDialogVisible } = confirmationDialogStore.getState();
266
+
267
+ if (isDialogVisible) {
268
+ this.onConfirmDialogClose();
269
+ } else {
270
+ showConfirmationDialog({
271
+ title: "",
272
+ message: alertConfirmationMesage,
273
+ confirmCompletion: this.performExit,
274
+ cancelCompletion: this.onConfirmDialogClose,
275
+ });
223
276
  }
224
- } catch (e) {
225
- // eslint-disable-next-line no-console
226
- console.warn("Failed to execute platformBack", e);
277
+ } else if (isLgPlatform()) {
278
+ this.handleWebOsBack();
227
279
  }
280
+ } catch (e) {
281
+ // eslint-disable-next-line no-console
282
+ console.warn("Failed to execute platformBack", e);
228
283
  }
284
+ }
229
285
 
230
- cursorVisibilityChange(event) {
231
- const visible = event.detail.visibility;
232
- const { setDisplayState, displayState } = this.props;
286
+ cursorVisibilityChange(event) {
287
+ const visible = event.detail.visibility;
288
+ const { setDisplayState, displayState } = this.props;
233
289
 
234
- if (visible && displayState === DISPLAY_STATES.PLAYER) {
235
- setDisplayState(DISPLAY_STATES.HUD);
236
- this.resetHudTimeout();
237
- }
290
+ if (visible && displayState === DISPLAY_STATES.PLAYER) {
291
+ setDisplayState(DISPLAY_STATES.HUD);
292
+ this.resetHudTimeout();
238
293
  }
294
+ }
239
295
 
240
- onMouseMove() {
241
- const { setDisplayState, displayState } = this.props;
296
+ onMouseMove() {
297
+ const { setDisplayState, displayState } = this.props;
242
298
 
243
- if (displayState === DISPLAY_STATES.PLAYER) {
244
- setDisplayState(DISPLAY_STATES.HUD);
245
- this.resetHudTimeout();
246
- }
299
+ if (displayState === DISPLAY_STATES.PLAYER) {
300
+ setDisplayState(DISPLAY_STATES.HUD);
301
+ this.resetHudTimeout();
247
302
  }
303
+ }
248
304
 
249
- isHomeScreen() {
250
- const { navigator } = this.props;
251
- const { rivers } = this.props;
305
+ isHomeScreen() {
306
+ const { navigator } = this.props;
307
+ const { rivers } = this.props;
252
308
 
253
- const homeId = R.compose(
254
- R.prop("id"),
255
- R.find(R.prop("home")),
256
- R.values
257
- )(rivers);
309
+ const homeId = R.compose(
310
+ R.prop("id"),
311
+ R.find(R.prop("home")),
312
+ R.values
313
+ )(rivers);
258
314
 
259
- const homePath = `/river/${homeId}`;
315
+ const homePath = `/river/${homeId}`;
260
316
 
261
- return homePath === navigator.currentRoute;
262
- }
317
+ return homePath === navigator.currentRoute;
318
+ }
263
319
 
264
- goToHome() {
265
- const { navigator } = this.props;
266
- const { rivers } = this.props;
267
- const homeRiver = R.find(R.propEq("home", true), R.values(rivers));
320
+ goToHome() {
321
+ const { navigator } = this.props;
322
+ const { rivers } = this.props;
323
+ const homeRiver = R.find(R.propEq("home", true), R.values(rivers));
268
324
 
269
- navigator.replace(homeRiver);
325
+ navigator.replace(homeRiver);
326
+ }
327
+
328
+ resetHudTimeout() {
329
+ const { resetHudTimer = noop } = this.props;
330
+
331
+ resetHudTimer?.();
332
+ }
333
+
334
+ clearTimeout() {
335
+ if (this.timeout) {
336
+ clearTimeout(this.timeout);
337
+ this.timeout = null;
270
338
  }
339
+ }
271
340
 
272
- resetHudTimeout() {
273
- const { resetHudTimer = noop } = this.props;
341
+ cancelHudTimeout() {
342
+ this.resetHudTimeout();
343
+ }
274
344
 
275
- resetHudTimer?.();
345
+ onKeyUp(event: KeyboardEvent) {
346
+ if (this.keySupportsLongPress(event)) {
347
+ this.longPressDetector.endKeyPressMonitoring(event.code);
276
348
  }
349
+ }
350
+
351
+ onKeyDown(event) {
352
+ const { displayState, setDisplayState, navigator } = this.props;
353
+ const { isKeyboardVisible } = this.state;
354
+
355
+ const { isDialogVisible } = confirmationDialogStore.getState();
277
356
 
278
- clearTimeout() {
279
- if (this.timeout) {
280
- clearTimeout(this.timeout);
281
- this.timeout = null;
357
+ if (isKeyboardVisible && shouldUseOnScreenKeyboard()) {
358
+ if (keyCode(event).matchesAny("Escape", "Back")) {
359
+ this.handleKeyboardDismiss();
282
360
  }
361
+
362
+ event.preventDefault();
363
+ event.stopPropagation();
364
+
365
+ return;
283
366
  }
284
367
 
285
- clearLongPressTimeout() {
286
- if (this.longKeyPressTimeout) {
287
- clearTimeout(this.longKeyPressTimeout);
288
- this.longKeyPressTimeout = null;
289
- }
368
+ if (this.keySupportsLongPress(event)) {
369
+ this.longPressDetector.monitorKeyPress(event.code, event);
290
370
  }
291
371
 
372
+ // None of the logic below should be executed for repeated keys
373
+ if (event.repeat) return;
374
+
292
375
  /**
293
- * If the keySupportsLongPress this method is called on longpress of key
294
- * Register your longpress handlers here, and make sure to add it to list of supported keys
376
+ When the player is displayed, we listen to Native Play / Pause / Stop
377
+ buttons. Any other key press will display the transport controls
295
378
  */
296
- onLongPress(event) {
297
- this.longPressActive = true;
298
- // Legacy way of determining whether controls are being displayed
299
- // Or if the player is in a playback state.
300
- const { displayState } = this.props;
301
- const playerState = displayState === DISPLAY_STATES.PLAYER;
302
- const hudState = displayState === DISPLAY_STATES.HUD;
303
- const playbackState = playerState || hudState;
304
-
305
- switch (event.code) {
306
- case KEYS.Back.code:
307
- // If you longpress the back button we probably want to exit
308
- this.platformBack();
379
+ if (
380
+ displayState === DISPLAY_STATES.PLAYER ||
381
+ displayState === DISPLAY_STATES.HUD
382
+ ) {
383
+ /**
384
+ registering native keys for playback
385
+ */
386
+ if (keyCode(event).matches(KEYS.TogglePlayPause)) {
387
+ const { playing } = playerManager.getState() || {};
309
388
 
310
- break;
389
+ if (!playing) {
390
+ return playerManager.togglePlayPause();
391
+ }
392
+ }
311
393
 
312
- case KEYS.Enter.code:
313
- focusManager.longPress();
394
+ if (keyCode(event).matches(KEYS.Play)) {
395
+ return playerManager.play();
396
+ }
314
397
 
315
- break;
398
+ if (keyCode(event).matches(KEYS.Pause)) {
399
+ setDisplayState(DISPLAY_STATES.HUD);
316
400
 
317
- case KEYS.Forward.code:
318
- playbackState && this.props.seekMethods.onForwardLongPress();
401
+ return playerManager.pause();
402
+ }
319
403
 
320
- break;
404
+ if (keyCode(event).matches(KEYS.Stop)) {
405
+ setDisplayState(DISPLAY_STATES.DEFAULT);
406
+ navigator.goBack();
321
407
 
322
- case KEYS.Rewind.code:
323
- playbackState && this.props.seekMethods.onRewindLongPress();
408
+ return;
409
+ }
324
410
 
325
- break;
411
+ if (keyCode(event).matches(KEYS.Forward)) {
412
+ setDisplayState(DISPLAY_STATES.HUD);
413
+ this.props.seekMethods.onForwardPress();
326
414
 
327
- default:
328
- break;
415
+ return;
416
+ }
417
+
418
+ if (keyCode(event).matches(KEYS.Rewind)) {
419
+ setDisplayState(DISPLAY_STATES.HUD);
420
+ this.props.seekMethods.onRewindPress();
421
+
422
+ return;
329
423
  }
330
424
  }
331
425
 
332
426
  /**
333
- * If the keySupportsLongPress this method is called on keyup of a longpress key
334
- * If you need to handle the release or keyup of a longpress key register the handler here
427
+ When pressing backspace or the Native "Back" button:
428
+ if on the transport controls: we show the player
429
+ if on the player : we show the UI
430
+ if on the UI : we exit the app
335
431
  */
336
- onLongPressRelease(event) {
337
- this.longPressActive = false;
338
- this.resetHudTimeout();
339
-
340
- focusManager.pressOut();
341
432
 
342
- switch (event.code) {
343
- case KEYS.Forward.code:
344
- this.props.seekMethods.onForwardPressOut();
433
+ if (
434
+ keyCode(event).matchesAny(
435
+ KEYS.Back,
436
+ KEYS.Escape,
437
+ KEYS.EscapeDevice,
438
+ KEYS.Backspace
439
+ )
440
+ ) {
441
+ switch (displayState) {
442
+ case DISPLAY_STATES.DEFAULT:
443
+ case DISPLAY_STATES.PLAYER:
444
+ if (isDialogVisible) {
445
+ this.onConfirmDialogClose();
446
+ } else if (navigator.canGoBack()) {
447
+ navigator.goBack();
448
+ } else if (this.isHomeScreen()) {
449
+ this.platformBack();
450
+ } else if (navigator.currentRoute.includes("hook")) {
451
+ // If code reaches this block navigator cant go back, it is not home,
452
+ // and this is a login plugin that was pushed on top of the home
453
+ // Treat it like a home screen and run platformBack()
454
+ const entryData = navigator?.data?.entry;
455
+ const isHookInHomescreen = entryData?.payload?.home;
456
+
457
+ const isHookFlowBlocker =
458
+ entryData?.hookPlugin?.module?.isFlowBlocker();
459
+
460
+ if (isHookInHomescreen && isHookFlowBlocker) {
461
+ this.platformBack();
462
+ } else {
463
+ navigator.goBack(false, true);
464
+ }
465
+ } else {
466
+ this.goToHome();
467
+ }
345
468
 
346
469
  break;
347
-
348
- case KEYS.Rewind.code:
349
- this.props.seekMethods.onForwardPressOut();
350
-
470
+ case DISPLAY_STATES.HUD:
471
+ // IF transport controls are open, close them instead of closing player
472
+ setDisplayState(DISPLAY_STATES.PLAYER);
351
473
  break;
352
-
353
474
  default:
475
+ setDisplayState(DISPLAY_STATES.PLAYER);
354
476
  break;
355
477
  }
356
478
  }
357
479
 
358
- /**
359
- * Resets the sequence of keys we captured.
360
- * Clear ensures that we don't interpret regular interactions
361
- * as a sequence used to unlock certain features
362
- */
363
- resetSequenceTimeout() {
364
- const SEQUENCE_ENTRY_TIME = 1900;
365
-
366
- if (this.sequenceTimeout) {
367
- // Start timer over
368
- clearTimeout(this.sequenceTimeout);
480
+ if (keyCode(event).matches(KEYS.Enter)) {
481
+ if (displayState === DISPLAY_STATES.PLAYER) {
482
+ setDisplayState(DISPLAY_STATES.HUD);
483
+ } else {
484
+ focusManager.press();
369
485
  }
370
486
 
371
- this.sequenceTimeout = setTimeout(() => {
372
- // User needs to complete sequence before SEQUENCE_ENTRY_TIME
373
- // or we set userSequence, and prev sequenceTimeout to empty values
374
- this.sequenceTimeout = null;
375
- this.userSequence = [];
376
- }, SEQUENCE_ENTRY_TIME);
487
+ return true;
377
488
  }
378
489
 
379
- /**
380
- * If a certain sequence of keys are pressed we enable special menus or settings
381
- * Each supported sequence can be stored as a constant
382
- */
383
- checkSequence(key) {
384
- const SINK_SEQUENCE = [
385
- KEYS.ArrowUp.keyCode,
386
- KEYS.ArrowUp.keyCode,
387
- KEYS.ArrowDown.keyCode,
388
- KEYS.ArrowDown.keyCode,
389
- KEYS.ArrowLeft.keyCode,
390
- KEYS.ArrowRight.keyCode,
391
- KEYS.ArrowLeft.keyCode,
392
- KEYS.ArrowRight.keyCode,
393
- ];
394
-
395
- this.resetSequenceTimeout();
396
- this.userSequence.push(key);
397
-
398
- // Remember [1,2,3] !== [1,2,3] in js, so string comparison is better
399
- const shouldAddSink =
400
- this.userSequence.toString() === SINK_SEQUENCE.toString();
401
-
402
- if (shouldAddSink) {
403
- this.props.xray.addRemoteSink({});
404
- }
405
- }
406
-
407
- /**
408
- * Determines whether the key supports or handles longpress
409
- * At the moment the only keys that support are the ones defined in
410
- * supportedEvents array. This logic just prevents us from registering timeouts for every key
411
- */
412
- keySupportsLongPress(event): boolean {
413
- const supportedEvents = [
414
- KEYS.Back,
415
- KEYS.Enter,
416
- KEYS.Rewind,
417
- KEYS.Forward,
418
- ];
419
-
420
- const eventMatcher = R.compose(R.prop("matches"), keyCode)(event);
490
+ if (keyCode(event).matches(KEYS.Exit)) {
491
+ if (isSamsungPlatform() || shouldUseOnScreenKeyboard()) {
492
+ this.performExit();
421
493
 
422
- const supportsLongPress = R.compose(
423
- R.any(R.equals(true)),
424
- R.map(eventMatcher)
425
- )(supportedEvents);
494
+ return;
495
+ }
426
496
 
427
- return supportsLongPress;
497
+ this.platformBack();
428
498
  }
429
499
 
430
- onKeyUp(event: KeyboardEvent) {
431
- // If this is a supported longpress key we need to clear the timeout and handle release of key
432
- // This logic is handled by onLongPressRelease
433
- if (this.keySupportsLongPress(event)) {
434
- if (this.longPressActive) {
435
- this.onLongPressRelease(event);
436
- } else {
437
- this.clearLongPressTimeout();
438
- }
500
+ if (
501
+ keyCode(event).matchesAny(...ARROW_KEYS) &&
502
+ !this.state.isKeyboardVisible
503
+ ) {
504
+ if (displayState === DISPLAY_STATES.PLAYER) {
505
+ setDisplayState(DISPLAY_STATES.HUD);
439
506
  }
440
- }
441
507
 
442
- onKeyDown(event) {
443
- const { displayState, setDisplayState, navigator } = this.props;
508
+ this.checkSequence(event.keyCode);
444
509
 
445
- const { isDialogVisible } = confirmationDialogStore.getState();
510
+ const direction = keyCode(event).direction();
446
511
 
447
- // If this is a supported longpress key we need to set the timeout and handle the longpress
448
- if (this.keySupportsLongPress(event)) {
449
- this.longKeyPressTimeout = setTimeout(() => {
450
- this.onLongPress(event);
451
- }, LONG_KEY_PRESS_TIMEOUT);
512
+ if (!isDialogVisible || !["up", "down"].includes(direction.value)) {
513
+ focusManager.moveFocus(direction);
514
+ }
515
+
516
+ if (!isDialogVisible) {
517
+ // run focus recovery on user interaction attempt
518
+ // probably we failed to set initial focus for some reason
519
+ focusManager.recoverFocus();
452
520
  }
453
521
 
454
- /**
455
- When the player is displayed, we listen to Native Play / Pause / Stop
456
- buttons. Any other key press will display the transport controls
457
- */
458
522
  if (
459
523
  displayState === DISPLAY_STATES.PLAYER ||
460
524
  displayState === DISPLAY_STATES.HUD
461
525
  ) {
462
- /**
463
- registering native keys for playback
464
- */
465
- if (keyCode(event).matches(KEYS.TogglePlayPause)) {
466
- const { playing } = playerManager.getState() || {};
467
-
468
- if (!playing) {
469
- return playerManager.togglePlayPause();
470
- }
471
- }
526
+ this.resetHudTimeout();
527
+ }
472
528
 
473
- if (keyCode(event).matches(KEYS.Play)) {
474
- return playerManager.play();
475
- }
529
+ return true;
530
+ }
531
+ }
476
532
 
477
- if (keyCode(event).matches(KEYS.Pause)) {
478
- setDisplayState(DISPLAY_STATES.HUD);
533
+ onScroll(event) {
534
+ const { displayState } = this.props;
479
535
 
480
- return playerManager.pause();
481
- }
536
+ const deltaY = event?.deltaY;
482
537
 
483
- if (keyCode(event).matches(KEYS.Stop)) {
484
- setDisplayState(DISPLAY_STATES.DEFAULT);
485
- navigator.goBack();
538
+ const DIRECTION = {
539
+ axis: "y",
540
+ isHorizontal: false,
541
+ isVertical: true,
542
+ value: "",
543
+ };
486
544
 
487
- return;
488
- }
545
+ if (deltaY > 0) {
546
+ DIRECTION.value = isLgPlatform() ? "down" : "up";
547
+ } else if (deltaY < 0) {
548
+ DIRECTION.value = isLgPlatform() ? "up" : "down";
549
+ }
489
550
 
490
- if (keyCode(event).matches(KEYS.Forward)) {
491
- setDisplayState(DISPLAY_STATES.HUD);
492
- this.props.seekMethods.onForwardPress();
551
+ if (
552
+ displayState === DISPLAY_STATES.PLAYER ||
553
+ displayState === DISPLAY_STATES.HUD
554
+ ) {
555
+ this.resetHudTimeout();
556
+ } else {
557
+ focusManager.moveFocus(DIRECTION);
558
+ }
559
+ }
493
560
 
494
- return;
495
- }
561
+ onMouseDown(event) {
562
+ const { button } = event;
563
+ const { displayState } = this.props;
496
564
 
497
- if (keyCode(event).matches(KEYS.Rewind)) {
498
- setDisplayState(DISPLAY_STATES.HUD);
499
- this.props.seekMethods.onRewindPress();
565
+ if (
566
+ button === 1 && // number 1 represents press on the scroll wheel
567
+ (displayState === DISPLAY_STATES.PLAYER ||
568
+ displayState === DISPLAY_STATES.HUD)
569
+ ) {
570
+ this.resetHudTimeout();
571
+ }
572
+ }
500
573
 
501
- return;
502
- }
503
- }
574
+ onKeyboardStateChange(event) {
575
+ this.setState({ isKeyboardVisible: event.visibility });
576
+ }
504
577
 
505
- /**
506
- When pressing backspace or the Native "Back" button:
507
- if on the transport controls: we show the player
508
- if on the player : we show the UI
509
- if on the UI : we exit the app
510
- */
578
+ onConfirmDialogOpen() {
579
+ this.confirmDialog.showDialog();
580
+ }
511
581
 
512
- if (
513
- keyCode(event).matchesAny(
514
- KEYS.Back,
515
- KEYS.Escape,
516
- KEYS.EscapeDevice,
517
- KEYS.Backspace
518
- )
519
- ) {
520
- switch (displayState) {
521
- case DISPLAY_STATES.DEFAULT:
522
- case DISPLAY_STATES.PLAYER:
523
- if (isDialogVisible) {
524
- this.onConfirmDialogClose();
525
- } else if (navigator.canGoBack()) {
526
- navigator.goBack();
527
- } else if (this.isHomeScreen()) {
528
- this.platformBack();
529
- } else if (navigator.currentRoute.includes("hook")) {
530
- // If code reaches this block navigator cant go back, it is not home,
531
- // and this is a login plugin that was pushed on top of the home
532
- // Treat it like a home screen and run platformBack()
533
- const entryData = navigator?.data?.entry;
534
- const isHookInHomescreen = entryData?.payload?.home;
535
-
536
- const isHookFlowBlocker =
537
- entryData?.hookPlugin?.module?.isFlowBlocker();
538
-
539
- if (isHookInHomescreen && isHookFlowBlocker) {
540
- this.platformBack();
541
- } else {
542
- navigator.goBack(false, true);
543
- }
544
- } else {
545
- this.goToHome();
546
- }
582
+ onConfirmDialogClose() {
583
+ this.confirmDialog.hideDialog();
547
584
 
548
- break;
549
- case DISPLAY_STATES.HUD:
550
- // IF transport controls are open, close them instead of closing player
551
- setDisplayState(DISPLAY_STATES.PLAYER);
552
- break;
553
- default:
554
- setDisplayState(DISPLAY_STATES.PLAYER);
555
- break;
556
- }
557
- }
585
+ // restore initial focus after closing dialog
586
+ focusManager.setInitialFocus();
587
+ }
558
588
 
559
- if (keyCode(event).matches(KEYS.Enter)) {
560
- if (displayState === DISPLAY_STATES.PLAYER) {
561
- setDisplayState(DISPLAY_STATES.HUD);
562
- } else {
563
- focusManager.press();
564
- }
589
+ handleInputFocus = (event: FocusEvent) => {
590
+ if (!shouldUseOnScreenKeyboard()) return;
591
+ const target = event.target as HTMLInputElement;
565
592
 
566
- return true;
567
- }
593
+ if (this.isDismissing) return;
568
594
 
569
- if (keyCode(event).matches(KEYS.Exit)) {
570
- if (Platform.OS === ("samsung_tv" as QuickBrickPlatforms)) {
571
- this.performExit();
595
+ if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
596
+ this.setState({
597
+ isKeyboardVisible: true,
598
+ keyboardInput: target.value || "",
599
+ activeInputRef: target,
600
+ });
601
+ }
602
+ };
572
603
 
573
- return;
574
- }
604
+ handleInputBlur(event: FocusEvent) {
605
+ if (!shouldUseOnScreenKeyboard()) return;
575
606
 
576
- this.platformBack();
577
- }
607
+ const target = event.target as HTMLElement;
608
+
609
+ if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
610
+ const relatedTarget = event.relatedTarget as HTMLElement;
578
611
 
579
612
  if (
580
- keyCode(event).matchesAny(...ARROW_KEYS) &&
581
- !this.state.isKeyboardVisible
613
+ !relatedTarget ||
614
+ (relatedTarget.tagName !== "INPUT" &&
615
+ relatedTarget.tagName !== "TEXTAREA")
582
616
  ) {
583
- if (displayState === DISPLAY_STATES.PLAYER) {
584
- setDisplayState(DISPLAY_STATES.HUD);
585
- }
617
+ this.setState({ isKeyboardVisible: false });
618
+ }
619
+ }
620
+ }
586
621
 
587
- this.checkSequence(event.keyCode);
622
+ handleKeyboardDismiss = () => {
623
+ this.isDismissing = true;
588
624
 
589
- const direction = keyCode(event).direction();
625
+ this.setState(
626
+ {
627
+ isKeyboardVisible: false,
628
+ keyboardInput: "",
629
+ activeInputRef: null,
630
+ },
631
+ () => {
632
+ this.isDismissing = false;
633
+ }
634
+ );
635
+ };
590
636
 
591
- if (!isDialogVisible || !["up", "down"].includes(direction.value)) {
592
- focusManager.moveFocus(direction);
593
- }
637
+ handleKeyboardInput = (value: string) => {
638
+ const { activeInputRef } = this.state;
594
639
 
595
- if (
596
- displayState === DISPLAY_STATES.PLAYER ||
597
- displayState === DISPLAY_STATES.HUD
598
- ) {
599
- this.resetHudTimeout();
600
- }
640
+ if (activeInputRef) {
641
+ // Update the input's value
642
+ activeInputRef.value = value;
601
643
 
602
- return true;
603
- }
604
- }
644
+ // Dispatch keyboard update event
645
+ const keyboardEvent = new CustomEvent("keyboardUpdate", {
646
+ detail: { value },
647
+ });
605
648
 
606
- onScroll(event) {
607
- const { displayState } = this.props;
649
+ document.dispatchEvent(keyboardEvent);
650
+ }
608
651
 
609
- const deltaY = event?.deltaY;
652
+ this.setState({ keyboardInput: value });
653
+ };
610
654
 
611
- const DIRECTION = {
612
- axis: "y",
613
- isHorizontal: false,
614
- isVertical: true,
615
- value: "",
616
- };
655
+ // TODO: use RemoteSequenceHandler class
656
+ private checkSequence(key: number) {
657
+ const SINK_SEQUENCE = [
658
+ KEYS.ArrowUp.keyCode,
659
+ KEYS.ArrowUp.keyCode,
660
+ KEYS.ArrowDown.keyCode,
661
+ KEYS.ArrowDown.keyCode,
662
+ KEYS.ArrowLeft.keyCode,
663
+ KEYS.ArrowRight.keyCode,
664
+ KEYS.ArrowLeft.keyCode,
665
+ KEYS.ArrowRight.keyCode,
666
+ ];
667
+
668
+ this.resetSequenceTimeout();
669
+ this.userSequence.push(key);
670
+
671
+ const shouldAddSink =
672
+ this.userSequence.toString() === SINK_SEQUENCE.toString();
673
+
674
+ if (shouldAddSink) {
675
+ this.props.xray.addRemoteSink({});
676
+ }
677
+ }
617
678
 
618
- if (deltaY > 0) {
619
- DIRECTION.value =
620
- Platform.OS === ("lg_tv" as QuickBrickPlatforms) ? "down" : "up";
621
- } else if (deltaY < 0) {
622
- DIRECTION.value =
623
- Platform.OS === ("lg_tv" as QuickBrickPlatforms) ? "up" : "down";
624
- }
679
+ private resetSequenceTimeout() {
680
+ const SEQUENCE_ENTRY_TIME = 1900;
625
681
 
626
- if (
627
- displayState === DISPLAY_STATES.PLAYER ||
628
- displayState === DISPLAY_STATES.HUD
629
- ) {
630
- this.resetHudTimeout();
631
- } else {
632
- focusManager.moveFocus(DIRECTION);
633
- }
682
+ if (this.sequenceTimeout) {
683
+ clearTimeout(this.sequenceTimeout);
634
684
  }
635
685
 
636
- onMouseDown(event) {
637
- const { button } = event;
638
- const { displayState } = this.props;
686
+ this.sequenceTimeout = setTimeout(() => {
687
+ this.sequenceTimeout = null;
688
+ this.userSequence = [];
689
+ }, SEQUENCE_ENTRY_TIME);
690
+ }
639
691
 
640
- if (
641
- button === 1 && // number 1 represents press on the scroll wheel
642
- (displayState === DISPLAY_STATES.PLAYER ||
643
- displayState === DISPLAY_STATES.HUD)
644
- ) {
645
- this.resetHudTimeout();
646
- }
692
+ private handleLongPress = (event: CustomEvent) => {
693
+ const { keyCode } = event.detail;
694
+ const { displayState } = this.props;
695
+ const playerState = displayState === DISPLAY_STATES.PLAYER;
696
+ const hudState = displayState === DISPLAY_STATES.HUD;
697
+ const playbackState = playerState || hudState;
698
+ this.longPressActive = true;
699
+ this.cancelHudTimeout();
700
+
701
+ switch (keyCode) {
702
+ case KEYS.Forward.code:
703
+ playbackState && this.props.seekMethods.onForwardLongPress();
704
+ break;
705
+
706
+ case KEYS.Rewind.code:
707
+ playbackState && this.props.seekMethods.onRewindLongPress();
708
+ break;
709
+
710
+ case KEYS.Enter.code:
711
+ focusManager.longPress();
712
+ break;
647
713
  }
714
+ };
648
715
 
649
- onKeyboardStateChange(event) {
650
- this.setState({ isKeyboardVisible: event.visibility });
651
- }
716
+ private handleLongPressRelease = (event: CustomEvent) => {
717
+ const { keyCode } = event.detail;
718
+ this.longPressActive = false;
719
+ this.resetHudTimeout();
652
720
 
653
- onConfirmDialogOpen() {
654
- this.confirmDialog.showDialog();
655
- }
721
+ switch (keyCode) {
722
+ case KEYS.Forward.code:
723
+ this.props.seekMethods.onForwardPressOut();
724
+ break;
656
725
 
657
- onConfirmDialogClose() {
658
- this.confirmDialog.hideDialog();
659
- }
726
+ case KEYS.Rewind.code:
727
+ this.props.seekMethods.onRewindPressOut();
728
+ break;
660
729
 
661
- render() {
662
- return <Component {...this.props} />;
730
+ case KEYS.Enter.code:
731
+ focusManager.pressOut();
732
+ break;
663
733
  }
734
+ };
735
+
736
+ private keySupportsLongPress(event: KeyboardEvent): boolean {
737
+ return [KEYS.Forward.code, KEYS.Rewind.code, KEYS.Enter.code].includes(
738
+ event.code
739
+ );
664
740
  }
665
741
 
666
- return R.compose(
667
- withNavigator,
668
- withUseSeek,
669
- withXray,
670
- PlayerContentContext.withConsumer,
671
- DisplayStateContext.withConsumer
672
- )(InteractionManager);
742
+ render() {
743
+ return (
744
+ (shouldUseOnScreenKeyboard() && this.state.isKeyboardVisible && (
745
+ <OnScreenKeyboard
746
+ value={this.state.keyboardInput}
747
+ onInput={this.handleKeyboardInput}
748
+ onDismiss={this.handleKeyboardDismiss}
749
+ />
750
+ )) ||
751
+ null
752
+ );
753
+ }
673
754
  }
755
+
756
+ export const InteractionManager = R.compose(
757
+ withNavigator,
758
+ withUseSeek,
759
+ withXray,
760
+ connectToStore(R.pick(["rivers"])),
761
+ PlayerContentContext.withConsumer,
762
+ DisplayStateContext.withConsumer
763
+ )(InteractionManagerClass);