import React, {useEffect, useState} from 'react' import {View} from 'react-native' import {ActivityIndicator} from 'react-native' import Animated, { Extrapolation, interpolate, runOnJS, SharedValue, useAnimatedProps, useAnimatedReaction, useAnimatedStyle, } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {BlurView} from 'expo-blur' import {useIsFetching} from '@tanstack/react-query' import {isIOS} from '#/platform/detection' import {RQKEY_ROOT as STARTERPACK_RQKEY_ROOT} from '#/state/queries/actor-starter-packs' import {RQKEY_ROOT as FEED_RQKEY_ROOT} from '#/state/queries/post-feed' import {RQKEY_ROOT as FEEDGEN_RQKEY_ROOT} from '#/state/queries/profile-feedgens' import {RQKEY_ROOT as LIST_RQKEY_ROOT} from '#/state/queries/profile-lists' import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext' import {atoms as a} from '#/alf' const AnimatedBlurView = Animated.createAnimatedComponent(BlurView) export function GrowableBanner({ backButton, children, }: { backButton?: React.ReactNode children: React.ReactNode }) { const pagerContext = usePagerHeaderContext() // plain non-growable mode for Android/Web if (!pagerContext || !isIOS) { return ( {children} {backButton} ) } const {scrollY} = pagerContext return ( {children} ) } function GrowableBannerInner({ scrollY, backButton, children, }: { scrollY: SharedValue backButton?: React.ReactNode children: React.ReactNode }) { const {top: topInset} = useSafeAreaInsets() const isFetching = useIsProfileFetching() const animateSpinner = useShouldAnimateSpinner({isFetching, scrollY}) const animatedStyle = useAnimatedStyle(() => ({ transform: [ { scale: interpolate(scrollY.get(), [-150, 0], [2, 1], { extrapolateRight: Extrapolation.CLAMP, }), }, ], })) const animatedBlurViewProps = useAnimatedProps(() => { return { intensity: interpolate( scrollY.get(), [-300, -65, -15], [50, 40, 0], Extrapolation.CLAMP, ), } }) const animatedSpinnerStyle = useAnimatedStyle(() => { const scrollYValue = scrollY.get() return { display: scrollYValue < 0 ? 'flex' : 'none', opacity: interpolate( scrollYValue, [-60, -15], [1, 0], Extrapolation.CLAMP, ), transform: [ {translateY: interpolate(scrollYValue, [-150, 0], [-75, 0])}, {rotate: '90deg'}, ], } }) const animatedBackButtonStyle = useAnimatedStyle(() => ({ transform: [ { translateY: interpolate(scrollY.get(), [-150, 10], [-150, 10], { extrapolateRight: Extrapolation.CLAMP, }), }, ], })) return ( <> {children} {backButton} ) } function useIsProfileFetching() { // are any of the profile-related queries fetching? return [ useIsFetching({queryKey: [FEED_RQKEY_ROOT]}), useIsFetching({queryKey: [FEEDGEN_RQKEY_ROOT]}), useIsFetching({queryKey: [LIST_RQKEY_ROOT]}), useIsFetching({queryKey: [STARTERPACK_RQKEY_ROOT]}), ].some(isFetching => isFetching) } function useShouldAnimateSpinner({ isFetching, scrollY, }: { isFetching: boolean scrollY: SharedValue }) { const [isOverscrolled, setIsOverscrolled] = useState(false) // HACK: it reports a scroll pos of 0 for a tick when fetching finishes // so paper over that by keeping it true for a bit -sfn const stickyIsOverscrolled = useStickyToggle(isOverscrolled, 10) useAnimatedReaction( () => scrollY.get() < -5, (value, prevValue) => { if (value !== prevValue) { runOnJS(setIsOverscrolled)(value) } }, [scrollY], ) const [isAnimating, setIsAnimating] = useState(isFetching) if (isFetching && !isAnimating) { setIsAnimating(true) } if (!isFetching && isAnimating && !stickyIsOverscrolled) { setIsAnimating(false) } return isAnimating } // stayed true for at least `delay` ms before returning to false function useStickyToggle(value: boolean, delay: number) { const [prevValue, setPrevValue] = useState(value) const [isSticking, setIsSticking] = useState(false) useEffect(() => { if (isSticking) { const timeout = setTimeout(() => setIsSticking(false), delay) return () => clearTimeout(timeout) } }, [isSticking, delay]) if (value !== prevValue) { setIsSticking(prevValue) // Going true -> false should stick. setPrevValue(value) return prevValue ? true : value } return isSticking ? true : value }