@applicaster/zapp-react-native-ui-components 16.0.0-rc.86 → 16.0.0-rc.87

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 (22) hide show
  1. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +15 -0
  2. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +1 -23
  3. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +1 -36
  4. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +1 -26
  5. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +30 -25
  6. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +7 -2
  7. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +48 -1
  8. package/Components/ModalComponent/AudioPlayer/Components/index.ts +0 -2
  9. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +18 -0
  10. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +54 -0
  11. package/Components/ModalComponent/BottomSheetModalContent.tsx +92 -51
  12. package/Components/ModalComponent/SortableList.tsx +38 -23
  13. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +17 -0
  14. package/Components/ModalComponent/index.tsx +7 -9
  15. package/Components/River/ComponentsMap/ComponentsMap.tsx +85 -59
  16. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +138 -75
  17. package/Components/River/__tests__/componentsMap.test.js +8 -3
  18. package/Components/Screen/navigationHandler.ts +8 -8
  19. package/Contexts/ModalNavigationContext/__tests__/index.test.tsx +15 -0
  20. package/Contexts/ModalNavigationContext/index.tsx +2 -2
  21. package/package.json +5 -5
  22. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +0 -314
@@ -1,4 +1,11 @@
1
- import React, { useCallback, useEffect, useMemo, useRef } from "react";
1
+ import React, {
2
+ memo,
3
+ useCallback,
4
+ useDeferredValue,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ } from "react";
2
9
  import {
3
10
  ActivityIndicator,
4
11
  ScrollView,
@@ -48,6 +55,10 @@ const styles = StyleSheet.create({
48
55
  fontSize: 14,
49
56
  textAlign: "center",
50
57
  },
58
+ scrollView: {
59
+ flexShrink: 1,
60
+ },
61
+ someStyles: { paddingHorizontal: 20, paddingTop: 8, paddingBottom: 12 },
51
62
  });
52
63
 
53
64
  type ModalComponentProps = {
@@ -81,9 +92,10 @@ type ModalComponentProps = {
81
92
  contentComponent?: React.ComponentType<any>;
82
93
  contentComponentProps?: Record<string, any>;
83
94
  styleOverrides?: ModalBlocksConfig | Record<string, any>;
95
+ measurementPhase?: boolean;
84
96
  };
85
97
 
86
- export function BottomSheetModalContent(props: ModalComponentProps) {
98
+ function _BottomSheetModalContent(props: ModalComponentProps) {
87
99
  const {
88
100
  items,
89
101
  currentRoute,
@@ -103,13 +115,21 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
103
115
  contentComponent: ContentComponent,
104
116
  contentComponentProps = {},
105
117
  styleOverrides: propStyleOverrides,
118
+ measurementPhase = false,
106
119
  } = props;
107
120
 
108
121
  const route = useRef(currentRoute);
109
122
  const theme = useTheme<BaseThemePropertiesMobile>();
123
+ const bottomInset = useSafeAreaInsets().bottom;
110
124
  const paddingTop = Number(theme.modal_bottom_sheet_padding_top);
111
125
  const paddingBottom = Number(theme.modal_bottom_sheet_padding_bottom);
112
126
 
127
+ const calculatedcontentContainerStyle = useMemo(() => {
128
+ return {
129
+ paddingBottom: paddingBottom + bottomInset,
130
+ };
131
+ }, [paddingBottom, bottomInset]);
132
+
113
133
  const {
114
134
  items: fetchedItems,
115
135
  showCentralLoading,
@@ -159,11 +179,28 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
159
179
  const createPlaylistTitle =
160
180
  pluginLocalizations?.create_new_playlist_title || "Create New Playlist";
161
181
 
182
+ const nextUpTitle =
183
+ pluginLocalizations?.next_up_title ||
184
+ (normalizedPluginId
185
+ ? pluginLocalizations?.[`${normalizedPluginId}_next_up_title`]
186
+ : undefined) ||
187
+ "Next Up";
188
+
189
+ const nowPlayingTitle =
190
+ pluginLocalizations?.now_playing_title ||
191
+ (normalizedPluginId
192
+ ? pluginLocalizations?.[`${normalizedPluginId}_now_playing_title`]
193
+ : undefined) ||
194
+ "Now Playing";
195
+
162
196
  const resolvedTitle = props.title || localizedHeaderTitle || title;
163
197
 
164
198
  const itemsToRender = viewModel ? fetchedItems : items;
165
199
  const operations = editableCollection?.operations || [];
166
- const isSortable = hasOperation(operations, OPERATIONS.REORDER);
200
+
201
+ const isSortable = useDeferredValue(
202
+ !measurementPhase && hasOperation(operations, OPERATIONS.REORDER)
203
+ );
167
204
 
168
205
  const cell = getCell(
169
206
  viewModel ? role : undefined,
@@ -264,7 +301,17 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
264
301
  ]
265
302
  );
266
303
 
267
- const bottomInset = useSafeAreaInsets().bottom;
304
+ const sortableListRenderItem = useCallback(
305
+ ({ item, index, renderHandle }) =>
306
+ cell.renderItem({
307
+ item,
308
+ index,
309
+ onPress: onItemPress,
310
+ context: renderContext,
311
+ renderHandle,
312
+ }),
313
+ [cell, onItemPress, renderContext]
314
+ );
268
315
 
269
316
  const hasNowPlaying = hasOperation(operations, OPERATIONS.NOW_PLAYING);
270
317
  const hasClearAll = hasOperation(operations, OPERATIONS.CLEAR_ALL);
@@ -291,6 +338,8 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
291
338
  component: () => (
292
339
  <NowPlayingHeaderSection
293
340
  onClear={hasClearAll ? handleDeleteCollection : undefined}
341
+ nextUpTitle={nextUpTitle}
342
+ nowPlayingTitle={nowPlayingTitle}
294
343
  />
295
344
  ),
296
345
  });
@@ -318,11 +367,32 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
318
367
  hasNowPlaying,
319
368
  hasClearAll,
320
369
  handleDeleteCollection,
370
+ nextUpTitle,
371
+ nowPlayingTitle,
321
372
  hasAdd,
322
373
  handleCreatePlaylist,
374
+ createPlaylistTitle,
323
375
  ]);
324
376
 
325
- const renderBody = () => {
377
+ const _items = useMemo(() => {
378
+ return (itemsToRender || []).map((item, index) =>
379
+ cell.renderItem({
380
+ item,
381
+ index,
382
+ onPress: onItemPress,
383
+ context: renderContext,
384
+ })
385
+ );
386
+ }, [itemsToRender, cell, onItemPress, renderContext]);
387
+
388
+ const _styles = useMemo(() => {
389
+ return {
390
+ maxHeight: props.maxHeight,
391
+ paddingTop: paddingTop,
392
+ };
393
+ }, [props.maxHeight, paddingTop]);
394
+
395
+ const renderedBody = (() => {
326
396
  if (ContentComponent) {
327
397
  const shouldWrapInScrollView =
328
398
  !contentComponentProps?.disableScrollViewWrap &&
@@ -331,9 +401,8 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
331
401
  if (shouldWrapInScrollView) {
332
402
  return (
333
403
  <ScrollView
334
- contentContainerStyle={{
335
- paddingBottom: paddingBottom + bottomInset,
336
- }}
404
+ keyboardShouldPersistTaps="handled"
405
+ contentContainerStyle={calculatedcontentContainerStyle}
337
406
  >
338
407
  <ContentComponent
339
408
  {...contentComponentProps}
@@ -346,12 +415,7 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
346
415
  }
347
416
 
348
417
  return (
349
- <View
350
- style={{
351
- flexShrink: 1,
352
- paddingBottom: paddingBottom + bottomInset,
353
- }}
354
- >
418
+ <View style={[styles.scrollView, calculatedcontentContainerStyle]}>
355
419
  <ContentComponent
356
420
  {...contentComponentProps}
357
421
  dismiss={dismiss}
@@ -395,43 +459,30 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
395
459
  <SortableList
396
460
  sortableData={itemsToRender}
397
461
  headers={headers}
398
- scrollViewStyle={{
399
- flexShrink: 1,
400
- }}
401
- contentContainerStyle={{
402
- paddingBottom: paddingBottom + bottomInset,
403
- }}
462
+ scrollViewStyle={styles.scrollView}
463
+ contentContainerStyle={calculatedcontentContainerStyle}
404
464
  onDragEnd={handleDragEnd}
405
- renderItem={({ item, index, renderHandle }) =>
406
- cell.renderItem({
407
- item,
408
- index,
409
- onPress: onItemPress,
410
- context: renderContext,
411
- renderHandle,
412
- })
413
- }
465
+ renderItem={sortableListRenderItem}
414
466
  />
415
467
  );
416
468
  }
417
469
 
418
470
  return (
419
471
  <ScrollView
472
+ keyboardShouldPersistTaps="handled"
420
473
  overScrollMode="never"
421
474
  bounces={false}
422
- contentContainerStyle={{
423
- paddingBottom: paddingBottom + bottomInset,
424
- }}
475
+ contentContainerStyle={calculatedcontentContainerStyle}
425
476
  >
426
477
  {hasNowPlaying ? (
427
478
  <NowPlayingHeaderSection
428
479
  onClear={hasClearAll ? handleDeleteCollection : undefined}
480
+ nextUpTitle={nextUpTitle}
481
+ nowPlayingTitle={nowPlayingTitle}
429
482
  />
430
483
  ) : null}
431
484
  {hasAdd ? (
432
- <View
433
- style={{ paddingHorizontal: 20, paddingTop: 8, paddingBottom: 12 }}
434
- >
485
+ <View style={styles.someStyles}>
435
486
  <AudioPlayerButton
436
487
  configuration={VARIANT_WITH_ICON}
437
488
  data={{ title: createPlaylistTitle }}
@@ -439,26 +490,14 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
439
490
  />
440
491
  </View>
441
492
  ) : null}
442
- {(itemsToRender || []).map((item, index) =>
443
- cell.renderItem({
444
- item,
445
- index,
446
- onPress: onItemPress,
447
- context: renderContext,
448
- })
449
- )}
493
+ {_items}
450
494
  </ScrollView>
451
495
  );
452
- };
496
+ })();
453
497
 
454
498
  return (
455
499
  <ModalBlocksStyleContext.Provider value={styleOverrides as any}>
456
- <View
457
- style={{
458
- maxHeight: props.maxHeight,
459
- paddingTop: paddingTop,
460
- }}
461
- >
500
+ <View style={_styles}>
462
501
  <BottomSheetDragArea>
463
502
  <Header
464
503
  dismiss={dismiss}
@@ -467,8 +506,10 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
467
506
  title={resolvedTitle}
468
507
  />
469
508
  </BottomSheetDragArea>
470
- {renderBody()}
509
+ {renderedBody}
471
510
  </View>
472
511
  </ModalBlocksStyleContext.Provider>
473
512
  );
474
513
  }
514
+
515
+ export const BottomSheetModalContent = memo(_BottomSheetModalContent);
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useState } from "react";
1
+ import React, { useCallback, useEffect, useMemo, useState } from "react";
2
2
  import Sortable from "react-native-sortables";
3
3
  import { Text, View, ViewStyle } from "react-native";
4
4
  import Animated, { useAnimatedRef } from "react-native-reanimated";
@@ -65,6 +65,9 @@ export type SortableListProps = {
65
65
  }) => void;
66
66
  };
67
67
 
68
+ const defaultKeyExtractor = (item, index) =>
69
+ `sortable-item-${item?.id ?? item?.title ?? index}`;
70
+
68
71
  export const SortableList = ({
69
72
  scrollViewStyle,
70
73
  contentContainerStyle,
@@ -73,8 +76,7 @@ export const SortableList = ({
73
76
  columns = 1,
74
77
  rowGap = 0,
75
78
  columnGap = 0,
76
- keyExtractor = (item, index) =>
77
- `sortable-item-${item?.id ?? item?.title ?? index}`,
79
+ keyExtractor = defaultKeyExtractor,
78
80
  renderItem,
79
81
  sortableData = DEFAULT_SORTABLE_DATA,
80
82
  headers = DEFAULT_HEADERS,
@@ -85,7 +87,9 @@ export const SortableList = ({
85
87
  const [localData, setLocalData] = useState(sortableData);
86
88
 
87
89
  useEffect(() => {
88
- setLocalData(sortableData);
90
+ setLocalData((localData) =>
91
+ localData !== sortableData ? sortableData : localData
92
+ );
89
93
  }, [sortableData]);
90
94
 
91
95
  const handleDragEnd = useCallback(
@@ -101,13 +105,24 @@ export const SortableList = ({
101
105
  [onDragEnd]
102
106
  );
103
107
 
104
- return (
105
- <Animated.ScrollView
106
- ref={scrollableRef}
107
- style={scrollViewStyle}
108
- contentContainerStyle={contentContainerStyle}
109
- >
110
- {(headers || []).map((header, idx) => (
108
+ const _renderItem = useCallback(
109
+ ({ item, index }) => (
110
+ <View key={keyExtractor(item, index)} style={itemStyle}>
111
+ {renderItem({
112
+ item,
113
+ index,
114
+ renderHandle: (children) => (
115
+ <Sortable.Handle>{children}</Sortable.Handle>
116
+ ),
117
+ })}
118
+ </View>
119
+ ),
120
+ [itemStyle, keyExtractor, renderItem]
121
+ );
122
+
123
+ const renderHeader = useMemo(
124
+ () =>
125
+ (headers || []).map((header, idx) => (
111
126
  <View key={`static-header-${idx}`} style={headerStyle}>
112
127
  {header.title ? (
113
128
  <SortableListHeader title={header.title} />
@@ -115,7 +130,17 @@ export const SortableList = ({
115
130
  renderHeaderComponent(header.component)
116
131
  )}
117
132
  </View>
118
- ))}
133
+ )),
134
+ [headers, headerStyle]
135
+ );
136
+
137
+ return (
138
+ <Animated.ScrollView
139
+ ref={scrollableRef}
140
+ style={scrollViewStyle}
141
+ contentContainerStyle={contentContainerStyle}
142
+ >
143
+ {renderHeader}
119
144
  <Sortable.Grid
120
145
  scrollableRef={scrollableRef}
121
146
  customHandle
@@ -125,17 +150,7 @@ export const SortableList = ({
125
150
  data={localData}
126
151
  keyExtractor={keyExtractor}
127
152
  onDragEnd={handleDragEnd}
128
- renderItem={({ item, index }) => (
129
- <View key={keyExtractor(item, index)} style={itemStyle}>
130
- {renderItem({
131
- item,
132
- index,
133
- renderHandle: (children) => (
134
- <Sortable.Handle>{children}</Sortable.Handle>
135
- ),
136
- })}
137
- </View>
138
- )}
153
+ renderItem={_renderItem}
139
154
  />
140
155
  </Animated.ScrollView>
141
156
  );
@@ -235,4 +235,21 @@ describe("BottomSheetModalContent role/behavior cells", () => {
235
235
  const { ScrollView } = require("react-native");
236
236
  expect(() => UNSAFE_getByType(ScrollView)).toThrow();
237
237
  });
238
+
239
+ it("sets keyboardShouldPersistTaps='handled' on ScrollView wrapping contentComponent", () => {
240
+ const CustomComponent = () => (
241
+ <React.Fragment>Custom Content</React.Fragment>
242
+ );
243
+
244
+ const { UNSAFE_getByType } = render(
245
+ <BottomSheetModalContent
246
+ {...baseProps}
247
+ contentComponent={CustomComponent}
248
+ />
249
+ );
250
+
251
+ const { ScrollView } = require("react-native");
252
+ const scrollView = UNSAFE_getByType(ScrollView);
253
+ expect(scrollView.props.keyboardShouldPersistTaps).toBe("handled");
254
+ });
238
255
  });
@@ -26,20 +26,18 @@ type Props = {
26
26
  export function ModalComponent(props: ModalBottomSheetContentProps & Props) {
27
27
  const [height, setHeight] = useState(0);
28
28
 
29
- const onLayout = useCallback(
30
- ({ nativeEvent }) => {
31
- if (height === 0) {
32
- setHeight(nativeEvent.layout.height);
33
- }
34
- },
35
- [height]
36
- );
29
+ const onLayout = useCallback(({ nativeEvent }) => {
30
+ setHeight((value) => (value === 0 ? nativeEvent.layout.height : value));
31
+ }, []);
37
32
 
38
33
  if (height === 0) {
39
34
  return (
40
35
  <MeasurementsPortal
41
36
  Component={BottomSheetModalContent}
42
- componentProps={props}
37
+ componentProps={{
38
+ ...props,
39
+ measurementPhase: true,
40
+ }}
43
41
  onLayout={onLayout}
44
42
  />
45
43
  );
@@ -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 }) => {
@@ -121,21 +128,13 @@ function ComponentsMapComponent(props: Props) {
121
128
 
122
129
  const screenStyle = React.useMemo(
123
130
  () => ({
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
131
  paddingLeft: ifEmptyUseFallback(
133
132
  screenConfig.paddingLeft,
134
- R.prop("screen_padding_left")(theme)
133
+ theme.screen_padding_left
135
134
  ),
136
135
  paddingRight: ifEmptyUseFallback(
137
136
  screenConfig.paddingRight,
138
- R.prop("screen_padding_right")(theme)
137
+ theme.screen_padding_right
139
138
  ),
140
139
  }),
141
140
  [riverId]
@@ -266,51 +265,79 @@ function ComponentsMapComponent(props: Props) {
266
265
  numberOfComponents={riverComponents.length}
267
266
  >
268
267
  <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),
268
+ <>
269
+ <FlatList
270
+ ListHeaderComponent={
271
+ <SafeAreaView
272
+ mode={safeArea?.mode ?? "margin"}
273
+ style={{
274
+ paddingTop: ifEmptyUseFallback(
275
+ screenConfig.paddingTop,
276
+ theme.screen_padding_top
277
+ ),
278
+ height: StyleSheet.hairlineWidth,
279
+ }}
280
+ edges={{ top: safeArea?.edges?.top ?? "off" }}
281
+ />
282
+ }
283
+ testID="components-map-flat-list"
284
+ ref={(ref) => {
285
+ flatListRef.current = ref;
286
+ }}
287
+ // Fix for WebView rerender crashes on Android API 28+
288
+ // https://github.com/react-native-webview/react-native-webview/issues/1915#issuecomment-964035468
289
+ overScrollMode={isAndroid ? "never" : "auto"}
290
+ scrollIndicatorInsets={scrollIndicatorInsets}
291
+ extraData={feed}
292
+ stickyHeaderIndices={stickyHeaderIndices}
293
+ removeClippedSubviews={isAndroid}
294
+ onLayout={handleOnLayout}
295
+ initialNumToRender={3}
296
+ maxToRenderPerBatch={10}
297
+ windowSize={12}
298
+ keyExtractor={keyExtractor}
299
+ renderItem={renderRiverItem}
300
+ data={riverComponents}
301
+ contentContainerStyle={contentContainerStyle}
302
+ ListFooterComponent={
303
+ <>
304
+ <RiverFooter
305
+ flatListHeight={flatListHeight}
306
+ loadingState={loadingState}
307
+ />
308
+ <SafeAreaView
309
+ mode={safeArea?.mode ?? "margin"}
310
+ style={{
311
+ paddingBottom: ifEmptyUseFallback(
312
+ screenConfig.paddingBottom,
313
+ theme.screen_padding_bottom
314
+ ),
315
+ height: StyleSheet.hairlineWidth,
316
+ }}
317
+ edges={{ bottom: safeArea?.edges?.bottom ?? "off" }}
318
+ />
319
+ </>
320
+ }
321
+ refreshControl={refreshControl}
322
+ onScrollBeginDrag={onScrollBeginDrag}
323
+ onScroll={onScroll}
324
+ onMomentumScrollEnd={_onMomentumScrollEnd}
325
+ onScrollEndDrag={_onScrollEndDrag}
326
+ scrollEventThrottle={16}
327
+ {...scrollViewExtraProps}
328
+ onEndReached={
329
+ /* When wrapped in a parent ScrollView (e.g. tabs),
304
330
  this FlatList doesn't scroll so onEndReached can fire repeatedly;
305
331
  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
- />
332
+ isScreenWrappedInContainer
333
+ ? undefined
334
+ : () => {
335
+ if (!hasUserScrolledRef.current) return;
336
+ emitScrollEndReached();
337
+ }
338
+ }
339
+ />
340
+ </>
314
341
  </ViewportTracker>
315
342
  </ScreenLoadingMeasurements>
316
343
  </ComponentsMapRefContext.Provider>
@@ -320,6 +347,5 @@ function ComponentsMapComponent(props: Props) {
320
347
  }
321
348
 
322
349
  export const ComponentsMap = React.memo(
323
- withComponentsMapProvider(ComponentsMapComponent),
324
- R.equals
350
+ withComponentsMapProvider(ComponentsMapComponent)
325
351
  );