Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at main 205 lines 6.4 kB view raw
1import {forwardRef, memo, useDeferredValue, useMemo} from 'react' 2import {RefreshControl, type ViewToken} from 'react-native' 3import { 4 type FlatListPropsWithLayout, 5 runOnJS, 6 useAnimatedScrollHandler, 7 useSharedValue, 8} from 'react-native-reanimated' 9import {updateActiveVideoViewAsync} from '@bsky.app/video' 10 11import {useDedupe} from '#/lib/hooks/useDedupe' 12import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 13import {useScrollHandlers} from '#/lib/ScrollContext' 14import {addStyle} from '#/lib/styles' 15import {useLightbox} from '#/state/lightbox' 16import {useTheme} from '#/alf' 17import {IS_IOS} from '#/env' 18import {FlatList_INTERNAL} from './Views' 19 20export type ListMethods = FlatList_INTERNAL 21export type ListProps<ItemT = any> = Omit< 22 FlatListPropsWithLayout<ItemT>, 23 | 'onMomentumScrollBegin' // Use ScrollContext instead. 24 | 'onMomentumScrollEnd' // Use ScrollContext instead. 25 | 'onScroll' // Use ScrollContext instead. 26 | 'onScrollBeginDrag' // Use ScrollContext instead. 27 | 'onScrollEndDrag' // Use ScrollContext instead. 28 | 'refreshControl' // Pass refreshing and/or onRefresh instead. 29 | 'contentOffset' // Pass headerOffset instead. 30 | 'progressViewOffset' // Can't be an animated value 31> & { 32 onScrolledDownChange?: (isScrolledDown: boolean) => void 33 onScrollOffsetChange?: (offsetY: number) => void 34 headerOffset?: number 35 refreshing?: boolean 36 onRefresh?: () => void 37 onItemSeen?: (item: ItemT) => void 38 desktopFixedHeight?: number | boolean 39 // Web only prop to contain the scroll to the container rather than the window 40 disableFullWindowScroll?: boolean 41 sideBorders?: boolean 42 progressViewOffset?: number 43} 44export type ListRef = React.RefObject<FlatList_INTERNAL | null> 45 46const SCROLLED_DOWN_LIMIT = 200 47 48let List = forwardRef<ListMethods, ListProps>( 49 ( 50 { 51 onScrolledDownChange, 52 onScrollOffsetChange, 53 refreshing, 54 onRefresh, 55 onItemSeen, 56 headerOffset, 57 style, 58 progressViewOffset, 59 automaticallyAdjustsScrollIndicatorInsets = false, 60 ...props 61 }, 62 ref, 63 ): React.ReactElement<any> => { 64 const isScrolledDown = useSharedValue(false) 65 const t = useTheme() 66 const dedupe = useDedupe(400) 67 const scrollsToTop = useAllowScrollToTop() 68 69 const handleScrolledDownChange = useNonReactiveCallback( 70 (didScrollDown: boolean) => { 71 onScrolledDownChange?.(didScrollDown) 72 }, 73 ) 74 const handleScrollOffsetChange = useNonReactiveCallback( 75 (offsetY: number) => { 76 onScrollOffsetChange?.(offsetY) 77 }, 78 ) 79 80 // Intentionally destructured outside the main thread closure. 81 // See https://github.com/bluesky-social/social-app/pull/4108. 82 const { 83 onBeginDrag: onBeginDragFromContext, 84 onEndDrag: onEndDragFromContext, 85 onScroll: onScrollFromContext, 86 onMomentumEnd: onMomentumEndFromContext, 87 } = useScrollHandlers() 88 const scrollHandler = useAnimatedScrollHandler({ 89 onBeginDrag(e, ctx) { 90 onBeginDragFromContext?.(e, ctx) 91 }, 92 onEndDrag(e, ctx) { 93 runOnJS(updateActiveVideoViewAsync)() 94 onEndDragFromContext?.(e, ctx) 95 }, 96 onScroll(e, ctx) { 97 onScrollFromContext?.(e, ctx) 98 99 const didScrollDown = e.contentOffset.y > SCROLLED_DOWN_LIMIT 100 if (isScrolledDown.get() !== didScrollDown) { 101 isScrolledDown.set(didScrollDown) 102 if (onScrolledDownChange != null) { 103 runOnJS(handleScrolledDownChange)(didScrollDown) 104 } 105 } 106 107 if (onScrollOffsetChange != null) { 108 runOnJS(handleScrollOffsetChange)(e.contentOffset.y) 109 } 110 111 if (IS_IOS) { 112 runOnJS(dedupe)(updateActiveVideoViewAsync) 113 } 114 }, 115 // Note: adding onMomentumBegin here makes simulator scroll 116 // lag on Android. So either don't add it, or figure out why. 117 onMomentumEnd(e, ctx) { 118 runOnJS(updateActiveVideoViewAsync)() 119 onMomentumEndFromContext?.(e, ctx) 120 }, 121 }) 122 123 const [onViewableItemsChanged, viewabilityConfig] = useMemo(() => { 124 if (!onItemSeen) { 125 return [undefined, undefined] 126 } 127 return [ 128 (info: { 129 viewableItems: Array<ViewToken> 130 changed: Array<ViewToken> 131 }) => { 132 for (const item of info.changed) { 133 if (item.isViewable) { 134 onItemSeen(item.item) 135 } 136 } 137 }, 138 { 139 itemVisiblePercentThreshold: 40, 140 minimumViewTime: 0.5e3, 141 }, 142 ] 143 }, [onItemSeen]) 144 145 let refreshControl 146 if (refreshing !== undefined || onRefresh !== undefined) { 147 refreshControl = ( 148 <RefreshControl 149 key={t.atoms.text.color} 150 refreshing={refreshing ?? false} 151 onRefresh={onRefresh} 152 tintColor={t.atoms.text.color} 153 titleColor={t.atoms.text.color} 154 progressViewOffset={progressViewOffset ?? headerOffset} 155 /> 156 ) 157 } 158 159 let contentOffset 160 if (headerOffset != null) { 161 style = addStyle(style, { 162 paddingTop: headerOffset, 163 }) 164 contentOffset = {x: 0, y: headerOffset * -1} 165 } 166 167 return ( 168 <FlatList_INTERNAL 169 showsVerticalScrollIndicator // overridable 170 onViewableItemsChanged={onViewableItemsChanged} 171 viewabilityConfig={viewabilityConfig} 172 {...props} 173 automaticallyAdjustsScrollIndicatorInsets={ 174 automaticallyAdjustsScrollIndicatorInsets 175 } 176 scrollIndicatorInsets={{ 177 top: headerOffset, 178 right: 1, 179 ...props.scrollIndicatorInsets, 180 }} 181 indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'} 182 contentOffset={contentOffset} 183 refreshControl={refreshControl} 184 onScroll={scrollHandler} 185 scrollsToTop={scrollsToTop} 186 scrollEventThrottle={1} 187 style={style} 188 // @ts-expect-error FlatList_INTERNAL ref type is wrong -sfn 189 ref={ref} 190 /> 191 ) 192 }, 193) 194List.displayName = 'List' 195 196List = memo(List) 197export {List} 198 199// We only want to use this context value on iOS because the `scrollsToTop` prop is iOS-only 200// removing it saves us a re-render on Android 201const useAllowScrollToTop = IS_IOS ? useAllowScrollToTopIOS : () => undefined 202function useAllowScrollToTopIOS() { 203 const {activeLightbox} = useLightbox() 204 return useDeferredValue(!activeLightbox) 205}