import React, {memo, useRef, startTransition} from 'react' import {FlatListProps, StyleSheet, View} from 'react-native' import {addStyle} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {batchedUpdates} from '#/lib/batchedUpdates' export type ListMethods = FlatList_INTERNAL export type ListProps = Omit< FlatListProps, | 'onScroll' // Use ScrollContext instead. | 'refreshControl' // Pass refreshing and/or onRefresh instead. | 'contentOffset' // Pass headerOffset instead. > & { onScrolledDownChange?: (isScrolledDown: boolean) => void headerOffset?: number refreshing?: boolean onRefresh?: () => void } export type ListRef = React.MutableRefObject function ListImpl( { ListHeaderComponent, ListFooterComponent, contentContainerStyle, data, desktopFixedHeight, // TODO headerOffset, keyExtractor, refreshing: _unsupportedRefreshing, onEndReached, onEndReachedThreshold = 0, onRefresh: _unsupportedOnRefresh, onScrolledDownChange, // TODO renderItem, extraData, style, ...props }: ListProps, ref: React.Ref, ) { const pal = usePalette('default') const {isMobile} = useWebMediaQueries() if (!isMobile) { contentContainerStyle = addStyle( contentContainerStyle, styles.containerScroll, ) } let header: ListProps['ListHeaderComponent'] = null if (ListHeaderComponent != null) { if (typeof ListHeaderComponent === 'object') { header = ListHeaderComponent } else if (typeof ListHeaderComponent === 'function') { // @ts-ignore We aren't using classes so it's a render function. header = ListHeaderComponent() } } let footer: ListProps['ListHeaderComponent'] = null if (ListFooterComponent != null) { if (typeof ListFooterComponent === 'object') { footer = ListFooterComponent } else if (typeof ListFooterComponent === 'function') { // @ts-ignore We aren't using classes so it's a render function. footer = ListFooterComponent() } } if (headerOffset != null) { style = addStyle(style, { paddingTop: headerOffset, }) } const nativeRef = React.useRef(null) React.useImperativeHandle( ref, () => ({ scrollToTop() { window.scrollTo({top: 0}) }, scrollToOffset({ animated, offset, }: { animated: boolean offset: number }) { window.scrollTo({ left: 0, top: offset, behavior: animated ? 'smooth' : 'instant', }) }, } as any), // TODO: Types. [], ) const [isVisible, setIsVisible] = React.useState(false) React.useEffect(() => { if (!isVisible) { return } function handleScroll() { // console.log(window.scrollY) } window.addEventListener('scroll', handleScroll) return () => window.removeEventListener('scroll', handleScroll) }, [isVisible]) const isScrolledDown = useRef(false) function handleScrolledDownDetectorVisibleChange(isMarkerVisible: boolean) { const didScrollDown = !isMarkerVisible if (isScrolledDown.current !== didScrollDown) { isScrolledDown.current = didScrollDown startTransition(() => { onScrolledDownChange?.(didScrollDown) }) } } return ( {header} {(data as Array).map((item, index) => ( key={keyExtractor!(item, index)} item={item} index={index} renderItem={renderItem} extraData={extraData} /> ))} {onEndReached && ( { if (isVisible) { onEndReached?.({ distanceFromEnd: onEndReachedThreshold || 0, }) } }} /> )} {footer} ) } let Row = function RowImpl({ item, index, renderItem, extraData: _unused, }: { item: ItemT index: number renderItem: | null | undefined | ((data: {index: number; item: any; separators: any}) => React.ReactNode) extraData: any }): React.ReactNode { if (!renderItem) { return null } return ( {renderItem({item, index, separators: null as any})} ) } Row = React.memo(Row) let Visibility = ({ topMargin = '0px', onVisibleChange, }: { topMargin?: string onVisibleChange: (isVisible: boolean) => void }): React.ReactNode => { const tailRef = React.useRef(null) const isIntersecting = React.useRef(false) const handleIntersection = useNonReactiveCallback( (entries: IntersectionObserverEntry[]) => { batchedUpdates(() => { entries.forEach(entry => { if (entry.isIntersecting !== isIntersecting.current) { isIntersecting.current = entry.isIntersecting onVisibleChange(entry.isIntersecting) } }) }) }, ) React.useEffect(() => { const observer = new IntersectionObserver(handleIntersection, { rootMargin: `${topMargin} 0px 0px 0px`, }) const tail: Element | null = tailRef.current! observer.observe(tail) return () => { observer.unobserve(tail) } }, [handleIntersection, topMargin]) return } Visibility = React.memo(Visibility) export const List = memo(React.forwardRef(ListImpl)) as ( props: ListProps & {ref?: React.Ref}, ) => React.ReactElement const styles = StyleSheet.create({ contentContainer: { borderLeftWidth: 1, borderRightWidth: 1, }, containerScroll: { width: '100%', maxWidth: 600, marginLeft: 'auto', marginRight: 'auto', }, row: { // @ts-ignore web only contentVisibility: 'auto', }, minHeightViewport: { // @ts-ignore web only minHeight: '100vh', }, visibilityDetector: { // @ts-ignore web only position: 'fixed', }, scrolledDownDetector: { position: 'absolute', left: 0, right: 0, pointerEvents: 'none', }, })