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 View,
9 type ViewStyle,
10} from 'react-native'
11import {useSafeAreaInsets} from 'react-native-safe-area-context'
12import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
13
14import {isIOS} from '#/platform/detection'
15import {
16 type BottomSheetState,
17 type BottomSheetViewProps,
18} from './BottomSheet.types'
19import {BottomSheetPortalProvider} from './BottomSheetPortal'
20import {Context as PortalContext} from './BottomSheetPortal'
21
22const screenHeight = Dimensions.get('screen').height
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 isIOS15 =
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
38export class BottomSheetNativeComponent extends React.Component<
39 BottomSheetViewProps,
40 {
41 open: boolean
42 viewHeight?: number
43 }
44> {
45 ref = React.createRef<any>()
46
47 static contextType = PortalContext
48
49 constructor(props: BottomSheetViewProps) {
50 super(props)
51 this.state = {
52 open: false,
53 }
54 }
55
56 present() {
57 this.setState({open: true})
58 }
59
60 dismiss() {
61 this.ref.current?.dismiss()
62 }
63
64 private onStateChange = (
65 event: NativeSyntheticEvent<{state: BottomSheetState}>,
66 ) => {
67 const {state} = event.nativeEvent
68 const isOpen = state !== 'closed'
69 this.setState({open: isOpen})
70 this.props.onStateChange?.(event)
71 }
72
73 private updateLayout = () => {
74 this.ref.current?.updateLayout()
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 (isIOS15 && this.state.viewHeight) {
95 const {viewHeight} = this.state
96 const cornerRadius = this.props.cornerRadius ?? 0
97 if (viewHeight < screenHeight / 2) {
98 extraStyles = {
99 height: viewHeight,
100 marginTop: screenHeight / 2 - viewHeight,
101 borderTopLeftRadius: cornerRadius,
102 borderTopRightRadius: cornerRadius,
103 }
104 }
105 }
106
107 return (
108 <Portal>
109 <BottomSheetNativeComponentInner
110 {...this.props}
111 nativeViewRef={this.ref}
112 onStateChange={this.onStateChange}
113 extraStyles={extraStyles}
114 onLayout={e => {
115 if (isIOS15) {
116 const {height} = e.nativeEvent.layout
117 this.setState({viewHeight: height})
118 }
119 if (Platform.OS === 'android') {
120 // TEMP HACKFIX: I had to timebox this, but this is Bad.
121 // On Android, if you run updateLayout() immediately,
122 // it will take ages to actually run on the native side.
123 // However, adding literally any delay will fix this, including
124 // a console.log() - just sending the log to the CLI is enough.
125 // TODO: Get to the bottom of this and fix it properly! -sfn
126 setTimeout(() => this.updateLayout())
127 } else {
128 this.updateLayout()
129 }
130 }}
131 />
132 </Portal>
133 )
134 }
135}
136
137function BottomSheetNativeComponentInner({
138 children,
139 backgroundColor,
140 onLayout,
141 onStateChange,
142 nativeViewRef,
143 extraStyles,
144 ...rest
145}: BottomSheetViewProps & {
146 extraStyles?: StyleProp<ViewStyle>
147 onStateChange: (
148 event: NativeSyntheticEvent<{state: BottomSheetState}>,
149 ) => void
150 nativeViewRef: React.RefObject<View>
151 onLayout: (event: LayoutChangeEvent) => void
152}) {
153 const insets = useSafeAreaInsets()
154 const cornerRadius = rest.cornerRadius ?? 0
155
156 const sheetHeight = isIOS ? screenHeight - insets.top : screenHeight
157
158 return (
159 <NativeView
160 {...rest}
161 onStateChange={onStateChange}
162 ref={nativeViewRef}
163 style={{
164 position: 'absolute',
165 height: sheetHeight,
166 width: '100%',
167 }}
168 containerBackgroundColor={backgroundColor}>
169 <View
170 style={[
171 {
172 flex: 1,
173 backgroundColor,
174 },
175 Platform.OS === 'android' && {
176 borderTopLeftRadius: cornerRadius,
177 borderTopRightRadius: cornerRadius,
178 },
179 extraStyles,
180 ]}>
181 <View onLayout={onLayout}>
182 <BottomSheetPortalProvider>{children}</BottomSheetPortalProvider>
183 </View>
184 </View>
185 </NativeView>
186 )
187}