forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import * as React from 'react'
2import {
3 Dimensions,
4 type LayoutChangeEvent,
5 type NativeSyntheticEvent,
6 Platform,
7 type StyleProp,
8 useWindowDimensions,
9 View,
10 type ViewStyle,
11} from 'react-native'
12import {useSafeAreaInsets} from 'react-native-safe-area-context'
13import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
14
15import {
16 type BottomSheetState,
17 type BottomSheetViewProps,
18} from './BottomSheet.types'
19import {
20 BottomSheetPortalProvider,
21 Context as PortalContext,
22} from './BottomSheetPortal'
23
24const NativeView: React.ComponentType<
25 BottomSheetViewProps & {
26 ref: React.RefObject<any>
27 style: StyleProp<ViewStyle>
28 }
29> = requireNativeViewManager('BottomSheet')
30
31const NativeModule = requireNativeModule('BottomSheet')
32
33const IS_IOS15 =
34 Platform.OS === 'ios' &&
35 // semvar - can be 3 segments, so can't use Number(Platform.Version)
36 Number(Platform.Version.split('.').at(0)) < 16
37// older android versions (15 and below) aren't naturally edge-to-edge
38// and behave a little differently
39const IS_NON_E2E_ANDROID =
40 Platform.OS === 'android' && Number(Platform.Version) < 35
41
42export class BottomSheetNativeComponent extends React.Component<
43 BottomSheetViewProps,
44 {
45 open: boolean
46 viewHeight?: number
47 }
48> {
49 ref = React.createRef<any>()
50
51 static contextType = PortalContext
52
53 constructor(props: BottomSheetViewProps) {
54 super(props)
55 this.state = {
56 open: false,
57 }
58 }
59
60 present() {
61 this.setState({open: true})
62 }
63
64 dismiss() {
65 this.ref.current?.dismiss()
66 }
67
68 private onStateChange = (
69 event: NativeSyntheticEvent<{state: BottomSheetState}>,
70 ) => {
71 const {state} = event.nativeEvent
72 const isOpen = state !== 'closed'
73 this.setState({open: isOpen})
74 this.props.onStateChange?.(event)
75 }
76
77 static dismissAll = async () => {
78 await NativeModule.dismissAll()
79 }
80
81 render() {
82 const Portal = this.context as React.ContextType<typeof PortalContext>
83 if (!Portal) {
84 throw new Error(
85 'BottomSheet: You need to wrap your component tree with a <BottomSheetPortalProvider> to use the bottom sheet.',
86 )
87 }
88
89 if (!this.state.open) {
90 return null
91 }
92
93 let extraStyles
94 if (IS_IOS15 && this.state.viewHeight) {
95 const screenHeight = Dimensions.get('screen').height
96 const {viewHeight} = this.state
97 const cornerRadius = this.props.cornerRadius ?? 0
98 if (viewHeight < screenHeight / 2) {
99 extraStyles = {
100 height: viewHeight,
101 marginTop: screenHeight / 2 - viewHeight,
102 borderTopLeftRadius: cornerRadius,
103 borderTopRightRadius: cornerRadius,
104 }
105 }
106 }
107
108 return (
109 <Portal>
110 <BottomSheetNativeComponentInner
111 {...this.props}
112 nativeViewRef={this.ref}
113 onStateChange={this.onStateChange}
114 extraStyles={extraStyles}
115 onLayout={
116 IS_IOS15
117 ? e => {
118 const {height} = e.nativeEvent.layout
119 this.setState({viewHeight: height})
120 }
121 : undefined
122 }
123 />
124 </Portal>
125 )
126 }
127}
128
129function BottomSheetNativeComponentInner({
130 children,
131 backgroundColor,
132 onLayout,
133 onStateChange,
134 nativeViewRef,
135 extraStyles,
136 ...rest
137}: BottomSheetViewProps & {
138 extraStyles?: StyleProp<ViewStyle>
139 onStateChange: (
140 event: NativeSyntheticEvent<{state: BottomSheetState}>,
141 ) => void
142 nativeViewRef: React.RefObject<View>
143 onLayout?: (event: LayoutChangeEvent) => void
144}) {
145 const insets = useSafeAreaInsets()
146 const cornerRadius = rest.cornerRadius ?? 0
147 const {height: screenHeight} = useWindowDimensions()
148
149 // sigh... on older Android versions, screenHeight does not include safe area insets
150 // on newer Androids + iOS, it does. we need to find the inner bit + the bottom inset
151 // for the sheet content
152 const sheetHeight = IS_NON_E2E_ANDROID
153 ? screenHeight + insets.bottom
154 : screenHeight - insets.top
155
156 return (
157 <NativeView
158 {...rest}
159 onStateChange={onStateChange}
160 ref={nativeViewRef}
161 style={{
162 position: 'absolute',
163 height: sheetHeight,
164 width: '100%',
165 }}
166 containerBackgroundColor={backgroundColor}>
167 <View
168 style={[
169 {
170 flex: 1,
171 backgroundColor,
172 },
173 Platform.OS === 'android' && {
174 borderTopLeftRadius: cornerRadius,
175 borderTopRightRadius: cornerRadius,
176 overflow: 'hidden',
177 },
178 extraStyles,
179 ]}>
180 <View onLayout={onLayout}>
181 <BottomSheetPortalProvider>{children}</BottomSheetPortalProvider>
182 </View>
183 </View>
184 </NativeView>
185 )
186}