import * as React from "react"; import * as R from "ramda"; import { FlatList, StyleSheet, View } from "react-native"; import { useTheme } from "@applicaster/zapp-react-native-utils/theme"; import { RiverItem } from "../RiverItem"; import { RiverFooter } from "../RiverFooter"; import { RiverError } from "../RiverError"; import { useScreenConfiguration } from "../useScreenConfiguration"; import { RefreshControl } from "../RefreshControl"; import { ifEmptyUseFallback } from "@applicaster/zapp-react-native-utils/cellUtils"; import { usePipesCacheReset, useProfilerLogging, } from "@applicaster/zapp-react-native-utils/reactHooks"; import { useLoadingState } from "./hooks/useLoadingState"; import { ViewportTracker } from "../../Viewport"; import { ScreenLoadingMeasurements, ScreenLoadingMeasurementsListItemWrapper, } from "@applicaster/zapp-react-native-utils/riverComponetsMeasurementProvider"; import { useScreenData } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenData"; import { isLast } from "@applicaster/zapp-react-native-utils/arrayUtils"; import { withComponentsMapProvider } from "@applicaster/zapp-react-native-ui-components/Decorators/ComponentsMapWrapper"; import { useScreenContextV2 } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext"; import { useShallow } from "zustand/react/shallow"; import { emitScrollEndReached } from "@applicaster/zapp-react-native-ui-components/events"; import { isAndroidPlatform } from "@applicaster/zapp-react-native-utils/reactUtils"; import { ComponentsMapHeightContext } from "./ContextProviders/ComponentsMapHeightContext"; import { ComponentsMapRefContext } from "./ContextProviders/ComponentsMapRefContext"; const isAndroid = isAndroidPlatform(); type Props = { feed: ZappFeed; groupId?: string; isScreenWrappedInContainer?: boolean; riverComponents: ZappUIComponent[]; scrollViewExtraProps?: {}; riverId?: string; getStaticComponentFeed: any; pullToRefreshPipesV1RefreshingStateUpdater: () => boolean; refreshingPipesV1?: boolean; stickyHeaderIndices?: number[]; }; const styles = StyleSheet.create({ container: { flex: 1, }, }); const getNearestValue = (value, optionA, optionB) => Math.abs(value - optionA) < Math.abs(value - optionB) ? optionA : optionB; const scrollIndicatorInsets = { right: 1 }; const keyExtractor = R.compose(String, R.prop("id")); function ComponentsMapComponent(props: Props) { const { riverComponents: components, scrollViewExtraProps, isScreenWrappedInContainer, feed, groupId, riverId, getStaticComponentFeed, // Method added to keep pipes v1 logic up to date with the pullToRefresh state. // TODO: Remove when pipes v1 is deprecated. pullToRefreshPipesV1RefreshingStateUpdater, refreshingPipesV1, stickyHeaderIndices, } = props; const flatListRef = React.useRef(null); const flatListWrapperRef = React.useRef(null); const hasUserScrolledRef = React.useRef(false); const screenConfig = useScreenConfiguration(riverId); const screenData = useScreenData(riverId); const pullToRefreshEnabled = screenData?.rules?.pull_to_refresh_enabled; const [flatListHeight, setFlatListHeight] = React.useState(null); const riverComponents = React.useMemo(() => { if (feed?.entry?.length < components.length) { return R.slice(0, feed?.entry?.length, components); } return components; }, [components, feed]); const logTimestamp = useProfilerLogging(); const onLoadDone = React.useCallback(() => { logTimestamp(riverId?.toString()); }, [logTimestamp]); const { loadingState, onLoadFinished, onLoadFailed, shouldShowLoadingError } = useLoadingState(riverComponents.length, onLoadDone); const theme = useTheme(); const renderRiverItem = React.useCallback( ({ item, index }) => { const riverItemProps = { riverId, item, index, isScreenWrappedInContainer, feed, groupId, onLoadFailed, onLoadFinished, getStaticComponentFeed, isLast: isLast(index, riverComponents.length), loadingState, }; return ( ); }, [feed, getStaticComponentFeed, onLoadFailed, onLoadFinished] ); const screenStyle = React.useMemo( () => ({ paddingTop: ifEmptyUseFallback( screenConfig.paddingTop, R.prop("screen_padding_top")(theme) ), paddingBottom: ifEmptyUseFallback( screenConfig.paddingBottom, R.prop("screen_padding_bottom")(theme) ), paddingLeft: ifEmptyUseFallback( screenConfig.paddingLeft, R.prop("screen_padding_left")(theme) ), paddingRight: ifEmptyUseFallback( screenConfig.paddingRight, R.prop("screen_padding_right")(theme) ), }), [riverId] ); const handleOnLayout = React.useCallback( ({ nativeEvent: { layout: { height }, }, }) => { if (!flatListHeight) { setFlatListHeight(height); } }, [flatListHeight] ); usePipesCacheReset(riverId, riverComponents); const refreshControl = React.useMemo( () => pullToRefreshEnabled ? ( ) : null, [] ); const navBarStore = useScreenContextV2()._navBarStore; const scrollY = navBarStore( useShallow(({ scrollYAnimated }) => scrollYAnimated) ); const canMomentum = React.useRef(false); const onScrollBeginDrag = React.useCallback(() => { canMomentum.current = true; }, []); /* onMomentumScrollEnd: Handling snap to expanded/collapsed state ( for navbar ) */ const _onMomentumScrollEnd = React.useCallback(({ nativeEvent }) => { if (!canMomentum.current) return; canMomentum.current = false; const { height: headerHeight, scrollState } = navBarStore.getState(); const offsetY = Math.max(0, nativeEvent.contentOffset.y); if ( !(scrollState === 0 || scrollState === -headerHeight) && flatListRef.current ) { const offset = getNearestValue(Math.round(scrollState), -headerHeight, 0) === -headerHeight ? Math.round(offsetY + scrollState + headerHeight) : Math.round(offsetY + scrollState); flatListRef.current.scrollToOffset({ animated: true, offset, }); } }, []); /* onScrollEnd: Handling snap to expanded/collapsed state ( for navbar ) */ const _onScrollEndDrag = React.useCallback(({ nativeEvent }) => { const { height: headerHeight, scrollState } = navBarStore.getState(); const offsetY = Math.max(0, nativeEvent.contentOffset.y); if ( !(scrollState === 0 || scrollState === -headerHeight) && flatListRef.current && nativeEvent.velocity.y === 0 ) { const offset = getNearestValue(Math.round(scrollState), -headerHeight, 0) === -headerHeight ? Math.round(offsetY + scrollState + headerHeight) : Math.round(offsetY + scrollState); flatListRef.current.scrollToOffset({ animated: true, offset, }); } }, []); const onScroll = React.useCallback((event) => { hasUserScrolledRef.current = true; const { nativeEvent: { contentOffset: { y }, layoutMeasurement: { height }, contentSize: { height: contentHeight }, }, } = event; if (y >= 0 && y + height <= contentHeight) { scrollY.setValue(y); } else { if (y <= 0) { scrollY.setValue(0); } else if (y + height > contentHeight) { scrollY.setValue(contentHeight - height); } } }, []); const contentContainerStyle = React.useMemo( () => (isScreenWrappedInContainer ? {} : screenStyle), [isScreenWrappedInContainer, screenStyle] ); if (shouldShowLoadingError) { return ; } // TODO: support both "isScreenWrappedInContainer" and hasScreenPicker(). // The Screen Picker in Mobile is completly different than the TV // so the various offsets / margins in TV do not apply here. return ( { flatListRef.current = ref; }} // Fix for WebView rerender crashes on Android API 28+ // https://github.com/react-native-webview/react-native-webview/issues/1915#issuecomment-964035468 overScrollMode={isAndroid ? "never" : "auto"} scrollIndicatorInsets={scrollIndicatorInsets} extraData={feed} stickyHeaderIndices={stickyHeaderIndices} removeClippedSubviews={isAndroid} onLayout={handleOnLayout} initialNumToRender={3} maxToRenderPerBatch={10} windowSize={12} keyExtractor={keyExtractor} renderItem={renderRiverItem} data={riverComponents} contentContainerStyle={contentContainerStyle} ListFooterComponent={ } refreshControl={refreshControl} onScrollBeginDrag={onScrollBeginDrag} onScroll={onScroll} onMomentumScrollEnd={_onMomentumScrollEnd} onScrollEndDrag={_onScrollEndDrag} scrollEventThrottle={16} {...scrollViewExtraProps} /* When wrapped in a parent ScrollView (e.g. tabs), this FlatList doesn't scroll so onEndReached can fire repeatedly; skip it here and let the parent ScrollView emit scroll-end instead. */ onEndReached={ isScreenWrappedInContainer ? undefined : () => { if (!hasUserScrolledRef.current) return; emitScrollEndReached(); } } /> ); } export const ComponentsMap = React.memo( withComponentsMapProvider(ComponentsMapComponent), R.equals );