mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import {useCallback, useEffect, useState} from 'react'
2import {BackHandler, useWindowDimensions, View} from 'react-native'
3import {Drawer} from 'react-native-drawer-layout'
4import {Gesture} from 'react-native-gesture-handler'
5import {useSafeAreaInsets} from 'react-native-safe-area-context'
6import {StatusBar} from 'expo-status-bar'
7import {useNavigation, useNavigationState} from '@react-navigation/native'
8
9import {useDedupe} from '#/lib/hooks/useDedupe'
10import {useIntentHandler} from '#/lib/hooks/useIntentHandler'
11import {useNotificationsHandler} from '#/lib/hooks/useNotificationHandler'
12import {useNotificationsRegistration} from '#/lib/notifications/notifications'
13import {isStateAtTabRoot} from '#/lib/routes/helpers'
14import {isAndroid, isIOS} from '#/platform/detection'
15import {useDialogStateControlContext} from '#/state/dialogs'
16import {useSession} from '#/state/session'
17import {
18 useIsDrawerOpen,
19 useIsDrawerSwipeDisabled,
20 useSetDrawerOpen,
21} from '#/state/shell'
22import {useLightStatusBar} from '#/state/shell/light-status-bar'
23import {useCloseAnyActiveElement} from '#/state/util'
24import {Lightbox} from '#/view/com/lightbox/Lightbox'
25import {ModalsContainer} from '#/view/com/modals/Modal'
26import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
27import {atoms as a, select, useTheme} from '#/alf'
28import {setNavigationBar} from '#/alf/util/navigationBar'
29import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
30import {SigninDialog} from '#/components/dialogs/Signin'
31import {Outlet as PortalOutlet} from '#/components/Portal'
32import {RoutesContainer, TabsNavigator} from '#/Navigation'
33import {BottomSheetOutlet} from '../../../modules/bottom-sheet'
34import {updateActiveViewAsync} from '../../../modules/expo-bluesky-swiss-army/src/VisibilityView'
35import {Composer} from './Composer'
36import {DrawerContent} from './Drawer'
37
38function ShellInner() {
39 const t = useTheme()
40 const isDrawerOpen = useIsDrawerOpen()
41 const isDrawerSwipeDisabled = useIsDrawerSwipeDisabled()
42 const setIsDrawerOpen = useSetDrawerOpen()
43 const winDim = useWindowDimensions()
44 const insets = useSafeAreaInsets()
45
46 const renderDrawerContent = useCallback(() => <DrawerContent />, [])
47 const onOpenDrawer = useCallback(
48 () => setIsDrawerOpen(true),
49 [setIsDrawerOpen],
50 )
51 const onCloseDrawer = useCallback(
52 () => setIsDrawerOpen(false),
53 [setIsDrawerOpen],
54 )
55 const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
56 const {hasSession} = useSession()
57 const closeAnyActiveElement = useCloseAnyActiveElement()
58
59 useNotificationsRegistration()
60 useNotificationsHandler()
61
62 useEffect(() => {
63 if (isAndroid) {
64 const listener = BackHandler.addEventListener('hardwareBackPress', () => {
65 return closeAnyActiveElement()
66 })
67
68 return () => {
69 listener.remove()
70 }
71 }
72 }, [closeAnyActiveElement])
73
74 // HACK
75 // expo-video doesn't like it when you try and move a `player` to another `VideoView`. Instead, we need to actually
76 // unregister that player to let the new screen register it. This is only a problem on Android, so we only need to
77 // apply it there.
78 // The `state` event should only fire whenever we push or pop to a screen, and should not fire consecutively quickly.
79 // To be certain though, we will also dedupe these calls.
80 const navigation = useNavigation()
81 const dedupe = useDedupe(1000)
82 useEffect(() => {
83 if (!isAndroid) return
84 const onFocusOrBlur = () => {
85 setTimeout(() => {
86 dedupe(updateActiveViewAsync)
87 }, 500)
88 }
89 navigation.addListener('state', onFocusOrBlur)
90 return () => {
91 navigation.removeListener('state', onFocusOrBlur)
92 }
93 }, [dedupe, navigation])
94
95 const swipeEnabled = !canGoBack && hasSession && !isDrawerSwipeDisabled
96 const [trendingScrollGesture] = useState(() => Gesture.Native())
97 return (
98 <>
99 <View style={[a.h_full]}>
100 <ErrorBoundary
101 style={{paddingTop: insets.top, paddingBottom: insets.bottom}}>
102 <Drawer
103 renderDrawerContent={renderDrawerContent}
104 drawerStyle={{width: Math.min(400, winDim.width * 0.8)}}
105 configureGestureHandler={handler => {
106 handler = handler.requireExternalGestureToFail(
107 trendingScrollGesture,
108 )
109
110 if (swipeEnabled) {
111 if (isDrawerOpen) {
112 return handler.activeOffsetX([-1, 1])
113 } else {
114 return (
115 handler
116 // Any movement to the left is a pager swipe
117 // so fail the drawer gesture immediately.
118 .failOffsetX(-1)
119 // Don't rush declaring that a movement to the right
120 // is a drawer swipe. It could be a vertical scroll.
121 .activeOffsetX(5)
122 )
123 }
124 } else {
125 // Fail the gesture immediately.
126 // This seems more reliable than the `swipeEnabled` prop.
127 // With `swipeEnabled` alone, the gesture may freeze after toggling off/on.
128 return handler.failOffsetX([0, 0]).failOffsetY([0, 0])
129 }
130 }}
131 open={isDrawerOpen}
132 onOpen={onOpenDrawer}
133 onClose={onCloseDrawer}
134 swipeEdgeWidth={winDim.width}
135 swipeMinVelocity={100}
136 swipeMinDistance={10}
137 drawerType={isIOS ? 'slide' : 'front'}
138 overlayStyle={{
139 backgroundColor: select(t.name, {
140 light: 'rgba(0, 57, 117, 0.1)',
141 dark: isAndroid
142 ? 'rgba(16, 133, 254, 0.1)'
143 : 'rgba(1, 82, 168, 0.1)',
144 dim: 'rgba(10, 13, 16, 0.8)',
145 }),
146 }}>
147 <TabsNavigator />
148 </Drawer>
149 </ErrorBoundary>
150 </View>
151 <Composer winHeight={winDim.height} />
152 <ModalsContainer />
153 <MutedWordsDialog />
154 <SigninDialog />
155 <Lightbox />
156 <PortalOutlet />
157 <BottomSheetOutlet />
158 </>
159 )
160}
161
162export const Shell: React.FC = function ShellImpl() {
163 const {fullyExpandedCount} = useDialogStateControlContext()
164 const lightStatusBar = useLightStatusBar()
165 const t = useTheme()
166 useIntentHandler()
167
168 useEffect(() => {
169 setNavigationBar('theme', t)
170 }, [t])
171
172 return (
173 <View testID="mobileShellView" style={[a.h_full, t.atoms.bg]}>
174 <StatusBar
175 style={
176 t.name !== 'light' ||
177 (isIOS && fullyExpandedCount > 0) ||
178 lightStatusBar
179 ? 'light'
180 : 'dark'
181 }
182 animated
183 />
184 <RoutesContainer>
185 <ShellInner />
186 </RoutesContainer>
187 </View>
188 )
189}