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 {IS_IOS} from '#/env'
15import {
16 type BottomSheetState,
17 type BottomSheetViewProps,
18} from './BottomSheet.types'
19import {
20 BottomSheetPortalProvider,
21 Context as PortalContext,
22} from './BottomSheetPortal'
23
24const screenHeight = Dimensions.get('screen').height
25
26const NativeView: React.ComponentType<
27 BottomSheetViewProps & {
28 ref: React.RefObject<any>
29 style: StyleProp<ViewStyle>
30 }
31> = requireNativeViewManager('BottomSheet')
32
33const NativeModule = requireNativeModule('BottomSheet')
34
35const IS_IOS15 =
36 Platform.OS === 'ios' &&
37 // semvar - can be 3 segments, so can't use Number(Platform.Version)
38 Number(Platform.Version.split('.').at(0)) < 16
39
40export class BottomSheetNativeComponent extends React.Component<
41 BottomSheetViewProps,
42 {
43 open: boolean
44 viewHeight?: number
45 }
46> {
47 ref = React.createRef<any>()
48
49 static contextType = PortalContext
50
51 constructor(props: BottomSheetViewProps) {
52 super(props)
53 this.state = {
54 open: false,
55 }
56 }
57
58 present() {
59 this.setState({open: true})
60 }
61
62 dismiss() {
63 this.ref.current?.dismiss()
64 }
65
66 private onStateChange = (
67 event: NativeSyntheticEvent<{state: BottomSheetState}>,
68 ) => {
69 const {state} = event.nativeEvent
70 const isOpen = state !== 'closed'
71 this.setState({open: isOpen})
72 this.props.onStateChange?.(event)
73 }
74
75 private updateLayout = () => {
76 this.ref.current?.updateLayout()
77 }
78
79 static dismissAll = async () => {
80 await NativeModule.dismissAll()
81 }
82
83 render() {
84 const Portal = this.context as React.ContextType<typeof PortalContext>
85 if (!Portal) {
86 throw new Error(
87 'BottomSheet: You need to wrap your component tree with a <BottomSheetPortalProvider> to use the bottom sheet.',
88 )
89 }
90
91 if (!this.state.open) {
92 return null
93 }
94
95 let extraStyles
96 if (IS_IOS15 && this.state.viewHeight) {
97 const {viewHeight} = this.state
98 const cornerRadius = this.props.cornerRadius ?? 0
99 if (viewHeight < screenHeight / 2) {
100 extraStyles = {
101 height: viewHeight,
102 marginTop: screenHeight / 2 - viewHeight,
103 borderTopLeftRadius: cornerRadius,
104 borderTopRightRadius: cornerRadius,
105 }
106 }
107 }
108
109 return (
110 <Portal>
111 <BottomSheetNativeComponentInner
112 {...this.props}
113 nativeViewRef={this.ref}
114 onStateChange={this.onStateChange}
115 extraStyles={extraStyles}
116 onLayout={e => {
117 if (IS_IOS15) {
118 const {height} = e.nativeEvent.layout
119 this.setState({viewHeight: height})
120 }
121 if (Platform.OS === 'android') {
122 // TEMP HACKFIX: I had to timebox this, but this is Bad.
123 // On Android, if you run updateLayout() immediately,
124 // it will take ages to actually run on the native side.
125 // However, adding literally any delay will fix this, including
126 // a console.log() - just sending the log to the CLI is enough.
127 // TODO: Get to the bottom of this and fix it properly! -sfn
128 setTimeout(() => this.updateLayout())
129 } else {
130 this.updateLayout()
131 }
132 }}
133 />
134 </Portal>
135 )
136 }
137}
138
139function BottomSheetNativeComponentInner({
140 children,
141 backgroundColor,
142 onLayout,
143 onStateChange,
144 nativeViewRef,
145 extraStyles,
146 ...rest
147}: BottomSheetViewProps & {
148 extraStyles?: StyleProp<ViewStyle>
149 onStateChange: (
150 event: NativeSyntheticEvent<{state: BottomSheetState}>,
151 ) => void
152 nativeViewRef: React.RefObject<View>
153 onLayout: (event: LayoutChangeEvent) => void
154}) {
155 const insets = useSafeAreaInsets()
156 const cornerRadius = rest.cornerRadius ?? 0
157
158 const sheetHeight = IS_IOS ? screenHeight - insets.top : screenHeight
159
160 return (
161 <NativeView
162 {...rest}
163 onStateChange={onStateChange}
164 ref={nativeViewRef}
165 style={{
166 position: 'absolute',
167 height: sheetHeight,
168 width: '100%',
169 }}
170 containerBackgroundColor={backgroundColor}>
171 <View
172 style={[
173 {
174 flex: 1,
175 backgroundColor,
176 },
177 Platform.OS === 'android' && {
178 borderTopLeftRadius: cornerRadius,
179 borderTopRightRadius: cornerRadius,
180 overflow: 'hidden',
181 },
182 extraStyles,
183 ]}>
184 <View onLayout={onLayout}>
185 <BottomSheetPortalProvider>{children}</BottomSheetPortalProvider>
186 </View>
187 </View>
188 </NativeView>
189 )
190}