mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at utm-source 213 lines 5.6 kB view raw
1import React, {useEffect, useState} from 'react' 2import {View} from 'react-native' 3import {ActivityIndicator} from 'react-native' 4import Animated, { 5 Extrapolation, 6 interpolate, 7 runOnJS, 8 SharedValue, 9 useAnimatedProps, 10 useAnimatedReaction, 11 useAnimatedStyle, 12} from 'react-native-reanimated' 13import {BlurView} from 'expo-blur' 14import {useIsFetching} from '@tanstack/react-query' 15 16import {isIOS} from '#/platform/detection' 17import {RQKEY_ROOT as STARTERPACK_RQKEY_ROOT} from '#/state/queries/actor-starter-packs' 18import {RQKEY_ROOT as FEED_RQKEY_ROOT} from '#/state/queries/post-feed' 19import {RQKEY_ROOT as FEEDGEN_RQKEY_ROOT} from '#/state/queries/profile-feedgens' 20import {RQKEY_ROOT as LIST_RQKEY_ROOT} from '#/state/queries/profile-lists' 21import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext' 22import {atoms as a} from '#/alf' 23 24const AnimatedBlurView = Animated.createAnimatedComponent(BlurView) 25 26export function GrowableBanner({ 27 backButton, 28 children, 29}: { 30 backButton?: React.ReactNode 31 children: React.ReactNode 32}) { 33 const pagerContext = usePagerHeaderContext() 34 35 // pagerContext should only be present on iOS, but better safe than sorry 36 if (!pagerContext || !isIOS) { 37 return ( 38 <View style={[a.w_full, a.h_full]}> 39 {children} 40 {backButton} 41 </View> 42 ) 43 } 44 45 const {scrollY} = pagerContext 46 47 return ( 48 <GrowableBannerInner scrollY={scrollY} backButton={backButton}> 49 {children} 50 </GrowableBannerInner> 51 ) 52} 53 54function GrowableBannerInner({ 55 scrollY, 56 backButton, 57 children, 58}: { 59 scrollY: SharedValue<number> 60 backButton?: React.ReactNode 61 children: React.ReactNode 62}) { 63 const isFetching = useIsProfileFetching() 64 const animateSpinner = useShouldAnimateSpinner({isFetching, scrollY}) 65 66 const animatedStyle = useAnimatedStyle(() => ({ 67 transform: [ 68 { 69 scale: interpolate(scrollY.get(), [-150, 0], [2, 1], { 70 extrapolateRight: Extrapolation.CLAMP, 71 }), 72 }, 73 ], 74 })) 75 76 const animatedBlurViewProps = useAnimatedProps(() => { 77 return { 78 intensity: interpolate( 79 scrollY.get(), 80 [-300, -65, -15], 81 [50, 40, 0], 82 Extrapolation.CLAMP, 83 ), 84 } 85 }) 86 87 const animatedSpinnerStyle = useAnimatedStyle(() => { 88 const scrollYValue = scrollY.get() 89 return { 90 display: scrollYValue < 0 ? 'flex' : 'none', 91 opacity: interpolate( 92 scrollYValue, 93 [-60, -15], 94 [1, 0], 95 Extrapolation.CLAMP, 96 ), 97 transform: [ 98 {translateY: interpolate(scrollYValue, [-150, 0], [-75, 0])}, 99 {rotate: '90deg'}, 100 ], 101 } 102 }) 103 104 const animatedBackButtonStyle = useAnimatedStyle(() => ({ 105 transform: [ 106 { 107 translateY: interpolate(scrollY.get(), [-150, 60], [-150, 60], { 108 extrapolateRight: Extrapolation.CLAMP, 109 }), 110 }, 111 ], 112 })) 113 114 return ( 115 <> 116 <Animated.View 117 style={[ 118 a.absolute, 119 {left: 0, right: 0, bottom: 0}, 120 {height: 150}, 121 {transformOrigin: 'bottom'}, 122 animatedStyle, 123 ]}> 124 {children} 125 <AnimatedBlurView 126 style={[a.absolute, a.inset_0]} 127 tint="dark" 128 animatedProps={animatedBlurViewProps} 129 /> 130 </Animated.View> 131 <View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}> 132 <Animated.View style={[animatedSpinnerStyle]}> 133 <ActivityIndicator 134 key={animateSpinner ? 'spin' : 'stop'} 135 size="large" 136 color="white" 137 animating={animateSpinner} 138 hidesWhenStopped={false} 139 /> 140 </Animated.View> 141 </View> 142 <Animated.View style={[animatedBackButtonStyle]}> 143 {backButton} 144 </Animated.View> 145 </> 146 ) 147} 148 149function useIsProfileFetching() { 150 // are any of the profile-related queries fetching? 151 return [ 152 useIsFetching({queryKey: [FEED_RQKEY_ROOT]}), 153 useIsFetching({queryKey: [FEEDGEN_RQKEY_ROOT]}), 154 useIsFetching({queryKey: [LIST_RQKEY_ROOT]}), 155 useIsFetching({queryKey: [STARTERPACK_RQKEY_ROOT]}), 156 ].some(isFetching => isFetching) 157} 158 159function useShouldAnimateSpinner({ 160 isFetching, 161 scrollY, 162}: { 163 isFetching: boolean 164 scrollY: SharedValue<number> 165}) { 166 const [isOverscrolled, setIsOverscrolled] = useState(false) 167 // HACK: it reports a scroll pos of 0 for a tick when fetching finishes 168 // so paper over that by keeping it true for a bit -sfn 169 const stickyIsOverscrolled = useStickyToggle(isOverscrolled, 10) 170 171 useAnimatedReaction( 172 () => scrollY.get() < -5, 173 (value, prevValue) => { 174 if (value !== prevValue) { 175 runOnJS(setIsOverscrolled)(value) 176 } 177 }, 178 [scrollY], 179 ) 180 181 const [isAnimating, setIsAnimating] = useState(isFetching) 182 183 if (isFetching && !isAnimating) { 184 setIsAnimating(true) 185 } 186 187 if (!isFetching && isAnimating && !stickyIsOverscrolled) { 188 setIsAnimating(false) 189 } 190 191 return isAnimating 192} 193 194// stayed true for at least `delay` ms before returning to false 195function useStickyToggle(value: boolean, delay: number) { 196 const [prevValue, setPrevValue] = useState(value) 197 const [isSticking, setIsSticking] = useState(false) 198 199 useEffect(() => { 200 if (isSticking) { 201 const timeout = setTimeout(() => setIsSticking(false), delay) 202 return () => clearTimeout(timeout) 203 } 204 }, [isSticking, delay]) 205 206 if (value !== prevValue) { 207 setIsSticking(prevValue) // Going true -> false should stick. 208 setPrevValue(value) 209 return prevValue ? true : value 210 } 211 212 return isSticking ? true : value 213}