Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
119
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})