@applicaster/zapp-react-native-ui-components 15.0.0-alpha.8680244503 → 15.0.0-alpha.9300744523
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.
- package/Components/BaseFocusable/index.ios.ts +12 -2
- package/Components/Cell/FocusableWrapper.tsx +3 -0
- package/Components/Cell/TvOSCellComponent.tsx +5 -0
- package/Components/Focusable/Focusable.tsx +4 -2
- package/Components/Focusable/FocusableTvOS.tsx +18 -1
- package/Components/Focusable/__tests__/__snapshots__/FocusableTvOS.test.tsx.snap +1 -0
- package/Components/FocusableGroup/FocusableTvOS.tsx +32 -1
- package/Components/GeneralContentScreen/utils/useCurationAPI.ts +14 -3
- package/Components/PlayerContainer/PlayerContainer.tsx +48 -38
- package/Components/PlayerContainer/useRestrictMobilePlayback.tsx +101 -0
- package/Components/River/TV/River.tsx +31 -14
- package/Components/River/TV/index.tsx +8 -4
- package/Components/River/TV/utils/__tests__/toStringOrEmpty.test.ts +30 -0
- package/Components/River/TV/utils/index.ts +4 -0
- package/Components/River/TV/withFocusableGroupForContent.tsx +71 -0
- package/Decorators/ConfigurationWrapper/__tests__/__snapshots__/withConfigurationProvider.test.tsx.snap +1 -0
- package/Decorators/ConfigurationWrapper/const.ts +1 -0
- package/Decorators/ZappPipesDataConnector/__tests__/zappPipesDataConnector.test.js +1 -1
- package/package.json +5 -5
- package/Components/River/TV/withTVEventHandler.tsx +0 -27
|
@@ -22,6 +22,7 @@ type Props = {
|
|
|
22
22
|
onFocus?: FocusManager.FocusEventCB;
|
|
23
23
|
onBlur?: FocusManager.FocusEventCB;
|
|
24
24
|
selected?: boolean;
|
|
25
|
+
skipFocusManagerRegistration?: boolean;
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
export class BaseFocusable<
|
|
@@ -61,10 +62,14 @@ export class BaseFocusable<
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
componentDidMount() {
|
|
64
|
-
const { id } = this.props;
|
|
65
|
+
const { id, skipFocusManagerRegistration } = this.props;
|
|
65
66
|
const component = this;
|
|
66
67
|
this.node = this.ref.current;
|
|
67
68
|
|
|
69
|
+
if (skipFocusManagerRegistration) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
68
73
|
focusManager.register({
|
|
69
74
|
id,
|
|
70
75
|
component: component,
|
|
@@ -118,7 +123,12 @@ export class BaseFocusable<
|
|
|
118
123
|
|
|
119
124
|
componentWillUnmount() {
|
|
120
125
|
this._isMounted = false;
|
|
121
|
-
const { id } = this.props;
|
|
126
|
+
const { id, skipFocusManagerRegistration } = this.props;
|
|
127
|
+
|
|
128
|
+
if (skipFocusManagerRegistration) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
122
132
|
focusManager.unregister(id, { group: this.isGroup || false });
|
|
123
133
|
}
|
|
124
134
|
|
|
@@ -10,6 +10,7 @@ type Props = {
|
|
|
10
10
|
children: (focused: boolean) => React.ReactNode;
|
|
11
11
|
onFocus: (arg1: any, index?: number) => void;
|
|
12
12
|
onBlur: Callback;
|
|
13
|
+
skipFocusManagerRegistration?: boolean;
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
export const FocusableWrapper = ({
|
|
@@ -20,6 +21,7 @@ export const FocusableWrapper = ({
|
|
|
20
21
|
applyWrapper,
|
|
21
22
|
onFocus,
|
|
22
23
|
onBlur,
|
|
24
|
+
skipFocusManagerRegistration,
|
|
23
25
|
}: Props) => {
|
|
24
26
|
if (applyWrapper) {
|
|
25
27
|
return (
|
|
@@ -34,6 +36,7 @@ export const FocusableWrapper = ({
|
|
|
34
36
|
// @ts-ignore
|
|
35
37
|
offsetUpdater={noop}
|
|
36
38
|
isFocusable
|
|
39
|
+
skipFocusManagerRegistration={skipFocusManagerRegistration}
|
|
37
40
|
>
|
|
38
41
|
{(focused) => children(focused)}
|
|
39
42
|
</Focusable>
|
|
@@ -80,6 +80,7 @@ type Props = {
|
|
|
80
80
|
componentsMapOffset: number;
|
|
81
81
|
applyFocusableWrapper: boolean;
|
|
82
82
|
hasFocusableInside: boolean;
|
|
83
|
+
skipFocusManagerRegistration?: boolean;
|
|
83
84
|
};
|
|
84
85
|
|
|
85
86
|
type State = {
|
|
@@ -266,6 +267,7 @@ class TvOSCell extends React.Component<Props, State> {
|
|
|
266
267
|
behavior,
|
|
267
268
|
applyFocusableWrapper,
|
|
268
269
|
hasFocusableInside,
|
|
270
|
+
skipFocusManagerRegistration,
|
|
269
271
|
} = this.props;
|
|
270
272
|
|
|
271
273
|
const { id } = item;
|
|
@@ -291,6 +293,7 @@ class TvOSCell extends React.Component<Props, State> {
|
|
|
291
293
|
onFocus={handleFocus}
|
|
292
294
|
onBlur={onBlur || this.onBlur}
|
|
293
295
|
applyWrapper={applyFocusableWrapper}
|
|
296
|
+
skipFocusManagerRegistration={skipFocusManagerRegistration}
|
|
294
297
|
>
|
|
295
298
|
{(focused) => (
|
|
296
299
|
<CellWithFocusable
|
|
@@ -305,6 +308,7 @@ class TvOSCell extends React.Component<Props, State> {
|
|
|
305
308
|
focused={focused || this.props.focused}
|
|
306
309
|
behavior={behavior}
|
|
307
310
|
isFocusable={isFocusable}
|
|
311
|
+
skipFocusManagerRegistration={skipFocusManagerRegistration}
|
|
308
312
|
/>
|
|
309
313
|
)}
|
|
310
314
|
</FocusableWrapper>
|
|
@@ -327,6 +331,7 @@ class TvOSCell extends React.Component<Props, State> {
|
|
|
327
331
|
offsetUpdater={offsetUpdater}
|
|
328
332
|
style={baseCellStyles}
|
|
329
333
|
isFocusable={isFocusable}
|
|
334
|
+
skipFocusManagerRegistration={skipFocusManagerRegistration}
|
|
330
335
|
>
|
|
331
336
|
{(focused) => (
|
|
332
337
|
<FocusableCell
|
|
@@ -8,6 +8,8 @@ import { withFocusableContext } from "../../Contexts/FocusableGroupContext/withF
|
|
|
8
8
|
import { StyleSheet, ViewStyle } from "react-native";
|
|
9
9
|
import { AccessibilityManager } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager";
|
|
10
10
|
|
|
11
|
+
import { isSearchInputId } from "@applicaster/zapp-react-native-utils/searchUtils";
|
|
12
|
+
|
|
11
13
|
type Props = {
|
|
12
14
|
initialFocus?: boolean;
|
|
13
15
|
id: string;
|
|
@@ -106,7 +108,7 @@ class Focusable extends BaseFocusable<Props> {
|
|
|
106
108
|
onMouseEnter() {
|
|
107
109
|
const { id } = this.props;
|
|
108
110
|
|
|
109
|
-
if (id
|
|
111
|
+
if (!isSearchInputId(id)) {
|
|
110
112
|
this.mouse = true;
|
|
111
113
|
this.props?.handleFocus?.({ mouse: true });
|
|
112
114
|
|
|
@@ -120,7 +122,7 @@ class Focusable extends BaseFocusable<Props> {
|
|
|
120
122
|
onMouseLeave() {
|
|
121
123
|
const { id } = this.props;
|
|
122
124
|
|
|
123
|
-
if (id
|
|
125
|
+
if (!isSearchInputId(id)) {
|
|
124
126
|
this.mouse = false;
|
|
125
127
|
this.blur(null);
|
|
126
128
|
}
|
|
@@ -10,8 +10,12 @@ import {
|
|
|
10
10
|
forceFocusableFocus,
|
|
11
11
|
} from "@applicaster/zapp-react-native-utils/appUtils/focusManager/index.ios";
|
|
12
12
|
import { findNodeHandle, ViewStyle } from "react-native";
|
|
13
|
+
import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
import {
|
|
16
|
+
emitFocused,
|
|
17
|
+
emitNativeRegistered,
|
|
18
|
+
} from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios";
|
|
15
19
|
|
|
16
20
|
type Props = {
|
|
17
21
|
id: string;
|
|
@@ -39,6 +43,7 @@ type Props = {
|
|
|
39
43
|
hasReceivedFocus: () => void;
|
|
40
44
|
offsetUpdater: (arg1: string, arg2: number) => number;
|
|
41
45
|
style: ViewStyle;
|
|
46
|
+
skipFocusManagerRegistration?: boolean;
|
|
42
47
|
};
|
|
43
48
|
|
|
44
49
|
export class Focusable extends BaseFocusable<Props> {
|
|
@@ -53,6 +58,7 @@ export class Focusable extends BaseFocusable<Props> {
|
|
|
53
58
|
this.nextFocusableReactTags = {};
|
|
54
59
|
this.preferredFocus = this.preferredFocus.bind(this);
|
|
55
60
|
this.measureView = this.measureView.bind(this);
|
|
61
|
+
this.onRegistered = this.onRegistered.bind(this);
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
/**
|
|
@@ -84,6 +90,9 @@ export class Focusable extends BaseFocusable<Props> {
|
|
|
84
90
|
});
|
|
85
91
|
}
|
|
86
92
|
|
|
93
|
+
const id: string = nativeEvent.itemID;
|
|
94
|
+
emitFocused(id);
|
|
95
|
+
|
|
87
96
|
onFocus(nativeEvent);
|
|
88
97
|
}
|
|
89
98
|
|
|
@@ -169,6 +178,13 @@ export class Focusable extends BaseFocusable<Props> {
|
|
|
169
178
|
});
|
|
170
179
|
}
|
|
171
180
|
|
|
181
|
+
onRegistered({ nativeEvent }) {
|
|
182
|
+
const groupId = nativeEvent?.groupId;
|
|
183
|
+
const id = nativeEvent?.itemId;
|
|
184
|
+
|
|
185
|
+
emitNativeRegistered({ id, groupId, isGroup: false });
|
|
186
|
+
}
|
|
187
|
+
|
|
172
188
|
render() {
|
|
173
189
|
const {
|
|
174
190
|
children,
|
|
@@ -203,6 +219,7 @@ export class Focusable extends BaseFocusable<Props> {
|
|
|
203
219
|
focusable={isFocusable}
|
|
204
220
|
{...this.nextFocusableReactTags}
|
|
205
221
|
{...otherProps}
|
|
222
|
+
onRegistered={this.onRegistered}
|
|
206
223
|
>
|
|
207
224
|
{typeof children === "function" ? children(focused) : children}
|
|
208
225
|
</FocusableItemNative>
|
|
@@ -2,6 +2,10 @@ import * as React from "react";
|
|
|
2
2
|
import { FocusableGroupNative } from "@applicaster/zapp-react-native-ui-components/Components/NativeFocusables";
|
|
3
3
|
import { BaseFocusable } from "@applicaster/zapp-react-native-ui-components/Components/BaseFocusable";
|
|
4
4
|
import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
5
|
+
import { LayoutContext } from "@applicaster/zapp-react-native-tvos-app/Context/LayoutContext";
|
|
6
|
+
import { useRoute } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useRoute";
|
|
7
|
+
import { isScreenPlayable } from "@applicaster/zapp-react-native-utils/navigationUtils/itemTypes";
|
|
8
|
+
import { emitNativeRegistered } from "@applicaster/zapp-react-native-utils/appUtils/focusManagerAux/utils/utils.ios";
|
|
5
9
|
|
|
6
10
|
const { log_verbose } = createLogger({
|
|
7
11
|
subsystem: "General",
|
|
@@ -33,7 +37,16 @@ type Props = {
|
|
|
33
37
|
screenData: { screenId: string; parentScreenId: string };
|
|
34
38
|
};
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
class FocusableGroupComponent extends BaseFocusable<Props> {
|
|
41
|
+
public readonly isGroup: boolean = true;
|
|
42
|
+
|
|
43
|
+
onRegistered = ({ nativeEvent }) => {
|
|
44
|
+
const groupId = nativeEvent?.groupId;
|
|
45
|
+
const id = nativeEvent?.itemId;
|
|
46
|
+
|
|
47
|
+
emitNativeRegistered({ id, groupId, isGroup: true });
|
|
48
|
+
};
|
|
49
|
+
|
|
37
50
|
render() {
|
|
38
51
|
const {
|
|
39
52
|
children,
|
|
@@ -66,9 +79,27 @@ export class FocusableGroup extends BaseFocusable<Props> {
|
|
|
66
79
|
onGroupBlur={onGroupBlur}
|
|
67
80
|
style={style}
|
|
68
81
|
{...otherProps}
|
|
82
|
+
onRegistered={this.onRegistered}
|
|
69
83
|
>
|
|
70
84
|
{children}
|
|
71
85
|
</FocusableGroupNative>
|
|
72
86
|
);
|
|
73
87
|
}
|
|
74
88
|
}
|
|
89
|
+
|
|
90
|
+
export const withFocusDisabled = (Component) => {
|
|
91
|
+
return function WithFocusDisabled(props) {
|
|
92
|
+
// @ts-ignore
|
|
93
|
+
const { screenFocusBlocked } = React.useContext(LayoutContext.ReactContext);
|
|
94
|
+
|
|
95
|
+
const { pathname } = useRoute();
|
|
96
|
+
|
|
97
|
+
const isPlayerPresented = isScreenPlayable(pathname);
|
|
98
|
+
|
|
99
|
+
const blockScreenFocus = isPlayerPresented === false && screenFocusBlocked;
|
|
100
|
+
|
|
101
|
+
return <Component {...props} isFocusDisabled={blockScreenFocus} />;
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const FocusableGroup = withFocusDisabled(FocusableGroupComponent);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { all, equals,
|
|
1
|
+
import { all, equals, isEmpty, path, pluck, prop, values } from "ramda";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useMemo } from "react";
|
|
4
4
|
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
|
|
25
25
|
import { produce } from "immer";
|
|
26
26
|
import { useLoadPipesDataDispatch } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
27
|
+
|
|
27
28
|
// types reference
|
|
28
29
|
|
|
29
30
|
declare interface CurationEntry {
|
|
@@ -35,6 +36,7 @@ type Feeds = Record<string, ZappPipesData>;
|
|
|
35
36
|
|
|
36
37
|
type LayoutPresets = PresetsMapping["presets_mappings"];
|
|
37
38
|
|
|
39
|
+
const TABS_SCREEN_TYPE = "tabs_screen";
|
|
38
40
|
const SMART_COMPONENT_TYPE = "quick-brick-smart-component";
|
|
39
41
|
const SOURCE_PATH = ["data", "source"];
|
|
40
42
|
const MAPPING_PATH = ["data", "mapping"];
|
|
@@ -53,7 +55,10 @@ export const getTransformedPreset = (
|
|
|
53
55
|
const presetComponent = layoutPresets?.[preset?.preset_name];
|
|
54
56
|
|
|
55
57
|
if (!presetComponent) {
|
|
56
|
-
logger.log_error(
|
|
58
|
+
logger.log_error(
|
|
59
|
+
`Preset "${preset?.preset_name}" missing or wrong data format`,
|
|
60
|
+
{ entry: preset }
|
|
61
|
+
);
|
|
57
62
|
|
|
58
63
|
return;
|
|
59
64
|
}
|
|
@@ -131,10 +136,16 @@ export const useCurationAPI = (
|
|
|
131
136
|
);
|
|
132
137
|
|
|
133
138
|
const { pathname } = useRoute();
|
|
134
|
-
const [entryContext] = ZappPipesEntryContext.useZappPipesContext(pathname);
|
|
135
139
|
const [searchContext] = ZappPipesSearchContext.useZappPipesContext();
|
|
136
140
|
const [screenContext] = ZappPipesScreenContext.useZappPipesContext();
|
|
137
141
|
|
|
142
|
+
const isNestedScreen = screenContext?.type === TABS_SCREEN_TYPE;
|
|
143
|
+
|
|
144
|
+
const [entryContext] = ZappPipesEntryContext.useZappPipesContext(
|
|
145
|
+
pathname,
|
|
146
|
+
isNestedScreen
|
|
147
|
+
);
|
|
148
|
+
|
|
138
149
|
const urlsMap = useMemo<{ [key: string]: string }>(() => {
|
|
139
150
|
const map = {};
|
|
140
151
|
|
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
PlayerNativeSendCommand,
|
|
62
62
|
} from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand";
|
|
63
63
|
import { useAppData } from "@applicaster/zapp-react-native-redux";
|
|
64
|
+
import { useRestrictMobilePlayback } from "./useRestrictMobilePlayback";
|
|
64
65
|
|
|
65
66
|
type Props = {
|
|
66
67
|
Player: React.ComponentType<any>;
|
|
@@ -264,7 +265,21 @@ const PlayerContainerComponent = (props: Props) => {
|
|
|
264
265
|
|
|
265
266
|
showNavBar(true);
|
|
266
267
|
navigator.goBack();
|
|
267
|
-
}, [isModal,
|
|
268
|
+
}, [isModal, state.playerId, showNavBar, navigator]);
|
|
269
|
+
|
|
270
|
+
const pluginConfiguration = React.useMemo(() => {
|
|
271
|
+
return (
|
|
272
|
+
playerManager.getPluginConfiguration() ||
|
|
273
|
+
R.prop("__plugin_configuration", Player)
|
|
274
|
+
);
|
|
275
|
+
}, [playerManager.isRegistered()]);
|
|
276
|
+
|
|
277
|
+
const { isRestricted } = useRestrictMobilePlayback({
|
|
278
|
+
player,
|
|
279
|
+
entry: item,
|
|
280
|
+
pluginConfiguration,
|
|
281
|
+
close,
|
|
282
|
+
});
|
|
268
283
|
|
|
269
284
|
const playEntry = (entry) => navigator.replaceTop(entry, { mode });
|
|
270
285
|
|
|
@@ -456,13 +471,6 @@ const PlayerContainerComponent = (props: Props) => {
|
|
|
456
471
|
}
|
|
457
472
|
}, []);
|
|
458
473
|
|
|
459
|
-
const pluginConfiguration = React.useMemo(() => {
|
|
460
|
-
return (
|
|
461
|
-
playerManager.getPluginConfiguration() ||
|
|
462
|
-
R.prop("__plugin_configuration", Player)
|
|
463
|
-
);
|
|
464
|
-
}, [playerManager.isRegistered()]);
|
|
465
|
-
|
|
466
474
|
const disableMiniPlayer = React.useMemo(() => {
|
|
467
475
|
return pluginConfiguration?.disable_mini_player_when_inline;
|
|
468
476
|
}, [pluginConfiguration]);
|
|
@@ -662,36 +670,38 @@ const PlayerContainerComponent = (props: Props) => {
|
|
|
662
670
|
<PlayerFocusableWrapperView
|
|
663
671
|
nextFocusDown={context.bottomFocusableId}
|
|
664
672
|
>
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
setNextVideoPreloadThresholdPercentage
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
673
|
+
{isRestricted ? null : (
|
|
674
|
+
<Player
|
|
675
|
+
source={{
|
|
676
|
+
uri,
|
|
677
|
+
entry: item,
|
|
678
|
+
}}
|
|
679
|
+
focused={isInlineTV ? true : undefined}
|
|
680
|
+
autoplay={true}
|
|
681
|
+
controls={false}
|
|
682
|
+
disableCastAction={disableCastAction}
|
|
683
|
+
docked={navigator.isVideoModalDocked()}
|
|
684
|
+
entry={item}
|
|
685
|
+
fullscreen={mode === VideoModalMode.FULLSCREEN}
|
|
686
|
+
inline={inline}
|
|
687
|
+
isModal={isModal}
|
|
688
|
+
isTabletPortrait={isTabletPortrait}
|
|
689
|
+
muted={false}
|
|
690
|
+
playableItem={item}
|
|
691
|
+
playerEvent={playerEvent}
|
|
692
|
+
playerId={state.playerId}
|
|
693
|
+
pluginConfiguration={pluginConfiguration}
|
|
694
|
+
ref={playerRef}
|
|
695
|
+
toggleFullscreen={toggleFullscreen}
|
|
696
|
+
style={videoStyle}
|
|
697
|
+
playNextData={playNextData}
|
|
698
|
+
setNextVideoPreloadThresholdPercentage={
|
|
699
|
+
setNextVideoPreloadThresholdPercentage
|
|
700
|
+
}
|
|
701
|
+
>
|
|
702
|
+
{renderApplePlayer(applePlayerProps)}
|
|
703
|
+
</Player>
|
|
704
|
+
)}
|
|
695
705
|
</PlayerFocusableWrapperView>
|
|
696
706
|
|
|
697
707
|
{state.error ? <ErrorDisplay error={state.error} /> : null}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Player } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/player";
|
|
2
|
+
import NetInfo from "@react-native-community/netinfo";
|
|
3
|
+
|
|
4
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { showAlertDialog } from "@applicaster/zapp-react-native-utils/alertUtils";
|
|
6
|
+
import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
|
|
7
|
+
import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
|
|
8
|
+
import { log_info } from "./logger";
|
|
9
|
+
|
|
10
|
+
type RestrictMobilePlaybackProps = {
|
|
11
|
+
player?: Player;
|
|
12
|
+
entry?: ZappEntry;
|
|
13
|
+
pluginConfiguration?: Record<string, string>;
|
|
14
|
+
close: () => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const useRestrictMobilePlayback = ({
|
|
18
|
+
player,
|
|
19
|
+
entry,
|
|
20
|
+
pluginConfiguration,
|
|
21
|
+
close,
|
|
22
|
+
}: RestrictMobilePlaybackProps): { isRestricted: boolean } => {
|
|
23
|
+
const dialogVisibleRef = useRef<boolean>(false);
|
|
24
|
+
const theme = useTheme();
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
return () => {
|
|
28
|
+
if (isTV()) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
dialogVisibleRef.current = false;
|
|
33
|
+
};
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
const isConnectionRestricted = useMemo(() => {
|
|
37
|
+
if (isTV()) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return player && entry?.extensions?.connection_restricted;
|
|
42
|
+
}, [player, entry]);
|
|
43
|
+
|
|
44
|
+
const [isRestricted, setIsRestricted] = useState<boolean>(
|
|
45
|
+
isConnectionRestricted
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!isConnectionRestricted) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const stopPlayer = () => {
|
|
54
|
+
log_info(
|
|
55
|
+
"Stopping player due to mobile restriction, connection_restricted: true"
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
player?.close();
|
|
59
|
+
|
|
60
|
+
dialogVisibleRef.current = true;
|
|
61
|
+
|
|
62
|
+
showAlertDialog({
|
|
63
|
+
title:
|
|
64
|
+
pluginConfiguration?.mobile_connection_restricted_alert_title ||
|
|
65
|
+
"Restricted Connection Type",
|
|
66
|
+
message:
|
|
67
|
+
pluginConfiguration?.mobile_connection_restricted_alert_message ||
|
|
68
|
+
"This content can only be viewed over a Wi-Fi or LAN network.",
|
|
69
|
+
okButtonText: theme.ok_button || "OK",
|
|
70
|
+
completion: () => {
|
|
71
|
+
dialogVisibleRef.current = false;
|
|
72
|
+
|
|
73
|
+
close();
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return NetInfo.addEventListener((state) => {
|
|
79
|
+
if (state.type === "cellular") {
|
|
80
|
+
setIsRestricted(true);
|
|
81
|
+
|
|
82
|
+
if (dialogVisibleRef.current) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
stopPlayer();
|
|
87
|
+
} else {
|
|
88
|
+
setIsRestricted(false);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}, [
|
|
92
|
+
close,
|
|
93
|
+
entry?.extensions?.connection_restricted,
|
|
94
|
+
pluginConfiguration,
|
|
95
|
+
player,
|
|
96
|
+
theme.ok_button,
|
|
97
|
+
isConnectionRestricted,
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
return { isRestricted };
|
|
101
|
+
};
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Text } from "react-native";
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
import { mergeRight } from "@applicaster/zapp-react-native-utils/utils";
|
|
6
7
|
|
|
7
8
|
import { GeneralContentScreen } from "../../GeneralContentScreen";
|
|
8
9
|
import { ScreenResolver } from "@applicaster/zapp-react-native-ui-components/Components/ScreenResolver";
|
|
@@ -13,6 +14,8 @@ import {
|
|
|
13
14
|
} from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
|
|
14
15
|
import { useRivers } from "@applicaster/zapp-react-native-utils/reactHooks/state";
|
|
15
16
|
|
|
17
|
+
import { toStringOrEmpty } from "./utils";
|
|
18
|
+
|
|
16
19
|
type Props = {
|
|
17
20
|
screenId: string;
|
|
18
21
|
screenData: ZappRiver | ZappEntry;
|
|
@@ -24,6 +27,7 @@ type Props = {
|
|
|
24
27
|
isInsideContainer?: boolean;
|
|
25
28
|
extraAnchorPointYOffset: number;
|
|
26
29
|
river?: ZappRiver | ZappEntry;
|
|
30
|
+
groupId: string;
|
|
27
31
|
};
|
|
28
32
|
|
|
29
33
|
export const River = (props: Props) => {
|
|
@@ -35,6 +39,7 @@ export const River = (props: Props) => {
|
|
|
35
39
|
componentsMapExtraProps,
|
|
36
40
|
isInsideContainer,
|
|
37
41
|
extraAnchorPointYOffset,
|
|
42
|
+
groupId,
|
|
38
43
|
} = props;
|
|
39
44
|
|
|
40
45
|
const { title: screenTitle, summary: screenSummary } = useNavbarState();
|
|
@@ -51,28 +56,41 @@ export const River = (props: Props) => {
|
|
|
51
56
|
[screenId]
|
|
52
57
|
);
|
|
53
58
|
|
|
54
|
-
const
|
|
55
|
-
|
|
59
|
+
const screenResolverData = React.useMemo(() => {
|
|
60
|
+
const extraData = mergeRight(extraProps, screenResolverExtraProps);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
extraData,
|
|
64
|
+
screenData: mergeRight(river, { groupId: extraData?.groupId }),
|
|
65
|
+
componentsMapExtraProps: mergeRight(componentsMapExtraProps, { groupId }),
|
|
66
|
+
};
|
|
67
|
+
}, [
|
|
68
|
+
extraProps,
|
|
69
|
+
screenResolverExtraProps,
|
|
70
|
+
river,
|
|
71
|
+
componentsMapExtraProps,
|
|
72
|
+
groupId,
|
|
73
|
+
]);
|
|
56
74
|
|
|
57
75
|
React.useEffect(() => {
|
|
58
76
|
if (!isInsideContainer) {
|
|
59
|
-
setScreenTitle(
|
|
60
|
-
setScreenSummary(
|
|
77
|
+
setScreenTitle(toStringOrEmpty(screenData?.title));
|
|
78
|
+
setScreenSummary(toStringOrEmpty(screenData?.summary));
|
|
61
79
|
}
|
|
62
80
|
}, [screenData.id]);
|
|
63
81
|
|
|
64
82
|
React.useEffect(() => {
|
|
65
83
|
if (feedData && !isInsideContainer) {
|
|
66
84
|
if (feedData.title && feedData.title !== screenTitle) {
|
|
67
|
-
setScreenTitle(
|
|
85
|
+
setScreenTitle(toStringOrEmpty(feedData.title));
|
|
68
86
|
}
|
|
69
87
|
|
|
70
88
|
if (feedData.summary && feedData.summary !== screenSummary) {
|
|
71
|
-
setScreenSummary(
|
|
89
|
+
setScreenSummary(toStringOrEmpty(feedData.summary));
|
|
72
90
|
}
|
|
73
91
|
} else {
|
|
74
|
-
setScreenTitle(
|
|
75
|
-
setScreenSummary(
|
|
92
|
+
setScreenTitle(toStringOrEmpty(screenData?.title));
|
|
93
|
+
setScreenSummary(toStringOrEmpty(screenData?.summary));
|
|
76
94
|
}
|
|
77
95
|
}, [feedData, screenData, screenTitle, screenSummary]);
|
|
78
96
|
|
|
@@ -86,15 +104,13 @@ export const River = (props: Props) => {
|
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
if (river.type !== "general_content") {
|
|
89
|
-
const extraData = { ...R.mergeRight(extraProps, screenResolverExtraProps) };
|
|
90
|
-
|
|
91
107
|
return (
|
|
92
108
|
<ScreenResolver
|
|
93
109
|
screenType={river.type}
|
|
94
110
|
screenId={screenId}
|
|
95
|
-
screenData={
|
|
96
|
-
componentsMapExtraProps={componentsMapExtraProps}
|
|
97
|
-
{...extraData}
|
|
111
|
+
screenData={screenResolverData.screenData}
|
|
112
|
+
componentsMapExtraProps={screenResolverData.componentsMapExtraProps}
|
|
113
|
+
{...screenResolverData.extraData}
|
|
98
114
|
/>
|
|
99
115
|
);
|
|
100
116
|
}
|
|
@@ -106,6 +122,7 @@ export const River = (props: Props) => {
|
|
|
106
122
|
isScreenWrappedInContainer={isInsideContainer}
|
|
107
123
|
extraAnchorPointYOffset={extraAnchorPointYOffset}
|
|
108
124
|
componentsMapExtraProps={componentsMapExtraProps}
|
|
125
|
+
groupId={groupId}
|
|
109
126
|
/>
|
|
110
127
|
);
|
|
111
128
|
};
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { compose } from "
|
|
1
|
+
import { compose, identity } from "@applicaster/zapp-react-native-utils/utils";
|
|
2
|
+
import { isTvOSPlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
|
|
3
|
+
|
|
2
4
|
import { River as RiverComponent } from "./River";
|
|
3
|
-
import { withTvEventHandler } from "./withTVEventHandler";
|
|
4
5
|
import { withComponentsMapOffsetContext } from "../../../Contexts/ComponentsMapOffsetContext";
|
|
5
6
|
import { withRiverDataLoader } from "./withRiverDataLoader";
|
|
7
|
+
import { withFocusableGroupForContent } from "./withFocusableGroupForContent";
|
|
8
|
+
|
|
9
|
+
const isTVOS = isTvOSPlatform();
|
|
6
10
|
|
|
7
11
|
export const River = compose(
|
|
8
|
-
withTvEventHandler,
|
|
9
12
|
withComponentsMapOffsetContext,
|
|
10
|
-
withRiverDataLoader
|
|
13
|
+
withRiverDataLoader,
|
|
14
|
+
isTVOS ? withFocusableGroupForContent : identity
|
|
11
15
|
)(RiverComponent);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { toStringOrEmpty } from "..";
|
|
2
|
+
|
|
3
|
+
describe("toStringOrEmpty", () => {
|
|
4
|
+
test("returns empty string for undefined", () => {
|
|
5
|
+
expect(toStringOrEmpty(undefined)).toBe("");
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
test("returns empty string for null", () => {
|
|
9
|
+
expect(toStringOrEmpty(null)).toBe("");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("converts number to string", () => {
|
|
13
|
+
expect(toStringOrEmpty(0)).toBe("0");
|
|
14
|
+
expect(toStringOrEmpty(123)).toBe("123");
|
|
15
|
+
expect(toStringOrEmpty(-42)).toBe("-42");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("returns string as is", () => {
|
|
19
|
+
expect(toStringOrEmpty("hello")).toBe("hello");
|
|
20
|
+
expect(toStringOrEmpty("")).toBe("");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("works with numeric strings", () => {
|
|
24
|
+
expect(toStringOrEmpty("123")).toBe("123");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("does not throw on falsy values like 0", () => {
|
|
28
|
+
expect(toStringOrEmpty(0)).toBe("0");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { View, StyleSheet } from "react-native";
|
|
3
|
+
|
|
4
|
+
import { FocusableGroup } from "@applicaster/zapp-react-native-ui-components/Components/FocusableGroup";
|
|
5
|
+
import { riverFocusManager } from "@applicaster/zapp-react-native-utils/appUtils/RiverFocusManager";
|
|
6
|
+
|
|
7
|
+
import { topMenuLayoutChange$ } from "@applicaster/zapp-react-native-tvos-app/Layout/topMenu";
|
|
8
|
+
|
|
9
|
+
const styles = StyleSheet.create({
|
|
10
|
+
flexOne: {
|
|
11
|
+
flex: 1,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const withFocusableGroupForContent = (Component) => {
|
|
16
|
+
return function WithFocusableGroupForContent(props) {
|
|
17
|
+
const { screenId, isInsideContainer } = props;
|
|
18
|
+
|
|
19
|
+
const [topMenuHeight, setTopMenuHeight] = React.useState(0);
|
|
20
|
+
|
|
21
|
+
React.useEffect(() => {
|
|
22
|
+
const subscription = topMenuLayoutChange$.subscribe((layout) => {
|
|
23
|
+
setTopMenuHeight(layout.height);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return () => {
|
|
27
|
+
subscription.unsubscribe();
|
|
28
|
+
};
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const focusableId = React.useMemo(
|
|
32
|
+
() =>
|
|
33
|
+
riverFocusManager.screenFocusableGroupId({
|
|
34
|
+
screenId,
|
|
35
|
+
isInsideContainer,
|
|
36
|
+
}),
|
|
37
|
+
[screenId, isInsideContainer]
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (isInsideContainer) {
|
|
41
|
+
return <Component {...props} />;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<FocusableGroup
|
|
46
|
+
key={focusableId}
|
|
47
|
+
id={focusableId}
|
|
48
|
+
// The top menu is rendered in its own FocusableGroup, anchored at the top of the screen.
|
|
49
|
+
// When the "content" FocusableGroup starts at y = 0 as well, the two groups visually overlap.
|
|
50
|
+
// On TvOS platform this overlap can confuse the focus engine, because the focusable bounds of
|
|
51
|
+
// the top-menu group and the content group intersect, leading to erratic navigation between
|
|
52
|
+
// the menu and the content (e.g. unexpected jumps or focus getting "stuck").
|
|
53
|
+
//
|
|
54
|
+
// To avoid this, we shift the entire content FocusableGroup down by the dynamic top menu
|
|
55
|
+
// height (marginTop: topMenuHeight). This separates the focus regions of the two groups in
|
|
56
|
+
// focus space, so they no longer intersect.
|
|
57
|
+
//
|
|
58
|
+
// The inner <View> below then applies the inverse margin (marginTop: -topMenuHeight) so that
|
|
59
|
+
// the actual visual position of the content on screen does not change; only the focusable
|
|
60
|
+
// bounds of the outer group are offset.
|
|
61
|
+
style={[styles.flexOne, { marginTop: topMenuHeight }]}
|
|
62
|
+
// this group does not have parent
|
|
63
|
+
groupId={undefined}
|
|
64
|
+
>
|
|
65
|
+
<View style={[styles.flexOne, { marginTop: -1 * topMenuHeight }]}>
|
|
66
|
+
<Component {...props} groupId={focusableId} />
|
|
67
|
+
</View>
|
|
68
|
+
</FocusableGroup>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
};
|
|
@@ -88,6 +88,7 @@ exports[`withConfigurationProvider correctly passes all the configuration keys c
|
|
|
88
88
|
tab_cell_padding_right={10}
|
|
89
89
|
tab_cell_padding_top={14}
|
|
90
90
|
tablet_theme={false}
|
|
91
|
+
tabs_screen_background_color="transparent"
|
|
91
92
|
target={false}
|
|
92
93
|
target_screen_switch={false}
|
|
93
94
|
text_label_active_font_color="rgba(239, 239, 239, 0.5)"
|
|
@@ -208,4 +208,5 @@ export const keysMap: Record<string, Function> = {
|
|
|
208
208
|
tab_bar_item_margin_right: castOrDefaultValue(Number, 0),
|
|
209
209
|
tab_bar_item_margin_bottom: castOrDefaultValue(Number, 0),
|
|
210
210
|
tab_bar_item_margin_left: castOrDefaultValue(Number, 0),
|
|
211
|
+
tabs_screen_background_color: castOrDefaultValue(R.identity, "transparent"),
|
|
211
212
|
};
|
|
@@ -3,7 +3,7 @@ import { renderWithProviders } from "@applicaster/zapp-react-native-utils/testUt
|
|
|
3
3
|
|
|
4
4
|
import * as zappPipesRedux from "@applicaster/zapp-react-native-redux/ZappPipes";
|
|
5
5
|
import configureStore from "redux-mock-store";
|
|
6
|
-
import thunk from "redux-thunk";
|
|
6
|
+
import { thunk } from "redux-thunk";
|
|
7
7
|
|
|
8
8
|
import { zappPipesDataConnector } from "../index";
|
|
9
9
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-ui-components",
|
|
3
|
-
"version": "15.0.0-alpha.
|
|
3
|
+
"version": "15.0.0-alpha.9300744523",
|
|
4
4
|
"description": "Applicaster Zapp React Native ui components for the Quick Brick App",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
},
|
|
29
29
|
"homepage": "https://github.com/applicaster/quickbrick#readme",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@applicaster/applicaster-types": "15.0.0-alpha.
|
|
32
|
-
"@applicaster/zapp-react-native-bridge": "15.0.0-alpha.
|
|
33
|
-
"@applicaster/zapp-react-native-redux": "15.0.0-alpha.
|
|
34
|
-
"@applicaster/zapp-react-native-utils": "15.0.0-alpha.
|
|
31
|
+
"@applicaster/applicaster-types": "15.0.0-alpha.9300744523",
|
|
32
|
+
"@applicaster/zapp-react-native-bridge": "15.0.0-alpha.9300744523",
|
|
33
|
+
"@applicaster/zapp-react-native-redux": "15.0.0-alpha.9300744523",
|
|
34
|
+
"@applicaster/zapp-react-native-utils": "15.0.0-alpha.9300744523",
|
|
35
35
|
"fast-json-stable-stringify": "^2.1.0",
|
|
36
36
|
"promise": "^8.3.0",
|
|
37
37
|
"url": "^0.11.0",
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/* eslint max-len: off */
|
|
2
|
-
|
|
3
|
-
import React from "react";
|
|
4
|
-
import { TVEventHandlerComponent } from "@applicaster/zapp-react-native-tvos-ui-components/Components/TVEventHandlerComponent";
|
|
5
|
-
import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
6
|
-
|
|
7
|
-
export const withTvEventHandler = (Component) => {
|
|
8
|
-
return function WithTVEventHandler(props) {
|
|
9
|
-
const navigator = useNavigation();
|
|
10
|
-
|
|
11
|
-
const remoteHandler = (event) => {
|
|
12
|
-
const { eventType } = event;
|
|
13
|
-
|
|
14
|
-
const canGoBack = navigator.canGoBack();
|
|
15
|
-
|
|
16
|
-
if (eventType === "menu" && canGoBack) {
|
|
17
|
-
navigator.goBack();
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
return (
|
|
22
|
-
<TVEventHandlerComponent tvEventHandler={remoteHandler}>
|
|
23
|
-
<Component {...props} />
|
|
24
|
-
</TVEventHandlerComponent>
|
|
25
|
-
);
|
|
26
|
-
};
|
|
27
|
-
};
|