@applicaster/zapp-react-native-ui-components 16.0.0-alpha.6861239588 → 16.0.0-alpha.6918106967
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/AudioPlayer/tv/__tests__/audioPlayer.test.js +19 -0
- package/Components/AudioPlayer/tv/__tests__/index.test.tsx +120 -0
- package/Components/AudioPlayer/tv/index.tsx +5 -1
- package/Components/Cell/Cell.tsx +3 -6
- package/Components/Cell/TvOSCellComponent.tsx +0 -1
- package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +15 -0
- package/Components/GeneralContentScreen/hookAdapter/__tests__/skipExpression.test.ts +294 -0
- package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +64 -0
- package/Components/GeneralContentScreen/hookAdapter/skipExpression.ts +252 -0
- package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +18 -0
- package/Components/MasterCell/DefaultComponents/ActionButton.tsx +1 -0
- package/Components/MasterCell/DefaultComponents/Button.tsx +1 -0
- package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
- package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
- package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
- package/Components/MasterCell/DefaultComponents/Toggle.tsx +36 -14
- package/Components/MasterCell/DefaultComponents/__tests__/Toggle.test.tsx +167 -0
- package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +54 -0
- package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +21 -0
- package/Components/ModalComponent/ActionsBottomSheet/index.ts +9 -0
- package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +58 -0
- package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +0 -18
- package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +32 -27
- package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +25 -73
- package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +0 -18
- package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +166 -84
- package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +89 -0
- package/Components/ModalComponent/AudioPlayer/Components/index.ts +3 -1
- package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +230 -0
- package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +332 -0
- package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
- package/Components/ModalComponent/BottomSheetModalContent.tsx +385 -60
- package/Components/ModalComponent/Button/index.tsx +5 -3
- package/Components/ModalComponent/Header/CloseButton.tsx +12 -0
- package/Components/ModalComponent/Header/__tests__/CloseButton.test.tsx +72 -0
- package/Components/ModalComponent/Header/index.tsx +20 -7
- package/Components/ModalComponent/Header/utils.ts +1 -1
- package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
- package/Components/ModalComponent/SortableList.tsx +142 -0
- package/Components/ModalComponent/SortableList.web.tsx +34 -0
- package/Components/ModalComponent/TextInputContent.tsx +100 -0
- package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
- package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +238 -0
- package/Components/ModalComponent/__tests__/SortableList.web.test.tsx +16 -0
- package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +120 -0
- package/Components/ModalComponent/__tests__/showTextInput.test.ts +95 -0
- package/Components/ModalComponent/index.tsx +11 -0
- package/Components/ModalComponent/presets/__tests__/getCell.test.ts +66 -0
- package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
- package/Components/ModalComponent/presets/index.ts +27 -0
- package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
- package/Components/ModalComponent/presets/standardCell.tsx +41 -0
- package/Components/ModalComponent/presets/types.ts +35 -0
- package/Components/ModalComponent/showTextInput.ts +43 -0
- package/Components/ModalComponent/utils.ts +82 -21
- package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
- package/Components/OfflineHandler/__tests__/index.test.tsx +78 -18
- package/Components/OfflineHandler/hooks.ts +111 -0
- package/Components/OfflineHandler/index.tsx +51 -73
- package/Components/OfflineHandler/utils/index.ts +1 -13
- package/Components/PlayerContainer/PlayerContainer.tsx +11 -7
- package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
- package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +66 -13
- package/Components/PreloaderWrapper/index.tsx +1 -1
- package/Components/River/ComponentsMap/ComponentsMap.tsx +79 -51
- package/Components/River/River.tsx +4 -0
- package/Components/River/__tests__/resultCallback.test.tsx +47 -0
- package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
- package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
- package/Components/ScreenRevealManager/ScreenRevealManager.ts +18 -2
- package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +36 -0
- package/Components/ScreenRevealManager/__tests__/withScreenRevealManager.test.tsx +56 -0
- package/Components/ScreenRevealManager/withScreenRevealManager.tsx +20 -3
- package/Components/TopCutoffOverlay/index.tsx +2 -2
- package/Components/VideoLive/LiveImageManager.ts +1 -1
- package/Hooks/useEntryActionsExpander.ts +63 -0
- package/package.json +6 -5
- package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +0 -112
- package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +0 -107
- package/Components/OfflineHandler/NotificationView/NotificationView.tsx +0 -153
- package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +0 -66
- package/Components/OfflineHandler/NotificationView/utils.ts +0 -34
- /package/Decorators/ZappPipesDataConnector/__tests__/{Hero.js → Hero.tsx} +0 -0
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
platformSelect,
|
|
10
10
|
isAndroidTVPlatform,
|
|
11
11
|
isTvOSPlatform,
|
|
12
|
+
isApplePlatform,
|
|
12
13
|
} from "@applicaster/zapp-react-native-utils/reactUtils";
|
|
13
14
|
import {
|
|
14
15
|
isAudioItem,
|
|
@@ -60,6 +61,7 @@ import {
|
|
|
60
61
|
PlayerNativeSendCommand,
|
|
61
62
|
} from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerNativeCommand";
|
|
62
63
|
import { useAppData } from "@applicaster/zapp-react-native-redux";
|
|
64
|
+
import { PLAYER_EVENTS } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/const";
|
|
63
65
|
|
|
64
66
|
type Props = {
|
|
65
67
|
Player: React.ComponentType<any>;
|
|
@@ -188,12 +190,10 @@ const renderApplePlayer = ({
|
|
|
188
190
|
return null;
|
|
189
191
|
}
|
|
190
192
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
+
// Title/Subtitle are not resolved here: `ProgramInfo` reads them from the
|
|
194
|
+
// player manager's content channel via `usePlayerContent`.
|
|
193
195
|
return (
|
|
194
196
|
<ProgramInfo
|
|
195
|
-
title={title}
|
|
196
|
-
subtitle={summary}
|
|
197
197
|
plugin_configuration={pluginConfiguration}
|
|
198
198
|
videoStyle={videoStyle}
|
|
199
199
|
/>
|
|
@@ -282,12 +282,16 @@ const PlayerContainerComponent = (props: Props) => {
|
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
log_debug(
|
|
285
|
-
`onPlayNextPerformNextVideoPlay: delay completed, replaceTop to item: ${playNextOverlayState.entry.title}, id: ${playNextOverlayState.entry.id}`
|
|
285
|
+
`onPlayNextPerformNextVideoPlay: delay completed, replaceTop to item: ${playNextOverlayState.entry.title}, id: ${playNextOverlayState.entry.id}, outgoing item id: ${item?.id}`
|
|
286
286
|
);
|
|
287
287
|
|
|
288
|
+
playNextOverlayState.hidePlayNext();
|
|
289
|
+
|
|
290
|
+
playerEvent(PLAYER_EVENTS.PlayNextTriggered, { id: item?.id });
|
|
291
|
+
|
|
288
292
|
setIsLoadingNextVideo(true);
|
|
289
293
|
playEntry(playNextOverlayState.entry);
|
|
290
|
-
}, [playNextOverlayState]);
|
|
294
|
+
}, [playNextOverlayState, item?.id]);
|
|
291
295
|
|
|
292
296
|
const playNextData: Nullable<PlayNextData> = React.useMemo(() => {
|
|
293
297
|
if (!playNextOverlayState) {
|
|
@@ -495,7 +499,7 @@ const PlayerContainerComponent = (props: Props) => {
|
|
|
495
499
|
const isAudioContent = isAudioItem(item);
|
|
496
500
|
|
|
497
501
|
useEffect(() => {
|
|
498
|
-
if (!isAudioContent) {
|
|
502
|
+
if (!isAudioContent && isApplePlatform()) {
|
|
499
503
|
sendQuickBrickEvent(
|
|
500
504
|
QUICK_BRICK_EVENTS.IDLE_TIMER_DISABLED as QuickBrickEvent,
|
|
501
505
|
{
|
|
@@ -7,12 +7,11 @@ import * as React from "react";
|
|
|
7
7
|
import { Text, TextStyle, View, ViewStyle } from "react-native";
|
|
8
8
|
import { QBImage as Image } from "@applicaster/zapp-react-native-ui-components/Components/Image";
|
|
9
9
|
|
|
10
|
+
import { usePlayerContent } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayerContent";
|
|
10
11
|
import * as programStyles from "./styles";
|
|
11
12
|
|
|
12
13
|
type Props = {
|
|
13
14
|
logo?: string;
|
|
14
|
-
title: string;
|
|
15
|
-
subtitle: string;
|
|
16
15
|
isAudioContent: boolean;
|
|
17
16
|
styles: {};
|
|
18
17
|
plugin_configuration: {
|
|
@@ -28,13 +27,13 @@ const defaultColor = "#EFEFEF";
|
|
|
28
27
|
|
|
29
28
|
function ProgramInfoComponent({
|
|
30
29
|
logo,
|
|
31
|
-
title = "",
|
|
32
|
-
subtitle = "",
|
|
33
30
|
isAudioContent,
|
|
34
31
|
styles,
|
|
35
32
|
plugin_configuration = {},
|
|
36
33
|
videoStyle = {},
|
|
37
34
|
}: Props) {
|
|
35
|
+
const { title = "", summary: subtitle = "" } = usePlayerContent();
|
|
36
|
+
|
|
38
37
|
const platformKey = styleKeys.style_namespace;
|
|
39
38
|
|
|
40
39
|
const colorTitle =
|
|
@@ -17,16 +17,37 @@ jest.mock(
|
|
|
17
17
|
);
|
|
18
18
|
|
|
19
19
|
jest.mock("@applicaster/zapp-react-native-utils/reactUtils", () => ({
|
|
20
|
+
// Defaults to `false` for the hardware-back-handling tests below, which
|
|
21
|
+
// key off `isTV`. The ProgramInfo/content-context describe block below
|
|
22
|
+
// overrides this per test (`isTV()` is also what PlayerContainer's
|
|
23
|
+
// `playerContentValue` uses to pick TV vs mobile data keys, decoupled
|
|
24
|
+
// from `isTvOSPlatform` below).
|
|
20
25
|
isTV: jest.fn(() => false),
|
|
21
26
|
isAndroidTVPlatform: jest.fn(() => false),
|
|
22
|
-
isTvOSPlatform
|
|
27
|
+
// `isTvOSPlatform` backs a module-level constant in PlayerContainer.tsx that
|
|
28
|
+
// is read once at import time, so it must already be `true` here in order
|
|
29
|
+
// to exercise `renderApplePlayer`'s tvOS-only ProgramInfo branch in this
|
|
30
|
+
// test file. It does not affect the hardware-back-handling tests below,
|
|
31
|
+
// which key off `isTV` instead.
|
|
32
|
+
isTvOSPlatform: jest.fn(() => true),
|
|
33
|
+
isApplePlatform: jest.fn(() => true),
|
|
23
34
|
platformSelect: jest.fn(({ native }) => native),
|
|
24
35
|
}));
|
|
25
36
|
|
|
26
|
-
jest.mock("@applicaster/zapp-react-native-utils/playerUtils", () =>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
jest.mock("@applicaster/zapp-react-native-utils/playerUtils", () => {
|
|
38
|
+
// Keep the real data-key resolver (createConfigValueGetter,
|
|
39
|
+
// resolvePlayerContentText, VIDEO_PLAYER_CONTENT_DATA_KEYS) so the tvOS
|
|
40
|
+
// ProgramInfo data-key tests exercise actual resolution logic, not a stub.
|
|
41
|
+
const actual = jest.requireActual(
|
|
42
|
+
"@applicaster/zapp-react-native-utils/playerUtils"
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
...actual,
|
|
47
|
+
isAudioItem: jest.fn(() => false),
|
|
48
|
+
isInlineTV: jest.fn(() => false),
|
|
49
|
+
};
|
|
50
|
+
});
|
|
30
51
|
|
|
31
52
|
jest.mock(
|
|
32
53
|
"@applicaster/zapp-react-native-tvos-ui-components/Components/TVEventHandlerComponent",
|
|
@@ -75,6 +96,11 @@ jest.mock("@applicaster/zapp-react-native-bridge/QuickBrick", () => ({
|
|
|
75
96
|
sendQuickBrickEvent: jest.fn(),
|
|
76
97
|
}));
|
|
77
98
|
|
|
99
|
+
// Stubbed so PlayerContainer can render without ProgramInfo's own
|
|
100
|
+
// redux/style plumbing. It no longer needs to capture Title/Subtitle: those
|
|
101
|
+
// come from the player manager's content channel, and the key-set selection
|
|
102
|
+
// they depend on is covered directly in
|
|
103
|
+
// `playerManager/__tests__/playerContent.test.ts`.
|
|
78
104
|
jest.mock("../ProgramInfo", () => ({
|
|
79
105
|
ProgramInfo: () => null,
|
|
80
106
|
}));
|
|
@@ -142,14 +168,33 @@ jest.mock(
|
|
|
142
168
|
jest.mock("../PlayerContainerContext", () => {
|
|
143
169
|
const ReactActual = require("react");
|
|
144
170
|
|
|
171
|
+
const PlayerContainerContext = ReactActual.createContext({
|
|
172
|
+
isLanguageOverlayVisible: false,
|
|
173
|
+
bottomFocusableId: "bottom",
|
|
174
|
+
showComponentsContainer: false,
|
|
175
|
+
refs: null,
|
|
176
|
+
});
|
|
177
|
+
|
|
145
178
|
return {
|
|
146
|
-
PlayerContainerContext
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
})
|
|
152
|
-
PlayerContainerContextProvider: ({ children }) =>
|
|
179
|
+
PlayerContainerContext,
|
|
180
|
+
// Mirrors just enough of the real provider for these tests: renders an
|
|
181
|
+
// actual `PlayerContainerContext.Provider` so the real
|
|
182
|
+
// `PlayerContainerContext.Consumer` inside PlayerContainer.tsx sees the
|
|
183
|
+
// container fields a real provider would give it, instead of a bare
|
|
184
|
+
// `({ children }) => children` passthrough.
|
|
185
|
+
PlayerContainerContextProvider: ({ children }: any) =>
|
|
186
|
+
ReactActual.createElement(
|
|
187
|
+
PlayerContainerContext.Provider,
|
|
188
|
+
{
|
|
189
|
+
value: {
|
|
190
|
+
isLanguageOverlayVisible: false,
|
|
191
|
+
bottomFocusableId: "bottom",
|
|
192
|
+
showComponentsContainer: false,
|
|
193
|
+
refs: null,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
children
|
|
197
|
+
),
|
|
153
198
|
};
|
|
154
199
|
});
|
|
155
200
|
|
|
@@ -200,7 +245,14 @@ const { __player: mockPlayer } = jest.requireMock(
|
|
|
200
245
|
"@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer"
|
|
201
246
|
);
|
|
202
247
|
|
|
203
|
-
const
|
|
248
|
+
const { isTV: mockIsTV } = jest.requireMock(
|
|
249
|
+
"@applicaster/zapp-react-native-utils/reactUtils"
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// Renders children so that `renderApplePlayer`'s returned <ProgramInfo>
|
|
253
|
+
// element (passed as this component's `children` prop) actually mounts —
|
|
254
|
+
// otherwise React never invokes the ProgramInfo function component at all.
|
|
255
|
+
const Player = React.forwardRef((props: any, _ref) => props.children ?? null);
|
|
204
256
|
|
|
205
257
|
const item = {
|
|
206
258
|
id: "entry-1",
|
|
@@ -234,6 +286,7 @@ const pressHardwareBack = () => {
|
|
|
234
286
|
describe("PlayerContainer hardware back handling", () => {
|
|
235
287
|
beforeEach(() => {
|
|
236
288
|
jest.clearAllMocks();
|
|
289
|
+
mockIsTV.mockReturnValue(false);
|
|
237
290
|
mockBackHandlerRef.current = null;
|
|
238
291
|
mockPlayer.getPluginConfiguration.mockReturnValue({});
|
|
239
292
|
mockPlayer.isPaused.mockReturnValue(false);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import * as R from "ramda";
|
|
3
3
|
import { FlatList, StyleSheet, View } from "react-native";
|
|
4
|
+
import { SafeAreaView, EdgeMode, Edge } from "react-native-safe-area-context";
|
|
4
5
|
import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
|
|
5
6
|
import { RiverItem } from "../RiverItem";
|
|
6
7
|
import { RiverFooter } from "../RiverFooter";
|
|
@@ -40,6 +41,12 @@ type Props = {
|
|
|
40
41
|
riverId?: string;
|
|
41
42
|
getStaticComponentFeed: any;
|
|
42
43
|
stickyHeaderIndices?: number[];
|
|
44
|
+
safeArea?:
|
|
45
|
+
| {
|
|
46
|
+
edges: Record<Edge, EdgeMode>;
|
|
47
|
+
mode: "padding" | "margin" | undefined;
|
|
48
|
+
}
|
|
49
|
+
| undefined;
|
|
43
50
|
};
|
|
44
51
|
|
|
45
52
|
const styles = StyleSheet.create({
|
|
@@ -64,6 +71,7 @@ function ComponentsMapComponent(props: Props) {
|
|
|
64
71
|
riverId,
|
|
65
72
|
getStaticComponentFeed,
|
|
66
73
|
stickyHeaderIndices,
|
|
74
|
+
safeArea,
|
|
67
75
|
} = props;
|
|
68
76
|
|
|
69
77
|
const flatListRef = React.useRef<FlatList | null>(null);
|
|
@@ -121,14 +129,6 @@ function ComponentsMapComponent(props: Props) {
|
|
|
121
129
|
|
|
122
130
|
const screenStyle = React.useMemo(
|
|
123
131
|
() => ({
|
|
124
|
-
paddingTop: ifEmptyUseFallback(
|
|
125
|
-
screenConfig.paddingTop,
|
|
126
|
-
R.prop("screen_padding_top")(theme)
|
|
127
|
-
),
|
|
128
|
-
paddingBottom: ifEmptyUseFallback(
|
|
129
|
-
screenConfig.paddingBottom,
|
|
130
|
-
R.prop("screen_padding_bottom")(theme)
|
|
131
|
-
),
|
|
132
132
|
paddingLeft: ifEmptyUseFallback(
|
|
133
133
|
screenConfig.paddingLeft,
|
|
134
134
|
R.prop("screen_padding_left")(theme)
|
|
@@ -266,51 +266,79 @@ function ComponentsMapComponent(props: Props) {
|
|
|
266
266
|
numberOfComponents={riverComponents.length}
|
|
267
267
|
>
|
|
268
268
|
<ViewportTracker>
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
269
|
+
<>
|
|
270
|
+
<FlatList
|
|
271
|
+
ListHeaderComponent={
|
|
272
|
+
<SafeAreaView
|
|
273
|
+
mode="margin"
|
|
274
|
+
style={{
|
|
275
|
+
paddingTop: ifEmptyUseFallback(
|
|
276
|
+
screenConfig.paddingTop,
|
|
277
|
+
R.prop("screen_padding_top")(theme)
|
|
278
|
+
),
|
|
279
|
+
height: StyleSheet.hairlineWidth,
|
|
280
|
+
}}
|
|
281
|
+
edges={{ top: safeArea?.edges?.top ?? "off" }}
|
|
282
|
+
/>
|
|
283
|
+
}
|
|
284
|
+
testID="components-map-flat-list"
|
|
285
|
+
ref={(ref) => {
|
|
286
|
+
flatListRef.current = ref;
|
|
287
|
+
}}
|
|
288
|
+
// Fix for WebView rerender crashes on Android API 28+
|
|
289
|
+
// https://github.com/react-native-webview/react-native-webview/issues/1915#issuecomment-964035468
|
|
290
|
+
overScrollMode={isAndroid ? "never" : "auto"}
|
|
291
|
+
scrollIndicatorInsets={scrollIndicatorInsets}
|
|
292
|
+
extraData={feed}
|
|
293
|
+
stickyHeaderIndices={stickyHeaderIndices}
|
|
294
|
+
removeClippedSubviews={isAndroid}
|
|
295
|
+
onLayout={handleOnLayout}
|
|
296
|
+
initialNumToRender={3}
|
|
297
|
+
maxToRenderPerBatch={10}
|
|
298
|
+
windowSize={12}
|
|
299
|
+
keyExtractor={keyExtractor}
|
|
300
|
+
renderItem={renderRiverItem}
|
|
301
|
+
data={riverComponents}
|
|
302
|
+
contentContainerStyle={contentContainerStyle}
|
|
303
|
+
ListFooterComponent={
|
|
304
|
+
<>
|
|
305
|
+
<RiverFooter
|
|
306
|
+
flatListHeight={flatListHeight}
|
|
307
|
+
loadingState={loadingState}
|
|
308
|
+
/>
|
|
309
|
+
<SafeAreaView
|
|
310
|
+
mode="margin"
|
|
311
|
+
style={{
|
|
312
|
+
paddingBottom: ifEmptyUseFallback(
|
|
313
|
+
screenConfig.paddingBottom,
|
|
314
|
+
R.prop("screen_padding_bottom")(theme)
|
|
315
|
+
),
|
|
316
|
+
height: StyleSheet.hairlineWidth,
|
|
317
|
+
}}
|
|
318
|
+
edges={{ bottom: safeArea?.edges?.bottom ?? "off" }}
|
|
319
|
+
/>
|
|
320
|
+
</>
|
|
321
|
+
}
|
|
322
|
+
refreshControl={refreshControl}
|
|
323
|
+
onScrollBeginDrag={onScrollBeginDrag}
|
|
324
|
+
onScroll={onScroll}
|
|
325
|
+
onMomentumScrollEnd={_onMomentumScrollEnd}
|
|
326
|
+
onScrollEndDrag={_onScrollEndDrag}
|
|
327
|
+
scrollEventThrottle={16}
|
|
328
|
+
{...scrollViewExtraProps}
|
|
329
|
+
onEndReached={
|
|
330
|
+
/* When wrapped in a parent ScrollView (e.g. tabs),
|
|
304
331
|
this FlatList doesn't scroll so onEndReached can fire repeatedly;
|
|
305
332
|
skip it here and let the parent ScrollView emit scroll-end instead. */
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
333
|
+
isScreenWrappedInContainer
|
|
334
|
+
? undefined
|
|
335
|
+
: () => {
|
|
336
|
+
if (!hasUserScrolledRef.current) return;
|
|
337
|
+
emitScrollEndReached();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/>
|
|
341
|
+
</>
|
|
314
342
|
</ViewportTracker>
|
|
315
343
|
</ScreenLoadingMeasurements>
|
|
316
344
|
</ComponentsMapRefContext.Provider>
|
|
@@ -20,6 +20,8 @@ type Props = ZappScreenProps & {
|
|
|
20
20
|
setIsScreenWrappedInContainer: (isInsideContainer: boolean) => void;
|
|
21
21
|
screenContext: ZappRiver;
|
|
22
22
|
river: ZappRiver;
|
|
23
|
+
/** Result channel for screens presented as a modal; forwarded to the resolved screen. */
|
|
24
|
+
resultCallback?: hookCallback;
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
export class RiverComponent extends React.Component<Props> {
|
|
@@ -64,6 +66,7 @@ export class RiverComponent extends React.Component<Props> {
|
|
|
64
66
|
isInsideContainer,
|
|
65
67
|
groupId,
|
|
66
68
|
scrollViewExtraProps,
|
|
69
|
+
resultCallback,
|
|
67
70
|
} = this.props;
|
|
68
71
|
|
|
69
72
|
const { id, type } = river;
|
|
@@ -77,6 +80,7 @@ export class RiverComponent extends React.Component<Props> {
|
|
|
77
80
|
})}
|
|
78
81
|
screenId={id}
|
|
79
82
|
screenType={type}
|
|
83
|
+
resultCallback={resultCallback}
|
|
80
84
|
/>
|
|
81
85
|
);
|
|
82
86
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "@testing-library/react-native";
|
|
3
|
+
|
|
4
|
+
import { RiverComponent } from "../River";
|
|
5
|
+
|
|
6
|
+
const mockResolverProps: any[] = [];
|
|
7
|
+
|
|
8
|
+
jest.mock("../../ScreenResolver", () => ({
|
|
9
|
+
ScreenResolver: (props: any) => {
|
|
10
|
+
mockResolverProps.push(props);
|
|
11
|
+
|
|
12
|
+
return null;
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const river = { id: "river-parent-lock", type: "parent_lock" } as any;
|
|
17
|
+
|
|
18
|
+
const baseProps = {
|
|
19
|
+
river,
|
|
20
|
+
screenData: {},
|
|
21
|
+
groupId: "group-1",
|
|
22
|
+
scrollViewExtraProps: {},
|
|
23
|
+
setIsScreenWrappedInContainer: jest.fn(),
|
|
24
|
+
screenContext: { navBar: { setTitle: jest.fn(), setSummary: jest.fn() } },
|
|
25
|
+
} as any;
|
|
26
|
+
|
|
27
|
+
const resolved = () => mockResolverProps[mockResolverProps.length - 1];
|
|
28
|
+
|
|
29
|
+
describe("RiverComponent", () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
mockResolverProps.length = 0;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("forwards resultCallback to the screen it resolves", () => {
|
|
35
|
+
const resultCallback = jest.fn();
|
|
36
|
+
|
|
37
|
+
render(<RiverComponent {...baseProps} resultCallback={resultCallback} />);
|
|
38
|
+
|
|
39
|
+
expect(resolved().resultCallback).toBe(resultCallback);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("resolves the screen without a resultCallback when none was given", () => {
|
|
43
|
+
render(<RiverComponent {...baseProps} />);
|
|
44
|
+
|
|
45
|
+
expect(resolved().resultCallback).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -8,9 +8,11 @@ import { componentsLogger } from "../../Helpers/logger";
|
|
|
8
8
|
const logger = componentsLogger.addSubsystem("ScreenFeedLoader");
|
|
9
9
|
|
|
10
10
|
/** Loads and provides `feedData` and store to */
|
|
11
|
-
export const ScreenFeedLoader: React.FC<
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
export const ScreenFeedLoader: React.FC<{
|
|
12
|
+
id: string;
|
|
13
|
+
feedData: any;
|
|
14
|
+
children: React.ReactElement | null;
|
|
15
|
+
}> = ({ id, feedData, children }) => {
|
|
14
16
|
const { source: feedUrl, mapping } = feedData;
|
|
15
17
|
|
|
16
18
|
const { data, loading, error } = useFeedLoader({
|
|
@@ -42,9 +42,25 @@ export class ScreenRevealManager {
|
|
|
42
42
|
private subject$ = new Subject<void>();
|
|
43
43
|
private subscription: Subscription;
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
/**
|
|
46
|
+
* @param initialNumberToLoad how many components to wait for before revealing
|
|
47
|
+
* the screen. Staggering exists so a screen full of network-backed components
|
|
48
|
+
* does not load them all at once. A caller that has nothing to stagger - see
|
|
49
|
+
* `disableIncrementalLoading` on the HOC - passes the full count here so the
|
|
50
|
+
* reveal waits for exactly what gets rendered. Omitted, the count is derived
|
|
51
|
+
* from the components themselves.
|
|
52
|
+
*/
|
|
53
|
+
constructor(
|
|
54
|
+
componentsToRender: ZappUIComponent[],
|
|
55
|
+
callback: Callback,
|
|
56
|
+
initialNumberToLoad?: number
|
|
57
|
+
) {
|
|
46
58
|
this.numberOfComponentsWaitToLoadBeforePresent =
|
|
47
|
-
|
|
59
|
+
initialNumberToLoad == null
|
|
60
|
+
? getNumberOfComponentsWaitToLoadBeforePresent(componentsToRender)
|
|
61
|
+
: // Clamped like the computed path: waiting for components that do not
|
|
62
|
+
// exist would hold the screen behind its overlay until the timeout.
|
|
63
|
+
Math.min(initialNumberToLoad, componentsToRender.length);
|
|
48
64
|
|
|
49
65
|
this.renderingState = makeListOf<ComponentLoadingState>(
|
|
50
66
|
COMPONENT_LOADING_STATE.UNKNOWN,
|
|
@@ -22,6 +22,42 @@ import { makeListOf } from "@applicaster/zapp-react-native-utils/arrayUtils";
|
|
|
22
22
|
import { isFirstComponentGallery } from "@applicaster/zapp-react-native-utils/componentsUtils";
|
|
23
23
|
import { withTimeout$ } from "@applicaster/zapp-react-native-utils/idleUtils";
|
|
24
24
|
|
|
25
|
+
describe("ScreenRevealManager explicit component count", () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
(isFirstComponentGallery as jest.Mock).mockReturnValue(false);
|
|
28
|
+
|
|
29
|
+
(makeListOf as jest.Mock).mockImplementation((value, length) =>
|
|
30
|
+
Array(length).fill(value)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
(withTimeout$ as jest.Mock).mockReturnValue(new Subject());
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("waits for the number of components it is given, not the computed default", () => {
|
|
37
|
+
const components = Array(6).fill({}) as any;
|
|
38
|
+
|
|
39
|
+
const manager = new ScreenRevealManager(components, jest.fn(), 6);
|
|
40
|
+
|
|
41
|
+
expect(manager.numberOfComponentsWaitToLoadBeforePresent).toBe(6);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("never waits for more components than it was handed", () => {
|
|
45
|
+
const components = Array(3).fill({}) as any;
|
|
46
|
+
|
|
47
|
+
const manager = new ScreenRevealManager(components, jest.fn(), 10);
|
|
48
|
+
|
|
49
|
+
expect(manager.numberOfComponentsWaitToLoadBeforePresent).toBe(3);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("falls back to the computed default when given no explicit count", () => {
|
|
53
|
+
const components = Array(6).fill({}) as any;
|
|
54
|
+
|
|
55
|
+
const manager = new ScreenRevealManager(components, jest.fn());
|
|
56
|
+
|
|
57
|
+
expect(manager.numberOfComponentsWaitToLoadBeforePresent).toBe(3);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
25
61
|
describe("ScreenRevealManager", () => {
|
|
26
62
|
let mockCallback: jest.Mock;
|
|
27
63
|
let timeout$: Subject<void>;
|
|
@@ -9,6 +9,12 @@ import {
|
|
|
9
9
|
TIMEOUT,
|
|
10
10
|
} from "../withScreenRevealManager";
|
|
11
11
|
|
|
12
|
+
jest.mock("@applicaster/zapp-react-native-utils/theme", () => ({
|
|
13
|
+
useTheme: () => ({ app_background_color: "#000000" }),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
jest.mock("../Overlay", () => ({ Overlay: () => null }));
|
|
17
|
+
|
|
12
18
|
// jest.mock("react-native/Libraries/Animated/NativeAnimatedHelper");
|
|
13
19
|
|
|
14
20
|
const MockComponent = ({
|
|
@@ -37,6 +43,56 @@ const MockComponent = ({
|
|
|
37
43
|
|
|
38
44
|
const WrappedComponent = withScreenRevealManager(MockComponent);
|
|
39
45
|
|
|
46
|
+
describe("withScreenRevealManager disableIncrementalLoading", () => {
|
|
47
|
+
const components = (count: number) =>
|
|
48
|
+
Array(count).fill({ component_type: "grid-qb" });
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
jest.clearAllMocks();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("asks for every component at once when incremental loading is disabled", () => {
|
|
55
|
+
render(
|
|
56
|
+
<WrappedComponent
|
|
57
|
+
componentsToRender={components(6)}
|
|
58
|
+
disableIncrementalLoading
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(screen.getByTestId("mock-component").props.initialNumberToLoad).toBe(
|
|
63
|
+
6
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("still tracks the component count after it changes", () => {
|
|
68
|
+
const { rerender } = render(
|
|
69
|
+
<WrappedComponent
|
|
70
|
+
componentsToRender={components(6)}
|
|
71
|
+
disableIncrementalLoading
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
rerender(
|
|
76
|
+
<WrappedComponent
|
|
77
|
+
componentsToRender={components(9)}
|
|
78
|
+
disableIncrementalLoading
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(screen.getByTestId("mock-component").props.initialNumberToLoad).toBe(
|
|
83
|
+
9
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("loads incrementally when the flag is not set", () => {
|
|
88
|
+
render(<WrappedComponent componentsToRender={components(6)} />);
|
|
89
|
+
|
|
90
|
+
expect(screen.getByTestId("mock-component").props.initialNumberToLoad).toBe(
|
|
91
|
+
3
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
40
96
|
describe.skip("withScreenRevealManager", () => {
|
|
41
97
|
beforeEach(() => {
|
|
42
98
|
jest.clearAllMocks();
|
|
@@ -21,11 +21,23 @@ export const SHOWN = 1; // opacity = 1
|
|
|
21
21
|
type Props = {
|
|
22
22
|
componentsToRender: ZappUIComponent[];
|
|
23
23
|
backgroundColor?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Renders every component at once instead of revealing them a few at a time.
|
|
26
|
+
*
|
|
27
|
+
* For a screen that builds its own components and holds all of them already -
|
|
28
|
+
* a form, say - staggering buys nothing and costs: each rebuild of the list
|
|
29
|
+
* tears it back down to the first few and lets the rest crawl in again.
|
|
30
|
+
*
|
|
31
|
+
* This is a flag rather than a count on purpose. A count would have to be
|
|
32
|
+
* re-read whenever the number of components changes, while "do not stagger"
|
|
33
|
+
* stays true whatever the screen ends up holding.
|
|
34
|
+
*/
|
|
35
|
+
disableIncrementalLoading?: boolean;
|
|
24
36
|
};
|
|
25
37
|
|
|
26
38
|
export const withScreenRevealManager = (Component) => {
|
|
27
39
|
return function WithScreenRevealManager(props: Props) {
|
|
28
|
-
const { componentsToRender } = props;
|
|
40
|
+
const { componentsToRender, disableIncrementalLoading } = props;
|
|
29
41
|
|
|
30
42
|
const [isContentReadyToBeShown, setIsContentReadyToBeShown] =
|
|
31
43
|
React.useState(false);
|
|
@@ -42,7 +54,8 @@ export const withScreenRevealManager = (Component) => {
|
|
|
42
54
|
() =>
|
|
43
55
|
new ScreenRevealManager(
|
|
44
56
|
componentsToRender,
|
|
45
|
-
handleSetIsContentReadyToBeShown
|
|
57
|
+
handleSetIsContentReadyToBeShown,
|
|
58
|
+
disableIncrementalLoading ? componentsToRender.length : undefined
|
|
46
59
|
)
|
|
47
60
|
);
|
|
48
61
|
|
|
@@ -80,7 +93,11 @@ export const withScreenRevealManager = (Component) => {
|
|
|
80
93
|
<Component
|
|
81
94
|
{...props}
|
|
82
95
|
initialNumberToLoad={
|
|
83
|
-
|
|
96
|
+
// Recomputed on every render, so the count follows the components
|
|
97
|
+
// even though the manager settled its own expectation at mount.
|
|
98
|
+
disableIncrementalLoading
|
|
99
|
+
? componentsToRender.length
|
|
100
|
+
: managerRef.current.numberOfComponentsWaitToLoadBeforePresent
|
|
84
101
|
}
|
|
85
102
|
onLoadFinishedFromScreenRevealManager={
|
|
86
103
|
managerRef.current.onLoadFinished
|