@applicaster/zapp-react-native-ui-components 16.0.0-alpha.9225550512 → 16.0.0-alpha.9338540010

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.
Files changed (111) hide show
  1. package/Components/AudioPlayer/tv/__tests__/audioPlayer.test.js +19 -0
  2. package/Components/AudioPlayer/tv/__tests__/index.test.tsx +120 -0
  3. package/Components/AudioPlayer/tv/index.tsx +5 -1
  4. package/Components/Cell/Cell.tsx +3 -6
  5. package/Components/Cell/TvOSCellComponent.tsx +0 -1
  6. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +15 -0
  7. package/Components/GeneralContentScreen/hookAdapter/__tests__/skipExpression.test.ts +294 -0
  8. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +64 -0
  9. package/Components/GeneralContentScreen/hookAdapter/skipExpression.ts +252 -0
  10. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +18 -0
  11. package/Components/LinkHandler/LinkHandler.tsx +2 -7
  12. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +5 -1
  13. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +19 -4
  14. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +43 -1
  15. package/Components/MasterCell/DefaultComponents/Button.tsx +1 -0
  16. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
  17. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
  18. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
  19. package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +2 -4
  20. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +14 -6
  21. package/Components/MasterCell/DefaultComponents/Toggle.tsx +36 -14
  22. package/Components/MasterCell/DefaultComponents/__tests__/Toggle.test.tsx +167 -0
  23. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +1 -1
  24. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +4 -2
  25. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/factory.test.ts +32 -0
  26. package/Components/MasterCell/__tests__/masterCell.test.js +60 -0
  27. package/Components/MasterCell/index.tsx +28 -4
  28. package/Components/MasterCell/utils/__tests__/withoutFocusedState.test.js +20 -0
  29. package/Components/MasterCell/utils/index.ts +18 -0
  30. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +56 -0
  31. package/Components/ModalComponent/ActionsBottomSheet/__tests__/openActionsBottomSheet.test.ts +38 -0
  32. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +24 -0
  33. package/Components/ModalComponent/ActionsBottomSheet/index.ts +9 -0
  34. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +61 -0
  35. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +87 -97
  36. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +87 -126
  37. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +140 -83
  38. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +274 -124
  39. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +205 -0
  40. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +164 -0
  41. package/Components/ModalComponent/AudioPlayer/Components/__tests__/NowPlayingHeaderSection.test.tsx +36 -0
  42. package/Components/ModalComponent/AudioPlayer/Components/index.ts +3 -3
  43. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +433 -0
  44. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +563 -0
  45. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  46. package/Components/ModalComponent/BottomSheetModalContent.tsx +440 -66
  47. package/Components/ModalComponent/Button/index.tsx +5 -3
  48. package/Components/ModalComponent/Header/CloseButton.tsx +12 -0
  49. package/Components/ModalComponent/Header/__tests__/CloseButton.test.tsx +72 -0
  50. package/Components/ModalComponent/Header/index.tsx +20 -7
  51. package/Components/ModalComponent/Header/utils.ts +1 -1
  52. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  53. package/Components/ModalComponent/SortableList.tsx +156 -0
  54. package/Components/ModalComponent/SortableList.web.tsx +34 -0
  55. package/Components/ModalComponent/TextInputContent.tsx +135 -0
  56. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  57. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +343 -0
  58. package/Components/ModalComponent/__tests__/SortableList.web.test.tsx +16 -0
  59. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +169 -0
  60. package/Components/ModalComponent/__tests__/showTextInput.test.ts +120 -0
  61. package/Components/ModalComponent/index.tsx +18 -9
  62. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +90 -0
  63. package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
  64. package/Components/ModalComponent/presets/index.ts +27 -0
  65. package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
  66. package/Components/ModalComponent/presets/standardCell.tsx +41 -0
  67. package/Components/ModalComponent/presets/types.ts +35 -0
  68. package/Components/ModalComponent/showTextInput.ts +54 -0
  69. package/Components/ModalComponent/utils.ts +82 -21
  70. package/Components/OfflineHandler/__tests__/index.test.tsx +109 -0
  71. package/Components/OfflineHandler/hooks.ts +0 -12
  72. package/Components/OfflineHandler/index.tsx +42 -4
  73. package/Components/PlayerContainer/PlayerContainer.tsx +19 -8
  74. package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
  75. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +66 -13
  76. package/Components/River/ComponentsMap/ComponentsMap.tsx +89 -60
  77. package/Components/River/River.tsx +4 -0
  78. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +140 -75
  79. package/Components/River/__tests__/componentsMap.test.js +8 -3
  80. package/Components/River/__tests__/resultCallback.test.tsx +47 -0
  81. package/Components/Screen/__tests__/Screen.test.tsx +24 -0
  82. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +2 -0
  83. package/Components/Screen/index.tsx +5 -0
  84. package/Components/Screen/navigationHandler.ts +8 -8
  85. package/Components/ScreenRevealManager/ScreenRevealManager.ts +18 -2
  86. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +36 -0
  87. package/Components/ScreenRevealManager/__tests__/withScreenRevealManager.test.tsx +56 -0
  88. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +21 -4
  89. package/Components/Transitioner/AnimationManager.js +1 -1
  90. package/Components/Transitioner/Scene.tsx +4 -3
  91. package/Components/Transitioner/Transitioner.tsx +71 -33
  92. package/Components/Transitioner/__tests__/transitioner.test.js +372 -3
  93. package/Components/VideoLive/LiveImageManager.ts +9 -3
  94. package/Components/VideoLive/__tests__/LiveImageManager.test.ts +59 -0
  95. package/Components/VideoModal/PlayerWrapper.tsx +4 -1
  96. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +1 -0
  97. package/Components/VideoModal/__tests__/playerWrapperStyle.test.ts +58 -0
  98. package/Components/VideoModal/__tests__/playerWrapperUtils.test.ts +16 -0
  99. package/Components/VideoModal/playerWrapperStyle.ts +21 -3
  100. package/Components/VideoModal/playerWrapperUtils.ts +7 -1
  101. package/Contexts/ModalNavigationContext/__tests__/index.test.tsx +15 -0
  102. package/Contexts/ModalNavigationContext/index.tsx +2 -2
  103. package/Decorators/ZappPipesDataConnector/__tests__/screenToolbox.test.ts +347 -0
  104. package/Decorators/ZappPipesDataConnector/__tests__/screenToolboxUnavailable.test.ts +53 -0
  105. package/Decorators/ZappPipesDataConnector/index.tsx +19 -4
  106. package/Decorators/ZappPipesDataConnector/screenToolbox.ts +384 -0
  107. package/Hooks/useEntryActionsExpander.ts +63 -0
  108. package/assets/empty-Info.png +0 -0
  109. package/package.json +6 -5
  110. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +0 -332
  111. /package/Decorators/ZappPipesDataConnector/__tests__/{Hero.js → Hero.tsx} +0 -0
@@ -29,12 +29,22 @@ export const OfflineHandler = ({ children }: Props) => {
29
29
  const { online } = parseConnectionInfo(connectionInfo);
30
30
  const prevOnline = usePrevious(online);
31
31
 
32
+ const stillOfflineAfterResumeCount =
33
+ connectionInfo?.stillOfflineAfterResumeCount ?? 0;
34
+
35
+ const prevStillOfflineAfterResumeCount = usePrevious(
36
+ stillOfflineAfterResumeCount
37
+ );
38
+
32
39
  const emitShowToast = useSubscriberFor(SHOW_TOAST_EVENT);
33
40
 
34
41
  const { message, extraMessage, style } = useOfflineHandlerProps(online);
35
42
 
36
43
  const showOnlineToast = useCallback(() => {
37
- logger.log("Device is back online, emit toast");
44
+ logger.info({
45
+ message: "OfflineHandler: device is back online, emit toast",
46
+ data: { online, prevOnline },
47
+ });
38
48
 
39
49
  emitShowToast({
40
50
  id: OFFLINE_STATUS_TOAST_ID,
@@ -43,10 +53,13 @@ export const OfflineHandler = ({ children }: Props) => {
43
53
  style,
44
54
  timeout: timers.goingOnline,
45
55
  });
46
- }, [emitShowToast, message, extraMessage, style]);
56
+ }, [emitShowToast, message, extraMessage, style, online, prevOnline]);
47
57
 
48
58
  const showOfflineToast = useCallback(() => {
49
- logger.log("Device went offline, emit toast");
59
+ logger.info({
60
+ message: "OfflineHandler: device is offline, emit toast",
61
+ data: { online, prevOnline, stillOfflineAfterResumeCount },
62
+ });
50
63
 
51
64
  emitShowToast({
52
65
  id: OFFLINE_STATUS_TOAST_ID,
@@ -55,7 +68,15 @@ export const OfflineHandler = ({ children }: Props) => {
55
68
  style,
56
69
  timeout: timers.goingOffline,
57
70
  });
58
- }, [emitShowToast, message, extraMessage, style]);
71
+ }, [
72
+ emitShowToast,
73
+ message,
74
+ extraMessage,
75
+ style,
76
+ online,
77
+ prevOnline,
78
+ stillOfflineAfterResumeCount,
79
+ ]);
59
80
 
60
81
  useEffect(() => {
61
82
  // On mount there is no previous state yet: only surface the offline toast
@@ -79,5 +100,22 @@ export const OfflineHandler = ({ children }: Props) => {
79
100
  }
80
101
  }, [online, prevOnline, showOfflineToast, showOnlineToast]);
81
102
 
103
+ useEffect(() => {
104
+ // NetworkStatusProvider bumps this counter when the app resumed while offline
105
+ // and is still offline after the verification delay: remind the user.
106
+ if (
107
+ prevStillOfflineAfterResumeCount !== undefined &&
108
+ stillOfflineAfterResumeCount > prevStillOfflineAfterResumeCount &&
109
+ !online
110
+ ) {
111
+ showOfflineToast();
112
+ }
113
+ }, [
114
+ stillOfflineAfterResumeCount,
115
+ prevStillOfflineAfterResumeCount,
116
+ online,
117
+ showOfflineToast,
118
+ ]);
119
+
82
120
  return children;
83
121
  };
@@ -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
- const { title, summary } = item || {};
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) {
@@ -400,10 +404,17 @@ const PlayerContainerComponent = (props: Props) => {
400
404
  const { eventType } = event;
401
405
 
402
406
  if (!isLanguageOverlayVisible && eventType === "menu") {
407
+ // First Back dismisses Play Next, the next one closes the player
408
+ if (playNextOverlayState) {
409
+ playNextOverlayState.handleUserCancelPlayNext();
410
+
411
+ return;
412
+ }
413
+
403
414
  close();
404
415
  }
405
416
  },
406
- [close]
417
+ [close, playNextOverlayState]
407
418
  );
408
419
 
409
420
  // Effects
@@ -495,7 +506,7 @@ const PlayerContainerComponent = (props: Props) => {
495
506
  const isAudioContent = isAudioItem(item);
496
507
 
497
508
  useEffect(() => {
498
- if (!isAudioContent) {
509
+ if (!isAudioContent && isApplePlatform()) {
499
510
  sendQuickBrickEvent(
500
511
  QUICK_BRICK_EVENTS.IDLE_TIMER_DISABLED as QuickBrickEvent,
501
512
  {
@@ -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: jest.fn(() => false),
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
- isAudioItem: jest.fn(() => false),
28
- isInlineTV: jest.fn(() => false),
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: ReactActual.createContext({
147
- isLanguageOverlayVisible: false,
148
- bottomFocusableId: "bottom",
149
- showComponentsContainer: false,
150
- refs: null,
151
- }),
152
- PlayerContainerContextProvider: ({ children }) => 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 Player = React.forwardRef(() => null);
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,6 @@
1
1
  import * as React from "react";
2
- import * as R from "ramda";
3
2
  import { FlatList, StyleSheet, View } from "react-native";
3
+ import { SafeAreaView, EdgeMode, Edge } from "react-native-safe-area-context";
4
4
  import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
5
5
  import { RiverItem } from "../RiverItem";
6
6
  import { RiverFooter } from "../RiverFooter";
@@ -40,6 +40,12 @@ type Props = {
40
40
  riverId?: string;
41
41
  getStaticComponentFeed: any;
42
42
  stickyHeaderIndices?: number[];
43
+ safeArea?:
44
+ | {
45
+ edges: Record<Edge, EdgeMode>;
46
+ mode: "padding" | "margin" | undefined;
47
+ }
48
+ | undefined;
43
49
  };
44
50
 
45
51
  const styles = StyleSheet.create({
@@ -52,7 +58,7 @@ const getNearestValue = (value, optionA, optionB) =>
52
58
  Math.abs(value - optionA) < Math.abs(value - optionB) ? optionA : optionB;
53
59
 
54
60
  const scrollIndicatorInsets = { right: 1 };
55
- const keyExtractor = R.compose(String, R.prop("id"));
61
+ const keyExtractor = (item) => item.id.toString();
56
62
 
57
63
  function ComponentsMapComponent(props: Props) {
58
64
  const {
@@ -64,6 +70,7 @@ function ComponentsMapComponent(props: Props) {
64
70
  riverId,
65
71
  getStaticComponentFeed,
66
72
  stickyHeaderIndices,
73
+ safeArea,
67
74
  } = props;
68
75
 
69
76
  const flatListRef = React.useRef<FlatList | null>(null);
@@ -77,7 +84,7 @@ function ComponentsMapComponent(props: Props) {
77
84
 
78
85
  const riverComponents = React.useMemo(() => {
79
86
  if (feed?.entry?.length < components.length) {
80
- return R.slice(0, feed?.entry?.length, components);
87
+ return components.slice(0, feed?.entry?.length);
81
88
  }
82
89
 
83
90
  return components;
@@ -92,7 +99,7 @@ function ComponentsMapComponent(props: Props) {
92
99
  const { loadingState, onLoadFinished, onLoadFailed, shouldShowLoadingError } =
93
100
  useLoadingState(riverComponents.length, onLoadDone);
94
101
 
95
- const theme = useTheme();
102
+ const theme = useTheme<BaseThemePropertiesMobile>();
96
103
 
97
104
  const renderRiverItem = React.useCallback(
98
105
  ({ item, index }) => {
@@ -111,7 +118,10 @@ function ComponentsMapComponent(props: Props) {
111
118
  };
112
119
 
113
120
  return (
114
- <ScreenLoadingMeasurementsListItemWrapper index={index}>
121
+ <ScreenLoadingMeasurementsListItemWrapper
122
+ index={index}
123
+ testID={`component-${item.id}`}
124
+ >
115
125
  <RiverItem {...riverItemProps} />
116
126
  </ScreenLoadingMeasurementsListItemWrapper>
117
127
  );
@@ -121,21 +131,13 @@ function ComponentsMapComponent(props: Props) {
121
131
 
122
132
  const screenStyle = React.useMemo(
123
133
  () => ({
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
134
  paddingLeft: ifEmptyUseFallback(
133
135
  screenConfig.paddingLeft,
134
- R.prop("screen_padding_left")(theme)
136
+ theme.screen_padding_left
135
137
  ),
136
138
  paddingRight: ifEmptyUseFallback(
137
139
  screenConfig.paddingRight,
138
- R.prop("screen_padding_right")(theme)
140
+ theme.screen_padding_right
139
141
  ),
140
142
  }),
141
143
  [riverId]
@@ -266,51 +268,79 @@ function ComponentsMapComponent(props: Props) {
266
268
  numberOfComponents={riverComponents.length}
267
269
  >
268
270
  <ViewportTracker>
269
- <FlatList
270
- testID="components-map-flat-list"
271
- ref={(ref) => {
272
- flatListRef.current = ref;
273
- }}
274
- // Fix for WebView rerender crashes on Android API 28+
275
- // https://github.com/react-native-webview/react-native-webview/issues/1915#issuecomment-964035468
276
- overScrollMode={isAndroid ? "never" : "auto"}
277
- scrollIndicatorInsets={scrollIndicatorInsets}
278
- extraData={feed}
279
- stickyHeaderIndices={stickyHeaderIndices}
280
- removeClippedSubviews={isAndroid}
281
- onLayout={handleOnLayout}
282
- initialNumToRender={3}
283
- maxToRenderPerBatch={10}
284
- windowSize={12}
285
- keyExtractor={keyExtractor}
286
- renderItem={renderRiverItem}
287
- data={riverComponents}
288
- contentContainerStyle={contentContainerStyle}
289
- ListFooterComponent={
290
- <RiverFooter
291
- flatListHeight={flatListHeight}
292
- loadingState={loadingState}
293
- />
294
- }
295
- refreshControl={refreshControl}
296
- onScrollBeginDrag={onScrollBeginDrag}
297
- onScroll={onScroll}
298
- onMomentumScrollEnd={_onMomentumScrollEnd}
299
- onScrollEndDrag={_onScrollEndDrag}
300
- scrollEventThrottle={16}
301
- {...scrollViewExtraProps}
302
- onEndReached={
303
- /* When wrapped in a parent ScrollView (e.g. tabs),
271
+ <>
272
+ <FlatList
273
+ ListHeaderComponent={
274
+ <SafeAreaView
275
+ mode={safeArea?.mode ?? "margin"}
276
+ style={{
277
+ paddingTop: ifEmptyUseFallback(
278
+ screenConfig.paddingTop,
279
+ theme.screen_padding_top
280
+ ),
281
+ height: StyleSheet.hairlineWidth,
282
+ }}
283
+ edges={{ top: safeArea?.edges?.top ?? "off" }}
284
+ />
285
+ }
286
+ testID="components-map-flat-list"
287
+ ref={(ref) => {
288
+ flatListRef.current = ref;
289
+ }}
290
+ // Fix for WebView rerender crashes on Android API 28+
291
+ // https://github.com/react-native-webview/react-native-webview/issues/1915#issuecomment-964035468
292
+ overScrollMode={isAndroid ? "never" : "auto"}
293
+ scrollIndicatorInsets={scrollIndicatorInsets}
294
+ extraData={feed}
295
+ stickyHeaderIndices={stickyHeaderIndices}
296
+ removeClippedSubviews={isAndroid}
297
+ onLayout={handleOnLayout}
298
+ initialNumToRender={3}
299
+ maxToRenderPerBatch={10}
300
+ windowSize={12}
301
+ keyExtractor={keyExtractor}
302
+ renderItem={renderRiverItem}
303
+ data={riverComponents}
304
+ contentContainerStyle={contentContainerStyle}
305
+ ListFooterComponent={
306
+ <>
307
+ <RiverFooter
308
+ flatListHeight={flatListHeight}
309
+ loadingState={loadingState}
310
+ />
311
+ <SafeAreaView
312
+ mode={safeArea?.mode ?? "margin"}
313
+ style={{
314
+ paddingBottom: ifEmptyUseFallback(
315
+ screenConfig.paddingBottom,
316
+ theme.screen_padding_bottom
317
+ ),
318
+ height: StyleSheet.hairlineWidth,
319
+ }}
320
+ edges={{ bottom: safeArea?.edges?.bottom ?? "off" }}
321
+ />
322
+ </>
323
+ }
324
+ refreshControl={refreshControl}
325
+ onScrollBeginDrag={onScrollBeginDrag}
326
+ onScroll={onScroll}
327
+ onMomentumScrollEnd={_onMomentumScrollEnd}
328
+ onScrollEndDrag={_onScrollEndDrag}
329
+ scrollEventThrottle={16}
330
+ {...scrollViewExtraProps}
331
+ onEndReached={
332
+ /* When wrapped in a parent ScrollView (e.g. tabs),
304
333
  this FlatList doesn't scroll so onEndReached can fire repeatedly;
305
334
  skip it here and let the parent ScrollView emit scroll-end instead. */
306
- isScreenWrappedInContainer
307
- ? undefined
308
- : () => {
309
- if (!hasUserScrolledRef.current) return;
310
- emitScrollEndReached();
311
- }
312
- }
313
- />
335
+ isScreenWrappedInContainer
336
+ ? undefined
337
+ : () => {
338
+ if (!hasUserScrolledRef.current) return;
339
+ emitScrollEndReached();
340
+ }
341
+ }
342
+ />
343
+ </>
314
344
  </ViewportTracker>
315
345
  </ScreenLoadingMeasurements>
316
346
  </ComponentsMapRefContext.Provider>
@@ -320,6 +350,5 @@ function ComponentsMapComponent(props: Props) {
320
350
  }
321
351
 
322
352
  export const ComponentsMap = React.memo(
323
- withComponentsMapProvider(ComponentsMapComponent),
324
- R.equals
353
+ withComponentsMapProvider(ComponentsMapComponent)
325
354
  );
@@ -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
  }