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