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