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

Configure Feed

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

at a876aae44ea07494ebea9727350aa060b81f317b 238 lines 6.0 kB view raw
1import { 2 forwardRef, 3 type JSX, 4 useCallback, 5 useEffect, 6 useImperativeHandle, 7 useMemo, 8 useRef, 9 useState, 10} from 'react' 11import { 12 type NativeScrollEvent, 13 type NativeSyntheticEvent, 14 Pressable, 15 RefreshControl, 16 ScrollView, 17 StyleSheet, 18 View, 19} from 'react-native' 20 21import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' 22import {usePalette} from '#/lib/hooks/usePalette' 23import {clamp} from '#/lib/numbers' 24import {colors, s} from '#/lib/styles' 25import {IS_ANDROID} from '#/env' 26import {Text} from './text/Text' 27import {FlatList_INTERNAL} from './Views' 28 29const HEADER_ITEM = {_reactKey: '__header__'} 30const SELECTOR_ITEM = {_reactKey: '__selector__'} 31const STICKY_HEADER_INDICES = [1] 32 33export type ViewSelectorHandle = { 34 scrollToTop: () => void 35} 36 37export const ViewSelector = forwardRef< 38 ViewSelectorHandle, 39 { 40 sections: string[] 41 items: any[] 42 refreshing?: boolean 43 swipeEnabled?: boolean 44 renderHeader?: () => JSX.Element 45 renderItem: (item: any) => JSX.Element 46 ListFooterComponent?: 47 | React.ComponentType<any> 48 | React.ReactElement<any> 49 | null 50 | undefined 51 onSelectView?: (viewIndex: number) => void 52 onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void 53 onRefresh?: () => void 54 onEndReached?: (info: {distanceFromEnd: number}) => void 55 } 56>(function ViewSelectorImpl( 57 { 58 sections, 59 items, 60 refreshing, 61 renderHeader, 62 renderItem, 63 ListFooterComponent, 64 onSelectView, 65 onScroll, 66 onRefresh, 67 onEndReached, 68 }, 69 ref, 70) { 71 const pal = usePalette('default') 72 const [selectedIndex, setSelectedIndex] = useState<number>(0) 73 const flatListRef = useRef<FlatList_INTERNAL>(null) 74 75 // events 76 // = 77 78 const keyExtractor = useCallback((item: any) => item._reactKey, []) 79 80 const onPressSelection = useCallback( 81 (index: number) => setSelectedIndex(clamp(index, 0, sections.length)), 82 [setSelectedIndex, sections], 83 ) 84 useEffect(() => { 85 onSelectView?.(selectedIndex) 86 }, [selectedIndex, onSelectView]) 87 88 useImperativeHandle(ref, () => ({ 89 scrollToTop: () => { 90 flatListRef.current?.scrollToOffset({offset: 0}) 91 }, 92 })) 93 94 // rendering 95 // = 96 97 const renderItemInternal = useCallback( 98 ({item}: {item: any}) => { 99 if (item === HEADER_ITEM) { 100 if (renderHeader) { 101 return renderHeader() 102 } 103 return <View /> 104 } else if (item === SELECTOR_ITEM) { 105 return ( 106 <Selector 107 items={sections} 108 selectedIndex={selectedIndex} 109 onSelect={onPressSelection} 110 /> 111 ) 112 } else { 113 return renderItem(item) 114 } 115 }, 116 [sections, selectedIndex, onPressSelection, renderHeader, renderItem], 117 ) 118 119 const data = useMemo(() => [HEADER_ITEM, SELECTOR_ITEM, ...items], [items]) 120 return ( 121 <FlatList_INTERNAL 122 // @ts-expect-error FlatList_INTERNAL ref type is wrong -sfn 123 ref={flatListRef} 124 data={data} 125 keyExtractor={keyExtractor} 126 renderItem={renderItemInternal} 127 ListFooterComponent={ListFooterComponent} 128 // NOTE sticky header disabled on android due to major performance issues -prf 129 stickyHeaderIndices={IS_ANDROID ? undefined : STICKY_HEADER_INDICES} 130 onScroll={onScroll} 131 onEndReached={onEndReached} 132 refreshControl={ 133 <RefreshControl 134 refreshing={refreshing!} 135 onRefresh={onRefresh} 136 tintColor={pal.colors.text} 137 /> 138 } 139 onEndReachedThreshold={0.6} 140 contentContainerStyle={s.contentContainer} 141 removeClippedSubviews={true} 142 scrollIndicatorInsets={{right: 1}} // fixes a bug where the scroll indicator is on the middle of the screen https://github.com/bluesky-social/social-app/pull/464 143 /> 144 ) 145}) 146 147export function Selector({ 148 selectedIndex, 149 items, 150 onSelect, 151}: { 152 selectedIndex: number 153 items: string[] 154 onSelect?: (index: number) => void 155}) { 156 const pal = usePalette('default') 157 const borderColor = useColorSchemeStyle( 158 {borderColor: colors.black}, 159 {borderColor: colors.white}, 160 ) 161 162 const onPressItem = (index: number) => { 163 onSelect?.(index) 164 } 165 166 return ( 167 <View 168 style={{ 169 width: '100%', 170 backgroundColor: pal.colors.background, 171 }}> 172 <ScrollView 173 testID="selector" 174 horizontal 175 showsHorizontalScrollIndicator={false}> 176 <View style={[pal.view, styles.outer]}> 177 {items.map((item, i) => { 178 const selected = i === selectedIndex 179 return ( 180 <Pressable 181 testID={`selector-${i}`} 182 key={item} 183 onPress={() => onPressItem(i)} 184 accessibilityLabel={item} 185 accessibilityHint={`Selects ${item}`} 186 // TODO: Modify the component API such that lint fails 187 // at the invocation site as well 188 > 189 <View 190 style={[ 191 styles.item, 192 selected && styles.itemSelected, 193 borderColor, 194 ]}> 195 <Text 196 style={ 197 selected 198 ? [styles.labelSelected, pal.text] 199 : [styles.label, pal.textLight] 200 }> 201 {item} 202 </Text> 203 </View> 204 </Pressable> 205 ) 206 })} 207 </View> 208 </ScrollView> 209 </View> 210 ) 211} 212 213const styles = StyleSheet.create({ 214 outer: { 215 flexDirection: 'row', 216 paddingHorizontal: 14, 217 }, 218 item: { 219 marginRight: 14, 220 paddingHorizontal: 10, 221 paddingTop: 8, 222 paddingBottom: 12, 223 }, 224 itemSelected: { 225 borderBottomWidth: 3, 226 }, 227 label: { 228 fontWeight: '600', 229 }, 230 labelSelected: { 231 fontWeight: '600', 232 }, 233 underline: { 234 position: 'absolute', 235 height: 4, 236 bottom: 0, 237 }, 238})