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