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 tmp-rm-bitdrift 394 lines 11 kB view raw
1import * as React from 'react' 2import { 3 LayoutChangeEvent, 4 NativeScrollEvent, 5 ScrollView, 6 StyleSheet, 7 View, 8} from 'react-native' 9import Animated, { 10 AnimatedRef, 11 runOnJS, 12 runOnUI, 13 scrollTo, 14 SharedValue, 15 useAnimatedRef, 16 useAnimatedStyle, 17 useSharedValue, 18} from 'react-native-reanimated' 19 20import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 21import {ScrollProvider} from '#/lib/ScrollContext' 22import {isIOS} from '#/platform/detection' 23import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager' 24import {useTheme} from '#/alf' 25import {ListMethods} from '../util/List' 26import {PagerHeaderProvider} from './PagerHeaderContext' 27import {TabBar} from './TabBar' 28 29export interface PagerWithHeaderChildParams { 30 headerHeight: number 31 isFocused: boolean 32 scrollElRef: React.MutableRefObject<ListMethods | ScrollView | null> 33} 34 35export interface PagerWithHeaderProps { 36 testID?: string 37 children: 38 | (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[] 39 | ((props: PagerWithHeaderChildParams) => JSX.Element) 40 items: string[] 41 isHeaderReady: boolean 42 renderHeader?: ({ 43 setMinimumHeight, 44 }: { 45 setMinimumHeight: (height: number) => void 46 }) => JSX.Element 47 initialPage?: number 48 onPageSelected?: (index: number) => void 49 onCurrentPageSelected?: (index: number) => void 50 allowHeaderOverScroll?: boolean 51} 52export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>( 53 function PageWithHeaderImpl( 54 { 55 children, 56 testID, 57 items, 58 isHeaderReady, 59 renderHeader, 60 initialPage, 61 onPageSelected, 62 onCurrentPageSelected, 63 allowHeaderOverScroll, 64 }: PagerWithHeaderProps, 65 ref, 66 ) { 67 const [currentPage, setCurrentPage] = React.useState(0) 68 const [tabBarHeight, setTabBarHeight] = React.useState(0) 69 const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0) 70 const scrollY = useSharedValue(0) 71 const headerHeight = headerOnlyHeight + tabBarHeight 72 73 // capture the header bar sizing 74 const onTabBarLayout = useNonReactiveCallback((evt: LayoutChangeEvent) => { 75 const height = evt.nativeEvent.layout.height 76 if (height > 0) { 77 // The rounding is necessary to prevent jumps on iOS 78 setTabBarHeight(Math.round(height * 2) / 2) 79 } 80 }) 81 const onHeaderOnlyLayout = useNonReactiveCallback((height: number) => { 82 if (height > 0) { 83 // The rounding is necessary to prevent jumps on iOS 84 setHeaderOnlyHeight(Math.round(height * 2) / 2) 85 } 86 }) 87 88 const renderTabBar = React.useCallback( 89 (props: RenderTabBarFnProps) => { 90 return ( 91 <PagerHeaderProvider 92 scrollY={scrollY} 93 headerHeight={headerOnlyHeight}> 94 <PagerTabBar 95 headerOnlyHeight={headerOnlyHeight} 96 items={items} 97 isHeaderReady={isHeaderReady} 98 renderHeader={renderHeader} 99 currentPage={currentPage} 100 onCurrentPageSelected={onCurrentPageSelected} 101 onTabBarLayout={onTabBarLayout} 102 onHeaderOnlyLayout={onHeaderOnlyLayout} 103 onSelect={props.onSelect} 104 scrollY={scrollY} 105 testID={testID} 106 allowHeaderOverScroll={allowHeaderOverScroll} 107 dragProgress={props.dragProgress} 108 dragState={props.dragState} 109 /> 110 </PagerHeaderProvider> 111 ) 112 }, 113 [ 114 headerOnlyHeight, 115 items, 116 isHeaderReady, 117 renderHeader, 118 currentPage, 119 onCurrentPageSelected, 120 onTabBarLayout, 121 onHeaderOnlyLayout, 122 scrollY, 123 testID, 124 allowHeaderOverScroll, 125 ], 126 ) 127 128 const scrollRefs = useSharedValue<Array<AnimatedRef<any> | null>>([]) 129 const registerRef = React.useCallback( 130 (scrollRef: AnimatedRef<any> | null, atIndex: number) => { 131 scrollRefs.modify(refs => { 132 'worklet' 133 refs[atIndex] = scrollRef 134 return refs 135 }) 136 }, 137 [scrollRefs], 138 ) 139 140 const lastForcedScrollY = useSharedValue(0) 141 const adjustScrollForOtherPages = () => { 142 'worklet' 143 const currentScrollY = scrollY.get() 144 const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight) 145 if (lastForcedScrollY.get() !== forcedScrollY) { 146 lastForcedScrollY.set(forcedScrollY) 147 const refs = scrollRefs.get() 148 for (let i = 0; i < refs.length; i++) { 149 const scollRef = refs[i] 150 if (i !== currentPage && scollRef != null) { 151 scrollTo(scollRef, 0, forcedScrollY, false) 152 } 153 } 154 } 155 } 156 157 const throttleTimeout = React.useRef<ReturnType<typeof setTimeout> | null>( 158 null, 159 ) 160 const queueThrottledOnScroll = useNonReactiveCallback(() => { 161 if (!throttleTimeout.current) { 162 throttleTimeout.current = setTimeout(() => { 163 throttleTimeout.current = null 164 runOnUI(adjustScrollForOtherPages)() 165 }, 80 /* Sync often enough you're unlikely to catch it unsynced */) 166 } 167 }) 168 169 const onScrollWorklet = React.useCallback( 170 (e: NativeScrollEvent) => { 171 'worklet' 172 const nextScrollY = e.contentOffset.y 173 // HACK: onScroll is reporting some strange values on load (negative header height). 174 // Highly improbable that you'd be overscrolled by over 400px - 175 // in fact, I actually can't do it, so let's just ignore those. -sfn 176 const isPossiblyInvalid = 177 headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight 178 if (!isPossiblyInvalid) { 179 scrollY.set(nextScrollY) 180 runOnJS(queueThrottledOnScroll)() 181 } 182 }, 183 [scrollY, queueThrottledOnScroll, headerHeight], 184 ) 185 186 const onPageSelectedInner = React.useCallback( 187 (index: number) => { 188 setCurrentPage(index) 189 onPageSelected?.(index) 190 }, 191 [onPageSelected, setCurrentPage], 192 ) 193 194 return ( 195 <Pager 196 ref={ref} 197 testID={testID} 198 initialPage={initialPage} 199 onPageSelected={onPageSelectedInner} 200 renderTabBar={renderTabBar}> 201 {toArray(children) 202 .filter(Boolean) 203 .map((child, i) => { 204 const isReady = 205 isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0 206 return ( 207 <View key={i} collapsable={false}> 208 <PagerItem 209 headerHeight={headerHeight} 210 index={i} 211 isReady={isReady} 212 isFocused={i === currentPage} 213 onScrollWorklet={i === currentPage ? onScrollWorklet : noop} 214 registerRef={registerRef} 215 renderTab={child} 216 /> 217 </View> 218 ) 219 })} 220 </Pager> 221 ) 222 }, 223) 224 225let PagerTabBar = ({ 226 currentPage, 227 headerOnlyHeight, 228 isHeaderReady, 229 items, 230 scrollY, 231 testID, 232 renderHeader, 233 onHeaderOnlyLayout, 234 onTabBarLayout, 235 onCurrentPageSelected, 236 onSelect, 237 allowHeaderOverScroll, 238 dragProgress, 239 dragState, 240}: { 241 currentPage: number 242 headerOnlyHeight: number 243 isHeaderReady: boolean 244 items: string[] 245 testID?: string 246 scrollY: SharedValue<number> 247 renderHeader?: ({ 248 setMinimumHeight, 249 }: { 250 setMinimumHeight: (height: number) => void 251 }) => JSX.Element 252 onHeaderOnlyLayout: (height: number) => void 253 onTabBarLayout: (e: LayoutChangeEvent) => void 254 onCurrentPageSelected?: (index: number) => void 255 onSelect?: (index: number) => void 256 allowHeaderOverScroll?: boolean 257 dragProgress: SharedValue<number> 258 dragState: SharedValue<'idle' | 'dragging' | 'settling'> 259}): React.ReactNode => { 260 const t = useTheme() 261 const [minimumHeaderHeight, setMinimumHeaderHeight] = React.useState(0) 262 const headerTransform = useAnimatedStyle(() => { 263 const translateY = 264 Math.min( 265 scrollY.get(), 266 Math.max(headerOnlyHeight - minimumHeaderHeight, 0), 267 ) * -1 268 return { 269 transform: [ 270 { 271 translateY: allowHeaderOverScroll 272 ? translateY 273 : Math.min(translateY, 0), 274 }, 275 ], 276 } 277 }) 278 const headerRef = React.useRef(null) 279 return ( 280 <Animated.View 281 pointerEvents={isIOS ? 'auto' : 'box-none'} 282 style={[styles.tabBarMobile, headerTransform, t.atoms.bg]}> 283 <View 284 ref={headerRef} 285 pointerEvents={isIOS ? 'auto' : 'box-none'} 286 collapsable={false}> 287 {renderHeader?.({setMinimumHeight: setMinimumHeaderHeight})} 288 { 289 // It wouldn't be enough to place `onLayout` on the parent node because 290 // this would risk measuring before `isHeaderReady` has turned `true`. 291 // Instead, we'll render a brand node conditionally and get fresh layout. 292 isHeaderReady && ( 293 <View 294 // It wouldn't be enough to do this in a `ref` of an effect because, 295 // even if `isHeaderReady` might have turned `true`, the associated 296 // layout might not have been performed yet on the native side. 297 onLayout={() => { 298 // @ts-ignore 299 headerRef.current?.measure( 300 (_x: number, _y: number, _width: number, height: number) => { 301 onHeaderOnlyLayout(height) 302 }, 303 ) 304 }} 305 /> 306 ) 307 } 308 </View> 309 <View 310 onLayout={onTabBarLayout} 311 style={{ 312 // Render it immediately to measure it early since its size doesn't depend on the content. 313 // However, keep it invisible until the header above stabilizes in order to prevent jumps. 314 opacity: isHeaderReady ? 1 : 0, 315 pointerEvents: isHeaderReady ? 'auto' : 'none', 316 }}> 317 <TabBar 318 testID={testID} 319 items={items} 320 selectedPage={currentPage} 321 onSelect={onSelect} 322 onPressSelected={onCurrentPageSelected} 323 dragProgress={dragProgress} 324 dragState={dragState} 325 /> 326 </View> 327 </Animated.View> 328 ) 329} 330PagerTabBar = React.memo(PagerTabBar) 331 332function PagerItem({ 333 headerHeight, 334 index, 335 isReady, 336 isFocused, 337 onScrollWorklet, 338 renderTab, 339 registerRef, 340}: { 341 headerHeight: number 342 index: number 343 isFocused: boolean 344 isReady: boolean 345 registerRef: (scrollRef: AnimatedRef<any> | null, atIndex: number) => void 346 onScrollWorklet: (e: NativeScrollEvent) => void 347 renderTab: ((props: PagerWithHeaderChildParams) => JSX.Element) | null 348}) { 349 const scrollElRef = useAnimatedRef() 350 351 React.useEffect(() => { 352 registerRef(scrollElRef, index) 353 return () => { 354 registerRef(null, index) 355 } 356 }, [scrollElRef, registerRef, index]) 357 358 if (!isReady || renderTab == null) { 359 return null 360 } 361 362 return ( 363 <ScrollProvider onScroll={onScrollWorklet}> 364 {renderTab({ 365 headerHeight, 366 isFocused, 367 scrollElRef: scrollElRef as React.MutableRefObject< 368 ListMethods | ScrollView | null 369 >, 370 })} 371 </ScrollProvider> 372 ) 373} 374 375const styles = StyleSheet.create({ 376 tabBarMobile: { 377 position: 'absolute', 378 zIndex: 1, 379 top: 0, 380 left: 0, 381 width: '100%', 382 }, 383}) 384 385function noop() { 386 'worklet' 387} 388 389function toArray<T>(v: T | T[]): T[] { 390 if (Array.isArray(v)) { 391 return v 392 } 393 return [v] 394}