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