mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React, {useImperativeHandle} from 'react'
2import {
3 Dimensions,
4 Keyboard,
5 Pressable,
6 StyleProp,
7 View,
8 ViewStyle,
9} from 'react-native'
10import Animated, {useAnimatedStyle} from 'react-native-reanimated'
11import {useSafeAreaInsets} from 'react-native-safe-area-context'
12import BottomSheet, {
13 BottomSheetBackdropProps,
14 BottomSheetFlatList,
15 BottomSheetFlatListMethods,
16 BottomSheetScrollView,
17 BottomSheetScrollViewMethods,
18 BottomSheetTextInput,
19 BottomSheetView,
20 useBottomSheet,
21 WINDOW_HEIGHT,
22} from '@discord/bottom-sheet/src'
23import {BottomSheetFlatListProps} from '@discord/bottom-sheet/src/components/bottomSheetScrollable/types'
24
25import {logger} from '#/logger'
26import {useDialogStateControlContext} from '#/state/dialogs'
27import {atoms as a, flatten, useTheme} from '#/alf'
28import {Context} from '#/components/Dialog/context'
29import {
30 DialogControlProps,
31 DialogInnerProps,
32 DialogOuterProps,
33} from '#/components/Dialog/types'
34import {createInput} from '#/components/forms/TextField'
35import {FullWindowOverlay} from '#/components/FullWindowOverlay'
36import {Portal} from '#/components/Portal'
37
38export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
39export * from '#/components/Dialog/types'
40export * from '#/components/Dialog/utils'
41// @ts-ignore
42export const Input = createInput(BottomSheetTextInput)
43
44function Backdrop(props: BottomSheetBackdropProps) {
45 const t = useTheme()
46 const bottomSheet = useBottomSheet()
47
48 const animatedStyle = useAnimatedStyle(() => {
49 const opacity =
50 (Math.abs(WINDOW_HEIGHT - props.animatedPosition.value) - 50) / 1000
51
52 return {
53 opacity: Math.min(Math.max(opacity, 0), 0.55),
54 }
55 })
56
57 const onPress = React.useCallback(() => {
58 bottomSheet.close()
59 }, [bottomSheet])
60
61 return (
62 <Animated.View
63 style={[
64 t.atoms.bg_contrast_300,
65 {
66 top: 0,
67 left: 0,
68 right: 0,
69 bottom: 0,
70 position: 'absolute',
71 },
72 animatedStyle,
73 ]}>
74 <Pressable
75 accessibilityRole="button"
76 accessibilityLabel="Dialog backdrop"
77 accessibilityHint="Press the backdrop to close the dialog"
78 style={{flex: 1}}
79 onPress={onPress}
80 />
81 </Animated.View>
82 )
83}
84
85export function Outer({
86 children,
87 control,
88 onClose,
89 nativeOptions,
90 testID,
91}: React.PropsWithChildren<DialogOuterProps>) {
92 const t = useTheme()
93 const sheet = React.useRef<BottomSheet>(null)
94 const sheetOptions = nativeOptions?.sheet || {}
95 const hasSnapPoints = !!sheetOptions.snapPoints
96 const insets = useSafeAreaInsets()
97 const closeCallbacks = React.useRef<(() => void)[]>([])
98 const {setDialogIsOpen} = useDialogStateControlContext()
99
100 /*
101 * Used to manage open/closed, but index is otherwise handled internally by `BottomSheet`
102 */
103 const [openIndex, setOpenIndex] = React.useState(-1)
104
105 /*
106 * `openIndex` is the index of the snap point to open the bottom sheet to. If >0, the bottom sheet is open.
107 */
108 const isOpen = openIndex > -1
109
110 const callQueuedCallbacks = React.useCallback(() => {
111 for (const cb of closeCallbacks.current) {
112 try {
113 cb()
114 } catch (e: any) {
115 logger.error('Error running close callback', e)
116 }
117 }
118
119 closeCallbacks.current = []
120 }, [])
121
122 const open = React.useCallback<DialogControlProps['open']>(
123 ({index} = {}) => {
124 // Run any leftover callbacks that might have been queued up before calling `.open()`
125 callQueuedCallbacks()
126
127 setDialogIsOpen(control.id, true)
128 // can be set to any index of `snapPoints`, but `0` is the first i.e. "open"
129 setOpenIndex(index || 0)
130 sheet.current?.snapToIndex(index || 0)
131 },
132 [setDialogIsOpen, control.id, callQueuedCallbacks],
133 )
134
135 // This is the function that we call when we want to dismiss the dialog.
136 const close = React.useCallback<DialogControlProps['close']>(cb => {
137 if (typeof cb === 'function') {
138 closeCallbacks.current.push(cb)
139 }
140 sheet.current?.close()
141 }, [])
142
143 // This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to
144 // happen before we run this. It is passed to the `BottomSheet` component.
145 const onCloseAnimationComplete = React.useCallback(() => {
146 // This removes the dialog from our list of stored dialogs. Not super necessary on iOS, but on Android this
147 // tells us that we need to toggle the accessibility overlay setting
148 setDialogIsOpen(control.id, false)
149 setOpenIndex(-1)
150
151 callQueuedCallbacks()
152 onClose?.()
153 }, [callQueuedCallbacks, control.id, onClose, setDialogIsOpen])
154
155 useImperativeHandle(
156 control.ref,
157 () => ({
158 open,
159 close,
160 }),
161 [open, close],
162 )
163
164 React.useEffect(() => {
165 return () => {
166 setDialogIsOpen(control.id, false)
167 }
168 }, [control.id, setDialogIsOpen])
169
170 const context = React.useMemo(() => ({close}), [close])
171
172 return (
173 isOpen && (
174 <Portal>
175 <FullWindowOverlay>
176 <View
177 // iOS
178 accessibilityViewIsModal
179 // Android
180 importantForAccessibility="yes"
181 style={[a.absolute, a.inset_0]}
182 testID={testID}
183 onTouchMove={() => Keyboard.dismiss()}>
184 <BottomSheet
185 enableDynamicSizing={!hasSnapPoints}
186 enablePanDownToClose
187 keyboardBehavior="interactive"
188 android_keyboardInputMode="adjustResize"
189 keyboardBlurBehavior="restore"
190 topInset={insets.top}
191 {...sheetOptions}
192 snapPoints={sheetOptions.snapPoints || ['100%']}
193 ref={sheet}
194 index={openIndex}
195 backgroundStyle={{backgroundColor: 'transparent'}}
196 backdropComponent={Backdrop}
197 handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
198 handleStyle={{display: 'none'}}
199 onClose={onCloseAnimationComplete}>
200 <Context.Provider value={context}>
201 <View
202 style={[
203 a.absolute,
204 a.inset_0,
205 t.atoms.bg,
206 {
207 borderTopLeftRadius: 40,
208 borderTopRightRadius: 40,
209 height: Dimensions.get('window').height * 2,
210 },
211 ]}
212 />
213 {children}
214 </Context.Provider>
215 </BottomSheet>
216 </View>
217 </FullWindowOverlay>
218 </Portal>
219 )
220 )
221}
222
223export function Inner({children, style}: DialogInnerProps) {
224 const insets = useSafeAreaInsets()
225 return (
226 <BottomSheetView
227 style={[
228 a.py_xl,
229 a.px_xl,
230 {
231 paddingTop: 40,
232 borderTopLeftRadius: 40,
233 borderTopRightRadius: 40,
234 paddingBottom: insets.bottom + a.pb_5xl.paddingBottom,
235 },
236 flatten(style),
237 ]}>
238 {children}
239 </BottomSheetView>
240 )
241}
242
243export const ScrollableInner = React.forwardRef<
244 BottomSheetScrollViewMethods,
245 DialogInnerProps
246>(function ScrollableInner({children, style}, ref) {
247 const insets = useSafeAreaInsets()
248 return (
249 <BottomSheetScrollView
250 keyboardShouldPersistTaps="handled"
251 style={[
252 a.flex_1, // main diff is this
253 a.p_xl,
254 a.h_full,
255 {
256 paddingTop: 40,
257 borderTopLeftRadius: 40,
258 borderTopRightRadius: 40,
259 },
260 style,
261 ]}
262 contentContainerStyle={a.pb_4xl}
263 ref={ref}>
264 {children}
265 <View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
266 </BottomSheetScrollView>
267 )
268})
269
270export const InnerFlatList = React.forwardRef<
271 BottomSheetFlatListMethods,
272 BottomSheetFlatListProps<any> & {webInnerStyle?: StyleProp<ViewStyle>}
273>(function InnerFlatList({style, contentContainerStyle, ...props}, ref) {
274 const insets = useSafeAreaInsets()
275
276 return (
277 <BottomSheetFlatList
278 keyboardShouldPersistTaps="handled"
279 contentContainerStyle={[a.pb_4xl, flatten(contentContainerStyle)]}
280 ListFooterComponent={
281 <View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
282 }
283 ref={ref}
284 {...props}
285 style={[
286 a.flex_1,
287 a.p_xl,
288 a.pt_0,
289 a.h_full,
290 {
291 marginTop: 40,
292 },
293 flatten(style),
294 ]}
295 />
296 )
297})
298
299export function Handle() {
300 const t = useTheme()
301
302 return (
303 <View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 40}]}>
304 <View
305 style={[
306 a.rounded_sm,
307 {
308 top: a.pt_lg.paddingTop,
309 width: 35,
310 height: 4,
311 alignSelf: 'center',
312 backgroundColor: t.palette.contrast_900,
313 opacity: 0.5,
314 },
315 ]}
316 />
317 </View>
318 )
319}
320
321export function Close() {
322 return null
323}