@applicaster/zapp-react-dom-app 12.0.0-rc.13 → 12.0.0-rc.130

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