import * as React from "react"; import { Platform, RefreshControl as RNRefreshControl, StyleSheet, } from "react-native"; import * as R from "ramda"; import { path } from "@applicaster/zapp-react-native-utils/utils"; import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers"; import { useTheme } from "@applicaster/zapp-react-native-utils/theme"; import { useLocalizedStrings } from "@applicaster/zapp-react-native-utils/localizationUtils"; import { useAnalytics } from "@applicaster/zapp-react-native-utils/analyticsUtils"; import { useSendAnalyticsEventWithFunction } from "@applicaster/zapp-react-native-utils/analyticsUtils/helpers/hooks"; import { useCurrentScreenData } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useCurrentScreenData"; import { useShallow } from "zustand/react/shallow"; import { useScreenContextV2 } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useLoadPipesDataDispatch } from "@applicaster/zapp-react-native-utils/reactHooks"; import { useGetUrlInflater } from "@applicaster/zapp-react-native-utils/reactHooks/feed/useInflatedUrl"; const BRIGHTNESS_THRESHOLD = 160; const ABOVE_DEFAULT_COLOR = "gray"; const BELOW_DEFAULT_COLOR = "white"; const BASE_INDICATOR_HEIGHT = 40; const BASE_INDICATOR_OFFSET = 16; /** Return spacing required to position indicator below the navbar bottom line (Android only) */ const getRefreshControlOffset = () => { return Platform.OS === "ios" ? 0 : BASE_INDICATOR_HEIGHT + BASE_INDICATOR_OFFSET; }; const styles = StyleSheet.create({ // eslint-disable-next-line react-native/no-color-literals control: { zIndex: 1, backgroundColor: "transparent", }, }); const getBrightness = (RGBcolor) => { const RGBvalues = RGBcolor.substring( RGBcolor.indexOf("(") + 1, RGBcolor.length - 1 ) .replace(/\s/g, "") .split(","); return Math.floor( Math.sqrt( Number(RGBvalues[0]) ** 2 * 0.241 + Number(RGBvalues[1]) ** 2 * 0.691 + Number(RGBvalues[2]) ** 2 * 0.068 ) ); }; export const usePullToRefresh = ( riverComponents, pullToRefreshPipesV1RefreshingStateUpdater, refreshingPipesV1 ) => { const isPipesV1 = !!pullToRefreshPipesV1RefreshingStateUpdater; const [refreshing, setRefreshing] = React.useState(false); const feedSources = React.useMemo( () => (riverComponents || []) .map((riverComponent) => ({ source: path(["data", "source"], riverComponent), mapping: path(["data", "mapping"], riverComponent), })) .filter(({ source }) => !isNilOrEmpty(source)), [riverComponents] ); const feedsLength = feedSources.length; const [requestsCompletedCounter, setRequestsCompletedCounter] = React.useState(0); const loadPipesDataDispatcher = useLoadPipesDataDispatch(); const urlInflater = useGetUrlInflater(); React.useEffect(() => { // will not work for pipes v1 on 1st level screens if (refreshing && !isPipesV1) { feedSources.forEach(({ source, mapping }) => { const inflatedUrl = urlInflater(source, mapping); if (inflatedUrl) { loadPipesDataDispatcher(inflatedUrl, { silentRefresh: true, clearCache: true, callback: () => { setRequestsCompletedCounter(R.inc); }, }); } else { setRequestsCompletedCounter(R.inc); } }); } }, [ refreshing, isPipesV1, feedSources, loadPipesDataDispatcher, urlInflater, ]); React.useEffect(() => { if (requestsCompletedCounter === feedsLength) { setRefreshing(false); } }, [requestsCompletedCounter, feedsLength]); const onRefresh = React.useCallback(() => { if (isPipesV1) { pullToRefreshPipesV1RefreshingStateUpdater(true); } else { setRefreshing(true); setRequestsCompletedCounter(0); } }, [isPipesV1]); return { refreshing: isPipesV1 ? refreshingPipesV1 : refreshing, onRefresh, }; }; /** Returns the offset for the progress view of the RefreshControl component * based on navbar content position */ export const useGetProgressViewOffset = () => { const { _navBarStore } = useScreenContextV2(); const { top } = useSafeAreaInsets(); const offset = getRefreshControlOffset(); const { height, contentPosition } = _navBarStore( useShallow(({ contentPosition, height }) => ({ contentPosition, height })) ); if (height === 0) { return offset; } switch (contentPosition) { case "on_bottom_status_bar": // navbar height return offset + height; case "on_top_screen": // navbar height + status bar height return offset + height + top; default: return offset; } }; export function RefreshControl(props: { pullToRefreshPipesV1RefreshingStateUpdater?: (refreshing: boolean) => void; refreshingPipesV1?: boolean; }) { const screenData = useCurrentScreenData(); const { refreshing, onRefresh } = usePullToRefresh( screenData.ui_components, props.pullToRefreshPipesV1RefreshingStateUpdater, props.refreshingPipesV1 ); const { app_background_color: themeBackgroundColor } = useTheme(); const localizedStrings = useLocalizedStrings({ localizations: screenData?.localizations || {}, }); const { indicatorColor, titleUnderIndicatorColor, indicatorBackgroundColor, indicatorSize, generalContentBackgroungColor, displayTitleIOS, } = React.useMemo( () => ({ indicatorColor: R.prop( "pull_to_refresh_indicator_color", screenData.styles ), titleUnderIndicatorColor: R.prop( "pull_to_refresh_title_color_under_indicator", screenData.styles ), indicatorBackgroundColor: R.prop( "pull_to_refresh_indicator_bg_color", screenData.styles ), indicatorSize: R.prop("pull_to_refresh_indicator_size", screenData.styles) === "large" ? "large" : "default", generalContentBackgroungColor: R.prop( "screen_background_color", screenData.styles ), displayTitleIOS: R.prop( "pull_to_refresh_display_title_ios", screenData.styles ), }), [screenData] ); const { preparedIndicatorColor, preparedTitleUnderIndicatorColor, preparedIndicatorBackgroundColor, } = React.useMemo(() => { if ( indicatorColor && titleUnderIndicatorColor && indicatorBackgroundColor ) { return { preparedIndicatorColor: indicatorColor, preparedTitleUnderIndicatorColor: titleUnderIndicatorColor, preparedIndicatorBackgroundColor: indicatorBackgroundColor, }; } const brightness = getBrightness( generalContentBackgroungColor || themeBackgroundColor ); const defaultColor = brightness > BRIGHTNESS_THRESHOLD ? ABOVE_DEFAULT_COLOR : BELOW_DEFAULT_COLOR; const defaultIndicatorBackgroundColor = brightness > BRIGHTNESS_THRESHOLD ? BELOW_DEFAULT_COLOR : ABOVE_DEFAULT_COLOR; return { preparedIndicatorColor: indicatorColor || defaultColor, preparedTitleUnderIndicatorColor: titleUnderIndicatorColor || defaultColor, preparedIndicatorBackgroundColor: indicatorBackgroundColor || defaultIndicatorBackgroundColor, }; }, [ indicatorColor, titleUnderIndicatorColor, indicatorBackgroundColor, generalContentBackgroungColor, themeBackgroundColor, ]); const { sendScreenEvent } = useAnalytics(); const callbackSendAnalyticsEvent = useSendAnalyticsEventWithFunction(); const onRefreshHandler = React.useCallback(() => { callbackSendAnalyticsEvent({ sendAnalyticsFunction: sendScreenEvent, extraProps: { forceReload: true }, }); onRefresh(); }, [onRefresh, screenData]); return ( ); }