Bluesky app fork with some witchin' additions 馃挮
witchsky.app
bluesky
fork
client
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 {IS_IOS} from '#/env'
16import {
17 type BottomSheetState,
18 type BottomSheetViewProps,
19} from './BottomSheet.types'
20import {
21 BottomSheetPortalProvider,
22 Context as PortalContext,
23} from './BottomSheetPortal'
24
25const NativeView: React.ComponentType<
26 BottomSheetViewProps & {
27 ref: React.RefObject<any>
28 style: StyleProp<ViewStyle>
29 }
30> = requireNativeViewManager('BottomSheet')
31
32const NativeModule = requireNativeModule('BottomSheet')
33
34const IS_IOS15 =
35 Platform.OS === 'ios' &&
36 // semvar - can be 3 segments, so can't use Number(Platform.Version)
37 Number(Platform.Version.split('.').at(0)) < 16
38
39export class BottomSheetNativeComponent extends React.Component<
40 BottomSheetViewProps,
41 {
42 open: boolean
43 viewHeight?: number
44 }
45> {
46 ref = React.createRef<any>()
47
48 static contextType = PortalContext
49
50 constructor(props: BottomSheetViewProps) {
51 super(props)
52 this.state = {
53 open: false,
54 }
55 }
56
57 present() {
58 this.setState({open: true})
59 }
60
61 dismiss() {
62 this.ref.current?.dismiss()
63 }
64
65 private onStateChange = (
66 event: NativeSyntheticEvent<{state: BottomSheetState}>,
67 ) => {
68 const {state} = event.nativeEvent
69 const isOpen = state !== 'closed'
70 this.setState({open: isOpen})
71 this.props.onStateChange?.(event)
72 }
73
74 private updateLayout = () => {
75 this.ref.current?.updateLayout()
76 }
77
78 static dismissAll = async () => {
79 await NativeModule.dismissAll()
80 }
81
82 render() {
83 const Portal = this.context as React.ContextType<typeof PortalContext>
84 if (!Portal) {
85 throw new Error(
86 'BottomSheet: You need to wrap your component tree with a <BottomSheetPortalProvider> to use the bottom sheet.',
87 )
88 }
89
90 if (!this.state.open) {
91 return null
92 }
93
94 let extraStyles
95 if (IS_IOS15 && this.state.viewHeight) {
96 const screenHeight = Dimensions.get('screen').height
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 const {height: screenHeight} = useWindowDimensions()
158
159 const sheetHeight = IS_IOS ? screenHeight - insets.top : screenHeight
160
161 return (
162 <NativeView
163 {...rest}
164 onStateChange={onStateChange}
165 ref={nativeViewRef}
166 style={{
167 position: 'absolute',
168 height: sheetHeight,
169 width: '100%',
170 }}
171 containerBackgroundColor={backgroundColor}>
172 <View
173 style={[
174 {
175 flex: 1,
176 backgroundColor,
177 },
178 Platform.OS === 'android' && {
179 borderTopLeftRadius: cornerRadius,
180 borderTopRightRadius: cornerRadius,
181 overflow: 'hidden',
182 },
183 extraStyles,
184 ]}>
185 <View onLayout={onLayout}>
186 <BottomSheetPortalProvider>{children}</BottomSheetPortalProvider>
187 </View>
188 </View>
189 </NativeView>
190 )
191}