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